Skip to content

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

Merged
os-justin merged 7 commits into
mainfrom
claude/issue-20901-inline-grid-column-carriers
Sep 30, 2026
Merged

os-justin merged 7 commits into
mainfrom
claude/issue-20901-inline-grid-column-carriers

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Closes #20901

Clause-②: yes (narrowing)

Evidence at feba1a99bd: pins packages/spec/src/inline-grid-column-carriers.test.ts 13/13; @objectstack/spec suite 584 files / 17179 tests green; @objectstack/spec typecheck green; 114/114 derived gates exit 0. Ablations, each restored with git diff HEAD empty: reference removed, 5 red; cross-reference call removed, 3 red. os validate on a probe stack: identity-only column, exit 1 STACK_CROSS_REFERENCE_INVALID; typed and bogus columns, exit 1 STACK_SCHEMA_INVALID; valid columns, exit 0.

Acceptance notes

  • No check read subforms[].childObject before this change (validateCrossReferences read only form.data.object), so the child-object lookup is new here.
  • The identity-only check runs in defineStack only. A view saved through the metadata door, or a subform whose child object lives in another package, gets the schema half alone (NOT MEASURED at the save door). objectui's @object-ui/types mirror is still z.any(), and the render-time warning stays the backstop there.
  • field.zod.ts's scale describe still names only the declared-type refusal. PR docs(spec): field.useGrouping is live at the objectui pin, and its docblock describes the heuristic the renderer uses #20908 holds that file.

Generated by Claude Code

… contract, and an identity-only column is judged as the type it renders

FormViewSchema.subforms[].columns references InlineGridColumnSchema instead of
z.array(z.any()). defineStack's cross-reference check re-parses an
identity-only inline grid column over a currency child field as a currency
column, on both carriers, so scale is refused there with the column schema's
own message.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…rency column; regenerate the references and record the carrier's dropped refinement

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…ine grid column carriers

The generated registry regions are not regenerated in this commit: that
waits for a main that carries the two in-flight registry changes.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…-typed build helper

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…id column D3 entries

Written after merging a main that carries both in-flight registry changes.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 6 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/objectui/concept.mdx (via FormViewSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/index.mdx (via FormViewSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/layout-dsl.mdx (via FormViewSchema (symbol, a top-level const))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-3.mdx (via FormViewSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4b1361d239dab4945749ce809f6cfd74b0223ee0 — the merge of head feba1a99bddefae5bd1f9271848add43864aa9db into base 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4b1361d239dab4945749ce809f6cfd74b0223ee0 && git checkout 4b1361d239dab4945749ce809f6cfd74b0223ee0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1 feba1a99bddefae5bd1f9271848add43864aa9db && git checkout -B drift-repro 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1 && git merge --no-ff feba1a99bddefae5bd1f9271848add43864aa9db

node scripts/docs-audit/affected-docs.mjs --json 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: feba1a99bddefae5bd1f9271848add43864aa9db
Local-runs: none

① Derived judgments

Inputs read: card #20901 (body; comments 5916820410 triage, 5916997003 claim, 5918594163 dev report, 5918621378 seat ruling), ruling B 5791803339 on #19629 and ruling 乙 5805782503 on #19910, PR #20927 body and file list, the net diff origin/main...feba1a99bd (merge-base def279a39b; 10 files, +573/−11), and the head's check-runs. Tree facts were read with git show / git grep on origin/main (aaad682dbc) and the head. Nothing was built, run or re-run; the dev's evidence lines are not re-measured here — the check-runs stand for them.

Accept-set and public-surface changes the diff implies

  1. FormViewSchema.subforms[].columns moves from z.array(z.any()) to z.array(InlineGridColumnSchema) (packages/spec/src/ui/view.zod.ts). RIGHT. It is the same schema object, not a copy: imported from ../data/field.zod, the module whose inlineColumns already takes it (field.zod.ts:1483), and the pin packages/spec/src/inline-grid-column-carriers.test.ts asserts element identity with toBe. Every rule the column schema holds lands on the form-view carrier with its own message: an unknown key is named, field / fieldName / key are refused with the prescription naming name (the strictObject alias table), name is required, and scale on a column declaring type: 'currency' is refused with the ruling-B/乙 first sentence (INLINE_GRID_CURRENCY_SCALE_REFUSAL, field.zod.ts:866). Rulings B and 乙 therefore hold on both carriers — triage item 1 and claim item 1 as ordered. Reach: every parse of FormViewSchema — defineStack (views: z.array(ViewSchema), stack.zod.ts:370; form / formViews at view.zod.ts:4811-4813), os validate (loads through defineStack, cli/commands/compile.ts:266), the view metadata type (ViewMetadataSchema, registered in kernel/metadata-type-schemas.ts:112) and the ViewItem form arms. The card's typed-currency-with-scale column and its bogus-key column are refused at the view parse; the identity-only column still passes the schema, correctly (the schema cannot see the child field), and is judged in item 2.

  2. New packages/spec/src/stack.zod.ts#collectHydratedInlineColumnErrors, called from validateCrossReferences on the PARSED stack (result.data, so every column it sees already passed the strict parse). RIGHT, and the lookup resolves correctly on both carriers:

    • inlineColumns: judged against obj.fields of the object that OWNS the relationship field. A master_detail field sits on the child object, so the column names a sibling field of the same object document — the card's own reading of site 2. Pinned at Object 'crm_invoice_line' field 'invoice' inlineColumns[1].scale.
    • subforms[].columns: judged against fieldsByObject.get(subform.childObject) for view.form and every view.formViews entry, which are the container's only form slots (view.zod.ts:4796). Pinned on both slots.
    • No second scale rule: the column is re-parsed as { ...column, type: resolved } through InlineGridColumnSchema.safeParse and every issue that parse raises is reported verbatim under a locating prefix. The only table is HYDRATED_INLINE_COLUMN_TYPE with the one row currency, and the column schema's superRefine has exactly one type-conditional rule (field.zod.ts:957), so the table is complete against the tree. Own-key lookup (hasOwnKey) keeps a column named constructor off the prototype chain. A column declaring a type, a column naming no field of the child, and a childObject the stack does not declare are skipped — all pinned as controls. Envelope STACK_CROSS_REFERENCE_INVALID / 422, pinned.
    • Reach is narrower than the schema half and the diff says so: config.objects only (not artifactObjects, which carry names and no fields), and only where defineStack runs (os validate, os build, the dev loader); the metadata save door gets the schema half alone (PR body, acceptance note 2).
  3. ADR-0087 route: two step-18 D3 entries, no D2. RIGHT under ruling B. A lossless conversion for scale would be a key-dropping load-time grace window, which B refused (「⛔ no alias, ⛔ no grace window」) and which the family's sibling entries field-currency-scale-refused and inline-grid-column-currency-scale-refused already declined for the same reason; an unknown key or a mixed field / name entry is the author's judgment (the field-column-lists-canonicalized conversion leaves exactly those alone by design). No conversion touches the key, so a stored view is never stripped. Registry: gen:migration-registry output, +77/−0 against the merge-base, both hunks byte-identical to their entry sources (mechanically diffed), entry count 328 to 330, each inserted in id order beside its family. The serial constraint held: the registry hunks are the last commit (feba1a99bd), after the merge of def279a39b, which carries feat(spec)!: connector-attached sync leaves the connector — syncConfig / fieldMappings retired, mapping gains the connectorSource pull binding (#20281 stage 1) #20903 (0efbdc3421) and fix(objectql,spec)!: a groupBy on a multi-value field and a count_distinct on a JSON-stored field are refused INVALID_FIELD / 400 at the engine aggregate door, on every driver (#20808) #20911 (975b2481cd); check:generated is green on the head. One route question stays open — ③ item 6.

  4. packages/spec/dropped-refinements.baseline.json hand edit: five new sites (form.subforms.element.columns.element and its formViews and union-arm positions) and the header total 612 to 617; publishedSchemasWithDroppedRefinements stays 212 because every new site sits under a schema already listed. LEGITIMATE. The ledger is hand-edited by design ("no gen: script: a generator would let a new gap be admitted by running a command instead of by a decision"), build-schemas.ts fails until the line moves with the site, and the growth is exactly the column superRefine now reachable by reference through the form-view carrier — z.toJSONSchema() cannot emit a custom check, so each new position is a real dropped site, not a weakening. No refinement was removed or loosened.

  5. Generated references content/docs/references/ui/view.mdx and api/protocol.mdx: regenerated; columns renders the declared column shape instead of any[]. RIGHT; the gate is green.

  6. The pin file: 13 cases (6 schema, 7 defineStack), importing ./ui/view.zod and ./stack.zod from src. The controls include { name: 'amount', type: 'number', scale: 2 } over a currency child field ACCEPTED — the existing contract (a declared type opts the column out of hydration), consistent with the schema message "scale stays valid on a number column".

Author-shown and AI-facing text, tested sentence by sentence (true unless noted)

  • view.zod.ts describe and comment: true. "referenced rather than copied" — pinned. "Until this was a reference the carrier was z.array(z.any())" — origin/main view.zod.ts:4374. "defineStack's cross-reference check judges that resolved type" — true from the moment this PR lands.
  • stack.zod.ts docblocks:
    • "objectui's hydrateColumns (packages/plugin-form/src/deriveMasterDetail.ts, at the .objectui-sha pin) leaves a column that declares a type alone and otherwise sets type: fieldTypeToColumnType(childField.type), whose only currency arm is the currency field type" — UNSOURCED within this review's inputs. The hydration half is sourced in-tree (the InlineGridColumnSchema docblock names the same file and says a declared type opts out); the fieldTypeToColumnType arm claim is a reading of objectui at pin db11afd4 that only the dev made, and the pin-citation gate skips a sha-less citation by design ("skipped, not excused"). If wrong, the consequence is under-reach only — a missing row means an unjudged column, never a false refusal — and the currency row itself is the card's own measured fact.
    • "Resolution is against the stack's own objects, like the view data-source check in validateCrossReferences" — OVER-BROAD. The data-source check also admits artifactObjects and REFUSES an undeclared object ("… which is not defined in objects."); this check reads config.objects only and SKIPS. The likeness is the resolution set, not the disposition; the sentence is exact only for a stack built with no artifactObjects.
    • "a new type-conditional rule on the column adds its row here" — an instruction with no pin behind it: nothing reds if InlineGridColumnSchema gains a second type-conditional rule and this table does not. ③ item 9.
  • D3 entry form-view-subform-columns-closed (runtime strings): surface, replacement and acceptance criteria — true. Reason: "a mis-keyed column published clean and drew a blank grid column" — sourced (INLINE_GRID_COLUMN_HISTORY, field.zod.ts:848); ruling dates 2026-09-23 (B) and 2026-09-24 (乙) — match 5791803339 and 5805782503; "zero authored subforms in the repository … one authored inlineColumns block" — re-measured on origin/main aaad682dbc: 0 authored subforms outside tests and docs (showcase project.view.ts:184 says why), 1 inlineColumns block (examples/app-showcase/src/data/objects/invoice.object.ts:244, seven { name } entries, no scale). "the one mechanical respelling the family had (field → name, conversion field-column-lists-canonicalized) is already retired from the load path" — TRUE of the conversion (conversions/registry.ts:7656, retiredFromLoadPath: true) but OVER-BROAD in what it implies: that conversion walks object FIELDS' inlineColumns / relatedListColumns only, so migrate meta never respells a form-view subform column; on this carrier the author respells by hand, which the replacement text does say. The comment above the entry (not a runtime string): "A stored view whose column fails is refused with the column schema's own prescription, never stripped" — true at the save door (saveMetaItem, 422) and for authored source; at the stored-row rehydration seam the ADR-0087 addendum registers the row anyway under a [metadata_spec_invalid] diagnostic — diagnosed, not refused, not stripped. No runtime string carries a tracker number.
  • D3 entry inline-grid-column-identity-only-currency-scale-refused: surface ("any value, scale: 0 included"; "a column declaring type: 'number', and a column over a field of any other type, keep scale") — true (column.scale !== undefined in the schema; one table row; a declared type is skipped). Replacement — the 乙 remedy, no pointer to currencyConfig.precision; "do not add type: 'number' to keep it on a currency amount" is guidance, not a refusal an author will meet: the schema and the controls ACCEPT that shape today. Reason: "the recommended form" — sourced (field.zod.ts: "The minimal — and recommended — authored entry is identity-only"); "the console ignored it" — card body (objectui PR ci(release): refresh the Version Packages PR on a schedule, not on every push #11238). The reach sentence matches the code. Acceptance criteria — true.
  • Changeset .changeset/20901-inline-grid-column-carriers.md: title with !, Clause-②: yes (narrowing), exactly one adr-0087: registered marker naming both ids (both new in this diff — the gate's registered condition), the **BREAKING** banner with minor under the pre-GA level rule (ADR-0087 amendment of 2026-09-13; check-changeset-no-major refuses major), the FROM → TO table and the one-line fix. Every sentence tests true. "wherever a view is parsed against the spec: defineStack, objectstack validate, and the view metadata type's registered schema (ViewMetadataSchema)" also reaches the ViewItem form arms — under-stated, not false. "The console fills such a column's type from the child field" — card body.
  • PR body: "No check read subforms[].childObject before this change" — OVER-BROAD. os doctor collects it as an object reference (packages/cli/src/commands/doctor.ts:811-812) and lint's react-page walk resolves it (packages/lint/src/validate-react-page-props.ts:784); true as "no defineStack cross-reference check read it", which is what the dev report says. "The identity-only check runs in defineStack only" — true, os validate reaching it through defineStack. "13/13", "584 files / 17179 tests", "114/114", the ablation counts and the os validate exit codes are the dev's own runs, not re-run here. Acceptance note 3 on the field.zod.ts describe — true and routed (③ item 5).
  • Sibling entry already on main, NOT in the diff but falsified in scope by it: packages/spec/src/migrations/entries/semantic/18.inline-grid-column-currency-scale-refused.ts — surface "and on a column that declares no type, is untouched", acceptance "columns declaring no type, keep their scale". True of that entry's own refusal; false as an author's takeaway for an identity-only column over a currency child field from the moment this PR lands, since defineStack then refuses it. The two step-18 entries disagree on that one sentence. ③ item 5.

② Semver level

Clause-②: yes (narrowing)

'@objectstack/spec': minor, title feat(spec)!:, **BREAKING** banner, one ADR-0087 marker — matches what the diff publishes: an accept-set narrowing on a published authoring surface (subforms[].columns) plus a new defineStack refusal; no export added, removed or renamed (check:api-surface green); no other released package changes code. yes is sourced by the generated reference diff — columns moves from any[] to a declared key set, keys newly declared on a published payload; (narrowing) is the arm the gate grades as breaking; both readings owe at least minor, which is declared. major is barred pre-GA. Ruling B's own PR (#19909) declared no (narrowing) for a refusal that declared no new key; the declared key set is the difference here, so the two declarations are consistent. Check Changeset is green on the head.

③ Boundary flags

Dev report 5918594163: open_questions: [] — nothing to answer. No deviations key; the one substantive deviation is stated in its summary and judged as item 1. out_of_scope_findings: four, answered as items 2 to 5. Items 6 to 9 are this review's own.

  1. Premise falsified — the dispatch assumed an existing check resolved subforms[].childObject; none did inside defineStack. ANSWERED: the dev built the resolver in validateCrossReferences, inside the dispatched surface ("the cross-reference check that resolves childObject … expected in packages/spec or packages/lint"; the dev located and named it), and field.zod.ts (held by PR docs(spec): field.useGrouping is live at the objectui pin, and its docblock describes the heuristic the renderer uses #20908) is untouched — the file list confirms. premise_still_valid: true reads as the card's finding still holding, which it does; the mechanism premise did not, and the report says so in words.
  2. Third carrier ObjectMasterDetailFormPropsSchema.details: z.array(z.unknown()) (packages/spec/src/ui/component.zod.ts:4563) — VERIFIED in the tree; class c with measured reach. ESCALATED: its own card in this family. The seat ruling says it files it; not verified by this record.
  3. packages/lint/src/validate-field-consumers.ts LITERAL_KEYS holds 'name' (:285), so a subform column's name never counts as a field reference and field-no-consumers calls a field the grid shows "inert" — VERIFIED in the tree; class a. ESCALATED: its own card in packages/lint. The seat ruling says it files it; not verified here.
  4. objectui mirror @object-ui/types subforms[].columns: z.any() — the coordination child triage named; it follows the pin bump. ESCALATED to the objectui lane; the card body already records it.
  5. field.zod.ts superRefine comment and scale describe now describe only the schema half — routed to PR docs(spec): field.useGrouping is live at the objectui pin, and its docblock describes the heuristic the renderer uses #20908's holder, correct since that file is held. ADDED by this review: the same staleness sits in 18.inline-grid-column-currency-scale-refused.ts (① last bullet), which is NOT held by docs(spec): field.useGrouping is live at the objectui pin, and its docblock describes the heuristic the renderer uses #20908 and lives beside the two entries this PR adds. Required follow-up, owner the domain:spec seat: amend that entry's surface and acceptance sentence to point at inline-grid-column-identity-only-currency-scale-refused, regenerate the registry. Before protocol step 18 ships a docs-only PR is enough; it can also ride the docs(spec): field.useGrouping is live at the objectui pin, and its docblock describes the heuristic the renderer uses #20908 round if the seat prefers one edit to the family. Not held against this head: the end state after applying both entries is right, and the runtime refusal carries the fix.
  6. Route question, ESCALATED to the seat: the launch-window policy asks for a retiredFromLoadPath chain step "when lossless". The field → name respelling on the NEW carrier is lossless and has no chain step (the family's conversion walks object fields only). Mitigating: 0 authored subforms in the repository, deployed metadata NOT MEASURED, and the grid has honoured no field spelling on any carrier since objectui#3951, so no working shape breaks; the D3 replacement text tells the author to respell. The seat decides whether to widen field-column-lists-canonicalized's walk to views[].form / formViews.* subforms[].columns in a follow-up, or record why not. This record does not hold the PR for it.
  7. Console Pin Gate SKIPPED by its paths filter (.objectui-sha, build-console.sh, check-console-sha.mjs untouched): the objectui build against the narrowed FormView type is NOT MEASURED on this head. Low risk — no export removed or renamed (the AGENTS.md pre-merge check is for removals), and objectui judges views through its own mirror. Noted, not held.
  8. Stored views: refused at the save door; diagnosed and registered at rehydration per the ADR-0087 addendum ("reads diagnose, never drop"). Triage's "refused with guidance, never silently stripped" holds at the authoring and save seams; the seat ruling's "A stored view is refused" is that sentence in the ruling's words. No action; recorded so nobody reads the read path as a refusal.
  9. The comment-only instruction "a new type-conditional rule on the column adds its row here" is unpinned. Optional hardening for the seat: a test that enumerates the column schema's type-conditional rules against HYDRATED_INLINE_COLUMN_TYPE.

Check-runs on feba1a99bd (read 2026-09-30T20:16Z; 39 runs, 35 names after dedupe keeping the newest started_at): 31 success, 4 skipped — Auto Label, Check PR Size, Console Pin Gate (paths filter), Packed-tarball smoke (opt-in) (label absent) — 0 failure, 0 still running. Green: TypeScript Type Check (every check:generated gate), Check Changeset, Lint & Repo Gates, Spec property liveness, Governed Surface Queue Guard, Type Check (workspace, source gates, consumer gates, debt ledger), Test Core 1-6 and aggregate, Dogfood Regression Gate 1-3 and aggregate, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Build Core, Build Docs, Check Documentation Links, Flag docs affected by code changes, filter, and the four PR-hygiene guards (same issue, same single-writer path, part-of, card claims branch). No governed surface in the file list.

Implemented-by: claude/issue-20901-inline-grid-column-carriers
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: PASS

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-30T20:21Z · rendered by the seat's at-tier review subagent on this head. The seat read its served tier family from the subagent transcript before posting.


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…`InlineGridColumnSchema`, by reference (objectui#11266) (objectstack-ai#11618)

Fixes objectstack-ai#11266
Clause-②: yes (narrowing)

`@objectstack/spec` 17.6.0 judges `FormViewSchema.subforms[].columns`
with its strict `InlineGridColumnSchema`
(objectstack-ai/objectstack#20927, `bee75cebe6`). The `object-form`
mirror here still held `z.array(z.any())`, so `objectui validate`
accepted columns that `os validate` refuses. This PR makes one column
the spec's schema, by reference, on the zod face and on the TypeScript
face.

Dispatched under claim `5981407176` (`domain:spec`, size S). Session:
`https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ`.

## What changes

- `packages/types/src/zod/objectql.zod.ts`, the `object-form` mirror's
`subforms[]` item: `columns` goes from `z.array(z.any())` to
`z.array(stripImportedDefaults(SpecInlineGridColumnSchema))`, imported
from `@objectstack/spec/data`. It crosses the objectui#8317 import
boundary like every other spec read. The column schema carries no
default, so the crossing is the identity and the member IS the spec's
object. No hand-copied column shape.
- `packages/types/src/objectql.ts`: `ObjectFormSchema`'s
`subforms[].columns` goes from `any[]` to the spec's `InlineGridColumn`
(its `z.input`, the authoring face), by a type-only import.
`ObjectViewSchema['form']` follows, because it picks from
`ObjectFormSchema`.
- Docs: `content/docs/plugins/plugin-view.mdx` and
`packages/plugin-view/README.md`, section "Child records
(master-detail)", wrote `columns: ['product', 'quantity', 'price']`.
Bare strings: `hydrateColumns` reads `col.name`, the spec refuses them,
and with the TypeScript face narrowed that snippet no longer compiles
under `check:doc-snippets`. Both now write `{ name: ... }` objects and
say what a column is. ⚠️ These two files are outside the claim's file
surface. They are added because this change makes them false: the
snippet gate would red on them.
- `.changeset/11266-subforms-columns-mirror.md`: `@object-ui/types:
minor`, with a BREAKING banner, FROM / TO migration lines and the
`Clause-②: yes (narrowing)` line.

## Premise readings (this worktree, base `2abec3a9`, which is
`origin/main` at PR time)

- `pnpm-lock.yaml` resolves `@objectstack/spec` at 17.6.0 only (two
entries, both 17.6.0). The package `packages/types` resolves answers
`17.6.0`.
- `@objectstack/spec/data` exports `InlineGridColumnSchema`.
- `FormViewSchema.safeParse` of a simple form whose
`subforms[0].columns` holds COL:
- COL `{ name: 'qty', bogusKey: 1 }` is refused: one `unrecognized_keys`
issue at `subforms.0.columns.0`, keys `bogusKey`.
  - COL `{ name: 'qty' }` is accepted (the control).
- Control version: 17.5.0, installed from npm into a scratch directory.
Its form view ACCEPTS the bogus key (the `z.any()` era), so the probe
can fire.

## The `currency` + `scale` finding

- A column that DECLARES `type: 'currency'` and carries `scale` (`{
name: 'amount', type: 'currency', scale: 2 }`) is refused by the spec's
`InlineGridColumnSchema` itself, at the zod level: one `custom` issue at
path `scale`. **It is pinned.** Through the mirror the same column is
refused at `form.subforms.0.columns.0.scale`, and the pin asserts that
the message equals the spec schema's own message, read in the same run.
No literal text is pinned.
- An identity-only column carrying `scale` (`{ name: 'amount', scale: 2
}`) is ACCEPTED by the spec's column schema, at 17.5.0 and at 17.6.0.
Its refusal is `defineStack`'s, which resolves `name` through
`childObject` to a `currency` field. The mirror judges a document that
does not carry the child object's fields, so it cannot reproduce that
check. The pin records the mirror ACCEPTING it, so reaching for it later
is a deliberate change. The render-time report from objectui#11238
(`reportCurrencyColumnScale` in `plugin-form`'s `hydrateColumns`) stays
the backstop. No `plugin-form` behaviour changes: patch round 1
(`b1d035bb`) corrects only its prose, see Acceptance note 2.

## The `@objectstack/spec` range

`packages/types/package.json` already reads `^17.6.0` on `origin/main`:
objectui#11438 (PR objectstack-ai#11531, the 17.6.0 lockfile move) raised it. So it
does not move here.

Would this change alone need 17.6.0? No. The mirror reads
`InlineGridColumnSchema` only, and 17.5.0 already exports it with the
same verdicts on the three probes: bogus key refused with
`unrecognized_keys`, typed currency with `scale` refused at `scale`, `{
name }` accepted. What 17.6.0 adds is the spec's own `FormViewSchema`
reading that schema. That is the other door's verdict, not this
mirror's. `check:spec-floors` is green on the built artifact.

## The pin


`packages/types/src/__tests__/object-form-subforms-columns-11266.test.ts`,
17 tests:

- **By reference.** The mirror's column schema is `toBe`
`stripImportedDefaults(SpecInlineGridColumnSchema)`, and `toBe` the spec
object itself.
- **On the tolerant face** (`safeValidateSchema`, which `objectui
validate` runs) **and on the strict authoring face**, through the
object-view `form` slot. That slot is the route an authored document
has: `subforms` is a form-VIEW member, which the `object-form` row
refuses in its `properties` bag.
  - `{ name }` is accepted: the lit control.
- A bogus key is refused: one `unrecognized_keys` issue at the column,
keys `bogusKey`.
- Typed currency with `scale` is refused: one `custom` issue at the
column's `scale`.
  - A bare string is refused: `invalid_type` at the column.
  - Identity-only with `scale` is accepted.
- **The flat mirror** gives the same codes at the same column.
- **One verdict across the two doors.** For each probe column,
`safeValidateSchema` succeeds exactly when the spec's `FormViewSchema`
does, read live.
- **Type level** (judged by `tsc -p tsconfig.test.json` inside
`type-check`): a declared column `Equal`s the spec's `InlineGridColumn`,
and a string column is a `@ts-expect-error`.

**Ablation.** The fix was committed first. The ablation used
objectstack's `scripts/ablation-replace.mjs` in wrap mode, inside
`os-verify-lock`.
- Mutation: anchor `columns:
z.array(stripImportedDefaults(SpecInlineGridColumnSchema)).optional()`
became `columns: z.array(z.any()).optional()`. Anchor count went from 1
to 0, and the blob from `27ae77d7` to `d6b4ac83`.
- Result: **11 failed, 6 passed (17)**. The six that stay green are the
two controls, the two identity-only rows, and the two cross-door rows
whose expected verdict is accept.
- Restore: the blob after restore is `27ae77d7`, the same as the blob at
HEAD, and `git diff HEAD` is empty. The pin rerun: **17 passed**.

## Tests moved

- `imported-defaults-8317.test.ts`: `IMPORTED` gains
`['InlineGridColumnSchema', SpecInlineGridColumnSchema]`. The census
"every symbol the mirrors import is covered by the differential above"
requires it. The row is measured as the identity (no default, no
`z.lazy`).
- No `zod-mirror-parity` `KnownDrift` or `SPEC_DERIVED_PAIRS` row moves:
  - `ObjectFormSchema` was already spec-derived.
- Its `KnownDrift` entry names only the five runtime-slot handler keys.
- The TypeScript and zod faces of `columns` now carry the same type.
Before, it was `any[]` against `z.any()`, so the ledger saw nothing
either way.
- `object-form-unmirrored-members-6152.test.ts`: the `subforms` row has
no columns in either value, keeps its verdicts, and is not edited.

**Repo-wide sweep.** I grepped for `subforms` and `columns` across
tests, docs, JSON and scripts. No test anywhere asserts that `objectui
validate` accepts a bogus column or a typed-currency column with
`scale`, so no pin flips. The two doc examples above were the only
documents writing a column the narrowed faces refuse.

## Gate readings, at head `ce55b465`

- `pnpm exec vitest run packages/types/` (under the lock): `Test Files
355 passed (355)`, `Tests 9492 passed (9492)`.
- `packages/types` `type-check` (the build program, the examples program
and the test program): exit 0.
- The import side of the narrowed TypeScript face: `type-check` for
`@object-ui/plugin-form`, `@object-ui/plugin-view` and
`@object-ui/app-shell` exits 0, and each echoes `type-check: Done`. They
ran against the closure that `check:doc-snippets --build-filter` names,
built with pnpm, not turbo.
- `vitest run packages/cli/` plus the `subforms`-adjacent consumer tests
(`deriveMasterDetail.currencyScale-10783`, `subformHosts`,
`MetadataProvider.merge`): 28 files, 390 tests, all passed.
- `eslint --no-inline-config --format json` over the four touched
TypeScript files: 4 files, 0 errors. The warnings are existing
`no-explicit-any` hits in `objectql.ts` and `objectql.zod.ts`; the pin
file has 0.
- This is a narrowing of `pnpm lint`, and it is declared. Population:
`eslint.config.js` lints `**/*.{ts,tsx}`, so the two edited `.md` /
`.mdx` files are outside it. File count: 4, from the JSON output.
Invariance: the config sets no `parserOptions.project` or
`projectService`, so type-aware linting is off, and this diff cannot
move the verdict on any untouched file. The full run belongs to CI.
- `check:doc-snippets`: 777 of 777 blocks judged, 0 failed.
- Exit 0 for each of: `check:doc-examples`, `check:doc-types`,
`check:doc-fences`, `check:doc-example-ids`,
`check:doc-example-readers`, `docs:check-links`.
- `check:new-line-citations`: 0 new. `check:control-bytes`: OK.
- Changeset checkers, exit 0 for each: `check:changeset-claims`,
`check:pending-changeset-literals`, `changeset:check` (fixed and
no-major), `check-changeset-presence` and `check-changeset-overwrite`.
- Exit 0 for each of: `check:spec-symbols`, `check:spec-floors`,
`check:installed-pin-claims`, `check:phantom-deps`, `check:unused-deps`,
`check:readme-exports`, `check:test-path-roots`,
`check:component-surface-parity`.
- `check:spec-floors` was first refused with `no-artifact` on the
unbuilt `@object-ui/plugin-tree`. I built that package and reran it.
- `check-governed-queue-guard --test` over the seven paths: NOT
GOVERNED.
- **NOT MEASURED locally, left to CI:** the full `pnpm test`, the
tree-wide `pnpm lint`, and `check:published-dist`.
- **Patch round 1, at head `b1d035bb`:** `@object-ui/plugin-form`
`type-check` passes, with its dependency closure built by pnpm, not
turbo. `vitest run` on `deriveMasterDetail.currencyScale-10783.test.ts`
passes, the renamed test among them. eslint over the two touched files
reports no errors; its warnings are existing `no-explicit-any` in code.
`check:new-line-citations` reports no new citation,
`check:control-bytes` passes, and the changeset checkers exit 0. The
readings above are at `ce55b465`; this round touched only the two
`plugin-form` files.

## Acceptance notes

1. **The parse output.** `InlineGridColumnSchema`'s `readonlyWhen` and
`requiredWhen` carry the spec's ExpressionInput pipe, so
`safeValidateSchema` returns a column's string predicate as `{ dialect:
'cel', source }`. Measured: `readonlyWhen: 'record.locked'` comes back
as the envelope. The input document is not changed.
   - This is the spec schema's own output, taken by reference.
- Four other imported crossings already return transformed values. I
counted them by walking `AnyComponentSchema`: a page's `slots.header`
and its `visibleWhen`, `listViews` `exportOptions`, and an action
`params` entry.
- objectui#8347's Q6 = B ("a string stays a string") was ruled for
`BaseSchema.visibleWhen`. Nothing extends it to imported subtrees.
   - The changeset states it. Flagged for the contract review.
2. **Stale prose, fixed in patch round 1 (`b1d035bb`).** Seat ruling: a
PR fixes the sentences it makes false.
- `reportCurrencyColumnScale`'s docblock in
`packages/plugin-form/src/deriveMasterDetail.ts`, the
`deriveMasterDetail.currencyScale-10783` test header and one of its test
names said a subform's `columns` is `z.array(z.any())` in the
`object-form` mirror and in the spec's `FormViewSchema`, and that the
spec does not judge that path.
- They now say both validators judge a declared column with
`InlineGridColumnSchema`. They keep the report's reason: neither zod
face sees an identity-only `{ name, scale }` column whose child field is
a currency (only `defineStack` does, at publish,
objectstack-ai/objectstack#20927), and nothing runs either validator
between a stored or code-built form view and the render.
- Comment and test-name bytes only. The TypeScript printer with
`removeComments` prints `deriveMasterDetail.ts` identically before and
after, and the test file differs only in that one test-name string. The
old test name was referenced nowhere else in the tree.
- Left as written: the released `@object-ui/plugin-form` and
`@object-ui/fields` CHANGELOG text that says the same, which is history.
3. **Looser local copies.** `DrawerForm.tsx` and `ModalForm.tsx` in
`plugin-form` declare their own `subforms` item types with `columns` as
`any[]`. They receive values from the now-typed face, so nothing breaks,
but they are looser than the spec. Carrier: none.

Serial: objectui#11608's PR objectstack-ai#11616 is still open. Its files
(`packages/types/src/index.ts` and the objectui#8347 pin) are disjoint
from these. `origin/main` has not moved since this branch was cut
(`2abec3a9`), so the merge before opening was a no-op.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Leehom <pm@objectstack.ai>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…columns as its own ADR-0087 step, and two step-18 entries read true (objectstack-ai#20901) (objectstack-ai#20953)

Part of objectstack-ai#20901

Clause-②: no

The follow-up the contract review of PR objectstack-ai#20927 owes (review record
`5919009567`, items 5, 6 and 9, and the ① text findings), dispatched by
landing record `5919493523`. objectstack-ai#20901 stays open for the seat to close by
hand.

## What changed

1. **Review item 5, the step-18 entry
`inline-grid-column-currency-scale-refused`.** Its surface said a column
that declares no `type` "is untouched", and its acceptance said such
columns "keep their `scale`". Since `bee75cebe6` that is false for an
identity-only column over a `currency` child field, which `defineStack`
refuses. The surface, the reach sentence of the reason and the
acceptance now say so, and each names the sibling entry
`inline-grid-column-identity-only-currency-scale-refused`. The registry
was regenerated with `gen:migration-registry`.
2. **Review item 6, ruled WIDEN: the `field` → `name` respelling is now
an ADR-0087 chain step on the form-view carrier.** New D2 conversion
`form-view-subform-columns-canonicalized` (protocol 18,
`retiredFromLoadPath: true`, `retiredAfter: '17.5.0'`). It walks every
FORM payload `mapViewPayloads` reaches, which is `form`, each
`formViews` entry, a form view item's `config` and a flattened form
overlay (the stored-row seam wraps a `view` row as `{ views: [row] }` in
any of those spellings). It also walks the assembled `viewItems`
channel. It uses the same respelling rule as
`field-column-lists-canonicalized`, now one shared function
(`respellInlineGridColumns`) called by both entries. An entry already
spelled `name` is left alone, and so is one carrying both keys, exactly
as the existing walk behaves. The D3 entry
`form-view-subform-columns-closed` said "NOT mechanically converted". It
now names this conversion, and its comment no longer says a stored row
is "refused": a stored row is diagnosed at rehydration.
3. **Review item 9.** A pin in `inline-grid-column-carriers.test.ts`
probes `InlineGridColumnSchema` for every (type, key, value) that parses
with the key alone and with the type alone but not with both. Today it
finds exactly `currency` + `scale` (at `0` and `2`), and it asserts that
`defineStack` refuses each such rule on an identity-only column over a
field of that type. A new type-conditional rule without a
`HYDRATED_INLINE_COLUMN_TYPE` row goes red, and the failure message
names the table. `stack.zod.ts` itself is unchanged apart from comments.
4. **The ① text findings, in the files touched above.** `stack.zod.ts`:
the objectui `hydrateColumns` / `fieldTypeToColumnType` claim is now a
historical pin citation (`.objectui-sha` pin `db11afd4967c`, re-read at
that sha: `hydrateColumns` leaves a typed column alone, and
`fieldTypeToColumnType`'s only `currency` arm is `case 'currency'`), and
`check:objectui-pin-citations` lists it. The over-broad "like the view
data-source check" is cut. The instruction "adds its row here" now names
the pin that holds it.

## Route change on item 6: its own entry, not a wider walk inside
`field-column-lists-canonicalized`

The ruling's intent is carried out exactly: a lossless respelling on the
new carrier, landed as a `retiredFromLoadPath` chain step in the same
release. The vehicle differs from the ruling's wording, for one ledger
fact the gates cannot see. `retiredAfter` is one value per entry: "the
last published `@objectstack/spec` whose authoring surface still
accepted the old shape" (ADR-0087 amendment of 2026-09-30, maintainer
ruling A on objectstack-ai#20390). `field-column-lists-canonicalized` is published
with `17.0.0`, and `retired-after.census.test.ts` pins that value, while
the form-view carrier accepted `field` through 17.5.0. The
artifact-ingestion door opens its window per entry by that value.
Measured at this head, with `applyArtifactForwardConversions`, an
artifact declaring `engines.protocol: ^17.5.0`, and runtime label
`17.5.0`:

- verdict `converted-retired-after`;
- the subform column `{ field: 'quantity' }` became `{ name: 'quantity'
}` through the new entry (listed in `replayedRetirements` with
`retiredAfter` `17.5.0`);
- the same `{ field: 'quantity' }` on a relationship field's
`inlineColumns` stayed as authored, because
`field-column-lists-canonicalized` (`17.0.0`) stays closed at that
floor. That is correct for that carrier, and it is what a folded-in
subform walk would have done to the new carrier: the refusal instead of
the rewrite.

A second, smaller reason: the per-release section of `spec-changes.json`
reports conversion ids that are new in a release, so a widened,
already-published id would be invisible there. If the seat still wants
one id, the fold-in is mechanical: move the `mapViewPayloads` walk into
`field-column-lists-canonicalized` and delete the sibling. The cost is
the artifact-door gap above.

## Verification (at `06f079864a`, which carries `origin/main`
`3fbf3ca617`)

- **Pins** `packages/spec/src/inline-grid-column-carriers.test.ts`: 21
passed (13 existing + 8 new).
- **Ablation 1** (`scripts/ablation-replace.mjs`, `registry.ts`: the new
entry's `respellInlineGridColumns(subform.columns, …)` call replaced by
`subform.columns`; anchor x1 → x0; blob `909148b74725` →
`3b07d9f2d145`), run over the pins plus `conversions.test.ts`: `Tests 4
failed | 251 passed (255)`. The red ones are the fixture pair of
`form-view-subform-columns-canonicalized`, the stored-row test in all
three `view` spellings, the `os migrate meta` chain test, and the
two-carriers-one-rule test. The controls (an entry spelled `name`, one
carrying both keys, and the authoring funnel's refusal) stayed green.
Restored: blob == HEAD, `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 is red with "…add
the row to HYDRATED_INLINE_COLUMN_TYPE in stack.zod.ts", and so are the
three existing hydrated refusals. Restored: blob == HEAD.
- Both ablations ran from committed state (`ca5ad202a9`). The subject
resolves to `src` through relative imports, so no dist leg was owed.
- **Spec suite** `vitest run --project local`: `Test Files 584 passed
(584) / Tests 17200 passed | 1 todo`. **Typecheck** `pnpm --filter
@objectstack/spec typecheck`: exit 0 (`check:test-typecheck: OK`;
`tsconfig.test.json --listFiles` includes the pin file). **Repo-project
subset** (major-18 merge, step-18 rationale merge, retired-key migrate
sentence, two view retirement pins): 82 passed. The full `test:repo`
project is left to CI.
- **`check:generated`**: all 15 artifacts up to date after a fresh `pnpm
--filter @objectstack/spec build`.
- **Derived gates** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`: 90 derived, 90 run, all exit 0.
`--ran` reconciliation reports "0 NOT-MEASURED (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 on a
re-run after `turbo run build` over `./packages/*` and `./packages/*/*`.
- **ESLint** (a narrowed run, stated as such): `eslint
--no-inline-config --format json` over the 7 changed `.ts` files reports
files 7, errors 0, warnings 0. The population is `eslint.config.mjs`'s
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. That config never enables
type-aware linting (its own comment: no `parserOptions.project`, no
typed rules), so this diff cannot move the verdict on any untouched
file.

## One test touched outside the dispatched files

`packages/spec/scripts/conversions-major18-merge.test.ts` went red on
this diff. It builds its synthetic retirement pair beside the entry at
the middle of `MAJOR_18_CONVERSIONS`. At the base that neighbour was
`hookTimeoutToTimeoutMs`, which predates the placement rule. With this
entry added, the neighbour is `formViewSubformColumnsCanonicalized`,
which is placed by the rule. The "defined after every other conversion"
variant then reports the neighbour too, because its entry is now
followed by the synthetic one. The expectation now derives that
neighbour finding when, and only when, the neighbour is not exempt. At
the base it reduces to the old single-element list. 12/12 pass.

## Changeset

`@objectstack/spec: patch`. The act adds no exported symbol
(`check:api-surface` green) and no accepted key or value on an authoring
surface: authored sources are refused exactly as before. It rewrites
stored and assembled data that the contract already names (`name`), so
under the "WHICH LEVEL" rule it is a fix and stays `patch`. `Clause-②:
no`, per the gate's definition (a new key on a published payload): the
conversion adds no key, and the step-18 edits are prose. It is not a
narrowing either, since the data-at-rest seams now accept strictly more.

## Acceptance notes

- `retiredAfter: '17.5.0'` is the package label, as
`retired-after.census.test.ts` requires for an unpublished entry. The
census is refreshed after the next publish.
- Step 18's `rationale` gains no fragment. PR objectstack-ai#20927 added none for its
two D3 entries either, and no gate requires one per entry.
- Not changed here: the PR objectstack-ai#20927 body sentence "No check read
`subforms[].childObject`", recorded by the adoption for its next touch;
`packages/spec/src/data/field.zod.ts`, which the dispatch keeps out of
scope; objectstack-ai#20928 and objectstack-ai#20929; and objectui#11266.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ui#11611, objectstack-ai#11614 and objectstack-ai#11619) (objectstack-ai#21800)

Fixes objectstack-ai#21772
Clause-②: no

This moves the bundled Console's objectui pin from `2e818d0b51ec`
(objectstack-ai#21710) to objectui `main` as of the write time,
`9dfaca654311cddd81714153c4f82c241d7cdc54`, which is past `c096f03`. The
Console now carries objectui#11611 (the `ref:dataset` spec-form widget),
objectui#11614 (the grid widget's camelCase keys) and objectui#11619
(the record chrome's picture). Those are the three merges objectstack-ai#21714, objectstack-ai#21768
and objectstack-ai#21765 wait on.

⛔ This is not a release act. Version PR objectstack-ai#21352 is untouched, and no tag,
publish or Release was made. The card's "release first" hold is lifted
by the maintainer's order recorded in the claim:
「浏览器测试已经在运行,还有很多问题在处理,服务端开发不应该被阻塞」.

## Range

- objectui `git ls-remote origin refs/heads/main` read
`9dfaca654311cddd81714153c4f82c241d7cdc54` at 2026-10-05T00:51:29Z, just
before the bump, and the same sha again before this PR was opened.
- `git merge-base --is-ancestor` exits 0 against `9dfaca654311` for all
three carried merges: `b508ac50d9` (objectui#11611), `2abec3a96c`
(objectui#11614) and `c096f03279` (objectui#11619). The objectui clone
is shallow, but exit 0 proves itself there.
- `2e818d0b51ec..9dfaca654311` has 17 commits and 0 merges.
- objectui declared 24 changesets over the range, and all 24 release.
There are 0 release-nothing changesets and 0 commits without a
changeset. None declares `major`. Four carry the author's breaking
annotation, and the highest declared level is `minor`, so the console
changeset is `minor`. These counts come from `scripts/objectui-range.mjs
--from 2e818d0b51ec --to 9dfaca654311 --json` and from the bump's own
digest. They were measured, not copied from the card.
- Four commit subjects carry `!`. `git log --format='%h %s'
2e818d0b5..9dfaca654 | grep -E '^[0-9a-f]+ [a-z]+(\([^)]*\))?!:'` gives
`9db9ff3f9`, `8b14aecbd`, `2abec3a96` and `b403bb36f`.

| objectui commit | landing | changesets | declared disposition |
|---|---|---|---|
| `9dfaca654` | objectui#11615: the default `simple` form draws a
self-describing inline section entry; `ObjectFormSection.fields` gains
the form view's `{ field }` arm | 1, minor, **BREAKING** (for TypeScript
readers) | releasing; declared breaking, answered below |
| `15f67025b` | objectui#11345: the inline grid's default columns derive
through the spec rule `deriveInlineGridColumns` | 1, patch | releasing |
| `4c127cdef` | objectui#11613: `record:related_list` stops requiring
`columns` | 1, minor | releasing; moves the manifest |
| `c096f0327` | objectui#11383 (PR objectui#11619): the record chrome
draws the record's picture from the object's `imageField` | 1, minor |
releasing; smoke below; unlocks objectstack-ai#21765 |
| `6e9090c26` | objectui#11336: an object document's served listViews
count as served views | 1, patch | releasing |
| `9db9ff3f9` `!` | objectui#11266: a form view's `subforms[].columns`
entry is the spec's `InlineGridColumnSchema`, by reference | 1, minor,
**BREAKING** | releasing; declared breaking, answered below |
| `7c9a6b194` | objectui#11340: the docs portal renders the book
resolver's answer | 1, patch | releasing |
| `8b14aecbd` `!` | objectui#11608: `PartialSchema` is retired from
`@object-ui/types` | 1, minor, **BREAKING** | releasing; declared
breaking, answered below |
| `2abec3a96` `!` | objectui#11610 (PR objectui#11614): the grid
widget's eight field-level keys are camelCase; the snake_case spellings
are refused by name | 1, minor, **BREAKING** | releasing; declared
breaking, answered below; unlocks objectstack-ai#21768 |
| `b403bb36f` `!` | objectui#8347: `BaseSchema` loses its index
signature | 1, minor (`Clause-②: yes (narrowing)`, no BREAKING word) |
releasing; declared breaking by its `!`, answered below |
| `fd060f076` | objectui#11605: bound registrations stop requiring
`objectName`, and a node with neither shows a no-object hint | 8, minor
| releasing; moves the manifest |
| `b508ac50d` | objectui#11601 (PR objectui#11611): the `ref:dataset`
spec-form widget, fed by the report inspector's dataset catalog | 1,
minor | releasing; unlocks objectstack-ai#21714 |
| `d2e859936` | objectui#11002: the console asks `GET /usage/storage`
only when the runtime serves `features.storageUsage` | 1, minor |
releasing; smoke below |
| `b92329c89` | objectui#11591: the Organization flows page copy | 1,
patch | releasing |
| `902ebab63` | objectui#11569: `record:line_items` stops requiring
`childObject` | 1, minor | releasing; moves the manifest |
| `278d2444e` | objectui#11546: a node click on a read-only flow canvas
opens the inspector read-only | 1, patch | releasing |
| `b61c116b2` | objectui#11577: `record:details` read mode keeps a
textarea's line breaks | 1, patch | releasing |

## Declared-breaking changes, and how this repo answers each

The ADR-0087 disposition is `not-required (no-migration-prescription)`.
It replaces the bump's `adr-0087: TODO` placeholder in
`.changeset/console-9dfaca654311.md`, and it covers all five entries
below. `check-adr-0087-registration --base origin/main` gives "1
declared-breaking changeset(s), each carrying an ADR-0087 disposition".
`check-changeset-no-major --base origin/main` gives "This diff
introduces no `major` bump".

1. **objectui#11610 / PR objectui#11614 (`2abec3a96`): the grid widget's
eight keys.** `min_rows`, `max_rows`, `allow_add`, `allow_delete`,
`allow_reorder`, `total_field`, `add_label` and `sort_field` become
`minRows` … `sortField`. objectui has no dual read: its zod faces refuse
a snake_case key by name, and its `GridField` draws an inline alert
instead of the grid.
- **This repo, today.** `@objectstack/spec`'s runtime form field
(`buildObjectFormRuntimeField`, `packages/spec/src/ui/component.zod.ts`)
already refuses the eight snake_case keys by name, with guidance that
names objectui#11610. The camelCase keys are not declared there yet.
- **The interim, stated plainly.** Between this PR and objectstack-ai#21768, an
ObjectStack-authored `grid` form field can spell the keys neither way.
Measured on this head's built spec: `object-form` with `customFields: [{
name: 'items', type: 'grid', min_rows: 1 }]` is refused with
`unrecognized_keys` at `customFields.0` and the grid-key guidance. The
same field with `minRows: 1` is refused with `unrecognized_keys` at
`customFields.0`, as an undeclared key. So a `grid` field takes its
`columns` and the widget's own defaults. The refusal's prescription says
these keys "come in once the widget reads a camelCase spelling". At this
pin the widget does, and objectstack-ai#21768 declares the keys and retargets that
prescription.
- **Nothing here carries the retired spelling.** `git grep` over
`examples/` and `packages/` finds no authored snake_case grid key. The
only hits are the spec's own refusal list and its pin test, migration
prose, and one historical note about objectui's internal master-detail
adapter.
2. **objectui#11615 (`9dfaca654`): the `simple` form's inline section
entries.** For TypeScript readers of `ObjectFormSection.fields`, the
type gains the spec's `FormFieldInput` arm. No code in this repo imports
`@object-ui/types`; the only `@object-ui/*` import is
`scripts/gen-sdui-manifest-node.mjs` reading `@object-ui/core` from the
built tree. The behaviour change is that the default `simple` form now
draws an inline `{ name, … }` entry, as the other five form types
already did. The showcase's `object-form` nodes are `wizard`, `drawer`
and `modal` forms whose sections list field names, so none of them draws
differently.
3. **objectui#11266 (`9db9ff3f9`): `subforms[].columns`.** objectui's
validator now judges a column by `@objectstack/spec`'s own
`InlineGridColumnSchema`. This repo has enforced that closed shape since
objectstack-ai#20927. objectui's verdict now matches `os validate`, and the
ObjectStack accept set does not move.
4. **objectui#11608 (`8b14aecbd`): `PartialSchema`.** It leaves
`@object-ui/types`. Nothing in this repo names it (`git grep
PartialSchema`: 0).
5. **objectui#8347 (`b403bb36f`): `BaseSchema` loses its index
signature.** This narrows the TypeScript face of objectui's node types,
and nothing here compiles against them. objectui's zod faces keep their
accept sets for every key except `visibleWhen`. That key widens to the
`{ dialect, source }` envelope this repo's own parse writes.

## The three cards this unblocks

- **objectstack-ai#21714**: objectui#11601's `ref:dataset` widget (PR objectui#11611,
`b508ac50d`) is now in the pinned build, so `report.form.ts`'s
joined-block `dataset` row can declare `widget: 'ref:dataset'`.
- **objectstack-ai#21768**: objectui#11610's camelCase keys (PR objectui#11614,
`2abec3a96`) are now what the pinned widget reads, so the spec's runtime
form field can declare them.
- **objectstack-ai#21765**: objectui#11383's record picture (PR objectui#11619,
`c096f0327`) is now in the pinned build, so `object.imageField`'s
liveness row can move to `live`. The smoke below observes that read in
the browser. The reader is
`packages/components/src/renderers/layout/containers.tsx`, in the
`page:header` record chrome.

## What changed here

- **`.objectui-sha` and `.changeset/console-9dfaca654311.md`.**
`scripts/bump-objectui.sh 9dfaca654311cddd81714153c4f82c241d7cdc54
--no-commit` wrote both, with `OBJECTUI_ROOT` set to the container's
objectui clone after `git fetch origin main`. The range walked
completely without a deepen, and the level was auto-set to `minor`.
- **`sdui.manifest.json` and `scripts/sdui-manifest.record.json`.**
`node scripts/gen-sdui-manifest-node.mjs` regenerated them over the tree
that `pnpm objectui:build` built at the pin. There are 107 components at
both pins, and the sha256 moves from `0ead67c1111d…` to `6f921896ffac…`.
**This time the manifest moves**:
- Eleven inputs lose `required: true` and gain a description. Nine are
`objectName`, on `object-grid`, `list-view`, `object-form`,
`embeddable-form`, `object-master-detail-form`, `object-kanban`,
`object-metric`, `object-chart` and `object-pivot` (objectui#11605). The
other two are `record:related_list` `columns` (objectui#11613) and
`record:line_items` `childObject` (objectui#11569).
- `object-master-detail-form`'s `fields` description is rewritten for
objectui#11615's inline entries.
- Where the spec has a `ComponentPropsMap` row, these inputs are already
optional there. Measured on the built spec: `object-grid`,
`object-form`, `object-kanban`, `object-metric` and
`object-master-detail-form` `objectName`, `record:related_list`
`columns` and `record:line_items` `childObject`. So the manifest now
agrees with the spec rows. The JSX page compile reads the manifest, and
it stops refusing a node whose `dataSource` binding names the object.
- The record moves its pin and `modulesRoot`. objectui's workspace
version stays 17.7.0.
- **`packages/sdui-parser/objectui-lockstep.json`.** `pnpm
gen:sdui-lockstep` re-recorded it against
`OBJECTUI_ROOT=.cache/objectui-9dfaca654311`. It records 214 grammar
lines (blob `0131f27cf86d`), 25 codes and containment predicate
`76c18fb95d1f`. All are unchanged, and objectui's `packages/sdui-parser`
has no diff over the range, so no port is owed.
- **The 54 asserting pin citations in `packages/spec/src`.** They were
re-measured at the new pin, not restamped:
- Each asserting record's anchors were resolved in objectui at
`2e818d0b5`, mapped through `git diff -U0 2e818d0b5 9dfaca654`, and
re-read at the new pin. Each record gains a dated 2026-10-05 hop
sentence and keeps its earlier history.
- In every cited file that changed, the cited lines moved with their
text byte-identical:
- objectui#8347 re-worded docblocks in `ObjectGrid.tsx`,
`ObjectTree.tsx`, `ObjectGantt.tsx`, `ObjectCalendar.tsx`,
`SchemaRenderer.tsx`, `plugin-view/src/ObjectView.tsx`,
`plugin-map/src/index.tsx` and `plugin-gantt/src/index.tsx`.
- In `ObjectKanban.tsx`, objectui#8347 added a private
`GateBoundKanbanSchema` read type, so its fetch, navigation reads and
spread moved +30.
- objectui#11605 touched `plugin-kanban/src/index.tsx`,
`plugin-dashboard/src/index.tsx` and `ElementDataSourceGate.tsx`. The
`object-metric` icon input moved `281` → `299` and is still `{ name:
'icon', type: 'string' }`.
- objectui#11619 moved the `page:tabs` and `page:accordion` icon anchors
in `containers.tsx` by +3.
- objectui#11615, objectui#11266 and objectui#8347 moved
`ObjectKanbanSchema.limit`, `ObjectMapConfigSchema` and
`LIST_VIEW_LOCAL_OVERRIDES` in `objectql.ts` and `objectql.zod.ts`.
- objectui#11605 added `view.noObject` to the `en`, `zh` and `de` locale
packs. Their cited `calendar.configRequired` strings did not change or
move.
- One cited line changed content. `ObjectKanban.tsx:10`, the type
import, gained `SortConfig` beside the `ObjectKanbanSchema` the record
cites.
- Two counts were re-taken by their records' own methods, and both read
the same: the `keyboardNavigation` hit lines (15, against the
`schema.editable` control's 3) and the `ElementDataSourceGate`
occurrences in five `src/index.tsx` shells (0, 3, 3, 3 and 4).
- The three quoted anchors in `ui/view.zod.ts`'s map record redded at
this pin, and each was re-read and re-pointed: `case 'map':` moved
`2299` → `2300`, `ObjectMapConfigSchema` `2370` → `2388`, and
`LIST_VIEW_LOCAL_OVERRIDES` `1419` → `1437`.
- **The six migration entries' corpus counts** were re-taken with `git
grep -o -F`. That method first reproduced every `2e818d0b5` number: 7579
files, `objectstack` 17227, `@objectstack/spec` 7134, `timeout` 1351,
`useState` 2477, `TTL` 182, `tenant` 1317 and `Span` 505.
- The new numbers are 7632 files, 17313, 7186, 1360, 2477, 182, 1318 and
508. The other controls read `RuntimeConfig` 276 → 293, `resourceLimits`
2 → 2, `window` 4175 → 4193, `period` 238, `interval` 195 and `metrics`
374 → 401.
- Every zero is still zero: 98 tokens were checked, the export lists of
the three cited spec files plus every named key. The only non-zero
tokens are the expected `Span` (508) and `SpanSchema` (57). The three
new `Span` hits are `colSpan` in objectui's
`object-form-section-field-entry-11615.test.ts`. Both `resourceLimits`
hits are still prose in `packages/app-shell`.
- `packages/spec/src/migrations/registry.ts` was regenerated with
`gen:migration-registry`.
- **`.changeset/objectui-pin-citations-9dfaca654311.md`** is a
`@objectstack/spec` patch, because the `FormField.span` describe and six
migration descriptions name the pin.
`content/docs/references/ui/view.mdx` was regenerated by
`check:generated --fix`.

No example, test or gate needed adapting, and no code changed outside
generated records, citations and changesets.

## Console build and canaries

`build-console.sh` ran locally under the verify lock, after `turbo run
build --filter=@objectstack/client...`, the same two steps the `Console
Pin Gate` job runs. Exit 0, 9m02s on a shared four-core box. The build
log reports:
- "Bundle canary 'import/jobs' present".
- "Single-zod canary: exactly one zod version literal
{major:4,minor:6,patch:5}".
- "Console bundle carries THIS tree's @objectstack/spec, and only it".
- "@objectstack/console dist ready (64192 KB) from
objectui@9dfaca654311".

`check:console-sha` and `check:console-injection` exit 0 against that
dist.

## Browser smoke: `examples/app-showcase` with the Console built at
`9dfaca654311`

The server ran `pnpm dev -- --fresh --ui --no-watch --compile -p 41877`
with `OS_PORT=41877`, on its own ephemeral DB with the seeded admin.
Headless Chromium (`/opt/pw-browsers/chromium`) drove it, signing in
through the console's own login form, with console messages, page errors
and every 4xx/5xx response captured. Only the PIDs this run started were
stopped.

**No showcase object declares `imageField`, so the record picture needs
one to exist.** `git grep imageField examples` gives 0 hits. To exercise
objectui#11619's read, the smoke made a temporary local edit:
- It added `imageField: 'f_image'` to `showcase_field_zoo`, through
`node scripts/ablation-replace.mjs --hold`.
- It compiled and booted the server.
- It restored the file at once with `--restore`. The tool reports the
blob back to HEAD's `4130858b8f8b` and `git diff HEAD` empty, and `git
status --porcelain` is empty.
- After the run, the showcase `dist/` was restored from turbo's cache
for the clean source: sha256 `cc1034dd9d23…`, as before the smoke. The
stray runtime bundle of the edited compile was deleted.

Nothing of the edit is in this diff. A real `sys_file` was uploaded
through `/api/v1/storage/upload/presigned` → `PUT` → `/upload/complete`
(a 64×64 red PNG) and written to "Specimen — Full"'s `f_image`.

| record page | `[data-record-picture]` | image |
|---|---|---|
| Field Zoo "Specimen — Full" (`f_image` set) | 1, beside the title in
the record header, `rounded-md` (an `image` field, not `avatar`) |
`src="/api/v1/storage/files/b1f4e384-…"`, `alt=""`, loaded,
`naturalWidth` 64 |
| Field Zoo "Specimen — Minimal" (`f_image` empty) | 0 | none, as
objectui#11619 specifies for an empty value |

**Verdict:** at this pin the record header draws the record's picture
from the object's `imageField`, drawing the stored file through
`/api/v1/storage/files/:id`, and draws nothing when the value is empty.
`showcase_task`'s record page is the custom `task-detail` page, which
has no `page:header`, so it draws no record chrome to put a picture in.
That page was smoked too: it renders its path bar, highlights and
sections with 0 page errors.

**Console messages across the run:** 0 page errors. The only failed
loads are a 401 on `GET /api/v1/auth/get-session` (the pre-login probe)
and a 404 for `/favicon.ico`. There is no `/api/v1/usage/storage` 404.
objectstack-ai#21625's and objectstack-ai#21710's smokes both recorded one, and objectui#11002
(`d2e859936`) stopped the request on a runtime that does not serve it.

## Gates and tests (head `4b9519ea23`)

- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 127 commands from the 22-path diff, and all were run
at `4b9519ea23`. `--ran` reports "127 derived, 127 run, 0 NOT-MEASURED,
0 UNRUN", with every exit code recorded.
- Two exited 3 (`PREREQUISITE NOT MET`) on the first pass:
`check:skill-examples` (no `client-react` dist) and
`check:dual-build-cjs-loads` (no dist for 34 packages).
- Both exited 0 after `turbo run build --filter=!@objectstack/docs` (71
of 72 tasks were cache hits), and those are the codes recorded.
- Also exit 0:
- `check:objectui-pin-citations --verify-anchors` with objectui at the
pin: 54 asserting citations match `9dfaca654`, and 7 anchor content
assertions are verified.
- `check:objectui-bump` (20 assertions across 5 cases),
`check:sdui-lockstep`, `check-sdui-manifest`, `check:console-sha`,
`check:console-injection`, `check:migration-registry`.
- `@objectstack/spec check:generated`: all 15 artifacts current after
the `--fix`.
- `pnpm --filter @objectstack/spec exec vitest run` (both projects,
`local` and `repo`): 668 files, 19259 passed, 1 todo. `pnpm --filter
@objectstack/spec typecheck`: exit 0.
- `@objectstack/sdui-parser` test (14 files, 225 passed) and typecheck:
exit 0.
- These suites read the regenerated manifest:
- `@objectstack/lint` test: 119 files, 5627 passed. It declares
`sdui.manifest.json` as a test input.
- `@objectstack/metadata-protocol`
`src/protocol.runtime-authoring-gate.test.ts`: 70 passed.
- `@objectstack/cli` unit tier `src/utils/sdui-manifest.test.ts` and
`test/validate-build-gate-parity.test.ts`: 2 files, 79 passed.
- `test/jsx-gate-manifest-notice.e2e.test.ts` belongs to neither CLI
tier; it runs nightly, so it is NOT MEASURED here and is left to CI.
- `eslint --no-inline-config --format json` on the 15 changed TS files:
15 files, 0 errors and 0 warnings. The lint population is
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` (`eslint.config.mjs:971`). The
config enables no type-aware linting (`:327-328`), so this diff cannot
move a verdict on an untouched file. Repo-wide `pnpm lint` is left to
CI.

## Acceptance notes

- `main` moved two commits past this branch's base (objectstack-ai#21783, objectstack-ai#21780).
Neither touches a path this diff touches, so no merge was made. The
merge queue rebuilds on the current `main`.
- Anchors in records that cite no asserting sha are outside the
pin-citation gate's population and were not re-measured. Several point
into files that changed here: `containers.tsx` anchors in
`ui/component.zod.ts` docblocks with no sha, and `component.test.ts`'s
`plugin-dashboard/src/index.tsx:204` (the `ObjectMetricPropsSchema icon
liveness` test, already reading an unrelated line at `ab1879721595`, as
objectstack-ai#21710 noted).
- After this pin, the spec's snake_case grid-key prescription ("these
come in once the widget reads a camelCase spelling") describes a
condition that now holds. objectstack-ai#21768 retargets it when it declares the
camelCase keys. It is left as is here, because the pin bump changes no
accept set.
- Writes: one draft PR through the relay and the report comment. No
label, no PR assignee, no ready flag and no auto-merge were written,
because this dispatch's write budget names none of them.

---
_Generated by [Claude
Code](https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

2 participants