Repository navigation
Commit 4034f47
feat(spec,lint): object-level composite / repeater are untyped JSON — a typed list is a child object (#25755)
Fixes #25681
Clause-②: yes (widening)
Metadata-protocol: semantic
Ruling-ref: 6386744473
Ruling 6386744473 (batch #12 item 4, letter b), verbatim:
> **b** = 不给对象字段的 `repeater` / `composite` 加子字段声明;明确写下「对象级 `repeater` /
`composite` 是无类型 JSON,要带列的列表请建子对象(master_detail)」,并对期望列的 repeater 做
lint/拒收。协议不新增面。
## What changed
- **`FieldType` (`packages/spec/src/data/field.zod.ts`).** The comments
on `composite` / `repeater` used to promise "declared sub-fields". They
now say these are untyped JSON containers of CMS lineage (Strapi
component, ACF group or repeater), that `FieldSchema` has no sub-field
member, and that a typed list is a child object with a `master_detail`
field (`inlineEdit: 'grid'` edits it inline on the parent's form). No
schema change: the enum, `FieldSchema` and every generated artifact are
byte-identical (`check:generated`: all 14 up to date).
- **The parse refusal names the route
(`packages/spec/src/data/authoring-key-lint.ts`).** Three
`FIELD_KEY_GUIDANCE` rows, `fields` / `subFields` / `columns`, fed into
the strict `FieldSchema` through its existing `guidance` channel. The
parse already refused all three before this PR, but its message named
only the key, so an author learned the key was wrong but not where the
list goes. The `columns` row also names the three real column keys
(`lookupColumns`, `relatedListColumns`, `inlineColumns`). This changes a
message, not what parses.
- **New lint rule `field/embedded-sub-fields` (error) in `lintDataModel`
(`packages/lint`).** It fires when a `composite` / `repeater` object
field carries `fields`, `subFields` or `columns`. It has an `os explain`
entry and a one-line `fix`. Its docblock's intake table states which
doors reach it (below).
- **Docs.** `data-modeling/field-types.mdx` gets the same sentence plus
a child-object example, the shape Salesforce, Dynamics, ServiceNow and
Odoo use. `data-modeling/validation-rules.mdx` loses the false
"Sub-field shapes are declared on the field".
`protocol/objectui/layout-dsl.mdx` now says that a form field's `fields`
list belongs to the form, not to the object field.
- Not touched, by the ruling: a form view's `fields` sub-rows
(`view.zod.ts`), `field.form.ts`, objectui. No member was added to
`FieldSchema`.
The published skill `skills/objectstack-data/rules/field-types.md`
repeated "declared sub-fields". `skills/**` is a governed surface (Tier
H), so that fix is its own draft PR on
`claude/issue-25681-skills-repeater-untyped` (Refs #25681). This PR
carries no governed path.
## Measured before building (on `origin/main` `47af3dde`)
- **Premise holds.** `field.zod.ts:96`–`:97` promised "declared
sub-fields", and so did `field-types.mdx:515`/`:522` and the skill file.
`FieldSchema` declares no sub-field member. The `FieldType` enum has no
`.describe()`, so the generated references never carried the claim.
- **Mechanism (the PM's lead): the parse already refuses.**
`FieldSchema.safeParse` on a `repeater` / `composite` carrying `fields`,
`subFields`, `columns`, `schema`, `items`, `properties`, `itemFields`,
`of` or `children` answers `unrecognized_keys` on every one. Before this
PR the message was ``Unrecognized key(s) on this field: `fields`. Until
this shape was closed these were dropped silently — …``: it names no
route. `defineStack(x)` (strict) throws the same message.
`defineStack(x, { strict: false })` ACCEPTS the field and keeps `fields`
verbatim.
- **So the lint arm reaches only the unparsed doors**, the posture
`security-owd-alias` documents. Its docblock and `os explain` both say
so:
| door | reaches `field/embedded-sub-fields`? |
|---|---|
| `defineStack(x)` (strict), hence `os validate` / `os build` / `os
lint` on such a config | no — refused at load, now naming the route |
| `saveMetaItem` (Studio / REST `/meta` / MCP) | no — refused at the
parse |
| `os lint` on a raw object-literal config (`lintConfig` runs before any
parse) | **yes** |
| `defineStack(x, { strict: false })` | **yes** |
| metadata-generation rubric (`scoreMetadata`: `lintConfig` on the
normalized input, beside its own schema verdict) | **yes** |
- **Producers.** This repo: the only object fields of these types are
`examples/app-showcase/src/data/objects/field-zoo.object.ts:166`–`:167`
(`f_composite`, `f_repeater`), and neither carries a sub-field key.
Every other hit is a metadata FORM row (`*.form.ts`, excluded by the
ruling). HotCRM at `a4b3560`: 0 object fields of type `repeater` /
`composite` (control: `type: 'lookup'` hits in
`src/sales/actions/activity-actions.ts`). objectui at `a3c0da9`:
`packages/fields/src/index.tsx:3625`–`:3626` already states that "the
spec's `FieldSchema` has no sub-field member for either type and refuses
`fields` / `subFields`". No producer is refused by this PR that was not
already refused.
## Ablations (commit first, mutate through
`scripts/ablation-replace.mjs`, self-restore)
| leg | mutation | predicted | observed |
|---|---|---|---|
| A | `data-model-rules.ts`: `if (declared.length > 0) {` → `if (false
&& …)` | 11 red (9 in `embedded-sub-fields.test.ts`, 2 in
`fix-lines.test.ts`), 13 green | **11 red, 13 green** |
| B | `authoring-key-lint.ts`: delete the `fields:` guidance row | 2 red
(`repeater + fields`, `composite + fields`), 19 green | **2 red, 19
green** |
Both restored blob-identical to HEAD (`678d7d76`, `a2d3a01b`), and `git
diff HEAD` was empty after each. Both suites resolve the mutated module
from `src/`, never through a `dist/` export, so no rebuild leg applies.
## Verification
Every reading below is on head `87ff1c79` (this branch with
`origin/main` `5cf1b9d2` merged in), after `pnpm install
--frozen-lockfile` and a rebuild of the `@objectstack/cli...` closure.
Each exit code was captured to a file before any pipe.
| command | exit | reading |
|---|---|---|
| `pnpm --filter @objectstack/spec test` | 1 | 720/721 files,
21818/21842 tests. All 23 failures are in
`scripts/liveness/check-liveness.test.ts`, the `main`-red below |
| `pnpm --filter @objectstack/spec test:repo` | 0 | 79/79 files, 1269
tests |
| `pnpm --filter @objectstack/spec typecheck` | 0 | |
| `pnpm --filter @objectstack/spec check:generated` | 1 | 13/14 up to
date; `liveness/state-counts/` is the `main`-red below. On the pre-merge
head `22414cf1`: 14/14 |
| `pnpm --filter @objectstack/lint test` | 0 | 162/162 files, 7301 tests
|
| `pnpm --filter @objectstack/lint typecheck` | 0 | |
| `pnpm --filter @objectstack/cli test` (unit **and** integration) | 0 |
501 passed + 1 skipped files, 6297 tests |
| `pnpm --filter @objectstack/cli typecheck` | 0 | |
| `node scripts/pm/dispatch-gates.mjs --ran …` | 0 | `✓ 111 derived
famil(ies) accounted for — 111 run, 0 NOT-MEASURED`. 110 exit 0;
`check:liveness` exit 1 is the `main`-red below |
| `node scripts/check-dev-prereqs.mjs` (local-only) | 0 | |
Pre-merge head `22414cf1`, the same battery, all green: spec `test`
721/721 (21829 tests), spec `test:repo` 79/79, lint 162/162 (7296
tests), cli 500 + 1 skipped files (6291 tests), and 111/111 families
(110 run green, `check:dual-build-cjs-loads` NOT MEASURED there and
green on `87ff1c79`).
**Not this PR's: `check:liveness` is red on `main` `5cf1b9d2`.** The
`translation` ledger row `flows.screens` cites
`packages/services/service-automation/src/engine.ts#renderFlowTextSlot`.
`engine.ts` no longer names `screens` in any spelling: the refactors
`f3fea0a0` and `0189524a` moved `renderFlowTextSlot` into
`node-text-rendering.ts`. Its one earlier hit was a coincidental comment
("states two screens down"), which moved to `run-resume.ts`. Neither
file of this PR touches it. Proposed patch, proved but **not** applied
here: delete the clause `;
packages/services/service-automation/src/engine.ts#renderFlowTextSlot
(…)` from that row's `evidence` in
`packages/spec/liveness/translation.json`. The
`screen-nodes.ts#registerScreenNodes` pointer beside it already cites
the caller and names the key. With that deletion, `pnpm check:liveness`
answers ✓ (run through `scripts/ablation-replace.mjs`, then restored
blob-identical). Repointing to `node-text-rendering.ts` alone would stay
red: that file does not name `screens` either. No open PR carries a fix
(searched 2026-10-25).
## Acceptance notes
- The guidance rows are an exact, case-sensitive channel
(`shared/suggestions.zod.ts`): `subfields` or `sub_fields` still gets
the bare refusal. The three spellings are the ones the ruling names.
- `record` (ADR-0007) is also stored as untyped JSON on an object field,
but the ruling names only `composite` / `repeater`, so the lint does not
judge `record` (pinned silent).
- `docs/audits/2026-06-handwritten-docs-accuracy-followups.md:66` asked
"whether sub-shapes are declared elsewhere". This PR answers it (they
are not, by ruling); the audit record is left as written.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01T2U7eGg8aRTpJ8kKXxDiog
---
_Generated by [Claude
Code](https://claude.ai/code/session_01T2U7eGg8aRTpJ8kKXxDiog)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>1 parent 3a2d38c commit 4034f47
12 files changed
Lines changed: 369 additions & 14 deletions
File tree
- .changeset
- content/docs
- data-modeling
- protocol/objectui
- packages
- lint/src
- spec/src/data
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
511 | 511 | | |
512 | 512 | | |
513 | 513 | | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
514 | 526 | | |
515 | | - | |
| 527 | + | |
516 | 528 | | |
517 | 529 | | |
518 | 530 | | |
519 | 531 | | |
520 | 532 | | |
521 | 533 | | |
522 | | - | |
| 534 | + | |
| 535 | + | |
523 | 536 | | |
524 | 537 | | |
525 | | - | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
526 | 551 | | |
527 | 552 | | |
528 | 553 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
391 | 391 | | |
392 | 392 | | |
393 | 393 | | |
394 | | - | |
395 | | - | |
396 | | - | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
397 | 399 | | |
398 | 400 | | |
399 | 401 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
465 | 465 | | |
466 | 466 | | |
467 | 467 | | |
468 | | - | |
469 | | - | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
470 | 472 | | |
471 | 473 | | |
472 | 474 | | |
| |||
| 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 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
9 | | - | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| 18 | + | |
17 | 19 | | |
18 | 20 | | |
19 | 21 | | |
| |||
36 | 38 | | |
37 | 39 | | |
38 | 40 | | |
| 41 | + | |
39 | 42 | | |
40 | 43 | | |
41 | 44 | | |
| |||
170 | 173 | | |
171 | 174 | | |
172 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
173 | 194 | | |
174 | 195 | | |
175 | 196 | | |
| |||
181 | 202 | | |
182 | 203 | | |
183 | 204 | | |
184 | | - | |
| 205 | + | |
185 | 206 | | |
186 | 207 | | |
187 | 208 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
264 | 308 | | |
265 | 309 | | |
266 | 310 | | |
| |||
778 | 822 | | |
779 | 823 | | |
780 | 824 | | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
781 | 844 | | |
782 | 845 | | |
783 | 846 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1009 | 1009 | | |
1010 | 1010 | | |
1011 | 1011 | | |
| 1012 | + | |
1012 | 1013 | | |
1013 | 1014 | | |
1014 | 1015 | | |
| |||
0 commit comments