Skip to content

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

Closed

Description

@objectstack-fleet

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/main 33b6e8bec)

  1. 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-form subforms[].columns) follows it with the same z.any().
  2. 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.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:spec · area:records · pm:queue. Direction: the form-view carrier references InlineGridColumnSchema, and the hydrated currency column is judged where the child field's type is known

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T17:59Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: packages/spec/src/ui/view.zod.ts and packages/spec/src/data/field.zod.ts ⇒ domain:spec.

    Why p2. Class (c): an unjudged carrier publishes green, including a bogus key, while the same column on its other carrier is refused. It is measured through objectui validate, and the spec accepts the same document.

    Direction.

    1. FormViewSchema.subforms[].columns references InlineGridColumnSchema. ⛔ Not a copy. Its rulings (B on [finding] FieldSchema.scale on a currency field is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629, 乙 on [Decision] Ruling 5791803339 points currency authors at currencyConfig.precision — but nothing reads that key, and removing scale leaves two console faces rendering currency with zero decimals until objectui#10221 lands #19910) apply on both carriers.
      • Clause-②: yes (narrowing), with an ADR-0087 disposition for a stored view whose column fails: refused with guidance, ⛔ never silently stripped.
    2. The hydrated column (identity-only, typed by the child field) cannot be judged by the schema, which sees only the declared type. It is judged at the one place that resolves the child object's fields: the stack's cross-reference check that already resolves childObject. The same refusal (scale on a currency column) is reached through the resolved type. ⛔ No second scale rule.
    • Pins: the card's three columns are refused on the form-view carrier; the identity-only currency column with scale is refused at the cross-reference check; valid columns are the control.

    finding comes off at grading.

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 28 · 2026-09-30T18:10Z
    Session: session_01Sfe5YjBLwB9J3y8fvm2xq1
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-20901-inline-grid-column-carriers
    Worktree: objectstack-issue-20901
    Domain: domain:spec
    Seat: domain:spec#5 (seat post #19357)
    Clause-②: yes (narrowing)
    Scope: triage 5916820410, both parts.

    1. The form-view carrier. FormViewSchema.subforms[].columns references InlineGridColumnSchema. ⛔ It is not a copy: rulings B on [finding] FieldSchema.scale on a currency field is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629 and 乙 on [Decision] Ruling 5791803339 points currency authors at currencyConfig.precision — but nothing reads that key, and removing scale leaves two console faces rendering currency with zero decimals until objectui#10221 lands #19910 apply on both carriers. A stored view whose column fails is refused with guidance through an ADR-0087 disposition. ⛔ It is never silently stripped.
    2. The hydrated column. An identity-only column typed by its child field is judged at the stack's cross-reference check, the one place that already resolves childObject. The same scale-on-currency refusal is reached through the resolved type. ⛔ No second scale rule.
    3. Pins: the card's three columns are refused on the form-view carrier. The identity-only currency column with scale is refused at the cross-reference check. Valid columns are the control.
      File surface (stop on breach and explain it in the report):

    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 20901,
    "status": "done",
    "branch": "claude/issue-20901-inline-grid-column-carriers",
    "pr": "#20927",
    "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 — the dispatching seat's id (mode:subagent)",
    "premise_still_valid": true,
    "summary": "FormViewSchema.subforms[].columns now references InlineGridColumnSchema (the same schema object, pinned by identity), so the card's typed currency+scale column and its zzz_not_a_key column are refused at the view parse with the column schema's own issues. The dispatch's mechanism assumption did not hold: no cross-reference check resolved subforms[].childObject at cb4c31d (validateCrossReferences read only form.data.object; the only childObject resolver was lint's react-page ObjectForm JSX walk), so the lookup is new: packages/spec/src/stack.zod.ts#collectHydratedInlineColumnErrors, called from validateCrossReferences, resolves the child field for every column that declares no type on BOTH carriers (a relationship field's inlineColumns against its owning object, form/formViews subforms[].columns against childObject), and re-parses { ...column, type: 'currency' } through InlineGridColumnSchema when the child field is currency — no second scale rule; the only table is HYDRATED_INLINE_COLUMN_TYPE { currency: 'currency' }, read from objectui fieldTypeToColumnType at the pin. ADR-0087 route: two D3 semantic entries, no D2 conversion — form-view-subform-columns-closed (the carrier closure; the family's one mechanical respelling field→name, conversion field-column-lists-canonicalized, is already retiredFromLoadPath, and every other refused shape is the author's judgment, so a stored view is refused with the schema's prescription, never stripped) and inline-grid-column-identity-only-currency-scale-refused (the reach extension of inline-grid-column-currency-scale-refused; a key-dropping conversion would be the grace window ruling B refused). Serial constraint: entry sources were committed first (b4f2c59) without touching registry.ts; once #20903 (0efbdc3) and #20911 (975b248) were both ancestors of origin/main def279a (merge-base --is-ancestor exit 0 for each), scripts/pm/os-regen-merge.sh merged it (72860fc), then gen:migration-registry wrote the registry hunks (feba1a9, +77/-0, sibling entries connector-sync-keys-retired and integration/Connector:syncConfig counted 1:1 against origin/main).",
    "tests": "All at HEAD feba1a9 unless noted. PINS packages/spec/src/inline-grid-column-carriers.test.ts: 13 passed (reference identity; typed currency+scale refused at subforms.0.columns.1.scale with the ruled first sentence; bogus key named + name required; retired field spelling prescription; identity-only passes the schema; defineStack refuses identity-only currency+scale on form, formViews.entry and inlineColumns with code STACK_CROSS_REFERENCE_INVALID status 422 and the located finding; typed/bogus refused STACK_SCHEMA_INVALID 422; controls on both carriers, undeclared child object, remedy builds). ABLATION 1 (node scripts/ablation-replace.mjs WRAP, view.zod.ts anchor 'columns: z.array(InlineGridColumnSchema).optional()' → 'columns: z.array(z.any()).optional()', anchor x1→x0, blob 7df808d003a2→e32867b8f630): 'Tests 5 failed | 8 passed (13)'; restored blob == HEAD 7df808d003a2, git diff HEAD empty. ABLATION 2 (stack.zod.ts anchor ' errors.push(...collectHydratedInlineColumnErrors(config));' → ' void collectHydratedInlineColumnErrors;', blob 9102d7a03161→dc3590a7fa8d): 'Tests 3 failed | 10 passed (13)' ('expected defineStack to REFUSE this stack, and it accepted it' x3); restored, git diff HEAD empty. Direction observed: red, as expected. The pins import ./ui/view.zod and ./stack.zod relatively, so the subject resolves to src, not dist: no build leg was owed. Both ablations ran at 1b1f4ef (fix committed first). SPEC SUITE: pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 → 'Test Files 584 passed (584) / Tests 17179 passed | 1 todo'. TYPECHECK: pnpm --filter @objectstack/spec typecheck → VERDICT command-exit 0 (check:test-typecheck OK). check:generated → all 15 artifacts up to date (migration-registry, spec-changes, upgrade-guide, docs, authorable-surface, api-surface ...); gen:docs regenerated content/docs/references/{ui/view,api/protocol}.mdx; dropped-refinements.baseline.json gained the 5 new sites of the column superRefine on the form-view carrier (hand-edited ledger, header total 612→617). DERIVED GATES: node scripts/pm/dispatch-gates.mjs --ran → '114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN'; all 114 exit 0 (7 first exited 3 PREREQUISITE NOT MET — lint doc-formula/doc-security-posture, skill-examples, docs-transcript-drift, dual-build-cjs-loads, lean-entry-closure, type-check-debt — and were re-run green after pnpm exec turbo run build --concurrency=2 over ./packages/). ESLINT (proven narrowing): npx eslint --no-inline-config --format json over the 6 changed .ts files → files 6, errors 0, warnings 0; population = eslint.config.mjs files glob **/.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; eslint.config.mjs states it never enables type-aware linting (no parserOptions.project), so the diff cannot move an untouched file's verdict. CONSUMERS: lint validate-predicate-path-refs + lint-liveness-properties + validate-react-page-props 300 passed; dogfood test/expression-conformance.test.ts 7 passed. spec test:repo: 6-file subset (step18-rationale-merge, strictness-ledger x2, schema-tree-freshness, references-banner, retired-key-migrate-sentence) 78 passed; NOT MEASURED: the full @objectstack/spec test:repo project, reason: two runs hit the 590s foreground timeout with zero files reported (no leftover process); CI's @objectstack/spec#test:repo owns it. PUBLIC DOOR: os validate --json (packages/cli bin at feba1a9) on a probe stack: identity-only amount+scale over a currency child → exit 1 code STACK_CROSS_REFERENCE_INVALID located View[0].form.subforms[0].columns[0].scale; typed currency+scale and zzz_not_a_key → exit 1 STACK_SCHEMA_INVALID; valid columns → exit 0 valid:true. CI (one read at feba1a9, deduped by name, 32): 12 success, 2 skipped — Console Pin Gate (paths filter 'console' = .objectui-sha / build-console.sh / check-console-sha.mjs, none touched), Packed-tarball smoke (opt-in: label needs:pack-smoke absent) — and 18 in_progress: Build Core, Build Docs, Dogfood Regression Gate 1-3/3, Dogfood Verify CLI, Lint & Repo Gates, Temporal Conformance, Test Core 1-6/6, Type Check x4. Not waited on.",
    "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 (#20927, draft forced, body 2107 bytes stored identical); label-write --assign → POST /repos//issues/20927/assignees os-justin (read back: assignees os-justin; size/l is the size labeler's); post-stamped → POST /repos//issues/20901/comments (this report). Zero labels written: the dispatch named none and skip-changeset does not apply (the spec publishes). Plus git push to the branch (not REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: c · reach: os validate --json (cli bin at feba1a9) exit 0 valid:true on a probe stack whose page carries an object-master-detail-form with details[0].columns [{ zzz_not_a_key: 1 }, { name: 'amount', type: 'currency', scale: 2 }]; named producer: examples/app-showcase/src/ui/pages/project-workspace.page.ts authors this block's details and its comment invites columns · evidence: packages/spec/src/ui/component.zod.ts ObjectMasterDetailFormPropsSchema details: z.array(z.unknown()) — a THIRD carrier of the inline grid column, unjudged · Seam: spec:ObjectMasterDetailFormPropsSchema.details → renderer:objectui MasterDetailForm details[].columns · route: same family as #20901, recommend a sub-issue of it; closing it needs a strict detail-entry shape, not only a column reference · dedupe words: object-master-detail-form details columns, details z.unknown master-detail, inline grid column third carrier",
    "class: a · reach: os validate --json on the probe stack with valid columns (exit 0, valid:true) warns field-no-consumers 'Verdict: inert — no site of any kind names it' for crm_invoice_line.quantity and crm_invoice_line.amount, both named by form.subforms[0].columns[].name · evidence: packages/lint/src/validate-field-consumers.ts lists 'name' in LITERAL_KEYS, so a column's name is never read as a field reference (the inlineColumns carrier is NOT MEASURED) · dedupe words: field-no-consumers subforms columns, LITERAL_KEYS name inline column, false inert warning grid column",
    "class: c · reach: objectui validate exit 0, measured by the card itself (objectui#11238 head 587c35168) · evidence: objectui packages/types/src/zod/objectql.zod.ts object-form subforms[].columns is z.any() · carrier: the objectui coordination child named by the dispatch; noted, not filed here",
    "carrier: PR #20908 (holds packages/spec/src/data/field.zod.ts) · noted, not filed: InlineGridColumnSchema's scale describe and its superRefine comment ('Reach, deliberately: only the DECLARED type is visible here') now describe only the schema half; defineStack also judges an identity-only column's resolved type since this change — a describe/comment line for whoever next edits that file"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling on the report 5918594163 · 2026-09-30T19:57Z

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5916997003. Draft PR #20927 at feba1a99bd.

    Accepted as delivered:

    Out-of-scope findings:

    1. A third unjudged carrier. ObjectMasterDetailFormPropsSchema.details is z.unknown(), so os validate passes a bogus-key or typed-currency+scale detail column. It is class c with measured reach, and the seat files it as its own card now.
    2. A false field-no-consumers "inert" warning on fields that form.subforms[].columns[].name names, because validate-field-consumers.ts lists name in LITERAL_KEYS. It is class a with measured reach, and the seat files it too.
    3. objectui's mirror (@object-ui/types subforms[].columns: z.any()) is the objectui coordination child the triage named. The seat files it when this lands, because it follows a pin bump.
    4. The field.zod.ts comment ("only the DECLARED type is visible here") now describes only the schema half. It goes to PR docs(spec): field.useGrouping is live at the objectui pin, and its docblock describes the heuristic the renderer uses #20908's holder, who owns that file.

    Next: needs:contract-review goes on the PR and this card, and the at-tier review runs once CI is green.


    Generated by Claude Code

  5. 7 remaining items

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatched: the rationale-fragment follow-up · 2026-10-01

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5916997003.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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, 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."
    ]
    }


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling on report 5925602674 (the rationale-fragment follow-up) · 2026-10-01

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim 5916997003.

    • Verified against GitHub: PR fix(spec): step 18's rationale gains the form-view-subform-columns-closed fragment #21090 is at head 8ebaf237c4, the head the report names.
      • 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:
    • Next:
      • needs:contract-review goes on the PR and this card, and the PR is subscribed to this session.
      • Then the CI wait and the at-tier review.
      • After it lands, the seat closes this card by hand.

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:specpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions