Repository navigation
docs(skills): the grid 2xl paragraph names objectui's own BreakpointColumnMap and reads all six keys - #10708
Conversation
…olumnMap and reads all six keys
`skills/objectui/rules/protocol.md` attributed the six-key breakpoint
vocabulary to the spec's `BreakpointColumnMapSchema`. objectstack#11027
retired that schema (released in @objectstack/spec 17.3.0; this repo
installs 17.4.0), and objectui#7580 re-homed the vocabulary as
`BreakpointColumnMap` in `@object-ui/layout`, so the paragraph now names
that owner instead.
The same paragraph said `grid` drops `2xl`. Re-measured on main, that no
longer holds: objectui#7097 gave the renderer a sixth read arm and class
map, and `{xs:1, "2xl":6}` now renders `grid grid-cols-1 2xl:grid-cols-6
gap-4`. The `{xs:1, xl:5}` row is unchanged. The paragraph now says the
renderer reads all six keys, and the obsolete "stop at `xl`" advice is gone.
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone. Prose only on ③ Boundary flags
Implemented-by: VERDICT: FAIL Wording only; three replacements, all inside
Neither replacement adds a fence, a marker, a path, a path:line citation or a version literal, so the four Generated by Claude Code |
…097 releases, and the eval follows Review round 2 on the same paragraph. The intro sentence of "Rule: Layout Responsiveness" now lists `2xl` among the object keys. The `2xl` paragraph names `BreakpointName` in `@object-ui/types` as the key set of `GridSchema.columns`, and `BreakpointColumnMap` in `@object-ui/layout` as `responsive-grid`'s spelling of the same six. It also keeps the old advice for readers on an `@object-ui/components` release that predates objectui#7097: the catalog is served from main, while npm 17.6.0 still drops the `2xl` step. Eval 3 in `skills/objectui/evals/protocol.json` stops expecting "the grid renderer reads only xs through xl" and expects the same two-release reading; its `must_contain` tokens are unchanged. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone. Prose only on ③ Boundary flags
Implemented-by: VERDICT: FAIL Wording only; two replacements, both inside
Neither replacement adds a fence, a marker, a path, a path:line citation or a version literal, and both keep the existing Generated by Claude Code |
…, not the old drop Review round 3. Two cross-references in the published bundle still stated the rule this branch retired. `guides/mobile.md` said the spec accepts `2xl` and the renderer drops it. SKILL.md, section 5, said protocol.md has the keys the renderer reads "and the one it drops". Both now point at the six keys the `grid` renderer reads and at the `@object-ui/components` release predating objectui#7097 that still drops `2xl`. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone. Prose only on ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)改了什么:对外发布的技能包 为什么改:原文两处已不对。一是它把六个断点键归给 spec 的 风险与代价(含回滚):纯文档与评测文本,不动代码、不发包、不欠 changeset。有一个时间差:技能包从 main 分发,合并即生效,而 npm 最新 17.6.0 里 席位意见:ui 席三轮契约复核,第 1、2 轮只在措辞上 FAIL,第 3 轮在 head 你要做的:一条 APPROVED review(os-zhuang 或 hotlong 账号,已请审)。批准后由持卡席位( Generated by Claude Code |
…nes it holds for another parent record (objectui#10740) (objectstack-ai#10751) Fixes objectstack-ai#10740 Clause-②: no A component's internal state and its Save guard; no declared surface moves. `@object-ui/plugin-form` publishes `.` only, and no export, type, prop or registration changes. Dispatched by the `domain:ui` seat 2, session `https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN` (`mode:subagent`), executing triage `5852239622`. Round 3 closes one residual write path (R7) the contract review found; the Round 3 section at the foot carries its head and evidence. ## What changed - **`packages/plugin-form/src/LineItemsPanel.tsx`** (+67 / −4 against base). The panel now records which parent its held `rows` / `original` belong to: one `useState`, `rowsHeldFor`, written wherever the rows are written — beside a load's commit (`setRowsHeldFor(parentId)` next to `setRows` / `setOriginal` / `setDirty(false)`), inside the CURRENT load's `catch` that fails while nothing is held (an updater that keeps a value already held and otherwise adopts `parentId`, inside `isCurrent()`), and in `onChange` for an edit made before any load has settled (the same keep-or-adopt updater, `parentId` narrowed to a string first). One predicate, `heldForAnotherParent = rowsHeldFor !== undefined && rowsHeldFor !== parentId`, is read at three sites: the Save button's `disabled`, the first statement of `save` after its existing `dataSource` / `parentId` / `childObject` guard (it returns before `setSaving`, before any batch is built), and the render, where a placeholder (`data-testid="line-items-held-for-another-parent"`) stands in for the grid ahead of `LineItemsField`. `save`'s and `onChange`'s dependency lists gain what they read (`heldForAnotherParent`, `parentId`). - Unchanged: the rows commit's three existing statements (`setRows` / `setOriginal` / `setDirty(false)`) and the `loading` release (objectui#10712's PR 2 surface), the `loadSeqRef` / `isCurrent()` error rule from PR objectui#10738, `save`'s two writes to `error`, every other dependency list, every hook call, prop, export and registration. The commit block gained the `setRowsHeldFor(parentId)` line and the `catch` was re-braced to add the adoption; the three commit statements themselves did not move. - **`packages/plugin-form/src/LineItemsPanel.heldForAnotherParent-10740.test.tsx`** (new, +389 / −0): nine rows through the real `SchemaRenderer` and `record:line_items`, every `find` and `batchTransaction` a deferred promise settled by hand. - **`.changeset/10740-line-items-held-for-another-parent.md`** (new, +24 / −0, `'@object-ui/plugin-form': patch`). ## H1 — the reproduction on base `5ad3b88861`, and its controls A throwaway probe (deleted, never committed) through the real `SchemaRenderer` and `record:line_items`, ids explicit. The column is `label` (aria-label `Line`), the child object `po_line`, the relationship field `po`. Readings at base, then at the current head `854e0922cc` (whose only change since `ef16b93486` is prose: the readings are the pin's, re-run green there): | sequence | base `5ad3b88861` | head `854e0922cc` | |:--|:--|:--| | `p1` answers `l1`, edited to `p1 line edited`; the host re-renders with `parentId: 'p2'`; `p2`'s load rejects `p2 load failed`; then Save | banner `p2 load failed`; drawn lines `['p1 line edited']`, inputs enabled; **Save enabled**; Save ⇒ `batchTransaction` ops `[{object: 'po_line', action: 'update', id: 'l1', data: {label: 'p1 line edited', po: 'p2'}}]` (`find` 2 calls, `batchTransaction` 1) | banner `p2 load failed`; drawn lines `[]`, no grid, the placeholder drawn; **Save disabled**; no batch (`find` 2, `batchTransaction` 0) | | **R7** (the round-3 route): mount `p1` with NO adapter (`load` returns at `!dataSource`, the grid is drawn, `rowsHeldFor` stays undefined); type `typed under p1`; the host moves to `p2`, still no adapter; the adapter arrives and `p2`'s load fails; then Save | grid drawn with `['typed under p1']`, Save enabled; Save ⇒ `[{object: 'po_line', action: 'create', data: {label: 'typed under p1', po: 'p2'}}]` — a p1-typed line created under p2 | placeholder drawn, `[]`, **Save disabled**, no batch | | lit control: the first row, but `p2`'s load answers `l2` `p2 line` | drawn `['p2 line']`, no banner, Save disabled (nothing dirty); edit `l2` + Save ⇒ `[{object: 'po_line', action: 'update', id: 'l2', data: {label: 'p2 line edited', po: 'p2'}}]` | identical | | same-parent failed RE-load: `p1`'s edit held, a host re-render with `limit: 200` re-runs `p1`'s load, which rejects `p1 reload failed` | banner; drawn `['p1 line edited']`, inputs enabled; Save enabled; Save ⇒ `[{object: 'po_line', action: 'update', id: 'l1', data: {label: 'p1 line edited', po: 'p1'}}]` | identical (the edits are kept under their own parent) | | the DECLINE path: `p1`'s edit held, a re-render with `parentId: 'p2'` and a refused filter (`{note: {$regex: 'a.c'}}`); the load declines before any fetch | malformed-filter notice drawn, no grid; **Save enabled** over the held (off-screen) rows; Save ⇒ `[{object: 'po_line', action: 'update', id: 'l1', data: {label: 'p1 line edited', po: 'p2'}}]` (`find` 1 call: the load never ran) | Save disabled; no batch | The first row is the card's reading, op for op. R7 and the DECLINE row are two routes the card did not name: R7 is a line typed under a grid drawn with no adapter, adopted by the first parent whose load later fails; the DECLINE row reaches the guard through the load declining before its `catch`. Both are closed by binding the owner to the rows wherever the rows are written. ## H2 — the shape: refuse by `rowsHeldFor`; nothing is cleared The triage offered two shapes, "clear `rows` / `original`" or "refuse Save". This PR refuses, on a value written wherever the rows are written, and clears nothing. Why, measured: 1. **The decline path.** The DECLINE row above: a swap to `p2` with a refused filter (and, by the same code path, a lost `dataSource`) declines the load before its `try`, so a clear in the `catch` leaves `p1`'s held rows saveable under `p2`. The guard reads the rows' own parent and closes this path too (pinned; red at base). 2. **One writer of the owner per writer of the rows.** `rowsHeldFor` moves where `rows` move (the load's commit, `onChange`) and once more where a CURRENT load's failure leaves the empty rows on screen, so the two cannot disagree. This also covers the one-render window between the host re-render that moves `parentId` and the effect that starts the new load: `rows` are still `p1`'s, `rowsHeldFor` still says `p1`, Save is off in that frame too. R7 is the case where this failed at the round-2 head: `onChange` moved the rows without moving `rowsHeldFor` while it was unset, so a line typed under an adapter-less mount stayed ownerless until the next failure adopted it. `onChange` now writes the owner too. 3. **The same-parent rule.** A transient failure on a refresh must not discard the author's unsaved edits (the RE-load row, and the "RE-load of the SAME parent" pin). Held rows are never cleared, so a swap `p1` → `p2` (failed) → `p1` (failed) draws `p1`'s held edit again under `p1` (pinned as a control, "a swap BACK to p1"). A clear on the `p2` failure would have discarded that edit. 4. **Adoption before a settle.** With nothing held yet, the parent on screen is adopted the moment the rows first take a value: an edit made in an unsettled grid (`onChange`, R7's fix), or the empty rows a CURRENT load's failure leaves on screen (the `catch`, inside `isCurrent()`). Either way a line the panel then holds belongs to the parent it was entered under, not to a parent the panel later moves to. Measured at base: a first load for `p1` fails, a line is added, the panel moves to `p2` whose load fails ⇒ the added line is drawn under `p2` with Save enabled (the defect, as a `create`). Pinned; red at base. A superseded failure adopts nothing (the `catch` is inside `isCurrent()`). 5. **What the refused state draws.** After a failed load, the failure's banner (the adapter's message, as before) and a muted placeholder where the grid would be; after a refused-filter decline, that filter's own notice, as before (only Save moves on that route); after a lost-adapter decline, the placeholder alone. No line, editable or otherwise. A first load that fails with nothing held is unchanged: banner plus the empty grid with its entry row (pinned as a control). A load for the current parent that commits takes the grid back (pinned). The function-level guard in `save` is the write contract's own check, on the function every caller reaches, in the same spirit as the `childObject` guard beside it. It is redundant on the UI path by design: the button and the function read the same render's predicate, so no click through the rendered UI reaches `save` while the button is off. See A2 below. ## H3 — every path from the held rows to a write 1. **`save`** → `buildMasterDetailEditBatch(parentObject ?? '', parentId, parentPatch, [{rows, original, childSchema, …}])` → `runBatchTransaction`. Guarded at both readers: the button's `disabled` and the function's first statement. The rollup `sumRows(rows, …)` is computed inside the same function, behind the same guard. 2. **`onChange`** → `setRows(next)`, `setDirty(true)`, and the owner adoption. While the grid is drawn, `rowsHeldFor` is the current parent — set by the load that offered the grid, or, when the grid was offered before any load settled, by this edit itself — so an edit belongs to the parent on screen, never to a parent the panel later moves to. In the refused state the grid is not drawn (ablation A3 re-opens exactly this and the "no p1 line is drawn" rows go red). 3. **The load's commit** → `setRows` / `setOriginal` / `setDirty(false)` / `setRowsHeldFor(parentId)`: the writer that defines whose the rows are on a successful load. 4. **`LineItemsField` (`GridField` in `@object-ui/fields`)**: `grep -c useEffect` on `GridField.tsx` at this base = 0; every `emit` / `onChange` call sits in a user handler (`setCellValue`, `addRow`, the remove handler, the reorder handler), all routed through the panel's `onChange` in (2). Nothing changes rows without user input, the reading PR objectui#10650 made and this PR re-took. 5. **No values bubble up.** The panel takes `schema` only; the `record:line_items` registration (`LineItemsPanelRenderer` inside `ElementDataSourceGate`) passes nothing else, and the post-save `load()` reads no rows. No `forwardRef` / `useImperativeHandle` / key handler; `save` is referenced once, at `onClick={save}`. So there is one write path (`save`), guarded twice, and one edit path (`onChange`), which brands every row it writes with the parent on screen. **R7 is closed:** a row can no longer reach a write bearing a parent it was not entered under, whether it was typed before a load settled or held from a previous parent — in the refused state the grid is not drawn, and in every drawn state the owner is unset or equals `parentId`, and the first edit sets it to `parentId`. ## Pins and ablations `LineItemsPanel.heldForAnotherParent-10740.test.tsx`, nine rows. Base and ablation legs ran on COMMITTED source (`ef16b93486`, panel blob `5884928c7a`) under `os-verify-lock`; the one later commit, `854e0922cc`, moves prose only (no code token, proof in the Round 3 section), and the pin is re-run green there. Each mutation went through objectstack's `scripts/ablation-replace.mjs` in WRAP mode (anchor declared to hit once, counts and blob hashes read back from disk, restore spelled `git checkout HEAD --` on the absolute path and proven by blob == HEAD and an empty `git diff HEAD`). Two whole-file base legs, each swapped in by hash under a shell `trap` and restored the same way: the card base blob `0c498ce007` (`held ?? parentId` occurrences 0), and the round-2 head blob `47696a5819` (occurrences 1 — the fix before R7 was closed). No build is on the resolution path: the pin imports `./index` relatively and every `@object-ui/*` specifier resolves to `src` through the root vitest alias, so `ablation-dist-preflight` does not apply. | row | base `0c498ce007` | round-2 head `47696a5819` | head `854e0922cc` | red under | |:--|:--|:--|:--|:--| | p1 edited, swap to p2 whose load FAILS: no p1 line drawn, Save off, no batch | **red** | green | green | A1, A3 | | the same through a load that DECLINES (refused filter): Save off, nothing sent | **red** | green | green | A1 | | a line added under a FIRST load that failed, then a swap to p2 whose load fails: not drawn under p2, Save off | **red** | green | green | A1, A3 | | **R7**: a line typed under an ADAPTER-LESS mount on p1, a swap to p2 with no adapter, then the adapter arrives and p2's load fails: no line drawn under p2, Save off, no batch | **red** | **red** | green | A1, A3, A6 | | control: a swap to p2 whose load SUCCEEDS replaces p1's lines; a save after editing p2's line carries `l2` under `p2` and nothing of `l1` | green | green | green | A4 | | control: a RE-load of the SAME parent that fails keeps the held edit drawn, editable, Save on; the save carries `l1` under `p1` | green | green | green | — | | after a refused state, a load for the CURRENT parent that commits takes the grid back | **red** | green | green | A3, A4 | | control: a swap BACK to p1 whose load fails again draws p1's held edit again | green | green | green | — | | control: a FIRST load that fails draws the banner and the empty grid, as before | green | green | green | — | - **Base leg, card base blob `0c498ce007`**: `Failed Tests 5` (the four refusal rows plus the recovery row), 4 green. Restore proven (blob == HEAD `5884928c7a`, `git diff HEAD` empty). - **Base leg, round-2 head blob `47696a5819`**: `Failed Tests 1` — exactly the R7 row. This is the one the round-3 close fixes; every other row was already green at the round-2 head. Restore proven. - **A1**, the button's `|| heldForAnotherParent` removed: 4 failed (the three original "Save off" rows and R7). - **A2**, the `save` guard neutralised (`if (false && heldForAnotherParent) return;`): **0 failed, 9 passed.** As under H2: with the button guard in place no UI click reaches `save`, so this hunk is observable only together with A1. Kept as the write contract's own guard, not claimed as pinned. - **A3**, the render branch made unreachable (`false && heldForAnotherParent ?`): 4 failed (the two "not drawn" rows, R7 and the recovery row). - **A4**, `setRowsHeldFor(parentId)` deleted from the load commit: 2 failed — the "swap to p2 SUCCEEDS" control and the recovery row, both at `expected [] to deeply equal ['p2 line']`: with the commit no longer branding the rows, `rowsHeldFor` stays at the value the p1 edit adopted, so p2's own freshly loaded lines are refused behind the placeholder. - **A5**, the `catch` failure-adoption deleted: **0 failed, 9 passed.** Since Round 3, `onChange` also adopts, so every row whose owner the `catch` used to set now has it set by the typed edit (or is an empty grid, never saveable). The `catch` adoption is retained as the documented adopt-on-failure behavior; like A2 it is kept and not independently pinned. (At the round-2 head, before `onChange` adopted, A5 reddened the "line added under a FIRST load that failed" row.) Its one remaining observable effect is the B5 Acceptance note below. - **A6**, the `onChange` owner adoption deleted (`setRowsHeldFor((held) => held ?? owner);`): **1 failed — exactly the R7 row.** This is the ablation of the round-3 close. - Each ablation leg printed `ok mutation landed: anchor 1 to 0` with the blob change, and `ok restored: blob == HEAD (5884928…) and git diff HEAD is empty`; the tree was clean before and after (`git status --short` empty). ## Gates at head `854e0922cc` Locked runs (`os-verify-lock`, `VERDICT command-exit 0` on each): - `turbo run build --filter='@object-ui/plugin-form^...' --concurrency=2`: `Tasks: 11 successful, 11 total`. - `pnpm --filter @object-ui/plugin-form run type-check`: exit 0 with `tsc --noEmit && tsc -p tsconfig.test.json` echoed; `--listFilesOnly` counts the pin once in the `tsconfig.test.json` program and `LineItemsPanel.tsx` once in the build program. - `pnpm --filter @object-ui/plugin-form build`: exit 0, `[unplugin:dts] Declaration files built`, `dist/index.js` emitted — the dts plugin fails on any type error and did not. The three built artifacts are byte-identical before and after the wording commit: `dist/index.js` sha256 `9e08b5589db5…`, `dist/index.d.ts` `08fe75fa41b4…`, `dist/index.umd.cjs` `19fddff1b34f…`, built at `ef16b93486` and again at `854e0922cc`. - the pin: `Tests 9 passed (9)`. The seven existing `LineItemsPanel*` pins plus `recordFormsErrorClears-10682.test.tsx` at `ef16b93486`: `Test Files 8 passed (8)`, `Tests 102 passed (102)` (the later commit moves no code token). - the whole package, `pnpm exec vitest run --maxWorkers=2 packages/plugin-form/`, at `7352890ae4` (round 1): `Test Files 131 passed (131)`, `Tests 1507 passed | 1 skipped (1508)`. The later commits touch only `LineItemsPanel.tsx`, its pin and the changeset, covered by the pin and sibling runs above. Light gates, each exit 0 at `854e0922cc`: `check:new-line-citations` (`VERDICT new-cross-file-line-citations: 0 new citation(s)`), `check:control-bytes`; and at `ef16b93486` (no source token moved since): `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:shell-escape-residue`, `check:i18n-keys`, `check:handler-key-reads`, `check:element-data-source-declaration`, `check:self-import`, `check:unreferenced-sources`, `type-check:coverage`, `lint:coverage`, `check:phantom-deps`, `check:published-tsconfig-exclude`, `check:metadata-write-doors`. Changeset gates at `854e0922cc`: `check-changeset-presence` (`2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)`), `check-changeset-claims` (`No pending changeset names a file this change touches`), `check-changeset-overwrite` (`No pre-existing changeset was modified or deleted`), `check-pending-changeset-literals`; `check-changeset-no-major` and `check-changeset-fixed` at `ef16b93486`. `check-governed-merges.mjs --test` over the three paths: NOT governed (0 of 3 paths hit the register). **eslint, narrowed to the 2 changed ts/tsx files** (`eslint --no-inline-config --format json`): exit 0, the JSON counts 2 files, 0 errors. Narrowing evidence: the population is `eslint.config.js`'s `files: ['**/*.{ts,tsx}']`; `--print-config` on `LineItemsPanel.tsx` shows `parserOptions: {}` and no `project` / `projectService`, so no rule is type-aware and this diff cannot move the verdict of a file it does not touch. `LineItemsPanel.tsx` carries 18 warnings, the same count and rule multiset as its base blob (14 `no-explicit-any`, 2 `set-state-in-effect`, 2 `exhaustive-deps`); the pin carries 12 `no-explicit-any`. **NOT MEASURED locally, declared to CI**: the repo-wide `pnpm lint` and the rest of the farm. ## Serial At claim time no open PR touched `LineItemsPanel.tsx` (claim `5852470959`). Before the round-1 pushes `origin/main` was `01700dcd7d` (four commits past the base); before the round-4 push it is `704e05b09d`, nine commits past the base. `git merge-tree --write-tree` against it exits 0 with a clean tree (`be8cdeefbd`); at the round-5 review `origin/main` is `f308a655b8` (objectstack-ai#10708, eleven commits past the base), against which the tree is `01802fbba7`, also clean. None of the eleven touches a file this branch touches (objectstack-ai#10708 touched `skills/objectui/` only); objectstack-ai#10752 (`704e05b09d`) touched `packages/plugin-form` elsewhere (six form arms, `recordInvalidation.ts`, `formArmsInvalidation-10715.test.tsx`, and the header comment of the sibling pin `recordFormsErrorClears-10682.test.tsx`), and objectstack-ai#10749 moved `pnpm-lock.yaml`. objectui#10712's PR 2 is serial behind this PR, as the claim says. ## H5 — pending changesets `git grep` over `.changeset/` for `LineItemsPanel`, `line-items`, `line items`: 14 pending files. The sentences that describe this panel's load, rows or save were re-read against this change: `10682-form-error-clears-2.md` (only the current load writes the banner; a superseded load neither raises nor clears it — still true), `10631-master-detail-save-in-flight.md` (the panel's grid is disabled while its own save is in flight — still true), `6194-line-items-row-fetch-decline.md` (the `childObject` declines and `save`'s guard — still true), `6188`, `9050`, `9925`, `10163`, `8221`, `9333` (each about another aspect of the panel — untouched). None describes what a failed or declined load for another parent leaves on screen, so no sentence is falsified and no dated note is needed; the new changeset declares this change. ## objectui#10712, measured, not widened The same probe took the two sequences objectui#10712's release comment carries for its PR 2, at base and at this head: - a late `p1` answer landing after `p2` committed: base draws `['p1 line']` under `p2`; head draws the placeholder (`rowsHeldFor` moved to `p1` with the superseded rows, so they are refused), Save off. The current answer, `p2`'s lines, is not kept: the run guard on the rows commit is still that PR's to add. - a save started on `p1` that lands after the swap to `p2`: its captured `load` issues `find` with `po: 'p1'`; base draws `['p1 line edited']` under `p2`; head draws the placeholder. The same partial mitigation, the same remaining item. objectui#10712 remains open; nothing here is scoped to it. ## Round 3 The round-2 head `11bf099f11` FAILED the contract review (`5852845813`) on R7 — a line typed into a grid drawn with no adapter, adopted by the first parent whose load later failed, then saved under it — and on four wording items. Every other round-2 claim was reproduced by the review. - **Round-3 range `11bf099f11..ef16b93`**, name-status / numstat: `M packages/plugin-form/src/LineItemsPanel.tsx` (+34 / −14), `M packages/plugin-form/src/LineItemsPanel.heldForAnotherParent-10740.test.tsx` (+42 / −0), `M .changeset/10740-line-items-held-for-another-parent.md` (+11 / −10). Two commits: `23f6bbc0` closed R7 in `onChange` and fixed the wording; `ef16b93486` gave the `onChange` owner updater a type-safe spelling (see Deviations). - **The close.** `onChange` now writes the rows' owner beside the rows when nothing is held yet — the seat's ruling permits the `onChange` form as "strictly safer" because it covers any way a grid can be offered before a load settles (the adapter-less decline, and a superseded run releasing `loading`, objectui#10712's surface), not only the two the ruling enumerated. `parentId` is narrowed in an `if (parentId)` block before the updater closes over it. - **The pin.** The new R7 row is red on the card base blob AND on the round-2 head blob `47696a5819`, green at head, and reddened by A6 (the `onChange` owner adoption deleted). Table above. - **Round-3 wording follow-up, `854e0922cc`** (prose and comment only; the round-3 contract review `5853196779` passed the code and failed the published text). One commit, `M .changeset/10740-line-items-held-for-another-parent.md` (+3 / −2), `M packages/plugin-form/src/LineItemsPanel.tsx` (+4 / −4). The changeset parenthetical now reads "(after a failed load a placeholder stands where the grid would be, under the failure's banner; after a refused-filter decline that filter's own notice stands there, as before; after a lost-adapter decline the placeholder stands alone)"; its frontmatter is byte-identical (md5 of the three frontmatter lines `21fc281303c02040481bc4b368c13334` before and after). The `onChange` comment's "The only grid offered before a load has settled…" became "The grid this was found on is the one drawn when the load declined for want of an adapter", which stays true against the superseded-release window (the empty grid drawn while a later read is pending). No code token moved: the commit's `.tsx` hunk has zero non-comment lines (`git diff ef16b93 854e092 -- packages/plugin-form/src/LineItemsPanel.tsx`, filtered to changed lines that do not start with `//`, is empty); type-check and the dts build exit 0; and the three `dist/` artifacts hash-equal before and after (Gates above). - **Gates.** The plugin-form dependency-closure build, `type-check` (both `tsc` programs) and `build` (the dts plugin) all exit 0 at `854e0922cc`; the pin (9) is green there; the seven sibling pins plus `recordFormsErrorClears-10682` (8 files / 102) and the light gates are green at `ef16b93486`; the changeset gates, `check:control-bytes` and `check:new-line-citations` are green at `854e0922cc`, listed above. ## Acceptance notes - **The function-level guard is unpinned.** A2 moves no row, for the structural reason under H2 and in the pin's header. Kept as the write contract's own check; dropping it is a one-line change if the seat prefers pins-only code. - **The `catch` failure-adoption is unpinned since Round 3.** A5 now moves no row: the `onChange` adoption covers the same typed-line rows, and an empty grid left by a failed load is never saveable. Kept as the documented adopt-on-failure behavior, parallel to A2. - **B5.** After a FIRST load that fails with nothing typed, a swap to another parent whose load also fails draws the placeholder, not the empty grid (base drew the empty editable grid); the catch adoption's one observable effect now that `onChange` adopts. Unpinned. - **The refused state's placeholder** is hard-coded English, like this panel's other placeholders (`Loading…`, `Save the record first to add line items.`, the `childObject` hint); only the malformed-filter state goes through `createSafeTranslation`. Observation, not filed. - **A superseded save's failure** is still written unguarded (PR objectui#10738's note); not re-measured here. - **A first load that fails** still draws an empty editable grid under the banner, as before this card; a line typed there now belongs to that parent and stays with it (H2 item 4). - No class (a), (b) or (c) finding beyond the objectui#10712 rows its release comment already carries. ## Deviations - **Attribution.** Commit trailers are the dispatch's model-free pair, and this body ends with the role file's session-URL footer. The harness attribution reminder asked for other lines. - **The seat's prescribed close needed a type-safe spelling.** The ruling's line `setRowsHeldFor((held) => held ?? parentId)` (and equivalently the `onChange` form) does not type-check: in `onChange`, `parentId` is `string | null | undefined` (`RecordContextValue.recordId` is `string | null | undefined`) with no `load` guard above it to narrow it, and the setter's state is `string | undefined`, so the updater's `held ?? parentId` widens to include `null` — CI job 108556174339 red at `src/LineItemsPanel.tsx` `error TS2345` on commit `23f6bbc0`, breaking the plugin-form build and the six gates behind it. Fixed in `ef16b93486` by capturing the narrowed value: `if (parentId) { const owner = parentId; setRowsHeldFor((held) => held ?? owner); }`. Semantics unchanged (a grid is only offered with a parent bound). The `catch`'s copy of the line type-checks because `load`'s early `if (!dataSource || !parentId) return;` narrows `parentId` to a string before it. - **The ablation legs run from inside the worktree**, because `ablation-replace` resolves the repository from its cwd; run through the lock wrapper's default cwd it exits 2 at its own prerequisite (nothing written, nothing measured). - **A removed pin row (Round 1).** The pin's first commit carried a "defence in depth" row that cleared the Save button's `disabled` on the DOM node and clicked. React filters a click by the fiber's `disabled` prop, not the DOM property, so the click never reached `save` and the row passed vacuously (A2 showed it). It was removed in `11bf099f11` rather than kept as a false pin; the pin header states what it would have pinned and why nothing can. - **Round-4 body corrections (published text only).** The round-3 review found four false evidence statements in the returned body and two overstatements; all are replaced here with the record's text: the pin table's "red under" column (A4 reddens only the p2-SUCCEEDS control and the recovery row), the A4 bullet's cause, the Serial paragraph (main re-read before the push, the merge-tree re-run at the head), the round numbering (`11bf099f11` was round 2; `23f6bbc0` and `ef16b93486` are round 3), H3's closing and H2 item 2. --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ding it hides (objectui#10746) (objectstack-ai#10762) Fixes objectstack-ai#10746 Clause-②: no — a Studio inspector's patch on a type switch; no declared surface moves ## What changed `packages/app-shell/src/views/metadata-admin/inspectors/ReportDefaultInspector.tsx`: the Report type picker's commit is now `commitType`. When the picked type is `joined`, the patch that commits `type: 'joined'` also carries `dataset`, `values`, `rows`, `columns`, `chart` and `order` as `undefined`-valued keys — only those the draft actually holds (`draft[key] !== undefined`). Any other type commits `{ type }` alone, as before. The list lives in `JOINED_CONTAINER_CLEARED_KEYS` with the reasoning beside it; `blocks`, `runtimeFilter` and `drilldown` are never named. `order` is a sixth key, beyond the card's five, under the role file's bounded in-place exemption (same defect class, mechanical, same file, same gate family, no other claim on the file): the spec's own `reportForm` hides `order` in the same "Dataset binding" section (`visibleWhen: "data.type != 'joined'"`), and the INSTALLED spec already refuses a container `order` on a joined report ("a `joined` report orders per block — move `order` onto `blocks[]`"), so the same invisible-key save refusal exists today for `order`. Evidence in H3 below. New pin file `ReportDefaultInspector.joinedClearsBinding-10746.test.tsx` (8 tests); changeset `.changeset/10746-joined-report-clears-binding.md` (`'@object-ui/app-shell': patch`). ## H1 — reproduction on `origin/main` (`1422a920ed`) The pin file run against the untouched component: `Tests 4 failed | 4 passed (8)`. The defect pin quotes the only `onPatch` argument the type picker commits today: `AssertionError: expected { type: 'joined' } to strictly equal { type: 'joined', …(6) }` — the commit is `{ type: 'joined' }` and nothing else. The 4 pins green on base are the boundary/control pins, which pin what must NOT change. ## H2 — the clear mechanism `onPatch` is a shallow patch in all three hosts: `ResourceEditPage` applies it as `handleDraftChange((d) => ({ ...d, ...patch }))`, `ReportConfigPanel.handlePatch` as `{ ...draftRef.current, ...patch }`, and `StudioDesignSurface.onPatch` as `setDraft((d) => ({ ...d, ...patch }))`; each saves through `client.save`, which is `JSON.stringify`. None offers a delete sentinel, so the one way to drop a key is an `undefined`-valued key — the spelling this inspector's own `commitChart` (`chart: next.type ? next : undefined`) and the siblings (`ActionDefaultInspector`, `ObjectDefaultInspector`, `DatasetDefaultInspector`'s "Clear all", pinned by its objectui#9372 suite) already use. Measured in the second pin: after the spread the key is an OWN property holding `undefined` (`Object.hasOwn` true, value `undefined` — not `null`, not an empty string), and `JSON.parse(JSON.stringify(committed))` — the shape `client.save` puts on the wire and the spec parses — has no such property. The spec's refinement itself skips `undefined` (`if (value === undefined || …) continue;` in the joined arm), so even the in-memory draft would not be refused. - Reach: The clear holds for the patch and for the save that follows it. The metadata-admin editor rebuilds its draft as the served draft spread over `layered.effective` (on load, after each save and after publish), and `effective` is the published layer, so a report whose PUBLISHED version was bound gets those keys back in the draft after the first draft save until it is published, and a report already saved `joined` with stale keys is not repaired on load. Both are the host's draft-over-baseline merge, objectui#10765. ## H3 — the spec check Installed `@objectstack/spec` is 17.4.0 (`packages/app-shell/node_modules/@objectstack/spec`). It PREDATES objectstack-ai/objectstack#20160: `grep 'selects per block'` over its `dist/` gives 0 hits; the control `grep 'orders per block'` gives 3. One-off probe with the installed `ReportSchema.safeParse` (not committed): - joined + one block: success - joined + stale `dataset` / `values` / `rows` / `columns` / `chart`: **success** — 17.4.0 accepts them - joined + stale `order`: refused, `custom` at `['order']`, "a `joined` report orders per block — move `order` onto `blocks[]`." - joined + `blocks: []`: refused at `['blocks']` - joined + `runtimeFilter` + `drilldown: false`: success - joined + `dataset: undefined` as an own key: success So the pins' parse leg measures the `order` half with the installed spec (before the fix: refused at `['order']`; after: parses), and for the four selection keys the pins assert ABSENCE and cite the rule read at objectstack `origin/main` `packages/spec/src/ui/report.zod.ts`: `JOINED_CONTAINER_SELECTION_KEYS = ['dataset', 'rows', 'columns', 'values']`, one `custom` issue per present key at `path: [key]`, message "a `joined` report selects per block — move `KEY` onto `blocks[]`, or delete it; on the container it selects nothing." (`KEY` stands for the key's own name.) No checkout on this box holds a built spec `dist` carrying that refusal, and building one was outside this card. ## H4 — edges - `joined` → non-joined: nothing is restored; the patch is `{ type }` alone and the author re-binds (pinned). - non-joined → non-joined: the binding is kept; the patch is `{ type }` alone (control, pinned). - `joined` with `blocks[]` present: untouched, same array reference, in both directions (pinned). - Undo: there is no undo stack behind the metadata draft. `ResourceEditPage`'s `Undo2` button is `doDiscardDraft` (ADR-0034: discard the whole pending draft), not a per-edit undo, and `handleDraftChange` keeps no history. One undo cannot restore the keys; switching the type back does not either. - `runtimeFilter` and `drilldown` survive the switch (pinned): the joined branch reads both. - Partially bound report: the patch names only the keys present (pinned), so `ReportConfigPanel.onFieldChange` sees no phantom clears, and an unbound report's switch stays `{ type: 'joined' }` (pinned). ## Pins and ablations Head `3f53c15776`, pin file plus the two existing `ReportDefaultInspector` suites: `Test Files 3 passed (3) · Tests 37 passed (37)`. Red on base (`1422a920ed`, component untouched, pin file present): `4 failed | 4 passed` — the defect pin, the own-key/serialised pin, the parse leg, the partially-bound pin. The other 4 are controls and boundaries, green on base by construction. Per-hunk ablations on the committed head, each through objectstack's `scripts/ablation-replace.mjs` (anchor must hit exactly once; the blob change is verified on disk; restore proven `blob == HEAD` `a9cb7e9c` and `git diff HEAD` empty), the prediction written before each run, observed direction = predicted: - M1, drop the `joined` guard (`if (nextType === 'joined')` becomes `if (typeof nextType === 'string')`): predicted CONTROL F red; observed `1 failed | 7 passed` (CONTROL: non-joined → non-joined keeps the binding). - M2, drop the presence filter (`if (draft[key] !== undefined) patch[key] = undefined;` becomes unconditional): predicted D and E red; observed `2 failed | 6 passed` (names only present keys; unbound one-key patch). The first attempt was a NO-OP the tool refused — the replacement text was a substring of the anchor, so its on-disk count could not rise (`1 to 1, a rise of 0`) — and it restored; the leg was re-run with a distinct replacement. - M3, add `blocks` to the cleared list: predicted C and G red; observed `2 failed | 6 passed` (parse leg: the joined report loses its blocks; CONTROL: blocks untouched). - M4, drop `order` from the list: predicted A, B and C red; observed `3 failed | 5 passed`. Assertion spelling: every patch is read with `toStrictEqual`, because `toHaveBeenCalledWith` / `toEqual` treat an `undefined`-valued key as absent and would read the defect and the fix alike. Round 2 (`d4945789f6`, the contract-review wording round: the changeset's reach sentence and the pin file's three-host comment; no source line changed — the diff's only non-changeset lines are the comment lines shown by `git diff -U0`, and `ReportDefaultInspector.tsx` is untouched): pin file re-run `Tests 8 passed (8)`. ## Gates (local, derived by hand from `package.json` and the workflows — objectui has no `dispatch-gates.mjs`) - ① closure build `turbo run build --filter='@object-ui/app-shell^...' --concurrency=2`: `Tasks: 28 successful, 28 total`, 2m12s, under the verify lock. - ② `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`): exit 0; `tsc -p tsconfig.test.json --listFiles` lists the pin file (1 hit among 4673 files). - ② vitest, the three files above: 37 passed; round 2 pin file: 8 passed. - `node scripts/check-changeset-presence.mjs`: ✅ (round 2: 2 source files of 1 released package changed, 1 changeset declared). `node scripts/check-changeset-no-major.mjs`: ✅. `pnpm check:changeset-claims`: ✅. `pnpm check:pending-changeset-literals`: ✅. - `pnpm check:control-bytes`: ✅ OK (round 2: 8942 tracked text files). The role file's control-byte grep over the changed files: no match. - `pnpm check:new-line-citations`: `0 new citation(s)` (both rounds). - `pnpm check:test-path-roots`: ✅ OK. `pnpm check:vi-mock-specifiers`: ✅ OK. - eslint with the package config on the two touched `.tsx` files: exit 0. - NOT MEASURED locally, CI-owned: `pnpm lint` (repo-wide), the 8 test shards, `Build & E2E`, `check:i18n-*` (no `t()` key was added or changed). Round 1 CI on `3f53c157`: 40 success, 3 skipped, 0 failed per the contract review. ## Serial Round 2: `origin/main` at `25c7d584e4` (+5 commits over BASE `1422a920ed`: objectstack-ai#10752, objectstack-ai#10753, objectstack-ai#10708, objectstack-ai#10751, objectstack-ai#10761); none touches the three files; `git merge-tree --write-tree` exits 0 (clean, tree `738f2bf534`). No merge commit was needed. ## Acceptance notes - `chart` is cleared per the triage direction and objectstack-ai/objectstack#20161 (inert on a joined container); the installed spec does not refuse it, so its clearing is pinned by absence only. - Deviation: the worktree-creating `git fetch origin main` ran against the shared checkout (the role-file recipe) rather than inside the worktree as the order asked; `origin/main` did not move (`1422a920ed` before and after), only the shared checkout's `FETCH_HEAD` was touched. Every later fetch used a private ref inside the worktree. - Observation, not filed: `ReportConfigPanel` hosts this inspector too, merges with the same spread and emits `onFieldChange(key, undefined, next)` once per cleared key — the documented "field changed" signal, correct for a live preview. - The draft-over-baseline merge that re-introduces a published binding after the first draft save is objectui#10765 (filed by the seat), not addressed here. --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #7599
Clause-②: no — prose in the published skills catalog; no declared surface moves
What changed
Five places in four files under
skills/objectui/. Nothing else changed.rules/protocol.md, the intro sentence of "Rule: Layout Responsiveness": the list of object keys now includes2xl.rules/protocol.md, the paragraph that opens with a bold2xlsentence. It was rewritten whole.evals/protocol.json, eval id 3:expected_outputwas replaced.must_containis unchanged (columns,node,xl,className), and every one of those tokens is still taught.guides/mobile.md, the cross-reference sentence after the KPI grid example. It was replaced.SKILL.md, "5. Layout as components": the parenthesis that points atrules/protocol.mdwas replaced. The file stays at 137 lines.xs/sm/md/lg/xlxs/sm/md/lg/xl/2xlBreakpointColumnMapSchema"BreakpointNamein@object-ui/typesis the key set ofGridSchema.columns;BreakpointColumnMapin@object-ui/layout(responsive-grid) spells the same sixgridreads{xs:1, "2xl":6}on maingrid grid-cols-1 gap-4grid grid-cols-1 2xl:grid-cols-6 gap-4{xs:1, xl:5}grid grid-cols-1 xl:grid-cols-5 gap-4xl, or carry the widest step inclassName", unconditional@object-ui/componentsrelease that predates objectui#7097, where2xlis accepted and silently dropped (grid grid-cols-1 gap-4)expected_outputxlpropsenvelope,cols); rewrite usescolumnson the node withxs,mdand2xl; the pre-objectui#7097 qualifierguides/mobile.mdcross-reference2xlentry the spec accepts and the renderer drops"gridrenderer reads, the2xlstep that an@object-ui/componentsrelease predating objectui#7097 accepts and silently drops"SKILL.mdsection 5 parenthesis2xl"Why the behaviour half changed
The triage (comment 5818990275) and boundary 1 in comment 5549222679 said to keep "
2xlis accepted and then dropped" and change only the attribution, after re-measuring on main. I re-measured on base1e946c96a, and that sentence is no longer true on main. After this card was filed, objectui#7097 (PR #8435) added a sixth read arm and theGRID_COLS_2XLclass map togrid.tsx. Its pin,grid-breakpoint-columns-7097.test.tsx, passes on base (11/11). The pin covers both rows the paragraph quotes:{ xs: 1, '2xl': 6 }givesgrid grid-cols-1 2xl:grid-cols-6 gap-4, and{ xs: 1, xl: 5 }givesgrid grid-cols-1 xl:grid-cols-5 gap-4.The old sentence is still true for readers on npm. The catalog is served from main by
npx skills add objectstack-ai/objectui, so this paragraph goes live at squash. Readers, though, run@object-ui/componentsfrom npm, and the latest published version is 17.6.0. Theregridstill drops2xl: the review measured{xs:1,"2xl":6}on the 17.6.0 tarball and gotgrid grid-cols-1 gap-4. So the paragraph states main's behaviour and keeps the old advice for a release that predates objectui#7097.The bundle's two other cross-references now point at the same two-release reading:
guides/mobile.md, andSKILL.mdsection 5. A bundle-wide grep in the round-2 review (2xl, thendrops) found them.Measurements
BreakpointColumnMap.export interface BreakpointColumnMapis declared inpackages/layout/src/ResponsiveGrid.tsx.@object-ui/layout's index publishes it throughexport * from './ResponsiveGrid'.packages/types/src, with tests and comment lines excluded, 0 lines nameBreakpointColumnMap. Control: 5 lines nameBreakpointNameunder the same filter.@object-ui/layout's d.ts carries the type, at that version as a re-export from@objectstack/spec/ui.@object-ui/types's d.ts files name it 0 times. Control: 7 of its d.ts files nameGridSchema.BreakpointNameis exported. It is declared once, asexport type BreakpointNameinpackages/types/src/mobile.ts. The package index re-exports it once, as a member of anexport typeblock.BreakpointName". That count usedexport.*BreakpointNameoverpackages/types/src, and it included two lines that are not exports of the name. One isexport type ResponsiveValue, which namesBreakpointNamewithout exporting it. The other is a comment inspec-ui-schema-reexports.test.ts.@objectstack/specis 17.4.0. Its dist has 0 files that containBreakpointColumnMapSchema. Control: 56 files containFieldSchema.gridregistration declares.inputsdeclarescolumnsastype: 'number', described as "Number of columns on mobile devices". It also declares the flatsmColumnstoxlColumnsinputs. There is no flat2xlinput.GridSchema.columnsin@object-ui/types, which is nownumber | Partialof aRecordoverBreakpointNamewith number values. Its zod mirror is az.partialRecordover the six names.griddoes not importBreakpointColumnMap: itscolumnsis keyed byBreakpointName. So the paragraph now namesBreakpointNameas the key set ofGridSchema.columns, andBreakpointColumnMapasresponsive-grid's spelling of the same six. The retired spec schema is not named anywhere.Gates, run on head
cfd9fed6dcheck:skills-pathscheck:skill-eval-tokens --self-testcheck:skill-eval-tokenscheck-shell-escape-residuecheck:new-line-citationscheck:control-bytescheck-changeset-presenceRound 1 (head
ae9ef099d) also ran these, all exit 0:check-changeset-claims(report-only; the 4 pending changesets it lists as namingprotocol.mdare about other rules),check-changeset-no-major,check:doc-types,check-prompt-component-keys,check:doc-fences,check:doc-example-idsanddocs:check-links. None of those five doc gates reachesskills/.check-governed-queue-guard --testexits 3, "GOVERNED", which is expected.skills/path.git grep -l -Eover*.test.tsand*.test.tsx, with the patternrules/protocol\.md|skills/objectui|'skills'. It selects 17 files. The narrower patternrules/protocol\.md|skills/objectuiselects 15. The two extra files areapps/console/src/__tests__/preview-samples-spec-valid.test.tsandscripts/__tests__/layered-read-declared-path-4016.test.ts.pnpm exec vitest run --maxWorkers=2over all 17, oncfd9fed6d, ran under the verify lock: "Test Files 17 passed (17)" and "Tests 429 passed (429)". The lock verdict was "command-exit 0".evals/are in the set:check-skill-eval-tokens.test.tsandskill-guide-provider-envelope.test.tsx.skills/, on base1e946c96a, one2d985c56and oncfd9fed6d: 422 fence-delimiter lines (211 fences), 88 lines containing theos:comment marker, and 88 exactos:checkmarker lines.protocol.md: 13 fences,38e8cf0aa790dff6.guides/mobile.md: 8 fences,3f0dd31cbbb4ee14.SKILL.md: 1 fence,61537070dd9f058c.protocol.mdchanges its hash to4d017f0b19a3a643.1e946c96a, onae9ef099dand one2d985c56.content/docs,apps/*/docs, package READMEs, rootdocs/and the rootREADME.md. It does not includeskills/.Skills catalog readings
skills/objectui/rules/protocol.md: 361 lines before, 364 after (15992 bytes before, 16227 after). The +3 lines come from the review's wording: two readings of2xl, one for main and one for pre-objectui#7097 releases.skills/objectui/evals/protocol.json: 55 lines before, 55 after (2892 bytes before, 3028 after). One string value was replaced.skills/objectui/guides/mobile.md: 168 lines before, 168 after (5057 bytes before, 5102 after). The rewrapped sentence still takes five lines.skills/objectui/SKILL.md, the only publishedSKILL.md: 137 lines before, 137 after (7178 bytes before, 7200 after). So the sum over allSKILL.mdfiles is 137 before and 137 after.skills/**.Acceptance notes
grid-breakpoint-columns-7097.test.tsxcalls the drop that existed before objectui#7097 "the declared-but-not-read shapeskills/objectui/rules/protocol.mdwarns authors about". That describes the history of the bug.protocol.mdstill warns about thecolsshape and thepropsshape, and now about the pre-objectui#7097 drop. No edit, becausepackages/is out of scope.@object-ui/componentsis 17.6.0, and theregriddrops2xl.guides/mobile.mdandSKILL.mdcross-references point at it.BreakpointColumnMap, objectui#7580, is also pending: in published 17.6.0,@object-ui/layoutexposes the same name as a re-export from the spec.responsive-gridis not touched.Merge safety
origin/mainf9d772b16was fetched into a private ref.git merge-tree --write-treeagainst it exited 0 (tree9d0b7ef3264). Since base, main has not changed any of the four files, so no merge commit was needed.维护者速读(草稿)
skills/objectui/里,四个文件共改了五处:rules/protocol.md「Layout Responsiveness」那一节的开头一句,断点键列表补上2xl;2xl的那一段整段重写;evals/protocol.json第 3 条评测的期望答案;guides/mobile.md里指向该规则的那句交叉引用;SKILL.md第 5 节里指向该规则的那个括注。BreakpointColumnMapSchema所有,可这个 schema 已经被 spec 退役了。现在grid的columns由@object-ui/types的BreakpointName定键,responsive-grid用的@object-ui/layout的BreakpointColumnMap是同样的六个键。grid会丢掉2xl、作者要「停在xl」。可 main 上 objectui#7097 已经让grid正确渲染2xl。grid仍然会丢掉2xl。所以这一段同时写了两种情况:main 或新版本读六个键;objectui#7097 之前的版本仍要停在xl,或者把最宽的那一档写进className。protocol.md净增 3 行,其余三个文件行数不变。cfd9fed6d上 PASS(记录已贴在本 PR)。改动只涉及技能包文案,不动代码、不发包,4 个文件都在skills/objectui/下,和其他开着的 PR 没有文件重叠。合并后从 main 分发的读者立即看到新说法;还在用 npm 17.6.0 的读者,段内限定语仍然给出正确做法。skills/**,需要os-zhuang或hotlong批准,之后由席位落地。Generated by Claude Code