Skip to content

feat(spec): narrow the per-option default key out of the form-view options vocabulary — the object-field face keeps it enforced - #13033

Merged
os-trump merged 2 commits into
mainfrom
claude/issue-12868-formview-option-default-narrow
Aug 28, 2026
Merged

feat(spec): narrow the per-option default key out of the form-view options vocabulary — the object-field face keeps it enforced#13033
os-trump merged 2 commits into
mainfrom
claude/issue-12868-formview-option-default-narrow

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#12868

Executes the maintainer's 2026-08-28 ruling (disposition 甲, carried verbatim on the card; analysis anchor on the objectui side): the per-option default key is narrowed OUT of the form-view options usage ONLY. The enforced object-field face — applyFieldDefaults, its precedence pin, and the alias rows — is untouched.

Hard precondition — census FIRST (part of the ruling)

Run at base 0da7cd2c1 (current origin/main at branch time), BEFORE any narrowing was written:

  • Instrument: TypeScript-AST scan over all 5385 tracked TS/JS files — every options: [...] array element checked for the keys default / isDefault / selected, with ancestor property chains for face classification — plus a structural scan over 412 parsed JSON files and a bracket-span scan over 1266 md/mdx files.
  • Positive control (the instrument reads): 47 TS hits, ALL on the enforced object-field face — the example apps' object definitions (app-crm, app-showcase, app-todo: 31 hits under fields.NAME.select().options), platform-objects/driver-sql/objectql/lint/spec test fixtures, including one isDefault alias hit in the objectql engine-select-option-default suite. The docs scan hit 32 object-field Field.select samples. This is exactly the enforced-usage population the card requires the control to hit.
  • Form-view options hits: ZERO — zero across the 17-file published *.form.ts corpus (all under packages/spec/src), zero in the example apps' form views, zero inline FormView literals anywhere in the tree, zero in JSON, zero in docs.
  • Blind-spot audit: options sites out of literal reach were enumerated (976 — nearly all generic request-option bags, options: as an object). The select-option-shaped remainder was audited by hand: two spread sites (BUILTIN_MEMBERSHIP_ROLE_OPTIONS in sys-invitation / sys-member) both sit on the object-field face; none is form-view.

Census = ZERO, so the narrowing proceeds per the ruling.

The narrowing

  • FormSelectOptionSchema (packages/spec/src/ui/view.zod.ts): an Omit-derivation at the shape level — SelectOptionSchema.shape minus exactly default, property schemas reused by reference so the two faces cannot drift key-by-key. Built through strictObject with its own surface metadata:
    • guidance tombstones for default (full prescription: delete the key; declare the pre-selected choice on the OBJECT definition — field-level defaultValue, or default: true on that field's own options entry, with the enforced precedence stated; house os migrate meta --from 17 sentence) and for the alias spellings isDefault / selected (guidance instead of a rename — an alias must name a key the shape accepts).
    • alias table = the object face's minus the two rows that pointed at default; the editability boundary guidance set rides along unchanged.
    • Rejection prose cites ADR-0049 / spec versions only — compliant with the just-widened doc-authoring Rule 3 (no internal tracker ids in customer-facing text).
  • Reference site: FormFieldBaseSchema.options now takes the narrowed schema. This is the single site where the FormView vocabulary consumed SelectOptionSchema (verified by grep over packages/spec/src/ui); it is NOT in data/field.zod.ts, so the fence never fired.
  • ADR-0087: protocol-18 conversion form-view-option-default-removed (D2, retiredFromLoadPath) strips the key from stored sources through form / named formViews, sections/groups/top-level fields, recursing into nested fields; fixture pins three notices across all three containers. D3: wired into the protocol-18 step's conversionIds + rationale. Changeset carries the FROM-TO mapping and the registration marker; shipped as minor per the launch-window convention for breaking changes (the sibling accept-set narrowings' precedent).
  • Docs projection: the generated reference now shows the form-field option row without default and with the pointer at the object definition; the object-field reference is unchanged.

Fences honoured (verified, not assumed)

  • git diff BASE..HEAD -- packages/spec/src/data/ is EMPTY — SelectOptionSchema (data/field.zod.ts) not edited, alias rows intact, and the on-hold Restart-touch trigger file untouched.
  • applyFieldDefaults and the precedence pin: no diff outside packages/spec + generated artifacts + changeset (12-file diff, listed in the checks below).
  • Object-face acceptance pinned in both directions in the new test file (SelectOptionSchema and a full FieldSchema select field still accept default: true).

Evidence (exits captured pre-pipe; gate verdict lines quoted from logs)

All measurements at commit a258d1a77 (the branch head; the tree has not moved since — every run below cites this sha):

  • Build: pnpm --filter @objectstack/spec buildcheck-dts-emitted: 34/34 declared declaration file(s) present, verify-lock VERDICT command-exit 0.
  • Typecheck: pnpm --filter @objectstack/spec typecheck — tsc + scripts + check:test-typecheck: OK (test layer compiles; debt ledger unchanged), VERDICT command-exit 0. The new test file is inside the compiled test layer (per-file debt gate would have named it otherwise).
  • Tests: full pnpm --filter @objectstack/spec testTest Files 441 passed (441), Tests 11691 passed (11691), VERDICT command-exit 0. Includes the new pin file (14 tests) and the conversion fixture replay (whole-table replay, notice attribution, and the retired-from-load-path zero-notice contract).
  • Reverse verification (ablation), predicted direction fixed BEFORE the run: mutate the reference site back to z.array(SelectOptionSchema) — prediction: the two DOOR pins (refusal through FormFieldSchema/FormViewSchema; nested-fields recursion) flip RED while the direct-schema and derivation pins stay green. Measured: mutation proven on disk (anchor counts 1 to 0 / 0 to 1, diff stat), run = 2 failed | 12 passed — exactly the predicted split; restore proven byte-identical to the HEAD blob (git hash-object = HEAD: blob, ec68b449), post-restore re-run 14 passed (14). No rebuild leg needed: spec's own tests import src relatively, so no dist resolution is in the loop.
  • Consumer sweep, direction stated (DOWNSTREAM dependents of spec, dep closures freshly built via turbo): @objectstack/metadata-protocol tests 144 passed | 2 skipped, @objectstack/lint tests 81 passed (81), dogfood expression-conformance3 passed (the retirement-radius suite). No in-repo reader of a form-view option default exists (grep over all non-spec packages).
  • Gates union derived mechanically: node scripts/pm/dispatch-gates.mjs (no paths passed — the script took the change-set from merge-base itself, stderr header: gate list derived from the tree of 'objectstack-ai/objectstack' at commit a258d1a77). 35 derived rows / 35 unique commands — ALL PASS: the spec artifact family (check:generated fully green incl. check:authorable-surface, check:api-surface, check:docs, check:strictness-ledger, check:spec-changes, check:upgrade-guide, check:migration-registry), the spec source audits (check:liveness, check:empty-state, check:variant-docs, check:skill-examples after building client-react — its first FAIL was PREREQUISITE-NOT-MET, not a red — check:exported-any, check:dual-source-exports), and the 24 repo-root gates incl. check:doc-authoring, check:adr-0087-registration, check:merge-driver, check:nul-bytes. Exits were captured before any pipe in every loop.
  • Artifact readings match the route (whole-def visibility, not enum-value blindness): authorable-surface gains the four ui/FormSelectOption rows (shape minus default); data/SelectOption:default stays (object face); api-surface/export-origins gain the three new exports; strictness-ledger counts +1 strict site in ui/; json-schema.manifest gains the def. No RETIRED_KEYS_BY_MAJOR entry: this route is a strict-shape narrowing via a new derived def — no tombstone key enters any walked shape, so gates (b)/(c) demand nothing and an entry naming the still-live data/SelectOption:default would itself be refused.
  • Console pin: the single per-option .default read in pinned objectui (190fbd01d, metadata-viewer state-machine panel) is typed against that file's own local interface and reads an OBJECT definition's field options — the untouched face. The ui-side derived type is already tombstoned there, so the pin gate has nothing to break.

Downstream and process notes

Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4

Generated by Claude Code


Generated by Claude Code

…cabulary
FormSelectOptionSchema (Omit-derivation of SelectOptionSchema minus 'default')
replaces SelectOptionSchema at the FormFieldBaseSchema.options reference site;
tombstone guidance for 'default' and the alias spellings isDefault/selected;
protocol-18 conversion form-view-option-default-removed strips stored sources;
pin tests both directions; changeset with ADR-0087 registration; regenerated
artifacts (authorable-surface, json-schema.manifest, api-surface,
export-origins, docs references, strictness-ledger counts).
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation protocol:ui tests tooling labels Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 12 documentable anchor(s). ⚠️4 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/export-origins/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 0da7cd2c1114649755b7f9f45a7dbbe34985ac6e.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/export-origins/ui.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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 126 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 0da7cd2c1114649755b7f9f45a7dbbe34985ac6epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 657b6de263a286f1c9f8b056cbef67d87a632c25 — the merge of head a258d1a7766cce41f93c001ffe0c0a0fea77cc4b into base 0da7cd2c1114649755b7f9f45a7dbbe34985ac6e, 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 657b6de263a286f1c9f8b056cbef67d87a632c25 && git checkout 657b6de263a286f1c9f8b056cbef67d87a632c25
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0da7cd2c1114649755b7f9f45a7dbbe34985ac6e a258d1a7766cce41f93c001ffe0c0a0fea77cc4b && git checkout -B drift-repro 0da7cd2c1114649755b7f9f45a7dbbe34985ac6e && git merge --no-ff a258d1a7766cce41f93c001ffe0c0a0fea77cc4b
node scripts/docs-audit/affected-docs.mjs --json 0da7cd2c1114649755b7f9f45a7dbbe34985ac6e

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

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

Labels

documentationImprovements or additions to documentationprotocol:uisize/lteststooling

Projects

None yet

2 participants

@os-trump@claude