Skip to content

Commit b1cbd92

Browse files
test(spec): record why eleven top-level keys are never offered by a metadata form (#20362)
Part of #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; #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 #19333 item 2, which comes after every flight. It also leaves out `action.aria`, because #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 #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 #20064 and #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 #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.** #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>
1 parent 5049a3c commit b1cbd92

1 file changed

Lines changed: 314 additions & 1 deletion

File tree

0 commit comments

Comments
 (0)