Repository navigation
test(spec): record why field.format is never offered by a metadata form - #20322
Merged
objectstack-fleet[bot] merged 1 commit intoSep 27, 2026
Merged
Conversation
The one top-level zod-only key that fit none of the reconciliation ledger's recorded-reason groups gets a root omit row under a fifth, renderer-owned-vocabulary group, whose admission test is written at the head of the group. Each premise of the reason was read on this tree: the spec declares no vocabulary for the key (a bare string, and its describe says so), its one server-side meaning is the autonumber shorthand that resolveAutonumberFormat reads only after autonumberFormat, which the field form and the object designer's fields grid both offer, and the remaining words are display hints the renderers declare. Test-only: no schema, form, describe, liveness row or generated artefact changes. Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 27, 2026
objectstack-fleet
Bot
deleted the
claude/issue-19333-field-format-recorded-reason
branch
September 27, 2026 22:00
This was referenced Sep 27, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…etadata form (objectstack-ai#20362) Part of objectstack-ai#19332 Clause-②: no ## What this changes One file changes: `packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`, +314 / -1. It adds 11 root `omit` rows to the reconciliation `LEDGER`, under three new reason classes, and one `describe` block of four admission tests for those classes. No schema, form, `describe()`, liveness row or generated artifact changes. This is the ledger flight of ruling record `5861442317` (letter A on groups G3, G4 and G5; objectstack-ai#19332). The G1 and G2 form-row flights are still to come, which is why this PR says `Part of`. It does **not** wire the top-level `zodOnly` direction. That is objectstack-ai#19333 item 2, which comes after every flight. It also leaves out `action.aria`, because objectstack-ai#20323 is re-grading its liveness verdict. | class (the `why` opens with it) | rows | what admits a row | |:--|:--|:--| | authored through its own editor | `object.actions` (the `action` type), `object.listViews` (the `view` type, per the objectstack-ai#19330 ruling), `page.slots` (the page designer), `object.external` (the import flow) | the ruling's key list; **and**, when the editor is a registered metadata type, the key must store that type's own node | | code-declared platform configuration | `object.tenancy`, `object.systemFields`, `app.contextSelectors`, `action.resultDialog` | the ruling's key list only (see below) | | no registered widget spells this union | `object.stageField`, `field.inlineEdit`, `field.relatedList` | the ruling's key list; **and** the node must be a union with an arm `FormSelectOptionSchema` refuses as an option `value` | Each row's `why` is one sentence. It names the editor, the writers or the union arms, and it cites the ruling record, with the tracker number beside it as a link. ## The admission tests (file lines 1251–1454) These five root reasons were already in the file: platform-written, deprecated alias, not enforced yet, overlay, renderer-owned vocabulary. Each of them is read off the key itself. The three new ones were **ruled**: for each key, the maintainer chose between a form row and a recorded reason. So the first admission is the ruling's own key list, transcribed as `RULED_ROOT_REASONS`. **Test 1: each class holds exactly the keys the ruling put in it.** A row that gives a ruled reason for any other key fails, and so does a ruled key that has lost its row. A key that a form offers later already has to drop its row, through the existing resolve test. The same edit then takes it out of the list. Where this package can read the class off the key's own node, the row must pass that reading too: - **Own editor: `editorTypeOf(type, key)`.** This returns the registered type whose own node the key stores, judged by node identity. It is never judged by name or by overlapping key sets. `object.actions` reads as `action`, because its element is `getMetadataTypeSchema('action')`. `object.listViews` reads as `view`, because its value is the node the `view` container declares under `listViews`. The existing `object.fields` subset row reads as `field`, and it serves as the lit control. For the dark controls, `object.tenancy` and `object.access` read as nothing. `page.slots` and `object.external` also read as nothing, and that is a measurement, not an assumption: the page designer lives in objectui, and the import flow lives in the datasource service. So those two rows rest on the ruling alone. - **Union: `unspellableArmValuesOf(type, key)`.** The node must be a union, and it must have an arm whose value `FormSelectOptionSchema.safeParse` refuses. That refusal is read from the schema, not from a list kept in the test. The lit controls are `stageField`, which returns `[false]`, and `inlineEdit`, which returns `[true, false]`. The dark controls are `object.requiredPermissions`, a union with no such arm, which returns `[]`, and `object.tenancy`, which is not a union. The half that says no widget renders the whole union is objectui's registry, and it rests on the ruling (the objectui readings are below). - **Code-declared platform configuration.** No reading in this package admits it. The four keys' `describe()` text does not say "platform only", and their writers are platform objects declared in other packages. A spec-package test could not read those writers without becoming a cross-package test. So this class is admitted by the ruling's key list alone, and each row names the writers I measured. **One dark control is deliberate: `object.systemFields`.** Its `false` arm passes the union reading (`[false]`), but the ruling put it in platform configuration. The test pins that fact, so the reading alone never decides the class. ## Mechanism assumptions (PM), measured 1. **Population on today's main: holds.** I re-derived it with the gate's own helper block, sliced verbatim into a throwaway probe beside the gate, with the prefix checked byte-identical on disk. The probe was deleted after each run. - Controls: LIT `name` is offered by 17 of 17 forms and declared by 17 of 17 schemas. DARK, a fabricated key, is offered by 0 and declared by 0. - At base `c74de10a9` (PM's `826f3279c` is an ancestor), the object-rooted residue is **39**, the ruling's 39. None were added, removed or retired, and all 11 keys of this flight are in it. The liveness rows are `live`, or drilled with every child `live`. - After the rows, the residue is **28**. Exactly the 11 keys left it, and none came in. All types: 81 → 70 (`view` 42). Root rows: 15 → 26. 2. **Root-coordinate discipline: partly false as stated.** - No existing root reason is admitted by a *test*. The phrase "its admission test heads its group below" refers to the group's head **comment** (the renderer-owned criteria). The three new classes get the file's first real admission tests. - Two of them are partly mechanical, as set out above. The platform-configuration class is not mechanical at all: it rests on the ruling's key list, and I say so plainly. - A reading I tried and dropped: "no registered form row sits over such a union" is **false**. The forms already spell one arm of unions with a boolean arm: `object.fields.unique` (`type: 'boolean'`), `field.unique` (derived), `action.visible` and `action.disabled` (`textarea`), and `dashboard.globalFilters.defaultValue`. So that fact cannot be the class's reading. 3. **Wording: holds.** Each row is one sentence naming the concrete editor, writers or arms, and each cites `5861442317`. ## What the rows rest on, measured Framework readings are at `154da111e`. objectui readings are at the `.objectui-sha` pin `f8a9d0fb0596`, from a shallow clone. HotCRM readings are at `2f7b2326`. - **The page designer exists.** objectui `app-shell/.../inspectors/PageBlockInspector.tsx:387` writes `slots: { ...slotsObj, [slotName]: nextArr }`. - **The import flow exists.** - REST routes: `POST /datasources/:name/external/tables/:remote/draft` and `/import` (`packages/rest/src/external-datasource-routes.ts:31-32`). - The draft writes `external: { remoteSchema?, remoteName }` (`packages/services/service-datasource/src/external-datasource-service.ts:486`, `:931-932`). - Callers: the Studio's `ImportObjectDialog.tsx` and `os datasource introspect`. - **List views are authored through the view type.** HotCRM writes `listViews` in 13 files, and every one is a `*.view.ts`. The examples write them in 9 files, all `*.view.ts`. - **Platform-configuration writers.** Examples and HotCRM author none of the four keys; the HotCRM lit control `fieldGroups` hits 18 files. The code writers are: - `tenancy`: `sys_api_key`, `sys_sso_provider`; - `systemFields`: `sys_metadata_activation` (`{ tenant: false }`); - `contextSelectors`: `studio.app.ts`; - `resultDialog`: actions on `sys_user` (4), `sys_two_factor` (2), `sys_oauth_application` (2), `sys_sso_provider` (1). - **The union bounds.** - `FormSelectOptionSchema` refuses `true` and `false` as an option `value`, and accepts `primary`, `grid` and `form` (measured by `safeParse`). - objectui `WIDGETS` (`widgets.tsx:2909`) registers 20 widgets, and none of them is a union selector. - `resolveUnionBranch` falls back to the first branch in create mode (`SchemaForm.tsx:238-250`). ## Ablation Every leg ran from committed state (`154da111e`, blob `d2fe718f77e5`) through `scripts/ablation-replace.mjs` in WRAP mode, under the verify lock. In every leg, the anchor hit once, the mutation was verified on disk (anchor count and blob change), and the gate plus a probe regenerated from the mutated file ran. The restore was proven each time: blob equal to HEAD, and `git diff HEAD` empty. The subject is imported from `src` by relative path, so there is no `dist/` between the mutation and the run. | leg | residue (28 before) | gate | |:--|:--|:--| | delete the `object.external` row (own editor) | 29, adds `object.external` | 1 red: *each class holds exactly the keys…* | | delete the `action.resultDialog` row (platform config) | 29, adds `action.resultDialog` | 1 red: the same test | | delete the `field.relatedList` row (union) | 29, adds `field.relatedList` | 1 red: the same test | | swap `key: 'actions'` → `'access'` | 28 (`object.actions` in, `object.access` out) | 2 red: the list test, and the own-editor test (`object.access: the ruling names no editor for this key`) | | swap `key: 'tenancy'` → `'access'` | 28 (`object.tenancy` in, `object.access` out) | 1 red: the list test | | swap `key: 'inlineEdit'` → `'readonlyWhen'` | 28 (`field.inlineEdit` in, `field.readonlyWhen` out) | 2 red: the list test, and the union test (`field.readonlyWhen: an option can spell every arm`) | Every other test stayed green on every leg (57 of 58, or 56 of 58). The resolve test stayed green on the swaps, because the swapped-in keys are authorable and unoffered. ## Verification (head `154da111e`) - **Gate and census probe:** 2 files, 58 tests passed, under the lock, `VERDICT command-exit 0`. This ran at `8f31b4a5d`; the gate file's blob there, `d2fe718f77e5`, is byte-identical at `154da111e`, and the ablation legs re-ran the same pair at `154da111e` before each mutation's verdict. - **`@objectstack/spec` build:** exit 0. The tree stayed clean: no `authorable-surface.base.json` or other artifact movement. - **`pnpm --filter @objectstack/spec typecheck`** (tsc, scripts-typecheck and test-typecheck): exit 0. - **`pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`:** 554 files, 16431 passed, 1 todo, exit 0. The gate file is in the `local` project; it is not in `vitest.repo-tests.json`. - **`pnpm --filter @objectstack/spec test:repo`** (`--project repo`, run under the lock as the seat asked): 33 files, 604 passed, `VERDICT command-exit 0`. - **Gates:** - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 78 commands. Each was run with its exit code captured after a redirect, never through a pipe. **74 exited 0.** - Four exited 3, `PREREQUISITE NOT MET` on other packages' `dist/`: `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt`. The objectstack-ai#20064 and objectstack-ai#20322 precedents hit the same four families. - The whole-closure build that would satisfy them queued for about 18 minutes under the shared lock, behind other seats' heavy runs, and never acquired. So these four are **NOT MEASURED** locally, a declared narrowing. They read built output of packages this test-only diff does not touch, and CI runs them. - `--ran` reports `78 derived famil(ies) accounted for — 74 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3)`, exit 0. - **Lint, narrowed with measurement:** - `eslint --no-inline-config --format json` on the one changed file reports 1 file, 0 errors and 0 warnings. - `--print-config` resolves a config for the file, so the file is inside the linted population. - That config has no `parserOptions.project` and no `projectService`, and it enables 4 rules, none of them type-aware. So this edit cannot move any other file's verdict. ## Changeset None is owed. The change is test-only and publishes nothing. `npm pack --dry-run` of `@objectstack/spec` lists 2027 files, and none is a `*.test.ts`. The changed path is absent from the list. The positive controls, `src/ui/view.zod.ts` and `liveness/object.json`, are present. The no-release route is the `skip-changeset` label, as on objectstack-ai#20064. ## Acceptance notes - **`object.external.introspectedAt` claims a writer that does not write it.** Its describe says "Set by `os datasource introspect`", but that command's draft path writes only `remoteSchema` and `remoteName` (`external-datasource-service.ts:486`, `:931-932`). This is a source reading only; I did not run it. Carrier: none. The own-editor reading deliberately does not lean on that describe. - **The forms already spell one arm of a boolean-armed union.** `field.unique` and `object.fields.unique` offer only the boolean arm: `object.form.ts:130` has `type: 'boolean'`, and `field.form.ts:26` is derived, so the first arm is the boolean. That makes `'global'` / `'organization'` (ADR-0120) unreachable from those Studio rows. It is the same shape the ruling refused as option C for G5. I read it from source and did not browser-run it. Carrier: the G2 flight that curates `indexes.unique`, or whoever builds a union-aware control. - **`action.aria` is still in the 28-key residue.** objectstack-ai#20323 carries it. - The section comments still quote the census figures ("274 … 132"). They are historical and were left as written, as the precedents did. --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #19333
Clause-②: no
What this changes
One root
omitrow in the metadata-form reconciliationLEDGER(packages/spec/src/system/metadata-form-zod-reconciliation.test.ts), forfield.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:describe()says the spec declares no vocabulary for its value;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-levelzodOnlyassertion 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)FieldSchema.formatis a barez.string().optional(): node typestring, 0 checks, read through the gate's ownsubSchemaOfandunwrap. 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".resolveAutonumberFormat(packages/spec/src/data/autonumber-format.ts). It is called by the query engine, the SQL driver and the autonumber lint. It returnsautonumberFormatfirst, and falls back to theformatshorthand only when that key is absent. Probe readings:{ autonumberFormat: 'A-{0000}', format: 'B-{0000}' }resolves toA-{0000}, and{ format: 'B-{0000}' }resolves toB-{0000}.packages/*/srcoutside spec finds no other read of a field definition's.format. The hits are CLI flags, loader stats, serializers and the validation rule's ownformatkey.field.form.tsoffersautonumberFormatat its root (visibleWhenautonumber). The object designer'sfieldsquick-add grid (object.form.ts) offers it as well. Neither offersformat. Both readings go through the gate'stopLevelFieldsandnestedLists..objectui-shaf8a9d0fb0596shows this. Inpackages/fields/src/index.tsx,@object-ui/fieldsdeclaresFORMAT_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) andplugin-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 (nameis 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 commit736c63a85yields sha2567b97432d8408…a50, which is the sha its report 5825062779 records. Before is2aa25efb4, after is096a8dbab.objectfieldhookreportdatasetviewappdashboardpositionactionpageagenttoolskillflowpermissionemail_templateview)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, userActionsfield(14): accept, currencyConfig, dependsOn, inlineColumns, inlineEdit, lookupColumns, lookupFilters, readonlyWhen, relatedList, relatedListColumns, requiredPermissions, requiredWhen, storage, visibleWhenapp(1): contextSelectorsaction(7): aria, bodyExtra, description, errorMessage, patch, requiredPermissions, resultDialogpage(1): slotspermission(1): adminScopeBefore 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.ownerandview.hiddenstill count at the view root.3cb84d084retired them on the view-item shape, but the flattened overlay member (flattenedViewOverlayFields()) still declares both live, which that change left alone by design.viewstays outside the top-level direction until its per-arm forms exist.Re-read on today's
origin/main4e0f72e8d. 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
scripts/ablation-replace.mjsin 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 wentc607298ce3d0→d320ed81cb8c.field.formatagain: 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.c607298ce3d0, equal to HEAD, andgit diff HEADcame back empty.Verification (all at
096a8dbab)pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2passed: 552 files, 16255 tests, 1 todo.pnpm --filter @objectstack/spec typecheckexited 0: tsc, scripts-typecheck, and test-typecheck holding its pinned debt.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 77 commands, and all 77 exited 0.check:doc-formula-expressions,check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt.--ranreports 77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN.eslint --no-inline-config --format jsonon the one changed file gave 1 file, 0 errors and 0 warnings.--print-configresolves a config for the file, so it is inside the linted population. The config has noparserOptions.projectand noprojectService, 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-runof@objectstack/speclists 2027 files and 0*.test.ts. The positive controlsliveness/field.jsonandsrc/data/field.zod.tsare present, and the changed path is absent (files[]ships onlysrc/**/*.zod.tsfromsrc). This repo refuses an empty-frontmatter changeset (check:empty-changeset), andCheck Changesetexempts a PR only through theskip-changesetlabel. So that label is the route here (AGENTS.md Post-Task Checklist step 3).Acceptance notes
viewreading block saysownerandhidden"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.Generated by Claude Code