You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
test(spec): walk every depth of hand-written form lists in the form ↔ Zod reconciliation gate (#14408)
`nestedLists` collected a hand-written list only for a top-level section entry,
so a repeater or composite nested inside another nested list — the object
designer's `fields.options` / `fields.summaryOperations` rows and its four
`lifecycle.*` blocks — was outside the population entirely. The walk is now
recursive and keyed by dotted path; each level's sub-schema is resolved by
walking `subSchemaOf` down the same path. The nested-list predicate is
extracted (`reconcileNestedLists`) and pinned against a synthetic depth-two
fixture: form-only and tombstoned keys go red at depth two, a designed subset
goes green only with a ledger entry at the full dotted path. Nested-list
assertions are soft so one run names every drifted list.
Triage of the six newly reached paths, from the gate's own first run: the one
form-only offer (`fields.options.icon`) is the already-carded withdrawal; the
four zod-only differences are designed and ledgered — the two `fields.*` rows
inherit the quick-add subset one level up, and `lifecycle.{retention,ttl}.onlyWhen`
is a row-filter map written only by code-declared platform system objects.
Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
Co-authored-by: Claude <noreply@anthropic.com>
why: "the object editor's inline column grid is a QUICK-ADD surface covering the common authoring keys; the full per-field editor is `field.form.ts` (registered as the `field` metadata type), which is where the long tail of FieldSchema is authored",
99
115
},
116
+
// ── Depth two (#14327): the lists nested inside the `fields` quick-add row ──
117
+
{
118
+
kind: 'subset',
119
+
type: 'object',
120
+
path: 'fields.options',
121
+
why: "one row of the `fields` quick-add grid (the subset entry above), so the same design applies one level down: an option is captured as label / value / color / description, and the long tail — `default`, the per-option `visibleWhen` CEL predicate — is authored in the full per-field editor (`field.form.ts`), whose `options` repeater is schema-derived and so offers every SelectOptionSchema key",
122
+
},
123
+
{
124
+
kind: 'subset',
125
+
type: 'object',
126
+
path: 'fields.summaryOperations',
127
+
why: "one row of the `fields` quick-add grid (the subset entry above): the roll-up is captured as object / field / function; `relationshipField` (auto-detected unless the child references this object twice) and the `filter` FilterCondition are authored in the full per-field editor (`field.form.ts`), whose own `summaryOperations` composite offers both with their dedicated widgets (`ref:object`, `filter-condition`)",
128
+
},
129
+
// ── Depth two (#14327): the lifecycle policy blocks ──
130
+
{
131
+
kind: 'omit',
132
+
type: 'object',
133
+
path: 'lifecycle.retention',
134
+
key: 'onlyWhen',
135
+
why: "a per-field row-filter map ({ field: value | { $in: [...] } | { $null: bool } }) with no scalar rendering among the block's text inputs; every writer of it today is a platform system object declared in code — sys_job_queue, sys_automation_run, the storage service's system_file / system_upload_session — where the interleaved live-vs-terminal rows it exists for live. A Studio-authored object gets the plain age window; offering the filter needs a structured control, a form-face addition rather than a reconciliation",
136
+
},
137
+
{
138
+
kind: 'omit',
139
+
type: 'object',
140
+
path: 'lifecycle.ttl',
141
+
key: 'onlyWhen',
142
+
why: "the mirror of `retention.onlyWhen` — one shape by design (`lifecycleOnlyWhenSchema`, object.zod.ts) — with the same boundary: a row-filter map with no scalar rendering among the ttl block's text inputs, and its one writer today is the code-declared sys_session object (`revoked_at: { $null: true }`). Offering it needs a structured control, a form-face addition rather than a reconciliation",
// A non-key-bearing sub-schema (a plain array of scalars, say) has nothing
320
439
// to reconcile against — but a hand-written list under it is then
321
440
// unanchored, so say so rather than skipping silently.
322
-
expect(subKeys,`${type}.${path}: hand-written sub-list over a non-key-bearing schema`).toBeTruthy();
441
+
expect.soft(v.unanchored,`${type}.${v.path}: hand-written sub-list over a non-key-bearing schema`).toBe(false);
323
442
324
-
expect(
325
-
offered.filter((k)=>!subKeys!.includes(k)),
326
-
`${type}.${path}: offered by the form but not declared by the Zod (saved value is dropped)`,
443
+
expect.soft(
444
+
v.formOnly,
445
+
`${type}.${v.path}: offered by the form but not declared by the Zod (saved value is dropped)`,
327
446
).toEqual([]);
328
447
329
-
expect(
330
-
offered.filter((k)=>isRetiredAt(sub,k)),
331
-
`${type}.${path}: offered by the form but RETIRED in the Zod (retiredKey tombstone — filling the control hard-fails the save). Delete the form entry and leave a comment naming the retirement`,
448
+
expect.soft(
449
+
v.retired,
450
+
`${type}.${v.path}: offered by the form but RETIRED in the Zod (retiredKey tombstone — filling the control hard-fails the save). Delete the form entry and leave a comment naming the retirement`,
332
451
).toEqual([]);
333
452
334
-
if(isSubset(type,path))continue;
335
-
constexcused=omittedAt(type,path);
336
-
// A tombstoned key needs no ledger entry to excuse its absence — the
337
-
// *only* correct thing to do with it is not offer it. Demanding one back
338
-
// (or a ledger row for it) is this gate's blind spot inverted: before
339
-
// #5280 the sole thing keeping `object.fields.conditionalRequired` off
340
-
// this list was an unrelated `subset` entry, i.e. luck.
0 commit comments