Skip to content

feat(spec)!: FormSectionSchema.columns drops default(1) — undeclared section density is the renderer's, declared is the author's ceiling - #23771

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23740-section-columns-no-default
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23740-section-columns-no-default

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23740

Clause-②: no

What

FormSectionSchema.columns (packages/spec/src/ui/view.zod.ts) drops .default(1), per the maintainer ruling recorded on objectstack-ai/objectui#12521 (comment 6204264665): 「不用决策卡,我同意 b) 改协议(推荐): 去掉分节 columns 的 default(1)」.

  • The key stays optional with values 1–4; the string spellings '1'–'4' keep their transform (now parseInt(val, 10)), and undefined passes through. Measured with zod 4.6.5: an absent key parses to an object with no columns key at all (not columns: undefined).
  • The schema gains a docblock and a .describe() stating the two meanings: declared = the author's ceiling (at most that many fields per row where the container is wide enough; collapsed by the renderer on narrow containers); undeclared = the renderer's choice by available width. No pixel or breakpoint value enters the protocol.
  • FormViewSchema.columns is untouched: its describe does not speak to sections.

Files

  • packages/spec/src/ui/view.zod.ts: the schema change.
  • packages/spec/scripts/lib/default-changes.ts: a new 18: [...] row declaring ui/FormSection:columns: 1 → (none). The spec build's default ratchet refused the change until it was declared; its reason explains what moves for a consumer and why no semantic migration entry comes with it. A conversion that writes columns: 1 onto stored sections would bring back the reading the ruling removed.
  • Generated: packages/spec/authorable-defaults/ui.json (the ui/FormSection:columns = 1 line leaves) and content/docs/references/ui/view.mdx (three tables lose default: 1 and gain the describe). check:generated named only check:docs as stale; --fix regenerated it.
  • Docs: content/docs/protocol/objectui/layout-dsl.mdx gets one paragraph on the two meanings.
  • Tests: the pins that asserted a parsed section's columns: 1 now assert the key is absent. None was deleted.
    • packages/spec/src/ui/view.test.ts (plus a new case: declared 1/'2'/4 survive, and 5/'0' are refused)
    • packages/spec/src/ui/view-union-diagnostics.test.ts (the SECTION parse output)
    • packages/spec/src/ui/view-overlay-viewkind-arm.test.ts (byte-identical form overlay output)
    • packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts
  • .changeset/23740-form-section-columns-no-default.md: @objectstack/spec minor, with a BREAKING line, a before/after table of parse outputs, and the ADR-0087 disposition not-required (no-migration-prescription). check:adr-0087-registration accepts that spelling (measured, exit 0).

Evidence

  • Readers of a form section's columns in packages/*/src outside view.zod.ts: 0, re-measured at 2906cd8d2. protocol.ts hand-builds a section with an explicit columns: 2, so it is unaffected.
  • pnpm --filter @objectstack/spec build: exit 0, after the default row was declared.
  • pnpm --filter @objectstack/spec test: 678 files passed, 1 skipped; 20755 tests passed.
  • pnpm --filter @objectstack/spec typecheck: exit 0.
  • @objectstack/metadata-protocol: its dependency closure was built, protocol.graft-folded-form-sections.test.ts passed 46 of 46, and typecheck exited 0.
  • Ablation, run once: node scripts/ablation-replace.mjs put .default(1) back on disk (anchor 1 → 0, blob 3dba72ea → f04fd827). view.test.ts then failed 1 test, FormSectionSchema > should accept minimal form section. The tool restored the file: blob back to the HEAD blob 3dba72ea, and git diff HEAD empty.
  • Derived gates (dispatch-gates --commands, at head 741552c0): 110 derived, 109 ran with exit 0. --ran reconciles 110 of 110.
    • Two gates first exited 3 on prerequisites and passed after the prerequisite was fixed: check-plugin-teardown-shape --self-test (the shallow clone lacked its pinned fixture commit, which was fetched) and check:skill-examples (the client-react closure was built first).
    • NOT MEASURED: check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET. It needs every package's dist/, which takes a repo-wide build. Left to CI.

Acceptance notes


Generated by Claude Code

…ection density is the renderer's, declared is the author's ceiling

Claude-Session: https://claude.ai/code/session_012vwv8sA1ZBBjmphdHWo9i5
Co-authored-by: Claude <noreply@anthropic.com>
…rmSection.columns

Claude-Session: https://claude.ai/code/session_012vwv8sA1ZBBjmphdHWo9i5
Co-authored-by: Claude <noreply@anthropic.com>
…once the default is dropped

Claude-Session: https://claude.ai/code/session_012vwv8sA1ZBBjmphdHWo9i5
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 1 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-defaults/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

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

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

  • content/docs/releases/v15.mdx (via FormSectionSchema (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/authorable-defaults/ui.json) — pages documenting those are invisible to this run
  • 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 — 142 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 29c7157af4fed4738b3da14bbfb008496cbd1f6a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d166e0d479a1fa76a5d1796c87c4d2b438bcdf8a — the merge of head 741552c04bdb6f52d2ffa261db609c365efbab88 into base 29c7157af4fed4738b3da14bbfb008496cbd1f6a, 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 d166e0d479a1fa76a5d1796c87c4d2b438bcdf8a && git checkout d166e0d479a1fa76a5d1796c87c4d2b438bcdf8a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 29c7157af4fed4738b3da14bbfb008496cbd1f6a 741552c04bdb6f52d2ffa261db609c365efbab88 && git checkout -B drift-repro 29c7157af4fed4738b3da14bbfb008496cbd1f6a && git merge --no-ff 741552c04bdb6f52d2ffa261db609c365efbab88

node scripts/docs-audit/affected-docs.mjs --json 29c7157af4fed4738b3da14bbfb008496cbd1f6a

⚠️ 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 29c7157af4fed4738b3da14bbfb008496cbd1f6a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 16, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #23771 @ 741552c04bdb6f52d2ffa261db609c365efbab88 (#23740) · domain:spec seat 1 · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T01:02Z

Reviewer of record. Every item below was checked against GitHub, not against the report (os-dev-report on #23740).

  • Shape:
    • Draft, base main. The first line is Fixes #23740, then Clause-②: no, matching claim 6204858565 and the changeset.
    • 10 files, +97/−12, none governed. Assignee os-elon-musk.
  • The ruling (6204264665), read in the diff: FormSectionSchema.columns drops .default(1) and becomes .optional(). The string transform is guarded (parseInt(val, 10), and undefined passes through).
    • The input accept set is unchanged: 1–4 and '1'–'4' parse as before, and nothing new is accepted or refused.
    • The docblock and describe state the two meanings, ceiling when declared and the renderer's choice when undeclared, with no pixel or breakpoint value.
    • FormViewSchema.columns is untouched.
  • The ratchet row: scripts/lib/default-changes.ts gains an 18 row, ui/FormSection:columns 1 → (none). The spec build's default ratchet refuses an undeclared default change, so this is the declaration the gate requires, not a change to its logic. Its reason quotes the ruling verbatim, with its record id and no tracker number.
  • Pins: four tests that asserted a parsed columns: 1 now assert the key is absent, and none was deleted: view.test.ts, view-union-diagnostics, view-overlay-viewkind-arm and metadata-protocol's graft-folded-form-sections. The reported ablation, putting .default(1) back, reds view.test.ts, and the restore was proven.
  • The report's open question, the seat's call: A, not-required (no-migration-prescription) with the ratchet row as the declaration. A semantic migration writing columns: 1 onto stored sections would re-create the very reading the ruling removed. The dev measured that the gate accepts A.
  • Changeset, sentences checked against the diff: @objectstack/spec minor, with BREAKING. The FROM → TO table matches the three input rows. The remedy says "declare columns: 1 to keep one column", and it says a consumer treats an absent key as "the renderer decides".
  • Docs: layout-dsl.mdx gains one paragraph stating the two meanings, and references/ui/view.mdx is regenerated.
  • CI on this head at this act: in progress. Not yet read as green.
  • Before the queue: a CONTRACT_REVIEW_TIER contract review (face packages/spec/src/**) by an isolated at-tier reviewer, and every check green on the head. spec(record-surface): a field-heavy create/edit flow on desktop derives a bounded modal, not full-screen; mobile keeps full (objectui#12521 letter B) #23734 already landed (941c78ae2e). Once this one lands, the spec release can carry both for console record form: a create/edit dialog with no authored layout opens at 95vw with one column of unequal field widths — a bounded dialog with a two-column field grid and one width per field kind objectui#12521.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 741552c04bdb6f52d2ffa261db609c365efbab88
Local-runs: none
Scope: packages/spec/src/ui/view.zod.ts + packages/spec/scripts/lib/default-changes.ts, packages/spec/authorable-defaults/ui.json, packages/spec/scripts/lib/authorable-defaults.ts (the gate the ledger row feeds, unchanged in the diff), packages/spec/src/ui/component.zod.ts (its FormSectionSchema.columns citation), .changeset/23740-form-section-columns-no-default.md, packages/spec/src/ui/view.test.ts, packages/spec/src/ui/view-union-diagnostics.test.ts, packages/spec/src/ui/view-overlay-viewkind-arm.test.ts, packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts, content/docs/references/ui/view.mdx, and in objectui at the pin 1f23d5b0 the three reader sites the card names (plugin-form SplitForm.tsx, ModalForm.tsx, app-shell metadata-admin SchemaForm.tsx)

Inputs: card #23740 (body and both comments), the ruling record on objectstack-ai/objectui#12521 (comment 6204264665), PR #23771 (body, file list, net diff against main at the head), and the check-runs on the head, read at 2026-10-16T01:08Z. The rest of the diff (docs paragraph, test pins, changeset) is the lane seat's ACCEPT at TIER_DEFAULT; it is read here only where a scoped hunk makes a claim about it.

① Derived judgments

  • Accept set — unchanged, right. FormSectionSchema.columns is still the union of '1' | '2' | '3' | '4' and the literals 1–4, and the key stays optional in the input type (.default(1) made it optional before; .optional() does now). Nothing newly accepted, nothing newly refused; the new view.test.ts case pins 1, '2', 4 kept and 5, '0' refused. The parseInt(val, 10) radix is a no-op on '1'–'4'.
  • Transform — passes undefined through, right. typeof undefined === 'string' is false, so the branch returns the value unchanged, and the cast 1 | 2 | 3 | 4 | undefined names exactly that. Under zod 4 (^4.6.5 at the head) an object key absent from the input whose result is undefined is not written to the output, so an undeclared section parses to no columns key — what all four flipped pins assert ('columns' in result false; the SECTION fixture; the byte-identical form-overlay string; the metadata-protocol stored-form pin). One corner named, not a defect: an explicitly authored columns: undefined keeps its key (zod 4 writes a key that was in the input) and parses to columns: undefined where it parsed to 1 before — inside the BREAKING line's meaning, outside the accept set.
  • The ruling's letter — met. Record 6204264665 binds: drop default(1); key optional with values 1–4; the string spellings keep their transform; both meanings stated (declared = the author's ceiling, collapsed on narrow containers; undeclared = the renderer's choice by available width); no pixel or breakpoint value. The docblock and the .describe() state both meanings and add no breakpoint; FormViewSchema.columns (:4337) is untouched, as the card required.
  • Output type — 1 | 2 | 3 | 4 becomes 1 | 2 | 3 | 4 | undefined; no reader in this repo sees it. Readers of a parsed form section's columns in packages/*/src outside view.zod.ts and outside tests: 0 (objectstack, local checkout at 9bb9370; the card and the PR body measured the same 0 at 2906cd8). The two look-alikes in component.zod.ts — RecordDetailsProps.sections[].columns (:1302) and objectFormSection.columns (:6740) — are separate schemas, already .optional() with no default; the :1302 docblock cites FormSectionSchema.columns only for how the docs generator renders literal unions, which stays true. deriveFieldGroupLayout sections deliberately leave columns out of derivedKeys, so a group section is unaffected. protocol.ts hand-builds its section with an explicit columns: 2.
  • The pinned sibling — not broken by the type move. objectui types a form section through its own @object-ui/types (ObjectFormSection), not the spec's inferred output; at the pin 1f23d5b0 plugin-form reads a section's columns through clampCol(n: unknown): number | undefined and Number(s.columns) || 1, and SchemaForm.tsx through s.columns ?? 1. Every site tolerates an absent key, so Console Pin Gate on main has nothing to trip on; the rendering of an undeclared section is objectui PR test(cli): read back the port the e2e serve child actually bound, and fail loudly on a silent drift #12546's side, as the PR body says.
  • Generated artifacts — right. authorable-defaults/ui.json loses exactly ui/FormSection:columns = 1; references/ui/view.mdx loses default: 1 on the three FormSection tables and gains the describe; the json-schema tree is gitignored (lint.yml), and no export was added or removed, so api-surface/ has nothing to move. Type Check · source gates, the job that runs check:authorable-surface and check:docs, is success on the head. layout-dsl.mdx is a hand-written tree and its one paragraph matches the describe.
  • default-changes.ts — a ledger row, not gate logic. The 18: row (ui/FormSection:columns, from: '1', to: '(none)') is a data entry in DEFAULT_CHANGES_BY_MAJOR; authorable-defaults.ts, where authoriseDefaultChanges and authorisingMajors live, is not in the diff. '(none)' is the gate's own NO_DEFAULT sentinel, already used by ten earlier rows. The base (29c7157a) and the head both carry @objectstack/spec 18.0.0-next.1, so the authorising window is major 18 and the row sits in the major the gate prescribes; its from is re-derived from the baseline and its to from what the build emits on every run, so the row cannot outlive the fact. This is the entry the ratchet requires for a lost default ("gained a value where it had none, or lost one"), the shape the file's own header names as belonging; its reason carries the consumer-facing "what moves, what to write to keep it" and cites the ruling by record id, no tracker number.
  • No ADR is contradicted. docs/adr/ names FormSectionSchema only in ADR-0085 (field-group derivation, which leaves columns alone); no accepted ADR fixes a section's columns default.

② Semver level

  • .changeset/23740-form-section-columns-no-default.md: @objectstack/spec minor with a BREAKING banner, a before/after table of parse outputs, and Clause-②: no. Right for the act: the accept set is unchanged and the public surface gains nothing, so clause ② is no with no direction arm ((narrowing) names a narrowed accept set, which this is not); what breaks is parse output, which the launch-window convention carries by the banner plus the ADR-0087 disposition, never by the level — .changeset/pre.json at the head is mode: pre, tag next, and major stays refused there. feat(spec)! cannot lower it below minor.
  • Clause-②: no agrees across the claim (comment 6204858565), the PR body and the changeset.
  • ADR-0087 disposition not-required (no-migration-prescription): Check Changeset — the job that runs check-adr-0087-registration.mjs and check-changeset-no-major.mjs against the merge base — is success on the head. On substance it is the honest category: no key is removed, renamed or moved; every declared value keeps its meaning; stored rows never carried the parse default (ruling B's storable rule, pinned in the metadata-protocol test), so objectstack migrate meta has nothing to rewrite. The "declare columns: 1 to keep one column" sentence is the ledger row's "what to write to keep it", which the gate's statement-against-statement check did not read as a rewrite prescription.

③ Boundary flags

  • Deviation 1 (surface widened by default-changes.ts): accepted — the ratchet hard-refuses an undeclared default change, so the row is the mechanism the ruled change requires, not scope creep; judged under ① as a ledger entry.
  • Deviation 2 (two more pins flipped: view-union-diagnostics SECTION, view-overlay-viewkind-arm byte-identical control): accepted — both are "tests that assert a parsed section's columns: 1", none deleted, each flip commented in place.
  • Deviation 3 (main not merged; three commits since the branch point, none in packages/spec or metadata-protocol): accepted — the overlap is empty and the queue rebuilds on current main.
  • Deviation 4 (no PR-activity subscription): procedural, no bearing on the contract.
  • Open question (ADR-0087 disposition A vs B): A, answered. B — a semantic entry writing columns: 1 onto stored sections — would materialise the very reading the ruling removed and alter deployed metadata the ruling meant to hand to the renderer; A keeps one strict contract with the ledger row as the declaration, and the gate accepts it.
  • Check-runs on the head at the reading above: Lint & Repo Gates, Build Core, Build Docs, Governed Surface Queue Guard, Check Changeset, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Spec property liveness and every lint lane are success; Test Core (1–6/6) and Type Check · workspace are still in_progress — recorded as in progress, not as a pass; the queue's required set waits on them. Console Pin Gate is skipped on a PR run and reads on main.
  • Nothing escalated.

Implemented-by: claude/issue-23740-section-columns-no-default
Reviewed-by: session_01HjR8RUwTPQ3TgbfKihmxp9

VERDICT: PASS

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/m tests tooling

Projects

None yet

2 participants