Skip to content

Commit 74554a3

Browse files
os-billclaude
andauthored
fix(spec): give the two declared-but-unauthorable metadata keys their form rows (#19182)
Fixes #19085 Clause-②: yes Two keys the served schema **declares** had **no row** in the form `METADATA_FORM_REGISTRY` supplies 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/main` The card's reading was relayed from the objectui#9912 dev, so it was re-taken here rather than trusted: | probe | result | |---|---| | `relatedListFilter` / `validations` rows across every `packages/spec/src/**/*.form.ts` | **0** | | lit control — `maskingRule` | 2 hits (`field.form.ts`, `object.form.ts`) | | dark control — a name no form carries | 0 hits | | `field.relatedListFilter` declared | `FilterConditionSchema.optional()`, `field.zod.ts:1501` | | `object.validations` declared | `z.array(ValidationRuleSchema).optional()`, `object.zod.ts:2050` | So 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.validations` **`live`** (`evaluateValidationRules`, the write-path evaluator), and `field.relatedListFilter` `planned` + `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-DSL `FilterCondition`, whose derivation is `allOf: [open record, { $and/$or/$not }]` with **no top-level `type`** — there is nothing for the generic renderer to derive a control from. `filter-condition` names the FilterCondition wire, and this file already uses it one section down for `summaryOperations.filter`, the sibling `FilterConditionSchema` key. 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-admin `SchemaForm`, whose own `WIDGETS` map registers no `filter-condition` (the `FilterConditionField` of that name lives in `@object-ui/fields`, on the ComponentRegistry path `ObjectForm` uses), and with the pointer unresolved neither structural fallback applies, so `resolveFieldFace` lands on `{ kind: 'raw-json', hint }` — the same face `summaryOperations.filter` gets. That editor hands `JSON.parse` output through verbatim and the save door judges it, so the wire is exact either way; the hint is the forward-looking half. ⛔ Deliberately **not** `filter-builder`: that widget consumes a rule **ARRAY** (what `view.filter`, `dataset.filter` and `page.filterBy` store), so routing this key there would write metadata the runtime refuses — the authoring trap this row exists to close, re-created one layer up. `visibleWhen` mirrors the key's own contract text (`lookup` / `master_detail`), a meaningfulness gate rather than a parse gate: `FieldSchema` accepts 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** (what `view.filter`, `dataset.filter` and `page.filterBy` store); routing this key there would write metadata the runtime refuses, re-creating the trap one layer up. `visibleWhen` mirrors the key's own contract text ("meaningful on a child's `master_detail`/`lookup` field"): a **meaningfulness** gate, not a parse gate — `FieldSchema` accepts 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 #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 a `oneOf` over the six `ValidationRule` members. 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: `json` is 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 an `object-rows` repeater over the first `oneOf` branch; that is the renderer's precedence, not this repo's contract. Same treatment as the sibling structured-array rows `permission.rowLevelSecurity` and `email_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.ts` reconciles the two directions asymmetrically, and only one of them at the top level: | direction | top level | nested lists | |---|---|---| | form-only (offered, not declared) | checked | checked | | retired (offered, tombstoned) | checked | checked | | zod-only (declared, not offered) | **unchecked** | checked (ledgerable) | 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.ts` pins both rows, both faces and the `visibleWhen`, and adds a registry-wide assertion — every row of every form, at every depth — that **no** form routes a `FilterCondition`-typed key to the rule-array builder. The negative half is written so it can fail: a lit control asserts the walk actually reaches both `field.relatedListFilter` and `field.summaryOperations.filter` before the misrouted set is reported empty. ⛔ Not measured here: the **browser** half. At the pinned `.objectui-sha` the 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.rowLevelSecurity` and `email_template.variables`. ## Acceptance notes - `METADATA_FORM_REGISTRY` itself is a pure import map — it declares no rows. The rows live in `packages/spec/src/data/{field,object}.form.ts`, so the landing file named at dispatch could not carry this change; the registry file is unmodified. - The regenerated `platform-objects` metadata-form bundles are the mechanical consequence of two new rows (`os i18n extract` walks the registry): 44 additive lines, translated locales filled with source text and still needing translation. - Noted, not filed: `relatedList`, `relatedListTitle` and `relatedListColumns` — the three siblings of `relatedListFilter` — are also declared and unoffered. They are part of the 276-key census above, not separate cards. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --- ⚠️ **Seat correction to this body, 2026-09-19T10:29Z** — applied by the `domain:spec` seat 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: 1. A dangling `; see the finding below` pointed 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, **#19187**. 2. + 3. The two face bullets are replaced **verbatim** with their corrected versions from `.changeset/19085-metadata-form-declared-rows.md`, which the implementer re-measured at the pinned `.objectui-sha` in the patch round: the face today is the announced **raw-JSON editor carrying the `filter-condition` hint**, ⛔ not `FilterConditionField` (a different renderer on the ComponentRegistry path `ObjectForm` uses); and `json` is 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. 4. The "browser half" paragraph rested on the same refuted renderer and is rewritten to match. ⭐ 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](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 564ac2f commit 74554a3

12 files changed

Lines changed: 328 additions & 1 deletion
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
`field.relatedListFilter` and `object.validations` are authorable in the metadata form. Both keys were **declared** by the served schema and offered by **no** form in `METADATA_FORM_REGISTRY`, so the generic metadata form never rendered a row for either and an author's only door was the Source tab — free-text JSON, where a mis-spelled sibling key is written, stored, and refused by the runtime later.
7+
8+
Measured on the tree before the change: zero rows for either key across every `*.form.ts` in `packages/spec/src`, with a lit control (`maskingRule`, offered twice) and a dark control (a name no form carries) in the same read — so the zero is a reading, not a dead probe.
9+
10+
**The face each row gets is a measurement, not a preference.** Both keys serve as JSON-Schema **pointer rows**, which is the shape a generic renderer cannot be assumed to resolve:
11+
12+
- **`field.relatedListFilter` → `widget: 'filter-condition'`.** The served node is `{ $ref: '#/$defs/…' }` onto the recursive Query-DSL `FilterCondition`, whose derivation is `allOf: [open record, { $and/$or/$not }]` with **no top-level `type`** — there is nothing for the generic renderer to derive a control from. `filter-condition` names the FilterCondition wire, and this file already uses it one section down for `summaryOperations.filter`, the sibling `FilterConditionSchema` key. 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-admin `SchemaForm`, whose own `WIDGETS` map registers no `filter-condition` (the `FilterConditionField` of that name lives in `@object-ui/fields`, on the ComponentRegistry path `ObjectForm` uses), and with the pointer unresolved neither structural fallback applies, so `resolveFieldFace` lands on `{ kind: 'raw-json', hint }` — the same face `summaryOperations.filter` gets. That editor hands `JSON.parse` output through verbatim and the save door judges it, so the wire is exact either way; the hint is the forward-looking half. ⛔ Deliberately **not** `filter-builder`: that widget consumes a rule **ARRAY** (what `view.filter`, `dataset.filter` and `page.filterBy` store), so routing this key there would write metadata the runtime refuses — the authoring trap this row exists to close, re-created one layer up. `visibleWhen` mirrors the key's own contract text (`lookup` / `master_detail`), a meaningfulness gate rather than a parse gate: `FieldSchema` accepts the key on every type, but the related-list derivation only ever reads it on the child-side FK.
13+
- **`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 a `oneOf` over the six `ValidationRule` members. 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: `json` is 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 an `object-rows` repeater over the first `oneOf` branch; that is the renderer's precedence, not this repo's contract. Same treatment as the sibling structured-array rows `permission.rowLevelSecurity` and `email_template.variables`. Upgrading it to a structured control is a form-face addition, ⛔ not a reconciliation.
14+
15+
A new pin (`metadata-form-declared-rows.pin.test.ts`) keeps both rows and both faces, and adds a registry-wide assertion — every row of every form, at every depth — that **no** form routes a `FilterCondition`-typed key to the rule-array builder, with a lit control proving the walk reaches both keys before it reports an empty misrouted set.
16+
17+
⛔ **No wire byte moves and no export changes.** `check:api-surface` and `check:api-surface-declarations` are green with no regeneration: `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so the row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves and the translation keys `os i18n extract` walks — hence the regenerated `platform-objects` metadata-form bundles (44 additive lines; the new `en` entries are source text, the translated locales still need translating).

‎packages/lint/src/validate-predicate-path-refs.test.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,22 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
556556
// `data.type == 'page'` — went with the key it configured. A form input for
557557
// an unwritable key is the false-compliant UI half of a retirement, so the
558558
// census falls by exactly one.
559-
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(52);
559+
// It is 53 today, and this one is an ADDITION: #19085 gave
560+
// `field.relatedListFilter` the form row its declaration always implied —
561+
// the served schema declared the key while no form offered it, so an
562+
// author's only door was the Source tab's free-text JSON. The row is gated
563+
// `data.type in ['lookup','master_detail']`, mirroring the key's own
564+
// contract text ("meaningful on a child's `master_detail`/`lookup` field")
565+
// and the `reference` row beside it in the same form; `FieldSchema` accepts
566+
// the key on every type, so that gate is a meaningfulness gate, not a parse
567+
// gate — the same shape as the `valueDomain` entry above.
568+
// Measured rather than inferred from the delta: the corpus was enumerated
569+
// on this tree and on the merge base `eeaa882459`, and differenced by
570+
// `<form>::<field>::<source>`. Exactly one entry was added —
571+
// `field :: relatedListFilter :: data.type in ['lookup','master_detail']`
572+
// — and NONE was removed. The same card's second row, `object.validations`,
573+
// carries no predicate at all, so it does not enter this census.
574+
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(53);
560575

561576
const findings = validatePredicatePathRefs(corrupted);
562577
expect(findings).toHaveLength(predicates);

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
249249
"enable.clone": {
250250
label: "Clone"
251251
},
252+
validations: {
253+
label: "Validations",
254+
helpText: "Object-level validation rules — an array of rule objects, e.g. [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]. State-machine transition tables are declared here too (ADR-0020)"
255+
},
252256
datasource: {
253257
label: "Datasource",
254258
helpText: "Target datasource ID (default: \"default\")"
@@ -424,6 +428,10 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
424428
label: "Delete Behavior",
425429
helpText: "What happens when referenced record is deleted"
426430
},
431+
relatedListFilter: {
432+
label: "Related List Filter",
433+
helpText: "Default filter for this relationship's related list on the parent's detail page — AND-composed with the parent-record match, and the tab badge counts the same set"
434+
},
427435
expression: {
428436
label: "Expression",
429437
helpText: "CEL expression to calculate this field (makes it read-only)"

‎packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
249249
"enable.clone": {
250250
label: "Clone"
251251
},
252+
validations: {
253+
label: "Validations",
254+
helpText: "Object-level validation rules — an array of rule objects, e.g. [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]. State-machine transition tables are declared here too (ADR-0020)"
255+
},
252256
datasource: {
253257
label: "Fuente de datos",
254258
helpText: "ID de fuente de datos de destino (valor predeterminado: \"default\")"
@@ -424,6 +428,10 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
424428
label: "Comportamiento al eliminar",
425429
helpText: "Qué ocurre cuando se elimina el registro referenciado"
426430
},
431+
relatedListFilter: {
432+
label: "Related List Filter",
433+
helpText: "Default filter for this relationship's related list on the parent's detail page — AND-composed with the parent-record match, and the tab badge counts the same set"
434+
},
427435
expression: {
428436
label: "Expresión",
429437
helpText: "Expresión CEL para calcular este campo (lo hace de solo lectura)"

‎packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export const esESGeneratedSourceHashes: Readonly<Record<string, string>> = {
6666
"metadataForms.email_template.fields.variables.helpText": "27197774c2cda633",
6767
"metadataForms.field.fields.placeholder.helpText": "07cecac0844860be",
6868
"metadataForms.field.fields.placeholder.label": "44d62b55b63fe718",
69+
"metadataForms.field.fields.relatedListFilter.helpText": "4d4f65b4886840a7",
70+
"metadataForms.field.fields.relatedListFilter.label": "0ba1fca22ec231f1",
6971
"metadataForms.field.fields.rows.helpText": "66755b9447e67a15",
7072
"metadataForms.field.fields.rows.label": "319af74cc41ea823",
7173
"metadataForms.field.fields.summaryOperations.field.helpText": "b6897e7341b31c09",
@@ -161,6 +163,8 @@ export const esESGeneratedSourceHashes: Readonly<Record<string, string>> = {
161163
"metadataForms.object.fields.lifecycle.ttl.field.label": "e21c314685cd95ca",
162164
"metadataForms.object.fields.lifecycle.ttl.helpText": "9b125bc1584a941f",
163165
"metadataForms.object.fields.lifecycle.ttl.label": "e37616f70b0d157a",
166+
"metadataForms.object.fields.validations.helpText": "1f6e59e364995ce8",
167+
"metadataForms.object.fields.validations.label": "06fc6dd288348779",
164168
"metadataForms.page.fields.interfaceConfig.addRecord.helpText": "3eb7b86c3a630db9",
165169
"metadataForms.page.fields.interfaceConfig.addRecord.label": "01068706d8b5418a",
166170
"metadataForms.page.fields.interfaceConfig.allowPrinting.helpText": "b8fa84e13cd1432c",

‎packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
249249
"enable.clone": {
250250
label: "Clone"
251251
},
252+
validations: {
253+
label: "Validations",
254+
helpText: "Object-level validation rules — an array of rule objects, e.g. [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]. State-machine transition tables are declared here too (ADR-0020)"
255+
},
252256
datasource: {
253257
label: "データソース",
254258
helpText: "対象データソース ID(既定: \"default\")"
@@ -424,6 +428,10 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
424428
label: "削除動作",
425429
helpText: "参照先レコード削除時の動作"
426430
},
431+
relatedListFilter: {
432+
label: "Related List Filter",
433+
helpText: "Default filter for this relationship's related list on the parent's detail page — AND-composed with the parent-record match, and the tab badge counts the same set"
434+
},
427435
expression: {
428436
label: "式",
429437
helpText: "このフィールドを計算する CEL 式(読み取り専用化)"

‎packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export const jaJPGeneratedSourceHashes: Readonly<Record<string, string>> = {
6666
"metadataForms.email_template.fields.variables.helpText": "27197774c2cda633",
6767
"metadataForms.field.fields.placeholder.helpText": "07cecac0844860be",
6868
"metadataForms.field.fields.placeholder.label": "44d62b55b63fe718",
69+
"metadataForms.field.fields.relatedListFilter.helpText": "4d4f65b4886840a7",
70+
"metadataForms.field.fields.relatedListFilter.label": "0ba1fca22ec231f1",
6971
"metadataForms.field.fields.rows.helpText": "66755b9447e67a15",
7072
"metadataForms.field.fields.rows.label": "319af74cc41ea823",
7173
"metadataForms.field.fields.summaryOperations.field.helpText": "b6897e7341b31c09",
@@ -161,6 +163,8 @@ export const jaJPGeneratedSourceHashes: Readonly<Record<string, string>> = {
161163
"metadataForms.object.fields.lifecycle.ttl.field.label": "e21c314685cd95ca",
162164
"metadataForms.object.fields.lifecycle.ttl.helpText": "9b125bc1584a941f",
163165
"metadataForms.object.fields.lifecycle.ttl.label": "e37616f70b0d157a",
166+
"metadataForms.object.fields.validations.helpText": "1f6e59e364995ce8",
167+
"metadataForms.object.fields.validations.label": "06fc6dd288348779",
164168
"metadataForms.page.fields.interfaceConfig.addRecord.helpText": "3eb7b86c3a630db9",
165169
"metadataForms.page.fields.interfaceConfig.addRecord.label": "01068706d8b5418a",
166170
"metadataForms.page.fields.interfaceConfig.allowPrinting.helpText": "b8fa84e13cd1432c",

‎packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
249249
"enable.clone": {
250250
label: "Clone"
251251
},
252+
validations: {
253+
label: "Validations",
254+
helpText: "Object-level validation rules — an array of rule objects, e.g. [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]. State-machine transition tables are declared here too (ADR-0020)"
255+
},
252256
datasource: {
253257
label: "数据源",
254258
helpText: "目标数据源 ID(默认:\"default\")"
@@ -424,6 +428,10 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
424428
label: "删除行为",
425429
helpText: "被引用记录删除时的处理方式"
426430
},
431+
relatedListFilter: {
432+
label: "Related List Filter",
433+
helpText: "Default filter for this relationship's related list on the parent's detail page — AND-composed with the parent-record match, and the tab badge counts the same set"
434+
},
427435
expression: {
428436
label: "表达式",
429437
helpText: "用 CEL 表达式计算此字段的值(自动设为只读)"

‎packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export const zhCNGeneratedSourceHashes: Readonly<Record<string, string>> = {
6565
"metadataForms.doc.label": "3f35cf5088b999ad",
6666
"metadataForms.field.fields.placeholder.helpText": "07cecac0844860be",
6767
"metadataForms.field.fields.placeholder.label": "44d62b55b63fe718",
68+
"metadataForms.field.fields.relatedListFilter.helpText": "4d4f65b4886840a7",
69+
"metadataForms.field.fields.relatedListFilter.label": "0ba1fca22ec231f1",
6870
"metadataForms.field.fields.rows.helpText": "66755b9447e67a15",
6971
"metadataForms.field.fields.rows.label": "319af74cc41ea823",
7072
"metadataForms.field.fields.summaryOperations.field.helpText": "b6897e7341b31c09",
@@ -128,6 +130,8 @@ export const zhCNGeneratedSourceHashes: Readonly<Record<string, string>> = {
128130
"metadataForms.object.fields.fields.valueDomain.label": "18ebeb7e56e792a1",
129131
"metadataForms.object.fields.fields.visibleWhen.helpText": "46de2d3ff57b6667",
130132
"metadataForms.object.fields.fields.visibleWhen.label": "c852d4249db93285",
133+
"metadataForms.object.fields.validations.helpText": "1f6e59e364995ce8",
134+
"metadataForms.object.fields.validations.label": "06fc6dd288348779",
131135
"metadataForms.page.fields.interfaceConfig.addRecord.helpText": "3eb7b86c3a630db9",
132136
"metadataForms.page.fields.interfaceConfig.addRecord.label": "01068706d8b5418a",
133137
"metadataForms.page.fields.interfaceConfig.allowPrinting.helpText": "b8fa84e13cd1432c",

‎packages/spec/src/data/field.form.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,49 @@ export const fieldForm = defineForm({
102102
{ label: 'Cascade (delete children)', value: 'cascade' },
103103
{ label: 'Restrict (block the delete)', value: 'restrict' },
104104
] },
105+
// #19085 — `relatedListFilter` gets the row its declaration always
106+
// implied. The served schema DECLARED the key and no form offered it,
107+
// so an author's only door was the Source tab's free-text JSON, where
108+
// nothing validates the sibling key they invent until the runtime
109+
// refuses it.
110+
//
111+
// The face is `filter-condition`, ⛔ NOT `filter-builder`: the two
112+
// widgets speak different wires. `filter-builder` consumes a rule
113+
// ARRAY (what `view.filter`, `dataset.filter` and `page.filterBy`
114+
// store); this key is a canonical Query-DSL `FilterCondition` — an
115+
// object keyed by field, with `$and`/`$or`/`$not`. Routing it to the
116+
// array widget would write metadata the runtime refuses, which is the
117+
// authoring trap this row exists to close, re-created one layer up.
118+
// `filter-condition` names the FilterCondition wire, and this file
119+
// already uses it one section down for `summaryOperations.filter`, the
120+
// sibling FilterConditionSchema key.
121+
//
122+
// ⚠ What the hint renders as TODAY, measured at the pinned
123+
// `.objectui-sha`, is the announced raw-JSON editor carrying the hint —
124+
// ⛔ NOT a criteria builder. The renderer that consumes this registry is
125+
// the metadata-admin `SchemaForm`, whose own `WIDGETS` map registers no
126+
// `filter-condition` (the `FilterConditionField` of that name lives in
127+
// `@object-ui/fields`, on the ComponentRegistry path `ObjectForm` uses,
128+
// not this one), and its `resolveFieldFace` falls past the registry,
129+
// past both structural fallbacks — the served node is
130+
// `{ $ref: '#/$defs/…' }` onto the recursive FilterCondition, an
131+
// `allOf: [open record, { $and/$or/$not }]` with NO top-level `type`,
132+
// so neither an object form nor an array-of-objects can be derived
133+
// (objectui#9912 measured the same for 12 of the 14 served pointer
134+
// rows) — and lands on `{ kind: 'raw-json', hint }`. That is the same
135+
// face `summaryOperations.filter` gets, it hands `JSON.parse` output
136+
// through verbatim, and the save door judges it, so the wire is exact
137+
// either way. The hint is the forward-looking half: it is what a
138+
// renderer resolves when it can, and it is ⛔ never `filter-builder`.
139+
//
140+
// `visibleWhen` mirrors the key's own contract text — "it is
141+
// meaningful on a child's `master_detail`/`lookup` field" — and the
142+
// `reference` row above. The schema accepts the key on every type, so
143+
// this is a MEANINGFULNESS gate, not a parse gate: on a non-reference
144+
// field the related-list derivation never reads it, and offering a
145+
// knob the runtime does not deliver is what Prime Directive #10
146+
// forbids.
147+
{ field: 'relatedListFilter', widget: 'filter-condition', visibleWhen: "data.type in ['lookup','master_detail']", helpText: "Default filter for this relationship's related list on the parent's detail page — AND-composed with the parent-record match, and the tab badge counts the same set" },
105148
],
106149
},
107150
{

0 commit comments

Comments
 (0)