Repository navigation
Commit b1cbd92
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
0 commit comments