Skip to content

Commit 3a9b07f

Browse files
os-justinclaude
andauthored
test(spec): give the metadata-form reconciliation ledger a root coordinate and an ADR-0010 overlay skip (#19639)
Fixes #19329 Clause-②: no Two instruments the top-level `zodOnly` direction (#19188) needs before it can be wired at all, both landing in the gate itself — `packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`. Neither wires that direction, and neither moves a verdict this gate reads today (before/after comparison below). ## ① The ledger's root coordinate — spelling: an explicit sentinel, `(root)` `ROOT_PATH = '(root)'`, plus one function that knows both coordinate spellings. `resolveCoordinate(form, root, path)` returns the pair a coordinate resolves to — the keys the form offers there, and the schema node they are judged against — and the 「every ledger entry still resolves on both sides」 test now goes through it. At the root that pair is `topLevelFields(form)` against the type's own root schema; at every other coordinate it is the `nestedLists` entry's `offered` against `subSchemaAt(root, path)`, i.e. the same two values the test read before. **Why a sentinel and not `''`** — the card left the spelling open, so this is the decision and its reasons: 1. `''` is **falsy**, and `path ? … : …` is the load-bearing spelling in this very file (`nestedLists`' own `prefix` test). Any reader written that way silently reads the root coordinate as "no path given" — the one confusion a coordinate must not have. 2. An unfilled `path` then cannot masquerade as a deliberate root row. Measured: `resolveCoordinate(form, schema, '')` is `undefined`, so an empty `path` fails the resolve test loudly instead of quietly excusing a top-level key. Ablation A below shows that pin go red the moment `ROOT_PATH` becomes `''`. 3. The dotted algebra has no zero-segment element: `''.split('.')` is `['']` — one empty segment, not none — so `subSchemaAt(root, '')` walks a segment no schema declares and resolves to `undefined`. `''` would need a guard wherever a path is walked; a sentinel needs one at the single resolve step, and it is visible at the call site. 4. Parentheses cannot occur in a form's `field:` name, so the sentinel cannot collide with a real dotted path — asserted over the live registry (0 collisions across all 17 forms), not assumed. 5. It reads unambiguously in a failure label: `object.(root).apiMethods`, not `object..apiMethods`. ## ② The framework-field skip — derived from the ADR-0010 shape, applied at the root only `FRAMEWORK_FIELDS` mirrors the liveness gate's precedent (`FRAMEWORK_FIELDS` in `packages/spec/scripts/liveness/check-liveness.mts` — located by symbol; read-only here, not edited), and `offerableKeysAt(sub, path)` is where it applies. **Derived, not hand-copied.** The seven `_`-prefixed keys are `Object.keys(MetadataProtectionFields)` — the one exported raw shape every metadata schema spreads — so a key added to or dropped from the envelope moves this skip in the same commit. A second copy of the liveness gate's eight literal names would have been the hand-copied-list shape this whole file exists to abolish, and that `const` is module-local to a `.mts` script, so there is nothing to import from it either way: the shape is the better source for both. `protection` is the one name written out — the author-facing block the loader translates INTO that envelope, spliced under that name by each schema rather than carried in a shape of its own — and it is pinned against what it resolves to on every live type that declares it: 13 of 17, each `["docsUrl","lock","reason"]`, equal to `keysOf(ProtectionSchema)`. **The skip stops at the root, deliberately**, and that is a measurement rather than a preference. The overlay is spread into nested shapes too (three times in `view.zod.ts` alone), and exactly one hand-written nested list resolves to a sub-schema carrying all seven `_`-prefixed keys: `object.fields`, whose `subset` entry is judged today on how much of that schema the quick-add grid does NOT cover. Skipping the overlay there would move a number an asserting leg reads. Pinned as a dark control over both a synthetic probe and that live instance; ablation B below shows it go red when the skip is applied at every coordinate. ## The fence: before/after verdict comparison Same command on both sides, exit code captured before any pipe, through `scripts/pm/os-verify-lock.sh` (slot `issue-19329`): ``` pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 \ --reporter=verbose src/system/metadata-form-zod-reconciliation.test.ts ``` | | unmodified `origin/main` (744a0a3) | this branch (7c1b59a) | |:---|:---|:---| | wrapper verdict | `VERDICT command-exit 0` | `VERDICT command-exit 0` | | test files | 1 passed (1) | 1 passed (1) | | tests | **46 passed (46)** | **53 passed (53)** | | non-pass marks | none | none | | verdict-set sha256 | `ad70459ed4c08edeaed52b8e8a7f9a64fa46a0b194c6a345f32407dbd547dcfd` | `3b801fc825039fa32cb9d080a864993e2d2fc511fd46006247c65c981099ecfd` | Exact set difference over the normalised verdict lines (status mark + full test path, per-test durations stripped), not a count: - **Missing from after: 0.** All 46 `origin/main` verdicts are present on the branch, byte-identical, and all 46 still pass. No rename, no removal, no status flip — so `formOnly` and offered-but-retired keep asserting exactly what they assert today. - **Added: 7**, all of them the new pins in one new `describe` block: - the root coordinate resolves to the top-level pair, which no nested path can - the empty string is not the coordinate — an unfilled path fails loudly instead - no live form has a hand-written list at the sentinel, so it cannot be shadowed - the overlay set IS the ADR-0010 shape, not a second hand-copied list - positive control: the overlay drops out of the offerable keys at the root, and nothing else does - dark control: the skip does not reach a nested coordinate, live instance included - a top-level omission is recordable, and a row naming the overlay is not No existing verdict changed, so the fork the card reserved for that outcome was not reached. ## Ablation — both pins can fail, and both legs restored Through `scripts/ablation-replace.mjs` (anchor must hit; the write is verified against the disk; restore proven by blob equality with `HEAD` plus an empty `git diff HEAD`), each wrapping the same locked vitest run. No `dist` leg: the subject is reached by relative `src` imports inside its own package, so nothing resolves through `exports`. | leg | mutation | on-disk evidence | result | |:---|:---|:---|:---| | A | `const ROOT_PATH = '(root)';` → `const ROOT_PATH = '';` | anchor 1 → 0, blob `b72a5edfcd72` → `d138333efcbf` | **1 failed \| 52 passed** — exactly 「the empty string is not the coordinate」 | | B | `offerableKeysAt`'s `path === ROOT_PATH ? … : keys` → skip applied unconditionally | anchor 1 → 0, blob `b72a5edfcd72` → `9ec64f2272af` | **1 failed \| 52 passed** — exactly 「dark control: the skip does not reach a nested coordinate」 | Both legs: `ok restored: blob == HEAD (b72a5ed) and git diff HEAD is empty`. Direction was predicted before each run and matched (turn red, one named test each). ## Census, re-derived on this tree ⚠️ These are **my readings on base 744a0a3**, not the card's figures restated. Probe: a temporary `.mts` script (deleted; untracked, never committed) that imports `METADATA_FORM_REGISTRY`, `getMetadataTypeSchema` and `MetadataProtectionFields`, and slices the gate's own helper block **verbatim** — lines 151-340 of the unmodified file, sha256 `d07ca156491f3dec` — so nothing is re-implemented by grep. Run as `OS_EAGER_SCHEMAS=1 tsx`, with a lit control and a dark control asserted in the same pass. | reading | value | card | |:---|:---|:---| | registered forms / types | **17** | 17 | | top-level keys in shape, summed | 524 | — | | top-level authorable keys (tombstones dropped), summed | 495 | — | | top-level keys the forms DO offer, summed | 222 | — | | **top-level authorable keys no form offers (the population)** | **274** | 274 | | of those, the ADR-0010 overlay | **132** | 132 | | — the seven `_`-prefixed keys × 17 forms | **119** | 119 | | — `protection` | **13** | 13 | | **the population minus the overlay** | **142** | 145 | | `formOnly` at top level | **0** | 0 | | offered-but-retired at top level | **0** | 0 | | lit control: `name`, offered by N of 17 forms | **17** | 17 | | dark control: a fabricated key | **0** | 0 | | dead ledger rows across these 17 `liveness/*.json` | **27** | 27 | | — of those, inside the 274 population | **0** | 0 | One figure does not reproduce: the keys needing a recorded reason are **142**, not 145. 274 − 132 = 142 exactly, on this tree and by that arithmetic, so the two published numbers cannot both be readings of this population. Everything else matches, including both "what is NOT wrong here" claims: no `dead` verdict touches the population, and the two directions the gate does assert are clean. ## Verification Final commit `7c1b59a8e`; the tree was clean for every run below and `git status --porcelain` is empty. - **Derived gate families** — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **77** commands from the actual diff (1 path, +291/−16). Every one was run, each exit code written to a file before any pipe, then reconciled: `✓ dispatch-gates --ran: 77 derived famil(ies) accounted for — 74 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3).` — 0 UNRUN. - **74 exit 0.** Includes `check:nul-bytes`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check:type-check-coverage`, `check:published-files`, `check:doc-authoring`, `check:issue-citations`, and the spec artifact set (`check:api-surface`, `check:authorable-surface`, `check:docs`, `check:liveness`, `check:empty-state`, `check:strictness-ledger`, …) after `pnpm --filter @objectstack/spec build` (green; no generated artifact moved). - **3 NOT MEASURED, reported as exactly that** — `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt` each exited 3, `PREREQUISITE NOT MET`: they read built output for 87 / 1 / 30 packages that have no `dist` in this checkout, which is the whole-monorepo build CI does before those steps. Not a pass and not a finding — they measured nothing. A fourth, `check:doc-formula-expressions`, also exited 3 and **was** cleared: `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint`, re-run, exit 0. - **`pnpm --filter @objectstack/spec test`** — `VERDICT command-exit 0`; 510 files, 14919 passed | 1 todo. - **`pnpm --filter @objectstack/spec typecheck`** — `VERDICT command-exit 0`, including `check:test-typecheck`: 53 files / 257 errors / 142 pinned signatures held, i.e. the shrink-only test-layer ledger is unmoved by the new code. - **`pnpm lint`** (`eslint . --no-inline-config`, the repo-wide union, not a narrowed run) — `VERDICT command-exit 0`, no output. - **Dependency closure** — `packages/spec` has no workspace dependencies, so there is no `^...` closure to build; its own `dist` was rebuilt before the gates that read it. ## Changeset: `skip-changeset`, measured `@objectstack/spec` publishes `files: [dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json]`. The landing file is `src/system/metadata-form-zod-reconciliation.test.ts`, which no entry matches. Measured after the build by grepping every published path (including all 202 tracked `src/**/*.zod.ts`) for each symbol this PR introduces: | symbol | published paths containing it | |:---|:---| | `ROOT_PATH` | **0** | | `FRAMEWORK_FIELDS` (this file's) | **0** | | `isFrameworkField` | **0** | | `resolveCoordinate` | **0** | | `offerableKeysAt` | **0** | | `MetadataProtectionFields` (positive control) | **71** | Zero hits with the positive control hitting ⇒ nothing publishes ⇒ no changeset is owed, and inventing a published-package changeset for an instrument change would be the wrong record. ## Acceptance notes - **Not filed, observation only.** The liveness gate still spells its eight overlay names by hand while this gate now derives them from `MetadataProtectionFields`. That is not a silent drift: a key added to the envelope stops being skipped there, falls through to the ledger/marker path and lands in `unclassified`, which reds `check:liveness` loudly. No reproducible defect, no contract violation, no authoring trap — so it stays here rather than becoming a card. Carrier: whoever next edits `check-liveness.mts`. - **Out of scope by construction, per the card's fence:** the top-level `zodOnly` assertion itself (#19188 and its splits). This PR makes it possible and asserts nothing about the 274. - No retirement work: zero `dead` verdicts touch the population (measured above). ## 维护者速读(草稿) **改了什么** — 只动一个测试文件:元数据表单 ↔ Zod 对账门禁。给它的「故意不提供」台账补了一个 顶层坐标(哨兵 `(root)`),并加了一条跳过规则:ADR-0010 的来源/锁定覆盖层(7 个下划线键加 `protection`)不是作者编写面,顶层对账时不算缺口。新增 7 条钉子测试。 **为什么改** — 这两件都是 #19188 顶层方向的前置件。台账每条都用点分路径作键,而路径只来自嵌套 清单,所以顶层的第一条「故意不提供」根本记不下来;覆盖层则占了 274 个顶层未提供键中的 132 个 (48%),不跳过的话新方向一半输出是噪音。本 PR 只把工具做出来,⛔ 没有打开那个方向。 **风险与代价(含回滚)** — 风险点是「给门禁加跳过」等于削弱门禁。已按卡上的围栏逐条证明没有: 门禁今天在断言的两条腿(`formOnly`、offered-but-retired)46 条判决前后逐字相同、全绿,只多出 7 条 新钉子;跳过规则只在根坐标生效,嵌套坐标有暗控测试守着(含 `object.fields` 这个真实实例)。两次 消融各证明一条新钉子真能变红。不发布任何包(已实测),回滚 = 还原这一个文件,无迁移、无下游。 **席位意见** — **你要做的** — 确认哨兵拼法 `(root)` 是你想要的(卡上留给实现者决定;备选是空字符串,正文列了 五条不选它的理由)。确认「工具先落地、断言留给 #19188」这个分工。其余无需动作:草稿 PR,⛔ 未翻 ready、⛔ 未挂 auto-merge。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent ec2ede0 commit 3a9b07f

1 file changed

Lines changed: 291 additions & 16 deletions

File tree

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

Lines changed: 291 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,106 @@
7373
* the walk is pinned at the bottom against a synthetic fixture, so a gate that
7474
* reaches nothing at depth two cannot report green.
7575
*
76+
* ## The coordinates include the root, and the overlay is not surface
77+
*
78+
* Two instruments the top-level direction (#19188) needs, neither of them
79+
* wired to an assertion here:
80+
*
81+
* - **The ledger had no top-level coordinate.** Every `path` was a
82+
* `nestedLists` path, so a deliberate omission at the *top* level could not
83+
* be recorded at all — the resolve test looks a coordinate up in
84+
* `nestedLists(form)`, which yields only nested paths, and
85+
* `subSchemaAt(root, '')` walks one empty segment because `''.split('.')` is
86+
* `['']` and not `[]`. `ROOT_PATH` is that missing coordinate and
87+
* `resolveCoordinate` is the single place that knows both spellings.
88+
* - **The ADR-0010 provenance/lock overlay is not authoring surface.** 132 of
89+
* the 274 top-level keys no form offers are that overlay — 119 of them the
90+
* seven `_`-prefixed envelope keys on all 17 forms, plus `protection` on 13
91+
* — so a top-level zod-only direction without a skip is half overlay noise,
92+
* and 132 ledger rows for one overlay with one reason is the wrong shape.
93+
* `FRAMEWORK_FIELDS` skips it, mirroring the liveness gate, which grades the
94+
* same set auto-live (`FRAMEWORK_FIELDS` in `scripts/liveness/`).
95+
*
96+
* Neither changes what this gate asserts: the top-level zod-only direction
97+
* stays unwired, and the skip is kept off every nested coordinate — where a
98+
* leg is asserting today, over a sub-schema that really does carry the
99+
* overlay.
100+
*
76101
* @see control-flow-form-zod-ledger.test.ts — same pattern for the flow designer
77102
*/
78103

79104
import { describe, it, expect } from 'vitest';
80105
import { z } from 'zod';
81106

82107
import { METADATA_FORM_REGISTRY } from './metadata-form-registry';
108+
import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
83109
import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas';
110+
import { ProtectionSchema } from '../shared/protection.zod';
84111
import { retiredKey } from '../shared/retired-key';
85112

113+
// ────────────────────────────────────────────────────────────────────────────
114+
// Coordinates — what a ledger `path` may say, including the one the dotted
115+
// algebra has no spelling for.
116+
// ────────────────────────────────────────────────────────────────────────────
117+
118+
/**
119+
* The ledger coordinate for a form's **top level**.
120+
*
121+
* Every other coordinate is a dotted path produced by `nestedLists`
122+
* (`fields`, `fields.options`, `lifecycle.ttl`). The top level is the
123+
* zero-segment path, and the dotted algebra has no zero-segment element, so it
124+
* needs a coordinate of its own. Why a sentinel and not `''`:
125+
*
126+
* - `''` is **falsy**, and `path ? … : …` is the load-bearing spelling in this
127+
* very file (`nestedLists`' own `prefix` test). Any reader written that way
128+
* reads the root coordinate as "no path given" — the one confusion a
129+
* coordinate must not have.
130+
* - A `path` a future author leaves unfilled then cannot masquerade as a
131+
* deliberate root row: `''` is not this sentinel, so an empty one fails the
132+
* resolve test loudly instead of quietly excusing a top-level key.
133+
* - Parentheses cannot occur in a form's `field:` name, so the sentinel cannot
134+
* collide with a real dotted path — asserted over the live registry below,
135+
* rather than assumed.
136+
* - It reads unambiguously in a failure label: `object.(root).apiMethods`, not
137+
* `object..apiMethods`.
138+
*/
139+
const ROOT_PATH = '(root)';
140+
141+
/**
142+
* The ADR-0010 provenance/lock overlay: system-stamped onto a metadata item by
143+
* the loader, never authored in a form. The liveness gate grades exactly this
144+
* set auto-live (`FRAMEWORK_FIELDS`, `scripts/liveness/check-liveness.mts`);
145+
* this is the reconciliation gate's equivalent, and it exists because the
146+
* overlay is 132 of the 274 top-level keys the forms do not offer.
147+
*
148+
* **Derived, not hand-copied.** The seven `_`-prefixed keys ARE
149+
* `MetadataProtectionFields` — the one raw shape every metadata schema spreads
150+
* — so a key added to or dropped from the envelope moves this set in the same
151+
* commit. A second copy of the liveness gate's eight names would have been the
152+
* hand-copied-list shape this whole file exists to abolish, and that `const`
153+
* is module-local to a `.mts` script, so there is nothing to import from it
154+
* anyway: the shape is the better source for both.
155+
*
156+
* `protection` is the one name written out. It is the author-facing block the
157+
* loader translates INTO that envelope (`applyProtection`,
158+
* `shared/protection.zod.ts`), spliced under that name by each schema rather
159+
* than carried in a shape of its own — so it is pinned below against what it
160+
* resolves to on every live type that declares it, and the name stays a
161+
* measured claim.
162+
*/
163+
const FRAMEWORK_FIELDS: ReadonlySet<string> = new Set<string>([
164+
...Object.keys(MetadataProtectionFields),
165+
'protection',
166+
]);
167+
168+
/** Is `key` part of that overlay — i.e. not authoring surface at all? */
169+
const isFrameworkField = (key: string): boolean => FRAMEWORK_FIELDS.has(key);
170+
86171
// ────────────────────────────────────────────────────────────────────────────
87172
// Ledger — deliberate zod-only omissions. `omit` names one key; `subset`
88173
// declares a whole nested list as a curated subset (coverage unenforced there,
89-
// the form-only direction still is).
174+
// the form-only direction still is). `path` is a `nestedLists` path or
175+
// {@link ROOT_PATH}.
90176
// ────────────────────────────────────────────────────────────────────────────
91177

92178
type OmitEntry = { kind: 'omit'; type: string; path: string; key: string; why: string };
@@ -339,6 +425,49 @@ function subSchemaAt(root: unknown, path: string): unknown {
339425
return node;
340426
}
341427

428+
/**
429+
* The two sides a ledger coordinate resolves to: the keys the form offers
430+
* there, and the schema node they are judged against. `undefined` when the
431+
* form has no hand-written list at that coordinate any more — the state the
432+
* resolve test reports.
433+
*
434+
* The root coordinate is why this is a function rather than a
435+
* `nestedLists(form).find(…)` at the call site. `nestedLists` yields only
436+
* nested paths, so a root entry looked up there is always missing, and
437+
* `subSchemaAt(root, '')` resolves to `undefined` because the walk takes one
438+
* empty segment. The top level is resolved instead from the pair that actually
439+
* describes it: every `field:` across every section, against the type's own
440+
* root schema.
441+
*/
442+
function resolveCoordinate(
443+
form: any,
444+
root: unknown,
445+
path: string,
446+
): { offered: string[]; sub: unknown } | undefined {
447+
if (path === ROOT_PATH) return { offered: topLevelFields(form), sub: root };
448+
const list = nestedLists(form).find((l) => l.path === path);
449+
return list ? { offered: list.offered, sub: subSchemaAt(root, path) } : undefined;
450+
}
451+
452+
/**
453+
* The keys a form could offer at a coordinate: authorable (not a tombstone),
454+
* and — at the root coordinate **only** — not the ADR-0010 overlay. `null`
455+
* when the node is not key-bearing, same as `authorableKeysOf`.
456+
*
457+
* The skip stops at the root deliberately. The overlay is spread into nested
458+
* shapes as well (three times in `view.zod.ts` alone), and one hand-written
459+
* nested list resolves to a sub-schema carrying all seven `_`-prefixed keys:
460+
* `object.fields`, whose `subset` entry has to keep earning its place against
461+
* the keys the quick-add grid really could offer. Skipping the overlay there
462+
* would move a number a leg reads today — and the top-level direction this
463+
* instrument is for is not that leg.
464+
*/
465+
function offerableKeysAt(sub: unknown, path: string): string[] | null {
466+
const keys = authorableKeysOf(sub);
467+
if (!keys) return null;
468+
return path === ROOT_PATH ? keys.filter((k) => !isFrameworkField(k)) : keys;
469+
}
470+
342471
type Ledger = ReadonlyArray<OmitEntry | SubsetEntry>;
343472
const TYPES = Object.keys(METADATA_FORM_REGISTRY);
344473
const ledgerFor = (ledger: Ledger, type: string, path: string) =>
@@ -464,33 +593,48 @@ describe('metadata form ↔ Zod reconciliation (#3786)', () => {
464593
const root = getMetadataTypeSchema(entry.type);
465594
expect(root, `ledger references unknown metadata type '${entry.type}'`).toBeDefined();
466595

467-
const lists = nestedLists(METADATA_FORM_REGISTRY[entry.type]);
468-
const list = lists.find((l) => l.path === entry.path);
469-
expect(list, `${entry.type}.${entry.path}: no hand-written list at this path any more`).toBeDefined();
596+
// One lookup for both coordinate spellings — a dotted `nestedLists`
597+
// path, and the root.
598+
const at = resolveCoordinate(METADATA_FORM_REGISTRY[entry.type], root, entry.path);
599+
expect(at, `${entry.type}.${entry.path}: no hand-written list at this path any more`).toBeDefined();
470600

471-
const sub = subSchemaAt(root, entry.path);
472-
const subKeys = keysOf(sub);
473-
expect(subKeys, `${entry.type}.${entry.path}: sub-schema is not key-bearing any more`).toBeTruthy();
601+
// Authorable, not merely present, and at the root not the overlay: the
602+
// keys the form COULD offer here. `null` iff the node is not key-bearing,
603+
// which is the same fact `keysOf` reports.
604+
const offerable = offerableKeysAt(at!.sub, entry.path);
605+
expect(offerable, `${entry.type}.${entry.path}: sub-schema is not key-bearing any more`).toBeTruthy();
474606

475607
if (entry.kind === 'omit') {
476-
// Authorable, not merely present: an `omit` whose key has since been
477-
// TOMBSTONED is excusing an omission that is now mandatory, and the
478-
// entry has to go — otherwise the ledger's own "still resolves" check
479-
// is what keeps a dead excuse alive.
608+
// The overlay is skipped at the root, so a row naming one of its keys
609+
// excuses an omission that was never owed — the same reasoning
610+
// `reconcileNestedLists` applies to a tombstone: the only correct thing
611+
// to do with a key that is not authoring surface is not to offer it,
612+
// and no ledger row is owed for it.
613+
if (entry.path === ROOT_PATH) {
614+
expect(
615+
isFrameworkField(entry.key),
616+
`${entry.type}.${entry.path}.${entry.key}: an ADR-0010 provenance/lock overlay field, skipped at the root coordinate — a ledger row excuses nothing here. Drop the entry`,
617+
).toBe(false);
618+
}
619+
620+
// An `omit` whose key has since been TOMBSTONED is excusing an omission
621+
// that is now mandatory, and the entry has to go — otherwise the
622+
// ledger's own "still resolves" check is what keeps a dead excuse alive.
480623
expect(
481-
authorableKeysOf(sub),
624+
offerable,
482625
`${entry.type}.${entry.path}.${entry.key}: not an authorable key any more — removed, or retired to a tombstone (a retired key is excused automatically). Drop the ledger entry`,
483626
).toContain(entry.key);
484627
expect(
485-
list!.offered,
628+
at!.offered,
486629
`${entry.type}.${entry.path}.${entry.key}: the form offers it now — drop the ledger entry`,
487630
).not.toContain(entry.key);
488631
} else {
489632
// A `subset` that covers everything is no longer a subset — counted over
490-
// AUTHORABLE keys, so a tombstone left in the shape cannot prop up an
491-
// entry whose real coverage gap has closed.
633+
// the keys the form could offer, so neither a tombstone left in the
634+
// shape nor (at the root) the overlay can prop up an entry whose real
635+
// coverage gap has closed.
492636
expect(
493-
authorableKeysOf(sub)!.filter((k) => !list!.offered.includes(k)).length,
637+
offerable!.filter((k) => !at!.offered.includes(k)).length,
494638
`${entry.type}.${entry.path}: the form now covers the whole authorable schema — drop the ledger entry`,
495639
).toBeGreaterThan(0);
496640
}
@@ -690,3 +834,134 @@ describe('the nested walk reaches every depth (#14327)', () => {
690834
expect(at(misfiled, 'items.options')?.zodOnly).toEqual(['extra']);
691835
});
692836
});
837+
838+
// ────────────────────────────────────────────────────────────────────────────
839+
// The root coordinate, and the overlay skip.
840+
//
841+
// Both are instruments for the top-level direction (#19188), and both are
842+
// pinned the way the depth-two walk above is: a SYNTHETIC form and schema
843+
// driven through the same `resolveCoordinate` / `offerableKeysAt` the live
844+
// resolve test uses, plus two readings taken from the live registry — so "a
845+
// root entry can be recorded" and "the overlay is skipped at the root and
846+
// nowhere else" are measured facts rather than assumptions.
847+
//
848+
// What is deliberately NOT here: an assertion that the top-level zod-only set
849+
// is empty. It is not — 274 keys across the 17 forms, 132 of them this overlay
850+
// — and wiring that direction is #19188's work, not this instrument's.
851+
// ────────────────────────────────────────────────────────────────────────────
852+
853+
describe('the ledger has a root coordinate, and the overlay is not surface', () => {
854+
const schema = z.object({
855+
name: z.string(),
856+
label: z.string().optional(),
857+
tags: z.array(z.string()).optional(),
858+
gone: retiredKey('`Probe.gone` was removed in @objectstack/spec 17.0.0. Delete the key.'),
859+
nested: z.object({ a: z.string(), b: z.string().optional(), ...MetadataProtectionFields }).optional(),
860+
protection: ProtectionSchema.optional(),
861+
...MetadataProtectionFields,
862+
});
863+
const form = {
864+
sections: [
865+
{ fields: [{ field: 'name' }, { field: 'label' }] },
866+
{ fields: [{ field: 'nested', fields: [{ field: 'a' }] }] },
867+
],
868+
};
869+
870+
it('the root coordinate resolves to the top-level pair, which no nested path can', () => {
871+
// The failure mode the coordinate exists to end, named: the resolve test
872+
// looks a coordinate up among the hand-written lists, and those are nested
873+
// by construction — the root is never among them.
874+
expect(nestedLists(form).map((l) => l.path)).toEqual(['nested']);
875+
expect(nestedLists(form).find((l) => l.path === ROOT_PATH)).toBeUndefined();
876+
877+
const at = resolveCoordinate(form, schema, ROOT_PATH);
878+
expect(at?.offered).toEqual(['label', 'name', 'nested']);
879+
expect(at?.sub).toBe(schema);
880+
// …and the zod side is the whole top-level shape, so a key no section
881+
// offers is reachable from the coordinate at all.
882+
expect(keysOf(at?.sub)).toContain('tags');
883+
});
884+
885+
it('the empty string is not the coordinate — an unfilled path fails loudly instead', () => {
886+
// Why the coordinate is a sentinel: a row whose `path` was never filled in
887+
// must not read as a deliberate root row. `''` resolves to nothing on
888+
// either side, which is what the resolve test reports as "no hand-written
889+
// list at this path any more".
890+
expect(resolveCoordinate(form, schema, '')).toBeUndefined();
891+
expect(subSchemaAt(schema, '')).toBeUndefined();
892+
});
893+
894+
it('no live form has a hand-written list at the sentinel, so it cannot be shadowed', () => {
895+
// Dark control over the real registry: parentheses cannot occur in a
896+
// `field:` name, and this is what keeps that a measurement.
897+
const collisions = TYPES.flatMap((type) =>
898+
nestedLists(METADATA_FORM_REGISTRY[type])
899+
.filter((l) => l.path === ROOT_PATH)
900+
.map((l) => `${type}.${l.path}`),
901+
);
902+
expect(collisions).toEqual([]);
903+
});
904+
905+
it('the overlay set IS the ADR-0010 shape, not a second hand-copied list', () => {
906+
const envelope = Object.keys(MetadataProtectionFields);
907+
expect(envelope.length).toBeGreaterThan(0);
908+
expect([...FRAMEWORK_FIELDS].sort()).toEqual([...envelope, 'protection'].sort());
909+
// Every `_`-prefixed member comes from the shape — nothing is hand-added
910+
// beside it, so the envelope cannot drift away from the skip.
911+
expect([...FRAMEWORK_FIELDS].filter((k) => k.startsWith('_')).sort()).toEqual([...envelope].sort());
912+
913+
// `protection` is the one name written out, so it is pinned against what it
914+
// resolves to on every live type that declares it.
915+
const declaring = TYPES.filter((type) => (keysOf(getMetadataTypeSchema(type)) ?? []).includes('protection'));
916+
expect(declaring.length).toBeGreaterThan(0);
917+
for (const type of declaring) {
918+
expect(
919+
keysOf(subSchemaOf(getMetadataTypeSchema(type), 'protection')),
920+
`${type}.protection no longer resolves to ProtectionSchema's shape — the one hand-written name in the overlay set`,
921+
).toEqual(keysOf(ProtectionSchema));
922+
}
923+
});
924+
925+
it('positive control: the overlay drops out of the offerable keys at the root, and nothing else does', () => {
926+
expect(FRAMEWORK_FIELDS.size).toBeGreaterThan(0);
927+
const offerable = offerableKeysAt(schema, ROOT_PATH)!;
928+
for (const key of FRAMEWORK_FIELDS) {
929+
expect(keysOf(schema), `the probe must declare ${key} for this control to measure anything`).toContain(key);
930+
expect(offerable, `${key} is overlay and must not be offerable at the root`).not.toContain(key);
931+
}
932+
// The skip is narrow: an ordinary key the form does not offer is still
933+
// offerable, so the top-level direction keeps something to ask for.
934+
expect(offerable).toContain('tags');
935+
// And a tombstone is still excluded — by `authorableKeysOf`, not by the skip.
936+
expect(offerable).not.toContain('gone');
937+
});
938+
939+
it('dark control: the skip does not reach a nested coordinate, live instance included', () => {
940+
// Applying it below the root would move a number an asserting leg reads
941+
// today: `object.fields` resolves to a sub-schema carrying the whole
942+
// envelope, and its `subset` entry is judged on how much of that schema the
943+
// quick-add grid does NOT cover.
944+
const envelope = Object.keys(MetadataProtectionFields);
945+
expect(offerableKeysAt(subSchemaAt(schema, 'nested'), 'nested')).toEqual(expect.arrayContaining(envelope));
946+
expect(offerableKeysAt(subSchemaAt(getMetadataTypeSchema('object'), 'fields'), 'fields')).toEqual(
947+
expect.arrayContaining(envelope),
948+
);
949+
});
950+
951+
it('a top-level omission is recordable, and a row naming the overlay is not', () => {
952+
// The whole point of the coordinate, driven through the same two functions
953+
// the live resolve test uses: `tags` is authorable, unoffered and not
954+
// overlay, so a root `omit` for it resolves on both sides…
955+
const at = resolveCoordinate(form, schema, ROOT_PATH)!;
956+
const offerable = offerableKeysAt(at.sub, ROOT_PATH)!;
957+
expect(offerable).toContain('tags');
958+
expect(at.offered).not.toContain('tags');
959+
expect(isFrameworkField('tags')).toBe(false);
960+
961+
// …while a root `omit` naming an overlay key is the row the resolve test
962+
// rejects: it is not offerable there, and the predicate says why.
963+
expect(isFrameworkField('_lock')).toBe(true);
964+
expect(isFrameworkField('protection')).toBe(true);
965+
expect(offerable).not.toContain('_lock');
966+
});
967+
});

0 commit comments

Comments
 (0)