You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(spec): an inline grid column is judged on only one of its carriers — FormViewSchema.subforms[].columns is z.array(z.any()), and a currency column's scale reaches no refusal when its type comes from the child field #20901
Filing-gate category: ① a contract gap with named sites, class (c). reach: a public door, measured: objectui validate (the CLI at objectui PR objectui#11238's head 587c35168, against @object-ui/types built from that branch) printed "Schema is valid!", exit 0, for an object-form whose subforms[0].columns carried:
a typed currency column with scale, which InlineGridColumnSchema refuses;
an identity-only column with scale over a currency child field;
a column whose only key is a bogus zzz_not_a_key.
The same document with childObject: 5 exited 1, so the control fires. @objectstack/spec 17.5.0's FormViewSchema.safeParse also accepts the typed currency + scale column that InlineGridColumnSchema refuses.
Reader: triage first (grade and route), then the domain:spec seat that dispatches it. Filed by the objectui domain:ui seat 2 (session_011p7ikEivgXefNDaE5S5Uec) at the ACCEPT of PR objectui#11238 (objectui#10783), from the dev report 5915518804 (out-of-scope 1, and open question 1) and the contract review 5915705951 (③ items 5 and 6), which routed both here. ⛔ Not graded here.
The two sites (read at objectstack origin/main33b6e8bec)
packages/spec/src/ui/view.zod.ts:4374, FormViewSchema.subforms[]: columns: z.array(z.any()).optional(). The column contract InlineGridColumnSchema (packages/spec/src/data/field.zod.ts:890) is strict, and it refuses scale on a typed currency column (rulings B on objectstack#19629 and 乙 on objectstack#19910). But a column on the form-view path is never judged: an unknown key is silently dropped by the grid, and a refused key passes. objectui's mirror (packages/types/src/zod/objectql.zod.ts, object-formsubforms[].columns) follows it with the same z.any().
The hydrated currency column.InlineGridColumnSchema's superRefine says "Reach, deliberately: only the DECLARED type is visible here" (field.zod.ts:952). An identity-only column ({ name: 'amount', scale: 2 }) whose name resolves to a sibling currency field therefore publishes green, and nothing refuses its scale. inlineColumns sits on the child object's own relationship field, so the sibling field the column names, and that field's type, are in the same object document os validate judges. The fact is statically known there.
objectui PR objectui#11238 makes the grid ignore that scale (the minor unit decides) and adds a render-time console.warn in plugin-form's hydrateColumns as the backstop. A console warning is seen only by whoever opens devtools.
Direction (for triage, not a ruling)
Site 1: FormViewSchema.subforms[].columns becomes z.array(InlineGridColumnSchema), or the same strict column contract, with its Clause-② narrowing stated. Then objectui's mirror follows at its next pin bump (a coordination child in objectui).
Site 2: an object-level refinement refuses an inlineColumns entry carrying scale whose name resolves to a sibling currency field, with the same message as the typed refusal. It does not reach subforms[].columns, where the child object is a different document; the objectui render-time report stays the backstop there.
Producers: 0 authored inlineColumns blocks declare type or scale in objectstack (measured on objectstack#20045), and 0 in objectui (measured on objectui#10783). So neither tightening needs a migration.
Dedupe
objectstack issues were searched for subforms columns z.any, inlineColumns scale currency sibling and FormViewSchema subforms columns: 0 hits.
Closes fired this time. The card closed at merge, with os-justin as the actor (the auto-merge enabler). The seat reopens it because adoption 5919009567 on the PR owes a follow-up that is Part of this card. The card closes when that follow-up lands.
Review item 5: packages/spec/src/migrations/entries/semantic/18.inline-grid-column-currency-scale-refused.ts says "columns declaring no type keep their scale". That is false from bee75cebe6 on, for an identity-only column over a currency child field. Its surface and acceptance sentences change to point at the new identity-only entry, and the registry is regenerated.
Review item 6, ruled WIDEN: field-column-lists-canonicalized (packages/spec/src/conversions/registry.ts) also respells field → name on subforms[].columns, both on views[].form and on every formViews entry. ADR-0087's pre-GA policy requires a lossless break to land as a retiredFromLoadPath chain step in the same release.
Optional item 9, and the review's ① text fixes, in the files it touches.
{
"issue": 20901,
"status": "done",
"branch": "claude/issue-20901-grid-column-followup",
"pr": "#20953",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 — the dispatching seat's id (mode:subagent)",
"premise_still_valid": true,
"summary": "Draft PR #20953 (Part of #20901; head 06f0798, which carries origin/main 3fbf3ca). (1) Review item 5: the step-18 entry inline-grid-column-currency-scale-refused no longer says a column declaring no type keeps its scale. Its surface, reach sentence and acceptance name inline-grid-column-identity-only-currency-scale-refused, and the registry was regenerated with gen:migration-registry. (2) Review item 6, WIDEN: the field -> name respelling on subforms[].columns is now a retiredFromLoadPath chain step. It walks every FORM payload mapViewPayloads reaches (form, each formViews entry, a form view item's config, a flattened form overlay) plus the assembled viewItems channel, and it shares one respell function (respellInlineGridColumns) with field-column-lists-canonicalized. An entry already spelled name, and one carrying both keys, stay as they are. The D3 entry form-view-subform-columns-closed now names the conversion. DEVIATION, stated loudly: the ruling's vehicle was a wider walk inside field-column-lists-canonicalized. I shipped the same walk as a sibling id, form-view-subform-columns-canonicalized, with retiredAfter 17.5.0. Reason: retiredAfter is one value per entry (ADR-0087 amendment of 2026-09-30, ruling A on #20390). The existing id is published with 17.0.0, pinned by the census, while this carrier accepted field through 17.5.0. Measured at the artifact door (applyArtifactForwardConversions, engines.protocol ^17.5.0, runtime label 17.5.0): verdict converted-retired-after. The subform field column was respelled by the new id, while the same field column on inlineColumns stayed as authored because the 17.0.0 entry stays closed. A folded-in walk would therefore have refused this carrier instead of rewriting it. No gate can see this. The ruling's intent (a lossless break lands as a retired chain step in the same release) is met exactly, and the fold-in stays a mechanical option (open question 1). (3) Item 9: a pin probes InlineGridColumnSchema for type-conditional rules (today exactly currency + scale) and asserts defineStack refuses each through HYDRATED_INLINE_COLUMN_TYPE. A new rule without a row goes red, with a message naming the table. (4) The review's ① text findings in the touched files: stack.zod.ts's objectui claim is now a historical pin citation (.objectui-sha pin db11afd4967c; re-read at that sha, it is true). The 'like the view data-source check' likeness is cut. The 'adds its row here' comment names its pin. The form-view D3 comment no longer says a stored row is refused. One file outside the dispatched set: packages/spec/scripts/conversions-major18-merge.test.ts went red on this diff. Its synthetic pair sits beside the middle entry: hookTimeoutToTimeoutMs, an exempt entry, at the base; the new rule-placed entry at the head. The expectation now derives the neighbour's finding when the neighbour is not exempt. Clause-②: the claim comment 5916997003 carries 'yes (narrowing)' for PR #20927's scope. os-dev.md says to copy a claim's line, while this dispatch says to decide it truthfully. Copying would be false and would force minor. I followed the dispatch: 'Clause-②: no' (no key added to any published payload, no narrowing), and the changeset grades @objectstack/spec patch.",
"tests": "All at 06f0798 unless noted. PINS packages/spec/src/inline-grid-column-carriers.test.ts: 21 passed (13 existing + 8 new: two for the table, six for the chain step — retiredAfter stamp; stored row in container/record/overlay spellings respelled and only then ViewMetadataSchema-valid; controls name-only and both-keys returned by reference; os migrate meta chain path views[0].formViews.entry.subforms[0].columns[0].name; authoring funnel still STACK_SCHEMA_INVALID 422; both carriers convert alike). ABLATION 1 (node scripts/ablation-replace.mjs, WRAP; registry.ts anchor 'const columns = respellInlineGridColumns(subform.columns, ...subforms[j].columns, emit);' -> 'const columns = subform.columns;', anchor x1->x0, blob 909148b74725->3b07d9f2d145; pins + conversions.test.ts): 'Tests 4 failed | 251 passed (255)'. The fixture pair, stored-row, chain and shared-rule tests went red; the controls stayed green. Restored: blob == HEAD 909148b74725, git diff HEAD empty. ABLATION 2 (stack.zod.ts: the currency row deleted from HYDRATED_INLINE_COLUMN_TYPE, blob f93870ec6a88->d3aca2ec1d38): 'Tests 4 failed | 17 passed (21)'. The new table pin was red ('...add the row to HYDRATED_INLINE_COLUMN_TYPE in stack.zod.ts') and so were the three existing hydrated refusals. Restored: blob == HEAD. Both ablations ran at ca5ad20 with the implementation committed. Direction observed: red, as predicted. The subject resolves to src via relative imports, so no dist leg was owed. SPEC SUITE: vitest run --project local -> 'Test Files 584 passed (584) / Tests 17200 passed | 1 todo' (VERDICT command-exit 0). TYPECHECK: pnpm --filter @objectstack/spec typecheck exit 0 (check:test-typecheck OK; --listFiles shows the pin file in tsconfig.test.json and the merge test in tsconfig.scripts.json). REPO subset (conversions-major18-merge, step18-rationale-merge, retired-key-migrate-sentence, view-item-owner-hidden-retirement, view-list-tabs-retirement): 82 passed. Before its fix the merge test read '1 failed | 11 passed' at a4e7143; after it, 12/12. check:generated: all 15 artifacts up to date after a fresh spec build. DERIVED GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 90 commands, all exit 0. The --ran reconciliation reads '90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 90 recorded an exit code)'. Four first exited 3 (PREREQUISITE NOT MET: lint check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) and were green after turbo run build over ./packages/* and ./packages// (71/71 tasks). The derivation printed a stale-tree note: origin/main moved 3 commits touching only scripts/adr-anchors files, none of them this diff's. ESLINT (narrowed run): eslint --no-inline-config --format json over the 7 changed .ts files -> files 7, errors 0, warnings 0. Population is eslint.config.mjs's files glob **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; the config states it never enables type-aware linting, so the diff cannot move an untouched file's verdict. Local dry runs of the PR-body gates on the body as sent: check-changeset-no-major --event -> 'LEVEL AXIS: this PR declares clause-② no', exit 0; check:partof-closing-keyword (PR_BODY) exit 0. check:nul-bytes exit 0 and the control-byte grep was empty. CI, one read at 22:16Z on 06f0798, not waited on: 32 runs — 10 success, 3 skipped, 19 in_progress (Build Core, Check Changeset, Dogfood 1-3/3, Dogfood Verify CLI, Governed Surface Queue Guard, Lint & Repo Gates, Temporal Conformance, Test Core 1-6/6, Type Check x4). NOT MEASURED: the full @objectstack/spec test:repo project (CI owns it), and the cli integration tier (not touched).",
"mcp_calls": "0",
"api_writes": "3 — all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, run success each): pr_create -> POST /repos/objectstack-ai/objectstack/pulls (#20953, draft forced; body 9512 bytes sent, 9512 stored, identical); label-write --assign -> POST /repos//issues/20953/assignees os-justin (read back: assignee os-justin; the labels documentation/size/m/tests/tooling are the labelers', not this write's); post-stamped -> POST /repos//issues/20901/comments (this report). Zero labels written: the dispatch named none, and skip-changeset does not apply. Plus git push to the branch (not REST).",
"open_questions": [
{
"question": "Item 6's vehicle: keep the sibling conversion id form-view-subform-columns-canonicalized (retiredAfter 17.5.0), or fold the walk into field-column-lists-canonicalized as the ruling worded it?",
"options": [
"A — keep the sibling id (as shipped): each retirement's retiredAfter is a true fact, the artifact door rescues a 17.5.0-floor artifact carrying a field subform column on a main still labelled 17.5.0 (measured), and the next release's per-release spec-changes section lists the new id; cost: one more conversion id in the family (the D3 entry names both)",
"B — fold into field-column-lists-canonicalized (move the mapViewPayloads walk into it, delete the sibling): one id, the ruling's literal wording; cost: that entry's census-pinned retiredAfter 17.0.0 is false for the new carrier, so the artifact door refuses what it should rewrite at floor 17.5.0 until the label passes it, and the per-release section never shows the widening"
],
"recommendation": "A, because ADR-0087's 2026-09-30 amendment (maintainer ruling A on #20390) makes retiredAfter a per-entry fact the door keys on, and B reintroduces for this carrier the exact main-vs-last-release refusal that amendment closed; the ruling's intent (a lossless break lands as a retired chain step in the same release) holds either way"
}
],
"out_of_scope_findings": [
"carrier: none — noted, not filed: step 18's rationale (STEP18_RATIONALE) carries no fragment for either #20927 D3 entry (form-view-subform-columns-closed, inline-grid-column-identity-only-currency-scale-refused) nor this conversion; its header asks one fragment per retirement but no gate requires it, so the upgrade guide's step-18 prose is silent on the form-view carrier (carrier: none; recorded in the PR's Acceptance notes)"
]
}
Q1 (the vehicle for item 6): A, the sibling id form-view-subform-columns-canonicalized with retiredAfter: 17.5.0. The seat read ADR-0087 on main, the amendment at :919-929 (Forward conversion never opens on unreleased main: spec still labelled 17.4.0 while main refuses 17.5.0 retirements, so artifacts built by the published 17.4.0 CLI are refused #20390 ruling A): "Every retired conversion carries a required retiredAfter … and the door replays an entry E when floor < runtime OR floor <= E.retiredAfter." The new carrier accepted field through 17.5.0, but field-column-lists-canonicalized is stamped 17.0.0. So the ruling's wording on 5919493523 ("widen field-column-lists-canonicalized") would have made the door refuse a 17.5.0-floor artifact that it should rewrite. The deviation is the right reading of the ruling's intent: a lossless break lands as a retired chain step in the same release. The seat's prescribed vehicle was wrong in detail.
conversions-major18-merge.test.ts is outside the dispatched set. The diff turned it red, and the fix derives the neighbour's finding. Accepted as the smallest consequence of the change.
Landed: PR #20953 → c6b3a01d5d (the follow-up); the card stays open for one rationale fragment · 2026-09-30T23:29Z
domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5916997003.
Landed: merged at 2026-09-30T23:25Z as c6b3a01d5d, with one parent (c1602ba429, read with git rev-list --parents). All 8 files have the same per-file git patch-id --stable as the reviewed head 06f079864a against the merge base 3fbf3ca617. Review record: 5921114874.
Delivered:
the step-18 entry's sentence is corrected, and the registry is regenerated;
form-view-subform-columns-canonicalized (retiredAfter: 17.5.0) respells field → name on subforms[].columns;
the hydrated-type table pin is in;
the review's text findings are fixed in the touched files.
Scope: one STEP18_RATIONALE fragment, id form-view-subform-columns-closed. It names the identity-only sibling (inline-grid-column-identity-only-currency-scale-refused) and the D2 conversion (form-view-subform-columns-canonicalized). It is inserted where its id sorts, and its order is one above the highest at the dev's base. The registry is regenerated, never edited by hand. A patch changeset. Part of #20901. Clause-②: no.
Branch claude/issue-20901-rationale-fragment, os-dev, mode:subagent. An at-tier review is owed before enqueue. The card closes by hand when it lands.
The landing record 5921552300 is corrected in this act: the family-wide gap it called filed was never filed. Its Acceptance-notes carrier is this PR.
{
"issue": 20901,
"status": "done",
"branch": "claude/issue-20901-rationale-fragment",
"pr": "#21090",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1, the dispatching seat's id (mode:subagent)",
"premise_still_valid": true,
"summary": "Draft PR #21090 (Part of #20901, Clause-②: no, assigned os-justin) adds one STEP18_RATIONALE fragment, id form-view-subform-columns-closed, order 54, inserted between form-view-option-default-retired and list-view-page-mount-retired, plus a patch changeset for @objectstack/spec. Seat facts re-measured at base 5dbeb7d: STEP18_RATIONALE at registry.ts:5024, 53 fragments parsed with the merge test's own element shape, orders 1..53 each used once (max 53), list sorted; zero fragment text hits for subform, formViews or inline grid (the only inlineColumns hit is the #9227 fragment). The list sits OUTSIDE the generated markers (registry.ts header :19-43; build-migration-registry.ts writes only marked regions), so the fragment is hand-written and gen:migration-registry is a byte-for-byte no-op on it. That differs from the dispatch note's 'The registry is regenerated, never edited by hand' (5924906441); I followed the file header. PR #21053 changed only the generated semantic region (one hunk at :8460). It landed on main as 11d28c1 while I worked, so I merged origin/main dff98c1 with scripts/pm/os-regen-merge.sh (merge commit 8ebaf23, no rebase). The delta vs main is still exactly 2 files, +41/-0, and the sibling entry dashboard-widget-dimensionless-multi-measure-refused is present at HEAD. Register: the fragment carries no tracker number, although its neighbours do (for example '(#9227)'). AGENTS.md says author-shown runtime strings carry none, and step 18's rationale is printed by os migrate meta --step. No gate scans the rationale text position either way.",
"fragment_text": "It also closes the form view's inline grid columns: subforms[].columns, on view.form and on formViews entries, was z.array(z.any()) while a relationship field's inlineColumns was already the strict InlineGridColumnSchema, so a mis-keyed column published clean and drew a blank grid column, and scale on a currency column, which the other carrier refuses under the maintainer's rulings of 2026-09-23 (option B) and 2026-09-24 (option 乙), published green. The carrier now references that schema, so both carriers are judged by it, with its own prescriptions. The D2 conversion form-view-subform-columns-canonicalized respells a { field } column as { name }, the respelling field-column-lists-canonicalized makes on inlineColumns: it rewrites stored rows and assembled artifacts and lists the edit under os migrate meta, and it is retired from the load path, so an author writing field meets the refusal. A view saved with a failing column is refused with the column schema's prescription, and a stored row carrying one is diagnosed at rehydration; neither is stripped, because which column an unknown key or a mixed field/name entry meant is the author's call, and a conversion that dropped the key would accept at load what the parse now refuses. Its D3 record is the semantic entry form-view-subform-columns-closed. On both carriers, the reach of inline-grid-column-currency-scale-refused extends to a column that declares no type: such a column takes its type from the child field, which the column schema cannot see, when the console hydrates it, so defineStack's cross-reference check re-parses a column whose name is a currency field of the child object as the type it renders as, and the refusal of its scale is the column schema's own. Reach: the child object must be declared in the same stack; a column naming no field of it, or a subform whose child object comes from another package, is not judged there. It has no D2 conversion, for the declared-type entry's reason: deleting the key is the migration, and a conversion that dropped it would accept it at load, the grace window ruling B refused. Its D3 record is the semantic entry inline-grid-column-identity-only-currency-scale-refused.",
"fragment_sentences_and_sources": [
{
"sentence": "It also closes the form view's inline grid columns: subforms[].columns, on view.form and on formViews entries, was z.array(z.any()) while a relationship field's inlineColumns was already the strict InlineGridColumnSchema, so a mis-keyed column published clean and drew a blank grid column, and scale on a currency column, which the other carrier refuses under the maintainer's rulings of 2026-09-23 (option B) and 2026-09-24 (option 乙), published green.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:6-7 (z.array(z.any()); inlineColumns strict InlineGridColumnSchema), :17 (surface: view.form and view.formViews.KEY subforms columns), :26-29 (mis-keyed column published clean, blank grid column; scale on a currency column refused by the other carrier under 2026-09-23 option B and 2026-09-24 option 乙, published green)"
},
{
"sentence": "The carrier now references that schema, so both carriers are judged by it, with its own prescriptions.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:8-9 (carrier now REFERENCES that schema, both carriers judged by one contract), :30-31 (every rule it holds applies here too, with its own prescription); packages/spec/src/ui/view.zod.ts:4399 columns: z.array(InlineGridColumnSchema)"
},
{
"sentence": "The D2 conversion form-view-subform-columns-canonicalized respells a { field } column as { name }, the respelling field-column-lists-canonicalized makes on inlineColumns: it rewrites stored rows and assembled artifacts and lists the edit under os migrate meta, and it is retired from the load path, so an author writing field meets the refusal.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:9-11 (the one mechanical respelling field to name is this D2 conversion, the respelling field-column-lists-canonicalized makes on inlineColumns), :31-33 (rewrites stored rows and assembled artifacts, lists the edit under os migrate meta, an author writing field meets the refusal); packages/spec/src/conversions/registry.ts:7634 (field 'x' to name 'x', every other key kept), :9878-9880 (id, toMajor 18, retiredFromLoadPath: true), :9872 (the parse refuses field with the prescription)"
},
{
"sentence": "A view saved with a failing column is refused with the column schema's prescription, and a stored row carrying one is diagnosed at rehydration; neither is stripped, because which column an unknown key or a mixed field/name entry meant is the author's call, and a conversion that dropped the key would accept at load what the parse now refuses.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:12-14 (a view saved with a failing column is refused with the column schema's own prescription; a stored row carrying one is diagnosed at rehydration; neither is stripped), :34-35 (which column an unknown key or a mixed field/name entry meant is the author's call; a conversion that dropped the key would accept on every load what the parse now refuses)"
},
{
"sentence": "Its D3 record is the semantic entry form-view-subform-columns-closed.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:16 (id: 'form-view-subform-columns-closed')"
},
{
"sentence": "On both carriers, the reach of inline-grid-column-currency-scale-refused extends to a column that declares no type: such a column takes its type from the child field, which the column schema cannot see, when the console hydrates it, so defineStack's cross-reference check re-parses a column whose name is a currency field of the child object as the type it renders as, and the refusal of its scale is the column schema's own.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.inline-grid-column-identity-only-currency-scale-refused.ts:5-6 (the reach of inline-grid-column-currency-scale-refused extended to the column that declares no type), :7-8 (takes its type from the child field when the console hydrates it), :9-10 (defineStack's cross-reference check re-parses such a column as the type it renders as and reports the column schema's own refusal), :16-18 (surface: inlineColumns and subforms columns, a column declaring no type whose name is a currency field of the child object), :28 (the column schema cannot see the child field)"
},
{
"sentence": "Reach: the child object must be declared in the same stack; a column naming no field of it, or a subform whose child object comes from another package, is not judged there.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.inline-grid-column-identity-only-currency-scale-refused.ts:32-34 (verbatim reach sentence)"
},
{
"sentence": "It has no D2 conversion, for the declared-type entry's reason: deleting the key is the migration, and a conversion that dropped it would accept it at load, the grace window ruling B refused.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.inline-grid-column-identity-only-currency-scale-refused.ts:11-13 (D3 only, for the reason the declared-type entry gives; a conversion that dropped the key would accept it on every load, the grace window the ruling refused), :20 (DELETE the key, that is the whole migration); packages/spec/src/migrations/entries/semantic/18.inline-grid-column-currency-scale-refused.ts:25 (no grace window per ruling B; NOT mechanically converted); method for 'no D2 conversion': git grep of both ids over packages/spec/src/conversions/registry.ts at 8ebaf23 = 0 hits"
},
{
"sentence": "Its D3 record is the semantic entry inline-grid-column-identity-only-currency-scale-refused.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.inline-grid-column-identity-only-currency-scale-refused.ts:15 (id)"
}
],
"files_changed": [
"packages/spec/src/migrations/registry.ts (+31/-0: the one fragment, hand-written region)",
".changeset/20901-step18-rationale-form-view-subform-columns.md (+10, '@objectstack/spec': patch)"
],
"changeset": "Required by the repo rule that publishing changes take a changeset (AGENTS.md Post-Task Checklist 3; os-dev: a published surface that moved needs one). Measured after the spec build: the new sentence 'It also closes the form view' is in packages/spec/dist/migrations/index.js and index.mjs, and dist is in files[]. Positive control: an existing fragment's text ('It also narrows an analytics cube member') is in the same two files. No gate was observed demanding it: Check Changeset was in_progress at the single CI read. The local dry run of check-changeset-no-major with the PR body as the event exited 0 with 'LEVEL AXIS: this PR declares clause-② no'.",
"tests": "Final head 8ebaf23 (merges origin/main dff98c1), all through os-verify-lock with VERDICT command-exit 0 unless noted. (1) Rationale pins, repo project: step18-rationale-merge + compliance-families-retirement + conversions-major18-merge, 'Test Files 3 passed (3) / Tests 31 passed (31)'. Local project: migrations.test.ts + inline-grid-column-carriers.test.ts, 'Tests 179 passed (179)'. (2) gen:migration-registry wrote registry.ts ('333 semantic, 242 retired-key, 211 retired-def'), and git diff --exit-code was clean, so it is a no-op. (3) After pnpm --filter @objectstack/spec build (a full turbo build of ./packages/* and ./packages//: 71/71 tasks, 1 cached), pnpm --filter @objectstack/spec check:generated printed '✓ All 15 generated artifacts are up to date', including check:upgrade-guide and check:spec-changes. (4) The upgrade-guide generator does not print step 18: build-upgrade-guide.ts:78 loops MIGRATION_SUPPORT_FLOOR+1 to PROTOCOL_MAJOR, PROTOCOL_VERSION is '17.0.0', the guide's only protocol heading is 'Protocol 16 → 17', and it has 0 hits for form-view-subform-columns. So gen:upgrade-guide was not run beyond its check, which is green. (5) Spec local suite: 'Test Files 588 passed (588) / Tests 17318 passed | 1 todo'. Spec typecheck exit 0 ('check:test-typecheck: OK'). Both runs were also green at the pre-merge head 51fceda: 588 files, 17239 passed, typecheck exit 0. (6) node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived at 8ebaf23 with the change set read from git (2 paths vs merge base dff98c1), gave 82 commands, all run with exit codes recorded. --ran printed '✓ dispatch-gates --ran: 82 derived famil(ies) accounted for — 82 run, 0 NOT-MEASURED (a DERIVED zero — all 82 recorded an exit code and none of them is 3)'. In the first pass at 51fceda, three gates exited 3 PREREQUISITE NOT MET (lint check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure). They passed after the package build, and all 82 passed again at 8ebaf23 after the post-merge rebuild. (7) ESLint, narrowed: eslint --no-inline-config --format json packages/spec/src/migrations/registry.ts gave files 1, errors 0, warnings 0 at 8ebaf23. Population: eslint.config.mjs files glob **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. Invariance: eslint.config.mjs:326-328 states it never enables type-aware linting, so this diff cannot change the verdict on an untouched file. The .changeset .md is not in the glob. (8) check:nul-bytes exit 0 (in the derived set). Control-byte grep over both changed files exited 1 (no match), captured before any pipe. (9) Local dry runs of the PR-body gates on the body as sent: check-partof-closing-keyword exit 0; check-changeset-no-major --base dff98c1 --event exit 0. (10) check-governed-merges --branch: NOT governed (0 of 2 paths hit the register), 41 changed lines, under 5000.",
"ablation": "The fragment was removed at committed head 51fceda with node scripts/ablation-replace.mjs --delete in WRAP mode, plus an outer trap restoring HEAD on EXIT/INT/TERM. The anchor was the whole element: 2617 bytes, 1 hit before, 0 after. The blob went from dda2626a17d9 to e186e8ef6570. On disk: marker 'It also closes the form view' count 1 to 0, while the id line stays 1 because the generated D3 entry carries the same id. Under the mutation: step18-rationale-merge + compliance-families-retirement 19 passed, migrations + inline-grid-column-carriers 179 passed, check:upgrade-guide exit 0, check:migration-registry exit 0. Every leg was green, so no pin went red. That was the expected direction: the merge test pins the sort order, kebab ids, order integrity and the joined text only, and nothing pins this fragment by id. I added no pin. Restored: blob == HEAD dda2626a17d9, git diff HEAD empty, git status clean. No dist leg was owed: the merge test imports ../src/migrations/registry relatively, and the guide check runs on src through tsx.",
"ci": "One read of the head 8ebaf23 check-runs at 2026-10-01T05:50:50Z: 32 runs, 32 names after dedupe by name keeping the latest started_at. 10 success (Auto Label, Check Documentation Links, Check PR Size, Flag docs affected by code changes, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, filter). 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)). 19 in_progress (Build Core, Check Changeset, Dogfood Regression Gate 1-3/3, Dogfood Verify CLI, Governed Surface Queue Guard, Lint & Repo Gates, Temporal Conformance, Test Core 1-6/6, Type Check x4). I did not wait on them.",
"governed": "node scripts/pm/check-governed-merges.mjs --branch claude/issue-20901-rationale-fragment on the final file list: '0 of 2 path(s) hit the register', '✅ NOT governed', size 41 ≤ 5000. Head repo NOT MEASURED by --branch, but the PR is internal (head ref is on objectstack-ai/objectstack).",
"acceptance_line_as_written": "The review's count (207 of 253 at 06f0798) and the per-family caveat are kept as given. I verified the clause 'the generated guide projects major 17 only' and it is true. I extended the 'no reach: today' part to stay true: step 18's rationale does reach os migrate meta --step. Its default terminus is CHAIN_TERMINUS_MAJOR = max(PROTOCOL_MAJOR, ...MIGRATION_MAJORS) = 18 (packages/cli/src/commands/migrate/meta.ts:89), it prints hop.rationale at :429-430, and --json hops carry the rationale at :660-668. There a family without a fragment is left out of the paragraph, not misstated. I read this from the source and did not measure it at that door, so no reach: is claimed.",
"mcp_calls": "0",
"api_writes": "3, all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, each run concluded success). pr_create → POST /repos/objectstack-ai/objectstack/pulls (#21090, draft forced, relay run 36821656431; read-back '2388 byte(s) sent, 2388 stored — identical'). label-write --assign → POST /repos//issues/21090/assignees os-justin (relay run 36821694090; read-back assignee os-justin; the labels documentation, size/s and tooling come from the labelers, not this write). post-stamped → POST /repos//issues/20901/comments (this report). Zero labels written: the dispatch named none, and skip-changeset does not apply. Not REST: git push x3 to the branch (empty probe, the fragment commit 51fceda, the merge 8ebaf23). Reads: REST GETs of the card, its comments, comment 5921114874, PR #21090 and its check-runs. One check-runs GET with a mistyped sha answered 422; it was a read only.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the family-wide STEP18_RATIONALE gap (entries without a fragment of their own id; families uncounted). It is recorded in PR #21090's Acceptance notes, with the os migrate meta --step reach nuance above. The declared-type sibling inline-grid-column-currency-scale-refused has no fragment of its own either. This fragment names it as the entry whose reach it extends, and it is not covered beyond that.",
"carrier: #20234 stage 11 (the registry's hand-written parts, queued behind this PR on the same file) · noted, not filed: most neighbouring STEP18_RATIONALE fragments carry tracker numbers in author-shown text ('(#9227)', '(#19992, ...)'), which AGENTS.md says runtime strings must not carry. check:doc-authoring Rule 3 recognises message/guidance/tombstone/describe positions and not a rationale fragment's text:, so no gate reads this population. Not class a/b/c: no wrong answer, no contract text, no metadata trap."
]
}
It has 2 files (+41 / -0), is a draft, is assigned to os-justin, and says Part of #20901 with Clause-②: no.
check-governed-merges --pr 21090: NOT governed.
Delivered:
One STEP18_RATIONALE fragment, form-view-subform-columns-closed, at order 54, inserted where its id sorts. Each sentence cites the two entry sources and the conversion source it rests on.
A patch changeset, because the text ships in dist/migrations.
Deviation from the dispatch note: accepted. The note 5924906441 said "the registry is regenerated, never edited by hand". The list sits outside the generated markers, as the file header says, so the fragment is hand-written and gen:migration-registry is a no-op on it. The note was wrong in detail; the dev followed the file.
Ablation: no pin went red, and no pin was added. That is as dispatched: the merge test pins the order and the joined text, not a fragment by id.
Out of scope:
The family-wide gap rides the PR's Acceptance notes, as ordered.
Landed: PR #21090 → 2742e53709 (the last follow-up); the card is done
domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-10-01. Holder of claim 5916997003.
Landed: PR fix(spec): step 18's rationale gains the form-view-subform-columns-closed fragment #21090 went through the merge queue as 2742e53709, with one parent (cf0346ec78, read with git rev-list --parents). Both files have the same per-file git patch-id --stable as the reviewed head 8ebaf237c4 against the merge base dff98c1f51. Review record: 5925959503 (PASS).
Delivered: step 18's rationale gains the form-view-subform-columns-closed fragment. It names the closed form-view carrier, the D2 conversion form-view-subform-columns-canonicalized, and the identity-only sibling entry.
Activity
objectstack-fleet commentedon Sep 30, 2026
Landed: PR #20927 →
bee75cebe6; the card is reopened for the follow-up the review adoption owes · 2026-09-30T20:49Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5916997003.bee75cebe6, a single parent (87847a27ce). All 10 files are per-filegit patch-id --stableequal to the reviewed headfeba1a99bd(merge basedef279a39b).5919009567on the PR owes a follow-up that is Part of this card. The card closes when that follow-up lands.claude/issue-20901-grid-column-followup), goes ahead of packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234 stage 11 on the migration registry:packages/spec/src/migrations/entries/semantic/18.inline-grid-column-currency-scale-refused.tssays "columns declaring notypekeep theirscale". That is false frombee75cebe6on, for an identity-only column over acurrencychild field. Its surface and acceptance sentences change to point at the new identity-only entry, and the registry is regenerated.field-column-lists-canonicalized(packages/spec/src/conversions/registry.ts) also respellsfield→nameonsubforms[].columns, both onviews[].formand on everyformViewsentry. ADR-0087's pre-GA policy requires a lossless break to land as aretiredFromLoadPathchain step in the same release.@object-ui/typessubforms[].columnsmirror atpackages/types/src/zod/objectql.zod.ts:640. It follows at the next@objectstack/specbump.ObjectMasterDetailFormPropsSchema.detailsisz.unknown(), a third unjudged carrier of the inline grid column: a bogus key or a typedcurrencycolumn withscalepublishes green #20928 (thedetails: z.unknown()third carrier) and finding(lint):field-no-consumerscalls a field "inert" when an inline grid column names it (form.subforms[].columns[].name), becausenameis inLITERAL_KEYS#20929 (lintfield-no-consumers).Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{
"issue": 20901,
"status": "done",
"branch": "claude/issue-20901-grid-column-followup",
"pr": "#20953",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 — the dispatching seat's id (mode:subagent)",
"premise_still_valid": true,
"summary": "Draft PR #20953 (Part of #20901; head 06f0798, which carries origin/main 3fbf3ca). (1) Review item 5: the step-18 entry inline-grid-column-currency-scale-refused no longer says a column declaring no type keeps its scale. Its surface, reach sentence and acceptance name inline-grid-column-identity-only-currency-scale-refused, and the registry was regenerated with gen:migration-registry. (2) Review item 6, WIDEN: the field -> name respelling on subforms[].columns is now a retiredFromLoadPath chain step. It walks every FORM payload mapViewPayloads reaches (form, each formViews entry, a form view item's config, a flattened form overlay) plus the assembled viewItems channel, and it shares one respell function (respellInlineGridColumns) with field-column-lists-canonicalized. An entry already spelled name, and one carrying both keys, stay as they are. The D3 entry form-view-subform-columns-closed now names the conversion. DEVIATION, stated loudly: the ruling's vehicle was a wider walk inside field-column-lists-canonicalized. I shipped the same walk as a sibling id, form-view-subform-columns-canonicalized, with retiredAfter 17.5.0. Reason: retiredAfter is one value per entry (ADR-0087 amendment of 2026-09-30, ruling A on #20390). The existing id is published with 17.0.0, pinned by the census, while this carrier accepted field through 17.5.0. Measured at the artifact door (applyArtifactForwardConversions, engines.protocol ^17.5.0, runtime label 17.5.0): verdict converted-retired-after. The subform field column was respelled by the new id, while the same field column on inlineColumns stayed as authored because the 17.0.0 entry stays closed. A folded-in walk would therefore have refused this carrier instead of rewriting it. No gate can see this. The ruling's intent (a lossless break lands as a retired chain step in the same release) is met exactly, and the fold-in stays a mechanical option (open question 1). (3) Item 9: a pin probes InlineGridColumnSchema for type-conditional rules (today exactly currency + scale) and asserts defineStack refuses each through HYDRATED_INLINE_COLUMN_TYPE. A new rule without a row goes red, with a message naming the table. (4) The review's ① text findings in the touched files: stack.zod.ts's objectui claim is now a historical pin citation (.objectui-sha pin db11afd4967c; re-read at that sha, it is true). The 'like the view data-source check' likeness is cut. The 'adds its row here' comment names its pin. The form-view D3 comment no longer says a stored row is refused. One file outside the dispatched set: packages/spec/scripts/conversions-major18-merge.test.ts went red on this diff. Its synthetic pair sits beside the middle entry: hookTimeoutToTimeoutMs, an exempt entry, at the base; the new rule-placed entry at the head. The expectation now derives the neighbour's finding when the neighbour is not exempt. Clause-②: the claim comment 5916997003 carries 'yes (narrowing)' for PR #20927's scope. os-dev.md says to copy a claim's line, while this dispatch says to decide it truthfully. Copying would be false and would force minor. I followed the dispatch: 'Clause-②: no' (no key added to any published payload, no narrowing), and the changeset grades @objectstack/spec patch.",
"tests": "All at 06f0798 unless noted. PINS packages/spec/src/inline-grid-column-carriers.test.ts: 21 passed (13 existing + 8 new: two for the table, six for the chain step — retiredAfter stamp; stored row in container/record/overlay spellings respelled and only then ViewMetadataSchema-valid; controls name-only and both-keys returned by reference; os migrate meta chain path views[0].formViews.entry.subforms[0].columns[0].name; authoring funnel still STACK_SCHEMA_INVALID 422; both carriers convert alike). ABLATION 1 (node scripts/ablation-replace.mjs, WRAP; registry.ts anchor 'const columns = respellInlineGridColumns(subform.columns, ...subforms[j].columns, emit);' -> 'const columns = subform.columns;', anchor x1->x0, blob 909148b74725->3b07d9f2d145; pins + conversions.test.ts): 'Tests 4 failed | 251 passed (255)'. The fixture pair, stored-row, chain and shared-rule tests went red; the controls stayed green. Restored: blob == HEAD 909148b74725, git diff HEAD empty. ABLATION 2 (stack.zod.ts: the currency row deleted from HYDRATED_INLINE_COLUMN_TYPE, blob f93870ec6a88->d3aca2ec1d38): 'Tests 4 failed | 17 passed (21)'. The new table pin was red ('...add the row to HYDRATED_INLINE_COLUMN_TYPE in stack.zod.ts') and so were the three existing hydrated refusals. Restored: blob == HEAD. Both ablations ran at ca5ad20 with the implementation committed. Direction observed: red, as predicted. The subject resolves to src via relative imports, so no dist leg was owed. SPEC SUITE: vitest run --project local -> 'Test Files 584 passed (584) / Tests 17200 passed | 1 todo' (VERDICT command-exit 0). TYPECHECK: pnpm --filter @objectstack/spec typecheck exit 0 (check:test-typecheck OK; --listFiles shows the pin file in tsconfig.test.json and the merge test in tsconfig.scripts.json). REPO subset (conversions-major18-merge, step18-rationale-merge, retired-key-migrate-sentence, view-item-owner-hidden-retirement, view-list-tabs-retirement): 82 passed. Before its fix the merge test read '1 failed | 11 passed' at a4e7143; after it, 12/12. check:generated: all 15 artifacts up to date after a fresh spec build. DERIVED GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 90 commands, all exit 0. The --ran reconciliation reads '90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 90 recorded an exit code)'. Four first exited 3 (PREREQUISITE NOT MET: lint check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) and were green after turbo run build over ./packages/* and ./packages// (71/71 tasks). The derivation printed a stale-tree note: origin/main moved 3 commits touching only scripts/adr-anchors files, none of them this diff's. ESLINT (narrowed run): eslint --no-inline-config --format json over the 7 changed .ts files -> files 7, errors 0, warnings 0. Population is eslint.config.mjs's files glob **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; the config states it never enables type-aware linting, so the diff cannot move an untouched file's verdict. Local dry runs of the PR-body gates on the body as sent: check-changeset-no-major --event -> 'LEVEL AXIS: this PR declares clause-② no', exit 0; check:partof-closing-keyword (PR_BODY) exit 0. check:nul-bytes exit 0 and the control-byte grep was empty. CI, one read at 22:16Z on 06f0798, not waited on: 32 runs — 10 success, 3 skipped, 19 in_progress (Build Core, Check Changeset, Dogfood 1-3/3, Dogfood Verify CLI, Governed Surface Queue Guard, Lint & Repo Gates, Temporal Conformance, Test Core 1-6/6, Type Check x4). NOT MEASURED: the full @objectstack/spec test:repo project (CI owns it), and the cli integration tier (not touched).",
"mcp_calls": "0",
"api_writes": "3 — all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, run success each): pr_create -> POST /repos/objectstack-ai/objectstack/pulls (#20953, draft forced; body 9512 bytes sent, 9512 stored, identical); label-write --assign -> POST /repos//issues/20953/assignees os-justin (read back: assignee os-justin; the labels documentation/size/m/tests/tooling are the labelers', not this write's); post-stamped -> POST /repos//issues/20901/comments (this report). Zero labels written: the dispatch named none, and skip-changeset does not apply. Plus git push to the branch (not REST).",
"open_questions": [
{
"question": "Item 6's vehicle: keep the sibling conversion id form-view-subform-columns-canonicalized (retiredAfter 17.5.0), or fold the walk into field-column-lists-canonicalized as the ruling worded it?",
"options": [
"A — keep the sibling id (as shipped): each retirement's retiredAfter is a true fact, the artifact door rescues a 17.5.0-floor artifact carrying a field subform column on a main still labelled 17.5.0 (measured), and the next release's per-release spec-changes section lists the new id; cost: one more conversion id in the family (the D3 entry names both)",
"B — fold into field-column-lists-canonicalized (move the mapViewPayloads walk into it, delete the sibling): one id, the ruling's literal wording; cost: that entry's census-pinned retiredAfter 17.0.0 is false for the new carrier, so the artifact door refuses what it should rewrite at floor 17.5.0 until the label passes it, and the per-release section never shows the widening"
],
"recommendation": "A, because ADR-0087's 2026-09-30 amendment (maintainer ruling A on #20390) makes retiredAfter a per-entry fact the door keys on, and B reintroduces for this carrier the exact main-vs-last-release refusal that amendment closed; the ruling's intent (a lossless break lands as a retired chain step in the same release) holds either way"
}
],
"out_of_scope_findings": [
"carrier: none — noted, not filed: step 18's rationale (STEP18_RATIONALE) carries no fragment for either #20927 D3 entry (form-view-subform-columns-closed, inline-grid-column-identity-only-currency-scale-refused) nor this conversion; its header asks one fragment per retirement but no gate requires it, so the upgrade guide's step-18 prose is silent on the form-view carrier (carrier: none; recorded in the PR's Acceptance notes)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Ruling on report
5920703403(the follow-up PR #20953) · 2026-09-30T22:19Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5916997003.06f079864a, the head the report names. It has 8 files (+427 / -58), is a draft, is assigned to os-justin, and saysPart of #20901withClause-②: no.form-view-subform-columns-canonicalizedwithretiredAfter: 17.5.0. The seat read ADR-0087 onmain, the amendment at:919-929(Forward conversion never opens on unreleased main: spec still labelled 17.4.0 while main refuses 17.5.0 retirements, so artifacts built by the published 17.4.0 CLI are refused #20390 ruling A): "Every retired conversion carries a requiredretiredAfter… and the door replays an entry E whenfloor < runtimeORfloor <= E.retiredAfter." The new carrier acceptedfieldthrough 17.5.0, butfield-column-lists-canonicalizedis stamped 17.0.0. So the ruling's wording on5919493523("widenfield-column-lists-canonicalized") would have made the door refuse a 17.5.0-floor artifact that it should rewrite. The deviation is the right reading of the ruling's intent: a lossless break lands as a retired chain step in the same release. The seat's prescribed vehicle was wrong in detail.Clause-②: no: accepted. This PR adds a conversion and rewrites text. It adds no key to a published payload, and nothing narrows. The claim'syes (narrowing)described PR feat(spec)!: a form view's subform columns are the inline grid column contract, and an identity-only column is judged as the type it renders (#20901) #20927's scope.conversions-major18-merge.test.tsis outside the dispatched set. The diff turned it red, and the fix derives the neighbour's finding. Accepted as the smallest consequence of the change.STEP18_RATIONALEfragment for the feat(spec)!: a form view's subform columns are the inline grid column contract, and an identity-only column is judged as the type it renders (#20901) #20927 entries or this conversion): noted in the PR's acceptance notes. The at-tier review is asked whether a fragment is owed.needs:contract-reviewgoes on the PR and this card, then the CI wait, then the at-tier review. After this lands, the seat closes finding(spec): an inline grid column is judged on only one of its carriers — FormViewSchema.subforms[].columns is z.array(z.any()), and a currency column's scale reaches no refusal when its type comes from the child field #20901 by hand.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Landed: PR #20953 →
c6b3a01d5d(the follow-up); the card stays open for one rationale fragment · 2026-09-30T23:29Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5916997003.c6b3a01d5d, with one parent (c1602ba429, read withgit rev-list --parents). All 8 files have the same per-filegit patch-id --stableas the reviewed head06f079864aagainst the merge base3fbf3ca617. Review record:5921114874.form-view-subform-columns-canonicalized(retiredAfter: 17.5.0) respellsfield→nameonsubforms[].columns;STEP18_RATIONALEfragment for this family (idform-view-subform-columns-closed, naming the identity-only sibling and the D2 conversion). It goes in as a small follow-up PR, Part of this card.STEP18_RATIONALElives inpackages/spec/src/migrations/registry.ts(≈:5024), the file [Decision] analytics field gate (#20917): an authored cube member whosesqlis an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943 has in flight, so the follow-up is queued BEHIND [Decision] analytics field gate (#20917): an authored cube member whosesqlis an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943 (serial on one file) rather than dispatched now. The card closes when it lands.The family-wide gap (207 of 253 step-18 entries have no fragment of their own) is filed as a separate finding for triage.Corrected 2026-10-01: it was never filed, and it is not a card under the filing gate (noreach:: the generated guide projects major 17 only today). The review's count is entries without a fragment of their own id; fragments are per retirement family, so how many families lack one is not counted. It rides the fragment follow-up PR's Acceptance notes with acarrier:line.migrations/registry.tsis behind [Decision] analytics field gate (#20917): an authored cube member whosesqlis an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943, which is in flight on the same file.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Dispatched: the rationale-fragment follow-up · 2026-10-01
domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5916997003.sqlis an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943's PR feat(spec)!: an analytics cube member's sql is a column reference, and the showcase done rate moves to its dataset (#20943) #20998 landed (5d5e679873), sopackages/spec/src/migrations/registry.tsis free for this card's turn.STEP18_RATIONALEfragment, idform-view-subform-columns-closed. It names the identity-only sibling (inline-grid-column-identity-only-currency-scale-refused) and the D2 conversion (form-view-subform-columns-canonicalized). It is inserted where its id sorts, and itsorderis one above the highest at the dev's base. The registry is regenerated, never edited by hand. Apatchchangeset.Part of #20901.Clause-②: no.claude/issue-20901-rationale-fragment,os-dev,mode:subagent. An at-tier review is owed before enqueue. The card closes by hand when it lands.5921552300is corrected in this act: the family-wide gap it called filed was never filed. Its Acceptance-notes carrier is this PR.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 20901,
"status": "done",
"branch": "claude/issue-20901-rationale-fragment",
"pr": "#21090",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1, the dispatching seat's id (mode:subagent)",
"premise_still_valid": true,
"summary": "Draft PR #21090 (Part of #20901, Clause-②: no, assigned os-justin) adds one STEP18_RATIONALE fragment, id form-view-subform-columns-closed, order 54, inserted between form-view-option-default-retired and list-view-page-mount-retired, plus a patch changeset for @objectstack/spec. Seat facts re-measured at base 5dbeb7d: STEP18_RATIONALE at registry.ts:5024, 53 fragments parsed with the merge test's own element shape, orders 1..53 each used once (max 53), list sorted; zero fragment text hits for subform, formViews or inline grid (the only inlineColumns hit is the #9227 fragment). The list sits OUTSIDE the generated markers (registry.ts header :19-43; build-migration-registry.ts writes only marked regions), so the fragment is hand-written and gen:migration-registry is a byte-for-byte no-op on it. That differs from the dispatch note's 'The registry is regenerated, never edited by hand' (5924906441); I followed the file header. PR #21053 changed only the generated semantic region (one hunk at :8460). It landed on main as 11d28c1 while I worked, so I merged origin/main dff98c1 with scripts/pm/os-regen-merge.sh (merge commit 8ebaf23, no rebase). The delta vs main is still exactly 2 files, +41/-0, and the sibling entry dashboard-widget-dimensionless-multi-measure-refused is present at HEAD. Register: the fragment carries no tracker number, although its neighbours do (for example '(#9227)'). AGENTS.md says author-shown runtime strings carry none, and step 18's rationale is printed by os migrate meta --step. No gate scans the rationale text position either way.",
"fragment_text": "It also closes the form view's inline grid columns:
subforms[].columns, onview.formand onformViewsentries, wasz.array(z.any())while a relationship field'sinlineColumnswas already the strictInlineGridColumnSchema, so a mis-keyed column published clean and drew a blank grid column, andscaleon a currency column, which the other carrier refuses under the maintainer's rulings of 2026-09-23 (option B) and 2026-09-24 (option 乙), published green. The carrier now references that schema, so both carriers are judged by it, with its own prescriptions. The D2 conversionform-view-subform-columns-canonicalizedrespells a{ field }column as{ name }, the respellingfield-column-lists-canonicalizedmakes oninlineColumns: it rewrites stored rows and assembled artifacts and lists the edit underos migrate meta, and it is retired from the load path, so an author writingfieldmeets the refusal. A view saved with a failing column is refused with the column schema's prescription, and a stored row carrying one is diagnosed at rehydration; neither is stripped, because which column an unknown key or a mixedfield/nameentry meant is the author's call, and a conversion that dropped the key would accept at load what the parse now refuses. Its D3 record is the semantic entryform-view-subform-columns-closed. On both carriers, the reach ofinline-grid-column-currency-scale-refusedextends to a column that declares notype: such a column takes its type from the child field, which the column schema cannot see, when the console hydrates it, sodefineStack's cross-reference check re-parses a column whosenameis acurrencyfield of the child object as the type it renders as, and the refusal of itsscaleis the column schema's own. Reach: the child object must be declared in the same stack; a column naming no field of it, or a subform whose child object comes from another package, is not judged there. It has no D2 conversion, for the declared-type entry's reason: deleting the key is the migration, and a conversion that dropped it would accept it at load, the grace window ruling B refused. Its D3 record is the semantic entryinline-grid-column-identity-only-currency-scale-refused.","fragment_sentences_and_sources": [
{
"sentence": "It also closes the form view's inline grid columns:
subforms[].columns, onview.formand onformViewsentries, wasz.array(z.any())while a relationship field'sinlineColumnswas already the strictInlineGridColumnSchema, so a mis-keyed column published clean and drew a blank grid column, andscaleon a currency column, which the other carrier refuses under the maintainer's rulings of 2026-09-23 (option B) and 2026-09-24 (option 乙), published green.","rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:6-7 (z.array(z.any()); inlineColumns strict InlineGridColumnSchema), :17 (surface: view.form and view.formViews.KEY subforms columns), :26-29 (mis-keyed column published clean, blank grid column; scale on a currency column refused by the other carrier under 2026-09-23 option B and 2026-09-24 option 乙, published green)"
},
{
"sentence": "The carrier now references that schema, so both carriers are judged by it, with its own prescriptions.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:8-9 (carrier now REFERENCES that schema, both carriers judged by one contract), :30-31 (every rule it holds applies here too, with its own prescription); packages/spec/src/ui/view.zod.ts:4399 columns: z.array(InlineGridColumnSchema)"
},
{
"sentence": "The D2 conversion
form-view-subform-columns-canonicalizedrespells a{ field }column as{ name }, the respellingfield-column-lists-canonicalizedmakes oninlineColumns: it rewrites stored rows and assembled artifacts and lists the edit underos migrate meta, and it is retired from the load path, so an author writingfieldmeets the refusal.","rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:9-11 (the one mechanical respelling field to name is this D2 conversion, the respelling field-column-lists-canonicalized makes on inlineColumns), :31-33 (rewrites stored rows and assembled artifacts, lists the edit under os migrate meta, an author writing field meets the refusal); packages/spec/src/conversions/registry.ts:7634 (field 'x' to name 'x', every other key kept), :9878-9880 (id, toMajor 18, retiredFromLoadPath: true), :9872 (the parse refuses field with the prescription)"
},
{
"sentence": "A view saved with a failing column is refused with the column schema's prescription, and a stored row carrying one is diagnosed at rehydration; neither is stripped, because which column an unknown key or a mixed
field/nameentry meant is the author's call, and a conversion that dropped the key would accept at load what the parse now refuses.","rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:12-14 (a view saved with a failing column is refused with the column schema's own prescription; a stored row carrying one is diagnosed at rehydration; neither is stripped), :34-35 (which column an unknown key or a mixed field/name entry meant is the author's call; a conversion that dropped the key would accept on every load what the parse now refuses)"
},
{
"sentence": "Its D3 record is the semantic entry
form-view-subform-columns-closed.","rests_on": "packages/spec/src/migrations/entries/semantic/18.form-view-subform-columns-closed.ts:16 (id: 'form-view-subform-columns-closed')"
},
{
"sentence": "On both carriers, the reach of
inline-grid-column-currency-scale-refusedextends to a column that declares notype: such a column takes its type from the child field, which the column schema cannot see, when the console hydrates it, sodefineStack's cross-reference check re-parses a column whosenameis acurrencyfield of the child object as the type it renders as, and the refusal of itsscaleis the column schema's own.","rests_on": "packages/spec/src/migrations/entries/semantic/18.inline-grid-column-identity-only-currency-scale-refused.ts:5-6 (the reach of inline-grid-column-currency-scale-refused extended to the column that declares no type), :7-8 (takes its type from the child field when the console hydrates it), :9-10 (defineStack's cross-reference check re-parses such a column as the type it renders as and reports the column schema's own refusal), :16-18 (surface: inlineColumns and subforms columns, a column declaring no type whose name is a currency field of the child object), :28 (the column schema cannot see the child field)"
},
{
"sentence": "Reach: the child object must be declared in the same stack; a column naming no field of it, or a subform whose child object comes from another package, is not judged there.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.inline-grid-column-identity-only-currency-scale-refused.ts:32-34 (verbatim reach sentence)"
},
{
"sentence": "It has no D2 conversion, for the declared-type entry's reason: deleting the key is the migration, and a conversion that dropped it would accept it at load, the grace window ruling B refused.",
"rests_on": "packages/spec/src/migrations/entries/semantic/18.inline-grid-column-identity-only-currency-scale-refused.ts:11-13 (D3 only, for the reason the declared-type entry gives; a conversion that dropped the key would accept it on every load, the grace window the ruling refused), :20 (DELETE the key, that is the whole migration); packages/spec/src/migrations/entries/semantic/18.inline-grid-column-currency-scale-refused.ts:25 (no grace window per ruling B; NOT mechanically converted); method for 'no D2 conversion': git grep of both ids over packages/spec/src/conversions/registry.ts at 8ebaf23 = 0 hits"
},
{
"sentence": "Its D3 record is the semantic entry
inline-grid-column-identity-only-currency-scale-refused.","rests_on": "packages/spec/src/migrations/entries/semantic/18.inline-grid-column-identity-only-currency-scale-refused.ts:15 (id)"
}
],
"files_changed": [
"packages/spec/src/migrations/registry.ts (+31/-0: the one fragment, hand-written region)",
".changeset/20901-step18-rationale-form-view-subform-columns.md (+10, '@objectstack/spec': patch)"
],
"changeset": "Required by the repo rule that publishing changes take a changeset (AGENTS.md Post-Task Checklist 3; os-dev: a published surface that moved needs one). Measured after the spec build: the new sentence 'It also closes the form view' is in packages/spec/dist/migrations/index.js and index.mjs, and dist is in files[]. Positive control: an existing fragment's text ('It also narrows an analytics cube member') is in the same two files. No gate was observed demanding it: Check Changeset was in_progress at the single CI read. The local dry run of check-changeset-no-major with the PR body as the event exited 0 with 'LEVEL AXIS: this PR declares clause-② no'.",
"tests": "Final head 8ebaf23 (merges origin/main dff98c1), all through os-verify-lock with VERDICT command-exit 0 unless noted. (1) Rationale pins, repo project: step18-rationale-merge + compliance-families-retirement + conversions-major18-merge, 'Test Files 3 passed (3) / Tests 31 passed (31)'. Local project: migrations.test.ts + inline-grid-column-carriers.test.ts, 'Tests 179 passed (179)'. (2) gen:migration-registry wrote registry.ts ('333 semantic, 242 retired-key, 211 retired-def'), and git diff --exit-code was clean, so it is a no-op. (3) After pnpm --filter @objectstack/spec build (a full turbo build of ./packages/* and ./packages//: 71/71 tasks, 1 cached), pnpm --filter @objectstack/spec check:generated printed '✓ All 15 generated artifacts are up to date', including check:upgrade-guide and check:spec-changes. (4) The upgrade-guide generator does not print step 18: build-upgrade-guide.ts:78 loops MIGRATION_SUPPORT_FLOOR+1 to PROTOCOL_MAJOR, PROTOCOL_VERSION is '17.0.0', the guide's only protocol heading is 'Protocol 16 → 17', and it has 0 hits for form-view-subform-columns. So gen:upgrade-guide was not run beyond its check, which is green. (5) Spec local suite: 'Test Files 588 passed (588) / Tests 17318 passed | 1 todo'. Spec typecheck exit 0 ('check:test-typecheck: OK'). Both runs were also green at the pre-merge head 51fceda: 588 files, 17239 passed, typecheck exit 0. (6) node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived at 8ebaf23 with the change set read from git (2 paths vs merge base dff98c1), gave 82 commands, all run with exit codes recorded. --ran printed '✓ dispatch-gates --ran: 82 derived famil(ies) accounted for — 82 run, 0 NOT-MEASURED (a DERIVED zero — all 82 recorded an exit code and none of them is 3)'. In the first pass at 51fceda, three gates exited 3 PREREQUISITE NOT MET (lint check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure). They passed after the package build, and all 82 passed again at 8ebaf23 after the post-merge rebuild. (7) ESLint, narrowed: eslint --no-inline-config --format json packages/spec/src/migrations/registry.ts gave files 1, errors 0, warnings 0 at 8ebaf23. Population: eslint.config.mjs files glob **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. Invariance: eslint.config.mjs:326-328 states it never enables type-aware linting, so this diff cannot change the verdict on an untouched file. The .changeset .md is not in the glob. (8) check:nul-bytes exit 0 (in the derived set). Control-byte grep over both changed files exited 1 (no match), captured before any pipe. (9) Local dry runs of the PR-body gates on the body as sent: check-partof-closing-keyword exit 0; check-changeset-no-major --base dff98c1 --event exit 0. (10) check-governed-merges --branch: NOT governed (0 of 2 paths hit the register), 41 changed lines, under 5000.",
"ablation": "The fragment was removed at committed head 51fceda with node scripts/ablation-replace.mjs --delete in WRAP mode, plus an outer trap restoring HEAD on EXIT/INT/TERM. The anchor was the whole element: 2617 bytes, 1 hit before, 0 after. The blob went from dda2626a17d9 to e186e8ef6570. On disk: marker 'It also closes the form view' count 1 to 0, while the id line stays 1 because the generated D3 entry carries the same id. Under the mutation: step18-rationale-merge + compliance-families-retirement 19 passed, migrations + inline-grid-column-carriers 179 passed, check:upgrade-guide exit 0, check:migration-registry exit 0. Every leg was green, so no pin went red. That was the expected direction: the merge test pins the sort order, kebab ids, order integrity and the joined text only, and nothing pins this fragment by id. I added no pin. Restored: blob == HEAD dda2626a17d9, git diff HEAD empty, git status clean. No dist leg was owed: the merge test imports ../src/migrations/registry relatively, and the guide check runs on src through tsx.",
"ci": "One read of the head 8ebaf23 check-runs at 2026-10-01T05:50:50Z: 32 runs, 32 names after dedupe by name keeping the latest started_at. 10 success (Auto Label, Check Documentation Links, Check PR Size, Flag docs affected by code changes, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, filter). 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)). 19 in_progress (Build Core, Check Changeset, Dogfood Regression Gate 1-3/3, Dogfood Verify CLI, Governed Surface Queue Guard, Lint & Repo Gates, Temporal Conformance, Test Core 1-6/6, Type Check x4). I did not wait on them.",
"governed": "node scripts/pm/check-governed-merges.mjs --branch claude/issue-20901-rationale-fragment on the final file list: '0 of 2 path(s) hit the register', '✅ NOT governed', size 41 ≤ 5000. Head repo NOT MEASURED by --branch, but the PR is internal (head ref is on objectstack-ai/objectstack).",
"acceptance_line_as_written": "The review's count (207 of 253 at 06f0798) and the per-family caveat are kept as given. I verified the clause 'the generated guide projects major 17 only' and it is true. I extended the 'no reach: today' part to stay true: step 18's rationale does reach os migrate meta --step. Its default terminus is CHAIN_TERMINUS_MAJOR = max(PROTOCOL_MAJOR, ...MIGRATION_MAJORS) = 18 (packages/cli/src/commands/migrate/meta.ts:89), it prints hop.rationale at :429-430, and --json hops carry the rationale at :660-668. There a family without a fragment is left out of the paragraph, not misstated. I read this from the source and did not measure it at that door, so no reach: is claimed.",
"mcp_calls": "0",
"api_writes": "3, all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, each run concluded success). pr_create → POST /repos/objectstack-ai/objectstack/pulls (#21090, draft forced, relay run 36821656431; read-back '2388 byte(s) sent, 2388 stored — identical'). label-write --assign → POST /repos//issues/21090/assignees os-justin (relay run 36821694090; read-back assignee os-justin; the labels documentation, size/s and tooling come from the labelers, not this write). post-stamped → POST /repos//issues/20901/comments (this report). Zero labels written: the dispatch named none, and skip-changeset does not apply. Not REST: git push x3 to the branch (empty probe, the fragment commit 51fceda, the merge 8ebaf23). Reads: REST GETs of the card, its comments, comment 5921114874, PR #21090 and its check-runs. One check-runs GET with a mistyped sha answered 422; it was a read only.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the family-wide STEP18_RATIONALE gap (entries without a fragment of their own id; families uncounted). It is recorded in PR #21090's Acceptance notes, with the os migrate meta --step reach nuance above. The declared-type sibling inline-grid-column-currency-scale-refused has no fragment of its own either. This fragment names it as the entry whose reach it extends, and it is not covered beyond that.",
"carrier: #20234 stage 11 (the registry's hand-written parts, queued behind this PR on the same file) · noted, not filed: most neighbouring STEP18_RATIONALE fragments carry tracker numbers in author-shown text ('(#9227)', '(#19992, ...)'), which AGENTS.md says runtime strings must not carry. check:doc-authoring Rule 3 recognises message/guidance/tombstone/describe positions and not a rationale fragment's text:, so no gate reads this population. Not class a/b/c: no wrong answer, no contract text, no metadata trap."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Ruling on report
5925602674(the rationale-fragment follow-up) · 2026-10-01domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5916997003.8ebaf237c4, the head the report names.Part of #20901withClause-②: no.check-governed-merges --pr 21090: NOT governed.STEP18_RATIONALEfragment,form-view-subform-columns-closed, atorder54, inserted where its id sorts. Each sentence cites the two entry sources and the conversion source it rests on.patchchangeset, because the text ships indist/migrations.5924906441said "the registry is regenerated, never edited by hand". The list sits outside the generated markers, as the file header says, so the fragment is hand-written andgen:migration-registryis a no-op on it. The note was wrong in detail; the dev followed the file.needs:contract-reviewgoes on the PR and this card, and the PR is subscribed to this session.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed: PR #21090 →
2742e53709(the last follow-up); the card is donedomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-10-01. Holder of claim5916997003.2742e53709, with one parent (cf0346ec78, read withgit rev-list --parents). Both files have the same per-filegit patch-id --stableas the reviewed head8ebaf237c4against the merge basedff98c1f51. Review record:5925959503(PASS).form-view-subform-columns-closedfragment. It names the closed form-view carrier, the D2 conversionform-view-subform-columns-canonicalized, and the identity-only sibling entry.bee75cebe6;c6b3a01d5d;@object-ui/typesmirror, at the next spec bump.5921552300said. The seat removespm:dispatchedand the assignee in the same act.Generated by Claude Code
feat(spec)!: a form view's subform columns are the inline grid column…
fix(spec): the field → name respelling reaches a form view's subform …
fix(spec): step 18's rationale gains the form-view-subform-columns-cl…
test(spec): the first root-level test file group's titles state each …