fix(spec): give the two declared-but-unauthorable metadata keys their form rows - #19182
Conversation
…he form rows their declarations implied
Both keys were declared by the served schema and offered by no form in
`METADATA_FORM_REGISTRY`, so the generic metadata form never rendered them
and an author's only door was the Source tab's free-text JSON.
The face each row gets is measured, not assumed:
- `field.relatedListFilter` -> `widget: 'filter-condition'`. The served node
is a `$ref` onto the recursive FilterCondition, whose derivation is
`allOf: [open record, { $and/$or/$not }]` with no top-level `type`, so the
generic renderer has nothing to derive from. `filter-condition` is the
registered widget whose storage contract is that object shape; the
rule-ARRAY `filter-builder` would write metadata the runtime refuses.
`visibleWhen` mirrors the key's own contract text.
- `object.validations` -> `widget: 'json'`. The served node is an array whose
items are a double-hop `$ref` landing on a `oneOf` over the six
ValidationRule members; a repeater would have to resolve both hops and pick
a branch before rendering a row, neither of which is measured for this node.
A new pin keeps both rows and both faces, including a registry-wide assertion
that no form routes a FilterCondition-typed key to the rule-array builder.
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
…nd add the changeset `os i18n extract` walks METADATA_FORM_REGISTRY, so the two rows added in the previous commit are new `metadataForms.*` keys. Regenerated with `node scripts/check-i18n-bundles.mjs --write`; 44 additive lines across the platform-objects bundles, translated locales filled with source text. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1d1445e88852d5231014cbcb6f07da05cadfe195 && git checkout 1d1445e88852d5231014cbcb6f07da05cadfe195
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eeaa882459edffd077c1cda2972af06d1f10550b 801690faaae1d01e8cde716caf00fb121b249c1e && git checkout -B drift-repro eeaa882459edffd077c1cda2972af06d1f10550b && git merge --no-ff 801690faaae1d01e8cde716caf00fb121b249c1e
node scripts/docs-audit/affected-docs.mjs --json eeaa882459edffd077c1cda2972af06d1f10550b
|
Contract reviewServed-tier: Isolated, adversarial contract review of PR #19182 (card #19085), judged against ① Derived judgmentsAccept/reject behaviour of a published contract: 0 changes. The diff touches no Item by item, with the implementer's classification judged:
② Semver level
③ Boundary flagsClause-② declaration — STANDS; its stated basis is overturned. Claim comment 5740363216 declared Do the two rows make an invalid document newly authorable, or only open a door onto keys the schema already declares? — A door, and only a door. Measured, not adopted: (a) both keys were declared before the diff ( Triage acceptance (grading, not a ruling), quoted as written: 「验收:该行在 Open question 1 — stale liveness row for Open question 2 — structured repeater for Owed before re-review — the reason for the verdict: at Implemented-by: VERDICT: FAIL Reading taken at 2026-09-19T09:37Z by the isolated contract-review subagent; scratch under the reviewing seat's Generated by Claude Code |
… relatedListFilter gate The `relatedListFilter` row added for #19085 carries a `visibleWhen`, so the shipped-predicate corpus the #7010 test pins grows by one. `Test Core (2/6)` failed on exactly that assertion at the previous head; the merge base is green on the same job, so no same-signature exemption applies. Census written in the file's own convention and MEASURED rather than inferred from the delta: the corpus was enumerated on this tree and on the merge base eeaa882 and differenced by `<form>::<field>::<source>`. Exactly one entry added -- `field :: relatedListFilter :: data.type in ['lookup','master_detail']` -- and none removed. The card's second row, `object.validations`, carries no predicate and does not enter the census. Also adopts the two prose corrections from the contract review, in the form comments and the changeset. Both were re-measured here against the pinned `.objectui-sha` rather than taken from the review: - the face is the ANNOUNCED RAW-JSON EDITOR carrying the `filter-condition` hint, not a criteria builder. The renderer consuming this registry is the metadata-admin `SchemaForm`, whose `WIDGETS` map registers no `filter-condition`; `resolveFieldFace` falls past the registry and both structural fallbacks (the pointer is unresolved) to `{ kind: 'raw-json' }`. - `json` is a passthrough only AFTER the structural fallbacks, so "derives nothing" was imprecise: this row reaches the raw editor because the unresolved double-hop pointer derives nothing, not because the hint suppresses derivation. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Isolated, adversarial contract review of PR #19182 (card #19085), judged at the head above against its merge base ① Derived judgmentsAccept/reject behaviour of a published contract: 0 changes. The diff touches no Item by item, with the implementer's classification judged:
② Semver level
③ Boundary flags
The two prose corrections the implementer accepted, measured at the pin
Do the two form rows let an author write a document the runtime refuses? — NO, measured. (i) Both faces at the pin are the raw-JSON editor, whose Triage acceptance (grading, not a ruling), quoted as written: 「验收:该行在 Open question 1 — the stale liveness row for Open question 2 — a structured repeater for Checks at this head: all seven required contexts — Owed before landing: nothing. The change the earlier record named is discharged and its number is right; the two accepted corrections are themselves correct at the pin; no undeclared change rode in with the round. Implemented-by: VERDICT: PASS Reading taken at 2026-09-19T10:45Z by the isolated contract-review subagent; scratch under the reviewing seat's scratchpad/pr-19182-r2/; no label, draft, auto-merge or push was touched. This PASS supersedes the FAIL in PR comment 5740807745 (head 608f0ed). Generated by Claude Code |
Clause-② provenance — carriers cleared on an at-tier PASS (second head, superseding a FAIL)
Record adopted: comment 5741170393, a ⭐ Both records stand on the thread. The FAIL is ⛔ not deleted and ⛔ not edited: it is the record of what was true at that head, and the patch round is legible only beside it. Tier, measured rather than taken from the reviewer's word. A fresh isolated at-tier subagent, fed only the card, its rulings and this PR, told that seat comments and the Carriers cleared in one act, both read back at 2026-09-19T10:47Z: card #19085 →
Landing preconditions, each measured at 2026-09-19T10:47Z, ⛔ not recalled:
Generated by Claude Code |
Fixes #19085
Clause-②: yes
Two keys the served schema declares had no row in the form
METADATA_FORM_REGISTRYsupplies for their type, so the generic metadata form never rendered them and an author's only door was the Source tab's free-text JSON.Measured first, on
origin/mainThe card's reading was relayed from the objectui#9912 dev, so it was re-taken here rather than trusted:
relatedListFilter/validationsrows across everypackages/spec/src/**/*.form.tsmaskingRulefield.form.ts,object.form.ts)field.relatedListFilterdeclaredFilterConditionSchema.optional(),field.zod.ts:1501object.validationsdeclaredz.array(ValidationRuleSchema).optional(),object.zod.ts:2050So the zero is a live probe, and both premises hold.
Neither key is declared-but-dead, so the ADR-0049 retirement fork does not fire: the liveness ledger reads
object.validationslive(evaluateValidationRules, the write-path evaluator), andfield.relatedListFilterplanned+authorWarn— whose stated flip condition (objectui#4664) has since landed; it is carded as #19187.The face each row gets is a measurement, not a preference
Both keys serve as JSON-Schema pointer rows — the shape the card warned must not be assumed renderable.
field.relatedListFilter→widget: 'filter-condition'. The served node is{ $ref: '#/$defs/…' }onto the recursive Query-DSLFilterCondition, whose derivation isallOf: [open record, { $and/$or/$not }]with no top-leveltype— there is nothing for the generic renderer to derive a control from.filter-conditionnames the FilterCondition wire, and this file already uses it one section down forsummaryOperations.filter, the siblingFilterConditionSchemakey. What the hint renders as today, measured at the pinned.objectui-sha, is the announced raw-JSON editor carrying the hint — not a criteria builder: the renderer that consumes this registry is the metadata-adminSchemaForm, whose ownWIDGETSmap registers nofilter-condition(theFilterConditionFieldof that name lives in@object-ui/fields, on the ComponentRegistry pathObjectFormuses), and with the pointer unresolved neither structural fallback applies, soresolveFieldFacelands on{ kind: 'raw-json', hint }— the same facesummaryOperations.filtergets. That editor handsJSON.parseoutput through verbatim and the save door judges it, so the wire is exact either way; the hint is the forward-looking half. ⛔ Deliberately notfilter-builder: that widget consumes a rule ARRAY (whatview.filter,dataset.filterandpage.filterBystore), so routing this key there would write metadata the runtime refuses — the authoring trap this row exists to close, re-created one layer up.visibleWhenmirrors the key's own contract text (lookup/master_detail), a meaningfulness gate rather than a parse gate:FieldSchemaaccepts the key on every type, but the related-list derivation only ever reads it on the child-side FK.⛔ Deliberately not
filter-builder— the boundary the card and the triage both drew. That widget consumes a rule ARRAY (whatview.filter,dataset.filterandpage.filterBystore); routing this key there would write metadata the runtime refuses, re-creating the trap one layer up.visibleWhenmirrors the key's own contract text ("meaningful on a child'smaster_detail/lookupfield"): a meaningfulness gate, not a parse gate —FieldSchemaaccepts the key on every type, but the related-list derivation only reads it on the child-side FK, and offering a knob the runtime does not deliver is what Prime Directive chore: version packages #10 forbids.object.validations→widget: 'json'. The served node is an array whose items are a double-hop pointer (items.$ref→$defs/__schema1→$defs/__schema2) landing on aoneOfover the sixValidationRulemembers. A repeater would have to resolve both hops and pick a union branch before it could render a row; neither half is measured for this node, and a repeater that resolves neither renders an empty row whose values never land — the offer-vs-door defect the reconciliation gate beside it exists to catch. The Zod parse still refuses a malformed rule loudly at publish. Precisely:jsonis in that renderer's passthrough set, but the set is consulted after the structural fallbacks, not instead of them — so this row reaches the raw-JSON editor because the unresolved double-hop pointer derives nothing, not because the hint suppresses derivation. Once the pin moves past objectui's pointer resolution the same hint derives anobject-rowsrepeater over the firstoneOfbranch; that is the renderer's precedence, not this repo's contract. Same treatment as the sibling structured-array rowspermission.rowLevelSecurityandemail_template.variables. Upgrading it to a structured control is a form-face addition, ⛔ not a reconciliation.⛔ No new form primitive was invented. Upgrading this to a structured control is a form-face addition — the same boundary the reconciliation ledger draws for
lifecycle.*.onlyWhen.Why nothing failed while both keys sat unauthorable
metadata-form-zod-reconciliation.test.tsreconciles the two directions asymmetrically, and only one of them at the top level:This card's class is exactly the unchecked cell. Closing it generally is not in this PR: measured on this tree, the top-level zod-only set is 276 keys across the 17 forms, each needing an offer or a ledgered reason. That census is filed as a finding; the new pin holds the two keys #19085 measured in the meantime.
Tests
metadata-form-declared-rows.pin.test.tspins both rows, both faces and thevisibleWhen, and adds a registry-wide assertion — every row of every form, at every depth — that no form routes aFilterCondition-typed key to the rule-array builder. The negative half is written so it can fail: a lit control asserts the walk actually reaches bothfield.relatedListFilterandfield.summaryOperations.filterbefore the misrouted set is reported empty.⛔ Not measured here: the browser half. At the pinned
.objectui-shathe face is the announced raw-JSON editor carrying the hint (resolveFieldFace→{ kind: 'raw-json', hint }), so what is unmeasured is whether that editor renders the hint as announced — ⛔ not a criteria builder's field picker, which is a different renderer on a different path and is not reached from this registry today. The picker question becomes live only once the pin moves past objectui's pointer resolution; whoever does that bump owns re-measuring this row and its two shape-siblings,permission.rowLevelSecurityandemail_template.variables.Acceptance notes
METADATA_FORM_REGISTRYitself is a pure import map — it declares no rows. The rows live inpackages/spec/src/data/{field,object}.form.ts, so the landing file named at dispatch could not carry this change; the registry file is unmodified.platform-objectsmetadata-form bundles are the mechanical consequence of two new rows (os i18n extractwalks the registry): 44 additive lines, translated locales filled with source text and still needing translation.relatedList,relatedListTitleandrelatedListColumns— the three siblings ofrelatedListFilter— are also declared and unoffered. They are part of the 276-key census above, not separate cards.Generated by Claude Code
domain:specseat 2 PM (session_01JbZnqu8bt6YqfJsr9vaFb3), ⛔ not by the implementer, whose standing clauses write a PR body exactly once and hand later corrections to the seat.Four edits, all of them corrections of this body's own claims, ⛔ none of them a change to the diff:
; see the finding belowpointed at a finding that lives in the delivery report rather than in this body. It now points at the card the seat filed for it, [finding]field.relatedListFilter的 liveness 行仍是planned+authorWarn,而它自己写明的 flip 条件早已落地 —— os lint 正在用一句假话把作者劝离一个可用的键 #19187..changeset/19085-metadata-form-declared-rows.md, which the implementer re-measured at the pinned.objectui-shain the patch round: the face today is the announced raw-JSON editor carrying thefilter-conditionhint, ⛔ notFilterConditionField(a different renderer on the ComponentRegistry pathObjectFormuses); andjsonis in the renderer's passthrough set but consulted after the structural fallbacks, so the row reaches the raw editor because the unresolved double-hop pointer derives nothing — ⛔ not because the hint suppresses derivation.⭐ The widget choices and the diff are unchanged; what was wrong was this body's account of which renderer consumes them. The durable artefacts — the changeset and the form comments — already carry the corrected wording; this edit makes the body agree with them instead of contradicting them.
Generated by Claude Code