Skip to content

Commit 53a5fef

Browse files
test(spec): record why field.format is never offered by a metadata form (#20322)
Part of #19333 Clause-②: no ## What this changes One root `omit` row in the metadata-form reconciliation `LEDGER` (`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`), for `field.format`, under a new fifth reason group, **renderer-owned vocabulary**. The group's head comment states what admits a key to it. All three conditions must hold: 1. the key's `describe()` says the spec declares no vocabulary for its value; 2. every server-side meaning it has is superseded by another key the form already offers; 3. every other word it carries is a display hint, and the renderers that read it declare its vocabulary. The root-coordinate intro comment names this fifth group next to the other four: platform-written, deprecated or legacy alias, declared but not enforced, and the ADR-0010 overlay, which has no rows. The existing three reason classes could not carry the key. Its liveness verdict is `live`, its describe carries no DEPRECATED marker, and authors write it. This is test-only. There is no schema, form, `describe()`, liveness row or generated artefact change. The top-level `zodOnly` assertion stays unwired. That wiring is item 2 of #19333, which waits on the 39 structured keys of #19332. This PR is item 1 of the remainder recorded on #19333 (landing note 5825783183). It takes the seat's scope call, option A (claim 5859116056), which answers the open question in the predecessor report 5825062779. ## The reason, measured on this tree (base `2aa25efb4`) - **(a) The spec declares no vocabulary.** `FieldSchema.format` is a bare `z.string().optional()`: node type `string`, 0 checks, read through the gate's own `subSchemaOf` and `unwrap`. Its describe opens with "Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string". - **(b) The one server-side meaning is superseded, and both forms offer the successor.** - The only server-side reader is `resolveAutonumberFormat` (`packages/spec/src/data/autonumber-format.ts`). It is called by the query engine, the SQL driver and the autonumber lint. It returns `autonumberFormat` first, and falls back to the `format` shorthand only when that key is absent. Probe readings: `{ autonumberFormat: 'A-{0000}', format: 'B-{0000}' }` resolves to `A-{0000}`, and `{ format: 'B-{0000}' }` resolves to `B-{0000}`. - A grep of `packages/*/src` outside spec finds no other read of a field definition's `.format`. The hits are CLI flags, loader stats, serializers and the validation rule's own `format` key. - `field.form.ts` offers `autonumberFormat` at its root (`visibleWhen` autonumber). The object designer's `fields` quick-add grid (`object.form.ts`) offers it as well. Neither offers `format`. Both readings go through the gate's `topLevelFields` and `nestedLists`. - **(c) The display words belong to the renderers.** objectui at the pinned `.objectui-sha` `f8a9d0fb0596` shows this. In `packages/fields/src/index.tsx`, `@object-ui/fields` declares `FORMAT_TO_RENDERER` (phone, tel, telephone, email, url, uri, link, currency, money, percent, percentage), applied only to textual base types. The same file has the date and datetime cells' own style words (`compact`, `relative`, `short`). `plugin-report` (`formatValue.ts`) and `plugin-dashboard` (`recordFields.tsx`) read the key with rules of their own. ## Census: unexplained top-level, non-overlay keys, per type **Instrument.** The gate's own helper block, meaning bytes 0 up to the gate file's first line-start `describe(`, is copied verbatim into a throwaway probe beside the gate and deleted after each run. The probe asserts two controls: LIT (`name` is offered by 17 of 17 forms) and DARK (a fabricated key is offered by 0 forms and declared by 0 schemas). Identity check: the same slicer at the predecessor's landed commit `736c63a85` yields sha256 `7b97432d8408…a50`, which is the sha its report 5825062779 records. Before is `2aa25efb4`, after is `096a8dbab`. | type | root kind | offered | not offered (overlay incl.) | overlay | root ledger rows before → after | unexplained at 736c63a | unexplained before → after | |---|---|---|---|---|---|---|---| | `object` | object | 17 | 26 | 8 | 3 → 3 | 15 | 15 → 15 | | `field` | object | 50 | 24 | 7 | 2 → **3** | 15 | 15 → **14** | | `hook` | object | 14 | 7 | 7 | 0 → 0 | 0 | 0 → 0 | | `report` | object | 13 | 8 | 8 | 0 → 0 | 0 | 0 → 0 | | `dataset` | object | 8 | 8 | 8 | 0 → 0 | 0 | 0 → 0 | | `view` | union | 29 | 57 | 8 | 5 → 5 | 43 | 44 → 44 | | `app` | object | 12 | 10 | 8 | 1 → 1 | 1 | 1 → 1 | | `dashboard` | object | 10 | 8 | 8 | 0 → 0 | 0 | 0 → 0 | | `position` | object | 4 | 8 | 8 | 0 → 0 | 0 | 0 → 0 | | `action` | object | 30 | 15 | 7 | 1 → 1 | 7 | 7 → 7 | | `page` | object | 14 | 9 | 7 | 1 → 1 | 1 | 1 → 1 | | `agent` | object | 15 | 9 | 8 | 1 → 1 | 0 | 0 → 0 | | `tool` | object | 6 | 8 | 8 | 0 → 0 | 0 | 0 → 0 | | `skill` | object | 8 | 8 | 8 | 0 → 0 | 0 | 0 → 0 | | `flow` | object | 13 | 8 | 8 | 0 → 0 | 0 | 0 → 0 | | `permission` | object | 11 | 9 | 8 | 0 → 0 | 1 | 1 → 1 | | `email_template` | object | 13 | 8 | 8 | 0 → 0 | 0 | 0 → 0 | | **all 17** | | | 230 | 132 | 14 → **15** | 83 | 84 → **83** | | **object-rooted (16, all but `view`)** | | | | | | 40 | 40 → **39** | After the row, the object-rooted residue is exactly the 39 keys that 5825062779 lists as carded on #19332, with 0 extra and 0 missing: - `object` (15): access, actions, activityMilestones, external, fieldGroups, highlightFields, indexes, listViews, publicSharing, requiredPermissions, searchableFields, stageField, systemFields, tenancy, userActions - `field` (14): accept, currencyConfig, dependsOn, inlineColumns, inlineEdit, lookupColumns, lookupFilters, readonlyWhen, relatedList, relatedListColumns, requiredPermissions, requiredWhen, storage, visibleWhen - `app` (1): contextSelectors - `action` (7): aria, bodyExtra, description, errorMessage, patch, requiredPermissions, resultDialog - `page` (1): slots - `permission` (1): adminScope Before the row, it was those 39 plus `field.format`. **What moved since the predecessor's reading.** The object-rooted set is identical (40 then, 40 before this row). The all-type count went from 83 to 84, and not-offered-with-overlay from 229 to 230. The only difference is `view.options`, the legacy per-kind bag that the flattened list overlay now declares (`6a4aec71d`). `view.owner` and `view.hidden` still count at the view root. `3cb84d084` retired them on the view-item shape, but the flattened overlay member (`flattenedViewOverlayFields()`) still declares both live, which that change left alone by design. `view` stays outside the top-level direction until its per-arm forms exist. **Re-read on today's `origin/main` `4e0f72e8d`.** I ran the same probe in a throwaway worktree, without the row and then with this commit cherry-picked. The readings are identical to the table: 40 → 39 object-rooted, 84 → 83 all types, and the same per-type numbers. The gate file has not moved on main, so this branch is not merged. ## Ablation - **Tool and run.** `scripts/ablation-replace.mjs` in WRAP mode, run under the verify lock. The anchor is the whole new row (lines 363–369). It hit once, the marker count went 1 → 0, and the blob went `c607298ce3d0` → `d320ed81cb8c`. - **What the wrapped run showed.** It regenerated the probe from the mutated file. The census saw `field.format` again: object-rooted 39 → 40, extra `[field.format]`. The gate stayed green at 54 of 54. That is the measured meaning of "unwired": no assertion reads the top-level direction yet. - **Restore.** The trap-armed restore brought the blob back to `c607298ce3d0`, equal to HEAD, and `git diff HEAD` came back empty. ## Verification (all at `096a8dbab`) - **Census probe and gate.** 2 files, 54 tests passed, under the lock, VERDICT command-exit 0. - **Full spec suite.** `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` passed: 552 files, 16255 tests, 1 todo. - **Spec typecheck.** `pnpm --filter @objectstack/spec typecheck` exited 0: tsc, scripts-typecheck, and test-typecheck holding its pinned debt. - **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 77 commands, and all 77 exited 0. - Four first exited 3 with PREREQUISITE NOT MET: `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt`. - After a closure build (turbo, 71 of 71 successful, under the lock), all four re-ran and exited 0. - `--ran` reports 77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN. - **Lint, narrowed with measurement.** Running `eslint --no-inline-config --format json` on the one changed file gave 1 file, 0 errors and 0 warnings. `--print-config` resolves a config for the file, so it is inside the linted population. The config has no `parserOptions.project` and no `projectService`, and it has 4 rules, none of them type-aware. So this edit cannot move any other file's verdict. ## Changeset None is owed, because the change is test-only and publishes nothing. `npm pack --dry-run` of `@objectstack/spec` lists 2027 files and 0 `*.test.ts`. The positive controls `liveness/field.json` and `src/data/field.zod.ts` are present, and the changed path is absent (`files[]` ships only `src/**/*.zod.ts` from `src`). This repo refuses an empty-frontmatter changeset (`check:empty-changeset`), and `Check Changeset` exempts a PR only through the `skip-changeset` label. So that label is the route here (AGENTS.md Post-Task Checklist step 3). ## Acceptance notes - The ledger's `view` reading block says `owner` and `hidden` "are no longer writable at all". That holds for the view-item shape only. At the view root, the flattened overlay member still declares both, and the census counts them. The block is not edited here, because it is outside this claim's scope. It is for whoever takes the first per-arm view form. - The section comment above the root-coordinate describe block still cites 274 top-level keys, 132 of them overlay. That is a historical figure, and today's reading is 230 / 132. It is not edited here. --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent de091b5 commit 53a5fef

1 file changed

Lines changed: 17 additions & 1 deletion

File tree

‎packages/spec/src/system/metadata-form-zod-reconciliation.test.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,9 @@ const LEDGER: ReadonlyArray<OmitEntry | SubsetEntry> = [
235235
// but not enforced yet. A fourth reason covers far more keys and has no row
236236
// at all: the ADR-0010 provenance/lock overlay, whose one reason is
237237
// `FRAMEWORK_FIELDS` above — a root row naming one of its keys is refused by
238-
// the resolve test below.
238+
// the resolve test below. A fifth, a renderer-owned vocabulary, is read off
239+
// the key's `describe()` and the forms together; its admission test heads
240+
// its group below.
239241
//
240242
// `view` rows are here although `view` is outside the top-level direction
241243
// until its per-arm forms exist: each reason holds on every arm, so none of
@@ -352,6 +354,20 @@ const LEDGER: ReadonlyArray<OmitEntry | SubsetEntry> = [
352354
why: 'declared, not enforced yet — both of its children (`navigate`, `openIn`) carry the liveness verdict `planned`: no console consumer reads the block yet. No offer until it is enforced; whether to offer it then is a ruling for the enforcement, not for this gate',
353355
},
354356

357+
// Renderer-owned vocabulary — a key is admitted here only when all three
358+
// hold: its `describe()` says the spec declares no vocabulary for its value;
359+
// every server-side meaning it has is superseded by another key this form
360+
// already offers; and every other word it carries is a display hint whose
361+
// vocabulary the renderers that read it declare. A control over such a key
362+
// is a free-text box with no contract behind it.
363+
{
364+
kind: 'omit',
365+
type: 'field',
366+
path: ROOT_PATH,
367+
key: 'format',
368+
why: "renderer-owned vocabulary — the schema's own words: `The spec declares NO vocabulary for it and checks nothing but that it is a string` (a bare `z.string()`). Its one server-side meaning is the autonumber pattern shorthand, which `resolveAutonumberFormat` reads only after the canonical `autonumberFormat` (that key wins when both are present), and both forms that author a field already offer `autonumberFormat` on an autonumber: this form at its root, and the object designer's `fields` quick-add grid. Every other word is a display hint the Studio renderers declare and read (the text-cell format-to-renderer map, the date and datetime display styles). A free-text control would invite the guess its describe warns about: `format: 'email'` on an autonumber mints `email1`",
369+
},
370+
355371
// Measured, and deliberately NOT recorded: seven `view` keys with no
356372
// liveness verdict at any coordinate. `liveness/view.json` cannot hold them:
357373
// the liveness walk stops at the union's `container` arm (its `shapeOf`

0 commit comments

Comments
 (0)