Repository navigation
Commit a29a0ea
Fixes #20928
Clause-②: yes
The third carrier of the inline grid column is brought to the contract
the other two already hold.
`ComponentPropsMap['object-master-detail-form'].details` was
`z.array(z.unknown())`; each entry is now a strict shape of the keys
objectui's `MasterDetailForm` reads, and its `columns` references
`InlineGridColumnSchema`. `defineStack`'s identity-only check
(`collectHydratedInlineColumnErrors`) now reaches the block on a page,
and `field-no-consumers` reads a detail entry as a child collection.
Governed by ruling `5791803339` (#19629, letter B) and ruling
`5805782503` (#19910, letter 乙); routed by triage `5919655566`.
## What changed
- **`packages/spec/src/ui/component.zod.ts`**: `details` is
`z.array(masterDetailDetailEntry())`. The entry is a `strictObject` with
`childObject` (required), `relationshipField`, `columns`
(`z.array(InlineGridColumnSchema)`, the same object, not a copy),
`formFields`, `inlineMode` (`grid` or `form`), `amountField`,
`sortField`, `totalField`, `title`, `minRows`, `maxRows`, `addLabel`.
The keys shared with a form view's `subforms[]` entry take that entry's
types and alias table. The entry is built inside the props schema's own
lazy body, as the `subforms[]` entry is: a standalone `lazySchema`
element is a function-typed proxy the alias-integrity graph walk never
descends into (measured: `alias-integrity.test.ts` went red on it, see
commit `e734f15e`). New type `ObjectMasterDetailFormPropsParsed`: the
props schema's input and output now differ, because the column schema's
are different.
- **`packages/spec/src/stack.zod.ts`**: the page-component traversal
that `collectInlinePageActions` owned (regions, slots, nested container
arrays) is extracted into `forEachPageComponent`, unchanged, and the
collector walks it for `object-master-detail-form` blocks, judging
`details[j].columns` against `details[j].childObject`. A column is
judged only once it parses as a column WITHOUT the resolved type. On the
two parsed carriers this was already true. On the page carrier, whose
`properties` the stack parse never reaches, it keeps every finding one
that the resolved type brings; a column with its own defects is left to
the component-props gate.
- **`packages/lint/src/validate-field-consumers.ts`**:
`CHILD_COLLECTION_KEYS` gains `details`. A detail entry's column `name`,
`amountField` and `relationshipField` credit the child's field,
`totalField` the parent's, and an entry without `columns` credits the
derived grid. The page slot map's own `details` key holds components,
which carry none of an entry's keys at their own level. A pinned control
shows such a component is read the same under `slots.details` as under
`slots.header`.
- **ADR-0087**: new D3 semantic entry
`ui-object-master-detail-form-details-closed`, plus a step-18 rationale
fragment (order 56). `gen:migration-registry` regenerated the registry.
No D2 conversion, per the census below.
- **`content/docs/protocol/objectui/layout-dsl.mdx`**: the master-detail
example's `columns: [product, quantity, unit_price]` (bare strings)
becomes three `name:` columns. The strict entry refuses bare strings,
and the renderer could not draw them either: `hydrateColumns` returns a
string column unchanged, and the grid reads `col.name`.
- Generated: `dropped-refinements.baseline.json` (one new site,
`ui/ObjectMasterDetailFormProps` at `details.element.columns.element`,
plus its two counts), `api-surface/ui.json`, `export-origins/ui.json`,
`content/docs/references/ui/component.mdx`,
`docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md`.
`type-alias-convention.pin.test.ts` drops the props schema's isomorphism
pin (779 to 778) for the new `XParsed` alias, the route that file
prescribes.
- Changeset `.changeset/20928-master-detail-details-closed.md`:
`@objectstack/spec` minor (BREAKING, `Clause-②: yes (narrowing)`, marker
`registered ui-object-master-detail-form-details-closed`),
`@objectstack/lint` patch.
## Measurements
**Premise** (`origin/main` `2c1cef3345` at dispatch, re-read at this
head): `details: z.array(z.unknown()).optional()` sat at
`component.zod.ts:4776`. Premise holds.
**The read set**, from objectui
`packages/plugin-form/src/MasterDetailForm.tsx` at the `.objectui-sha`
pin `31971ff1e28f`. The file is byte-identical at the dispatch pin
`e420df310f5b`. `MasterDetailDetailConfig` is declared at `:64-93`. Each
of its twelve keys has a live read: `childObject` `:986`, `:1003`;
`relationshipField` `:1033`, `:1312`; `columns` `:858`, `:928`, `:1034`;
`formFields` `:847`, `:1063`; `inlineMode` `:850`, `:1064`;
`amountField` `:861`, `:1035`, `:1550`; `sortField` `:862`, `:1038`;
`totalField` `:1549`; `title` `:755`; `minRows` `:863`; `maxRows`
`:864`; `addLabel` `:865`. A grep of every `d.X`, `detail.X` and
`e.config.X` member read in the file finds no thirteenth key;
`detail.saving` and `detail.add` are i18n keys. `deriveDetail` hydrates
an authored list with `hydrateColumns` and derives with `deriveColumns`,
the same rule `deriveInlineGridColumns` reproduces.
**Census** (`origin/main` `ebdb6f2aca`, the branch point): blocks typed
`object-master-detail-form` appear in the showcase
`project-workspace.page.ts` (one entry, `{ title, childObject, addLabel
}`, no columns), in one doc example (three bare-string columns,
rewritten here), and in the lint and spec test fixtures (copies of the
showcase entry). `sdui.manifest.json` is objectui's registry, not a
producer. `field`-keyed detail columns: 0. The regex `details:.*field:`
does match three API-error `details` payloads elsewhere, so the matcher
is live. Control: one authored `inlineColumns` block in `examples/`
(showcase `invoice.object.ts`). So only a narrowing marker is owed.
Page-component `properties` is not parsed on the save or load path, so
no stored row is ever refused, and no conversion is registered.
**Collector reach**: `config.pages` is reachable from the collector, and
the existing page walk already reached component `properties` with hard
cross-reference errors (inline actions). Measured below.
**Field-consumer scan**: before this change it did not read
`details[].columns[].name` (`CHILD_COLLECTION_KEYS` was `subforms`
only). Ablation A4 below confirms it.
**Pinned objectui** (`31971ff1e28f`, `git grep`): the only non-test
importer is `packages/types/src/zod/objectql.zod.ts:52` / `:3913`, which
takes `ObjectMasterDetailFormPropsSchema` by reference through
`stripImportedDefaults`, a generic walk. The tests
`registered-type-arms-10859-b2.test.ts` compare the node's props to
`SpecObjectMasterDetailFormProps` by reference on both sides, and their
showcase fixture `{ title, childObject, addLabel }` stays valid. No
value the narrowed type refuses was found at that pin. objectui's own
suites against this spec: NOT MEASURED (objectui coordination, below).
**Public door** (`os validate --json`, CLI built at `bfb26d52`, a probe
stack with a currency child field):
| probe column(s) | exit | `valid` | finding |
|:--|:--|:--|:--|
| `{ name: 'amount', type: 'currency', scale: 2 }` | 0 | true | advisory
`component-props-invalid` at `properties.details.0.columns.1.scale`, the
ruled first sentence |
| `{ zzz_not_a_key: 1 }` | 0 | true | advisory
`component-props-unknown-key` (`zzz_not_a_key`) and
`component-props-invalid` (`name` required) |
| `{ name: 'amount', scale: 2 }` over a `currency` field | 1 | false |
`STACK_CROSS_REFERENCE_INVALID`: `Page 'probe_invoice_entry'
(regions.0.components.0) object-master-detail-form
details[0].columns[1].scale: column 'amount' declares no type, so it
renders as a currency column …` |
| `{ name: 'quantity' }, { name: 'amount' }` (control) | 0 | true |
none; and `field-no-consumers` reports neither child field |
## Verification (at `bfb26d52`, after merging `origin/main` `0d421041`
through `scripts/pm/os-regen-merge.sh`)
- `@objectstack/spec` `vitest run --project local`, two shards: 297 +
297 files passed, 8915 + 8511 tests passed, 1 todo. `--project repo`
subset (`step18-rationale-merge`, `conversions-major18-merge`,
`retired-key-migrate-sentence`): 35 passed. `pnpm --filter
@objectstack/spec typecheck`: exit 0. `tsc -p tsconfig.test.json
--listFiles` includes both edited test files, and none of the errors
held in the debt ledger names them.
- `@objectstack/lint` `vitest run`: 118 files / 5494 passed.
`typecheck`: exit 0.
- Consumers, each named package exactly (`--filter PKG`, not a closure):
`@objectstack/sdui-parser` 13 files / 217 passed;
`@objectstack/metadata-protocol` 200 passed, 3 skipped files / 2973
passed, 19 skipped. Packages whose fixtures write the block: spec and
lint only, covered above.
- `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts up
to date after a fresh build.
- Derived gates `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands`: 114 derived, 114 run, all exit
0. `--ran` reconciliation: "0 NOT-MEASURED (a DERIVED zero)". Three
first exited 3 (PREREQUISITE NOT MET: `check:doc-formula-expressions`,
`check:doc-security-posture`, `check:skill-examples`) and were green
after `turbo run build` over `./packages/*` and `./packages/*/*`. Five
roster gates marked as sharing a directory with this diff
(`check-changeset-fixed`, `check:meta-url-spelling`,
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`) and `check-sdui-manifest`: exit 0.
- ESLint, a narrowed run, stated as such. `eslint --no-inline-config
--format json` over the 8 changed `.ts` files reports files 8, errors 0,
warnings 0. The population is `eslint.config.mjs:971`,
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. That config never enables
type-aware linting (its comment at `:326-328`: no
`parserOptions.project`, no typed rules), so this diff cannot move the
verdict on any untouched file.
- Ablations, each through `scripts/ablation-replace.mjs` from committed
state (`bfb26d52`), each restored with blob equal to HEAD and `git diff
HEAD` empty. A first pass at `074d6ca4`, before the entry became a
factory, gave the same four counts. The subjects resolve to `src`
through relative imports, so no dist leg was owed.
- A1 `details: z.array(masterDetailDetailEntry())` →
`z.array(z.unknown())` (blob `72d6abd987ce` → `242fee1c1fb5`): carriers
pin `8 failed | 28 passed (36)`. All seven props-half refusals went red,
plus the first assertion of the "left to the props half" pin. I
predicted 7; the eighth is that pin's own props-half check.
- A2 the collector's page loop emptied (`aa6368d1f138` →
`318e3e0ee35f`): `2 failed | 34 passed`. Those are the region refusal
and the nested and slot refusal.
- A3 the valid-without-type precondition deleted (`aa6368d1f138` →
`047cac280736`): `1 failed | 35 passed`. The red run printed the
misattribution the precondition exists to stop: "column 'amount'
declares no type, so it renders as a currency column … Unrecognized
key(s) on this inline grid column: zzz_not_a_key".
- A4 `CHILD_COLLECTION_KEYS` back to `subforms` only (`3efd1236571e` →
`7b8b0ab03798`): `3 failed | 66 passed (69)`. Those are the authored
column, `amountField`, and the derived grid.
- A5 the doc's old bare-string columns restored (`b6df7f7748e0` →
`2aef1e440273`): `check:yaml-examples` exit 1, "1 of 19 tagged YAML
block(s) do not hold their claim … expected object, received string" at
`layout-dsl.mdx:774`.
## Acceptance notes
- **Two enforcement tiers on one carrier.** The entry and column half is
read where every page component's props are read: the component-props
gate, advisory by its own declared posture
(`validate-component-props.ts` header). So the card's typed and bogus
columns move from silent green to `valid: true` with advisory findings.
The identity-only column is a hard cross-reference error, like the
inline-action targets already judged on the same pages. Upgrading the
props gate is that gate's own step.
- **Save door**: a page saved through the metadata door keeps a failing
detail entry, because `properties` is not parsed there. This is the same
posture as every page-component narrowing in step 18. NOT MEASURED at
the door.
- **Scan, adjacent position**: `field-no-consumers` still reads a detail
entry's `sortField` and `formFields` in the parent's context, through
the general walk. Not changed here. #21091's positions are not touched.
- **Files beyond the claim's list**: `layout-dsl.mdx` (a producer the
census found, which `check:yaml-examples` reds on, see A5),
`type-alias-convention.pin.test.ts` and
`dropped-refinements.baseline.json` (both forced by referencing the
column schema). `conversions/registry.ts` and the showcase page are
untouched, because nothing needed converting or rewriting.
- The D3 entry `inline-grid-column-identity-only-currency-scale-refused`
still names its two carriers. The new entry records the third rather
than editing a step-18 text that other open PRs hold.
- `origin/main` moved after the merge (`e18fea6d`). Its only spec file
is `data/filter.zod.ts`, which this diff does not touch. Merge-queue
revalidation covers it.
- objectui's `MasterDetailForm` mirror and its spec-parity probes over
this row are not in this PR. The seat files the objectui coordination
child.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e2ed61a commit a29a0ea
15 files changed
Lines changed: 730 additions & 75 deletions
File tree
- .changeset
- content/docs
- protocol/objectui
- references/ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- lint/src
- spec
- api-surface
- export-origins
- src
- migrations
- entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
765 | 765 | | |
766 | 766 | | |
767 | 767 | | |
768 | | - | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
769 | 772 | | |
770 | 773 | | |
771 | 774 | | |
| |||
779 | 782 | | |
780 | 783 | | |
781 | 784 | | |
782 | | - | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
783 | 789 | | |
784 | 790 | | |
785 | 791 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
950 | 950 | | |
951 | 951 | | |
952 | 952 | | |
953 | | - | |
| 953 | + | |
954 | 954 | | |
955 | 955 | | |
956 | 956 | | |
| |||
959 | 959 | | |
960 | 960 | | |
961 | 961 | | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
962 | 979 | | |
963 | 980 | | |
964 | 981 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
| 49 | + | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
65 | | - | |
| 65 | + | |
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
835 | 835 | | |
836 | 836 | | |
837 | 837 | | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
283 | 283 | | |
284 | 284 | | |
285 | 285 | | |
286 | | - | |
287 | | - | |
288 | | - | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
289 | 297 | | |
290 | | - | |
| 298 | + | |
291 | 299 | | |
292 | 300 | | |
293 | 301 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
297 | 297 | | |
298 | 298 | | |
299 | 299 | | |
| 300 | + | |
300 | 301 | | |
301 | 302 | | |
302 | 303 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | | - | |
| 5 | + | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
1354 | 1354 | | |
1355 | 1355 | | |
1356 | 1356 | | |
| 1357 | + | |
| 1358 | + | |
| 1359 | + | |
| 1360 | + | |
| 1361 | + | |
1357 | 1362 | | |
1358 | 1363 | | |
1359 | 1364 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| 296 | + | |
296 | 297 | | |
297 | 298 | | |
298 | 299 | | |
| |||
0 commit comments