Skip to content

Commit 47ecd08

Browse files
committed
test(spec): wire the top-level zod-only direction of the metadata-form reconciliation gate
The per-type top level asserted only form-only and retired, so a key the schema declares and no form row offers had no reader there. Every object-rooted type now reconciles its root the way nested lists already did: an authorable key is offered, or a root ledger row records why it is not, and any other key fails the gate by name. The ADR-0010 overlay and tombstones need no row. `view` stays outside the direction by name, with its reason, until its per-arm forms exist, and a pin holds that deferral equal to the union-rooted registered types. Re-derived first with the gate's own helper block: the object-rooted residue is 0 on this base, so the ledger is unchanged. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4a1df19 commit 47ecd08

1 file changed

Lines changed: 114 additions & 14 deletions

File tree

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

Lines changed: 114 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@
7575
*
7676
* ## The coordinates include the root, and the overlay is not surface
7777
*
78-
* Two instruments the top-level direction (#19188) needs, neither of them
79-
* wired to an assertion here:
78+
* Two instruments the top-level direction (#19188) needs:
8079
*
8180
* - **The ledger had no top-level coordinate.** Every `path` was a
8281
* `nestedLists` path, so a deliberate omission at the *top* level could not
@@ -86,17 +85,28 @@
8685
* `['']` and not `[]`. `ROOT_PATH` is that missing coordinate and
8786
* `resolveCoordinate` is the single place that knows both spellings.
8887
* - **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.
88+
* the 274 top-level keys no form offered when the skip was written were that
89+
* overlay — 119 of them the seven `_`-prefixed envelope keys on all 17 forms,
90+
* plus `protection` on 13 — so a top-level zod-only direction without a skip
91+
* would have been half overlay noise, and 132 ledger rows for one overlay
92+
* with one reason is the wrong shape.
9393
* `FRAMEWORK_FIELDS` skips it, mirroring the liveness gate, which grades the
9494
* same set auto-live (`FRAMEWORK_FIELDS` in `scripts/liveness/`).
9595
*
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.
96+
* The skip is kept off every nested coordinate, where a leg asserts over a
97+
* sub-schema that really does carry the overlay.
98+
*
99+
* ## The top-level zod-only direction is wired (#19188, #19333)
100+
*
101+
* The per-type top level used to assert only form-only and retired, so "the
102+
* schema declares this key and no form row offers it" had no reader there.
103+
* Now it does: on every object-rooted type, a key the author may write at the
104+
* top level is either offered by the form or excused by a root ledger row that
105+
* carries its reason, and any other key fails the gate by name. The overlay
106+
* and tombstones need no row, for the reasons above. `view` is the one type
107+
* outside the direction, recorded by name with its reason in
108+
* `TOP_LEVEL_DEFERRED`: its root is a union, and it is reconciled per arm once
109+
* an arm form exists (the #19330 ruling, letter A).
100110
*
101111
* @see control-flow-form-zod-ledger.test.ts — same pattern for the flow designer
102112
*/
@@ -144,7 +154,8 @@ const ROOT_PATH = '(root)';
144154
* the loader, never authored in a form. The liveness gate grades exactly this
145155
* set auto-live (`FRAMEWORK_FIELDS`, `scripts/liveness/check-liveness.mts`);
146156
* this is the reconciliation gate's equivalent, and it exists because the
147-
* overlay is 132 of the 274 top-level keys the forms do not offer.
157+
* overlay was 132 of the 274 top-level keys the forms did not offer when it
158+
* was written.
148159
*
149160
* **Derived, not hand-copied.** The seven `_`-prefixed keys ARE
150161
* `MetadataProtectionFields` — the one raw shape every metadata schema spreads
@@ -849,6 +860,38 @@ function reconcileNestedLists(type: string, form: any, root: unknown, ledger: Le
849860
});
850861
}
851862

863+
/**
864+
* The top-level zod-only predicate: the keys an author may write at the root
865+
* that the form does not offer and no root ledger row excuses. It is the
866+
* nested predicate's `zodOnly` at {@link ROOT_PATH}, resolved through the same
867+
* `resolveCoordinate` / `offerableKeysAt` pair the resolve test uses, so a
868+
* tombstone and the ADR-0010 overlay are left out without a row. `null` when
869+
* the root is not key-bearing.
870+
*/
871+
function reconcileRoot(type: string, form: any, root: unknown, ledger: Ledger): string[] | null {
872+
const at = resolveCoordinate(form, root, ROOT_PATH)!;
873+
const offerable = offerableKeysAt(at.sub, ROOT_PATH);
874+
if (!offerable) return null;
875+
if (isSubset(ledger, type, ROOT_PATH)) return [];
876+
const excused = omittedAt(ledger, type, ROOT_PATH);
877+
return offerable.filter((k) => !at.offered.includes(k) && !excused.includes(k));
878+
}
879+
880+
/**
881+
* The registered types outside the top-level zod-only direction, each with its
882+
* reason. A union root answers `keysOf` with the union of its arms' keys: the
883+
* safe side for form-only, and the unsafe side here, because one form would be
884+
* asked to offer mutually exclusive arms. The test below holds this map equal
885+
* to the union-rooted registered types, so it cannot excuse an object-rooted
886+
* type, and a union-rooted one cannot slip into the direction unexcused.
887+
*/
888+
const TOP_LEVEL_DEFERRED: Readonly<Record<string, string>> = {
889+
view: "union-rooted: its four arms declare mutually exclusive keys, so the one registered view form cannot offer them all. It is reconciled per arm, each arm against its own registered form (the #19330 ruling, letter A); until the first arm form exists, the top-level direction covers the object-rooted types only",
890+
};
891+
892+
/** The types the top-level zod-only direction judges. */
893+
const TOP_LEVEL_TYPES = TYPES.filter((type) => !(type in TOP_LEVEL_DEFERRED));
894+
852895
describe('metadata form ↔ Zod reconciliation (#3786)', () => {
853896
it('the registry is non-empty and every form resolves a schema', () => {
854897
// Without this the per-type assertions below would pass over an empty set —
@@ -881,6 +924,32 @@ describe('metadata form ↔ Zod reconciliation (#3786)', () => {
881924
).toEqual([]);
882925
});
883926

927+
it.each(TOP_LEVEL_TYPES)('%s: every top-level key the author may write is offered, or its omission is recorded', (type) => {
928+
// The cell that had no reader: a key the schema declares that no form row
929+
// offers is unauthorable in the Studio, and every gate stayed green over it.
930+
const zodOnly = reconcileRoot(type, METADATA_FORM_REGISTRY[type], getMetadataTypeSchema(type), LEDGER);
931+
expect(zodOnly, `${type}: root schema is not key-bearing`).not.toBeNull();
932+
expect(
933+
zodOnly,
934+
`${type}.${ROOT_PATH}: accepted by the Zod but unauthorable in the form — offer it, or add a root ledger entry that records why it is not offered`,
935+
).toEqual([]);
936+
});
937+
938+
it('the types outside the top-level direction are exactly the union-rooted ones, each with its reason', () => {
939+
const unionRooted = TYPES.filter((type) => {
940+
const u = unwrap(getMetadataTypeSchema(type));
941+
const kind = (u?.def ?? u?._def)?.type;
942+
return kind === 'union' || kind === 'discriminated_union';
943+
});
944+
expect(Object.keys(TOP_LEVEL_DEFERRED).sort(), 'the deferred types are not the union-rooted types').toEqual(unionRooted.sort());
945+
for (const [type, why] of Object.entries(TOP_LEVEL_DEFERRED)) {
946+
expect(why.length, `${type} needs a reason a reader can act on`).toBeGreaterThan(20);
947+
}
948+
// Not vacuous: the direction judges every registered type but the deferred ones.
949+
expect(TOP_LEVEL_TYPES.length).toBeGreaterThan(10);
950+
expect(TOP_LEVEL_TYPES.length).toBe(TYPES.length - unionRooted.length);
951+
});
952+
884953
it.each(TYPES)('%s: every hand-written nested list matches its sub-schema', (type) => {
885954
const root = getMetadataTypeSchema(type);
886955

@@ -1170,9 +1239,12 @@ describe('the nested walk reaches every depth (#14327)', () => {
11701239
// root entry can be recorded" and "the overlay is skipped at the root and
11711240
// nowhere else" are measured facts rather than assumptions.
11721241
//
1173-
// What is deliberately NOT here: an assertion that the top-level zod-only set
1174-
// is empty. It is not — 274 keys across the 17 forms, 132 of them this overlay
1175-
// — and wiring that direction is #19188's work, not this instrument's.
1242+
// The direction itself is asserted over the live registry in the first block
1243+
// of this file. Here the same `reconcileRoot` is driven over the synthetic
1244+
// pair, so the predicate is shown to name an unexcused key (positive control)
1245+
// and to stay quiet over an excused key, the overlay and a tombstone (negative
1246+
// control): a direction observed only green would otherwise be
1247+
// indistinguishable from one that matches nothing.
11761248
// ────────────────────────────────────────────────────────────────────────────
11771249

11781250
describe('the ledger has a root coordinate, and the overlay is not surface', () => {
@@ -1289,6 +1361,34 @@ describe('the ledger has a root coordinate, and the overlay is not surface', ()
12891361
expect(isFrameworkField('protection')).toBe(true);
12901362
expect(offerable).not.toContain('_lock');
12911363
});
1364+
1365+
it('positive control: the top-level direction names an unoffered, unexcused key', () => {
1366+
// `tags` is authorable and no section offers it. The overlay, `protection`
1367+
// and the tombstone `gone` are not reported, and neither is the offered
1368+
// composite `nested`.
1369+
expect(reconcileRoot('probe', form, schema, [])).toEqual(['tags']);
1370+
});
1371+
1372+
it('negative control: a root row excuses the key, and a row at any other coordinate does not', () => {
1373+
expect(
1374+
reconcileRoot('probe', form, schema, [
1375+
{ kind: 'omit', type: 'probe', path: ROOT_PATH, key: 'tags', why: 'synthetic: tags is deliberately not offered' },
1376+
]),
1377+
).toEqual([]);
1378+
expect(
1379+
reconcileRoot('probe', form, schema, [
1380+
{ kind: 'subset', type: 'probe', path: ROOT_PATH, why: 'synthetic: the probe form is a curated subset' },
1381+
]),
1382+
).toEqual([]);
1383+
// Keyed by coordinate: the same key at a nested path, or a root row for
1384+
// another type, excuses nothing at this root.
1385+
expect(
1386+
reconcileRoot('probe', form, schema, [
1387+
{ kind: 'omit', type: 'probe', path: 'nested', key: 'tags', why: 'synthetic: filed at the nested coordinate' },
1388+
{ kind: 'omit', type: 'other', path: ROOT_PATH, key: 'tags', why: 'synthetic: filed against another type' },
1389+
]),
1390+
).toEqual(['tags']);
1391+
});
12921392
});
12931393

12941394
// ────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)