|
75 | 75 | * |
76 | 76 | * ## The coordinates include the root, and the overlay is not surface |
77 | 77 | * |
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: |
80 | 79 | * |
81 | 80 | * - **The ledger had no top-level coordinate.** Every `path` was a |
82 | 81 | * `nestedLists` path, so a deliberate omission at the *top* level could not |
|
86 | 85 | * `['']` and not `[]`. `ROOT_PATH` is that missing coordinate and |
87 | 86 | * `resolveCoordinate` is the single place that knows both spellings. |
88 | 87 | * - **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. |
93 | 93 | * `FRAMEWORK_FIELDS` skips it, mirroring the liveness gate, which grades the |
94 | 94 | * same set auto-live (`FRAMEWORK_FIELDS` in `scripts/liveness/`). |
95 | 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. |
| 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). |
100 | 110 | * |
101 | 111 | * @see control-flow-form-zod-ledger.test.ts — same pattern for the flow designer |
102 | 112 | */ |
@@ -144,7 +154,8 @@ const ROOT_PATH = '(root)'; |
144 | 154 | * the loader, never authored in a form. The liveness gate grades exactly this |
145 | 155 | * set auto-live (`FRAMEWORK_FIELDS`, `scripts/liveness/check-liveness.mts`); |
146 | 156 | * 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. |
148 | 159 | * |
149 | 160 | * **Derived, not hand-copied.** The seven `_`-prefixed keys ARE |
150 | 161 | * `MetadataProtectionFields` — the one raw shape every metadata schema spreads |
@@ -849,6 +860,38 @@ function reconcileNestedLists(type: string, form: any, root: unknown, ledger: Le |
849 | 860 | }); |
850 | 861 | } |
851 | 862 |
|
| 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 | + |
852 | 895 | describe('metadata form ↔ Zod reconciliation (#3786)', () => { |
853 | 896 | it('the registry is non-empty and every form resolves a schema', () => { |
854 | 897 | // Without this the per-type assertions below would pass over an empty set — |
@@ -881,6 +924,32 @@ describe('metadata form ↔ Zod reconciliation (#3786)', () => { |
881 | 924 | ).toEqual([]); |
882 | 925 | }); |
883 | 926 |
|
| 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 | + |
884 | 953 | it.each(TYPES)('%s: every hand-written nested list matches its sub-schema', (type) => { |
885 | 954 | const root = getMetadataTypeSchema(type); |
886 | 955 |
|
@@ -1170,9 +1239,12 @@ describe('the nested walk reaches every depth (#14327)', () => { |
1170 | 1239 | // root entry can be recorded" and "the overlay is skipped at the root and |
1171 | 1240 | // nowhere else" are measured facts rather than assumptions. |
1172 | 1241 | // |
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. |
1176 | 1248 | // ──────────────────────────────────────────────────────────────────────────── |
1177 | 1249 |
|
1178 | 1250 | 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', () |
1289 | 1361 | expect(isFrameworkField('protection')).toBe(true); |
1290 | 1362 | expect(offerable).not.toContain('_lock'); |
1291 | 1363 | }); |
| 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 | + }); |
1292 | 1392 | }); |
1293 | 1393 |
|
1294 | 1394 | // ──────────────────────────────────────────────────────────────────────────── |
|
0 commit comments