Repository navigation
Commit a91ad42
fix(metadata-protocol,cli,cloud-connection)!: refuse an object extension that re-declares an owner's field beyond its display properties (ADR-0146 D10) (#30712)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DQCNH4WvC9WPtKsTwjY6Nrq)_
Fixes #30610
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: no move — `skills/**` and `content/docs/**` already teach
`fieldPatches` as the way to change a base field's display, and no page
teaches taking an owner field over through `fields`; the prescription
ships in the refusal text.
ADR-0087 disposition (in the changeset): `not-required
(no-migration-prescription)` — a refused shape with no mechanical
rewrite (which keys of a re-declared field were meant as display and
which as a takeover is a judgement about the field), and no first-party
artifact writes the shape (measured below). A stored row that already
re-declares an owner field keeps registering with the registry's warning
and is judged when next saved or published.
## 维护者速读
- 改了什么:模块包在 `objectExtensions[].fields` 里重新声明属主对象已有的字段(例如把 `amount`
的类型从数字改成文本、去掉必填、改成只读)时,现在在三道门被拒:元数据保存/发布/恢复(400 `VALIDATION_ERROR`)、`os
validate`(退出码 1)、市场安装(422 `VALIDATION_ERROR`)。提示语点名 `fieldPatches`
和可改的展示属性白名单。
- 为什么改:ADR-0146 D10 规定基础字段只能改展示属性;但扩展在 `fields`
里写同名字段会整体替换属主字段,过去只有注册表打一条警告,任何门都不拦。
- 风险与回滚:属于收窄,但第一方产物(examples、HotCRM)实测 0
处使用这种写法;只改展示属性的重复声明照常放行,注册表警告保留。回滚即回退本 PR。
-
先看:`packages/metadata-protocol/src/protocol-extension-redeclared-owner-field.ts`(判定本身,`redeclaredFieldChanges`
的比较规则是风险点),`packages/metadata-protocol/src/protocol-object-extension-item.ts`
第 7
步,`packages/cloud-connection/src/marketplace-install-local-plugin.ts` 的
2e'。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run
src/protocol-extension-redeclared-owner-field.test.ts`,预期 12
个用例全绿(类型变更被拒、只改标签放行)。
- 需要你做的:无,席位落地(契约档复审由席位执行)。
## What changes
ADR-0146 D10, second rule: "Everything else on a base field — type,
`required`, `unique`, `reference`, `defaultValue`, formulas, `options`
membership — is refused on the extension with a prescription naming the
whitelist." An `objectExtensions[].fields` key naming a field the
extended object's owner declares replaces that field whole in
`mergeObjectDefinitions` (`{ ...base.fields, ...extension.fields }`),
and before this PR no door refused it; only `reportRedeclaredFields`
warned.
**One judge** —
`packages/metadata-protocol/src/protocol-extension-redeclared-owner-field.ts`,
beside the `__c` judge (`judgeExtensionFieldSuffix`):
- `judgeRedeclaredOwnerFields({ packageId, objectExtensions, ownerOf, at
})` returns every entry that re-declares an owner field and changes
something outside `BASE_FIELD_DISPLAY_WHITELIST` (read from
`@objectstack/spec/data`, never re-spelled), located, with `changed`
(the keys, sorted) and the sentence.
- `redeclaredFieldChanges(extensionField, ownerField)`: both bodies are
read through `FieldSchema` first (a default the parse stamps on one side
is no change), then compared key by key, skipping the whitelisted keys,
`name` and the ADR-0010 `_` envelope. `options` is compared as
membership keyed by stored `value`, each option without its whitelisted
`label`, so a relabel or reorder is no change.
- Not judged: an extension of an object its own package owns, an object
with no known owner at that door, and a key the owner does not declare
(an added field).
- Envelopes: `redeclaredOwnerFieldRefusalError` (`VALIDATION_ERROR` /
400 with `issues[]`, the `__c` judge's envelope) and
`redeclaredOwnerFieldInstallMessage` (the install door's sentence).
**Three doors call it:**
- **Publish** — `objectExtensionItemRefusal` step 7
(`protocol-object-extension-item.ts`), which the save (draft and live),
the per-item and package publish, and the restore doors already ask. The
owner is the registry's `getObjectOwner(target)` — its own declaration,
never the resolved object, which already carries this package's earlier
extension.
- **`os validate`** —
`packages/cli/src/utils/extension-redeclared-owner-fields.ts` walks the
artifact (each `packages[]` entry under its id, or the stack under
`manifest.id`); step 3a-v-bis of `validate.ts` exits 1 with the rows on
`errors` (`code: VALIDATION_ERROR`). An owner outside the artifact is
judged at install.
- **Install** — `POST /api/v1/marketplace/install-local` step 2e', on
the managed and the `.custom` pair branches, beside the field-patch
target check: `422 VALIDATION_ERROR`, every refusal in
`details.refusals`, before any write. The owner is a package this
artifact delivers, or the installed owner (`getObjectOwner`).
`reportRedeclaredFields` (`packages/objectql/src/registry.ts`) is
unchanged and stays a warning.
## Measurement (the card's step 1)
At `origin/main` `f4d568ba`:
- No door refuses the shape: `judgeExtensionFieldSuffix` judges only the
suffix; the field-patch target check judges `fieldPatches` keys; the
registry warns.
- First-party re-declarations of an owner field: **0**.
`examples/app-showcase/src/data/extensions/account.extension.ts` (the
only extension under `examples/**`) adds `loyalty_tier`, `linkedin_url`,
`csat_score`, none of which `showcase_account` declares, and extends its
own package's object. `packages/plugins/plugin-auth/src/manifest.ts`
extends `sys_sso_provider` with `oidc_client_secret`, which
`packages/platform-objects/src/identity/sys-sso-provider.object.ts` does
not declare. HotCRM (`objectstack-ai/hotcrm` `6961ab1`): no
`objectExtensions` anywhere.
## Tests
All at head `435f30c7`.
- **The judge** —
`packages/metadata-protocol/src/protocol-extension-redeclared-owner-field.test.ts`,
12 cases: refused on a `type` change, a dropped `required`, `readonly` +
`defaultValue`, an added option value, the array spelling located by
index; admitted on a display-only re-declaration (labels, help text,
option labels, reordered options), a parse-stamped default (`required:
false` against an owner that omits it), an added field, the package's
own object, an unknown owner, the `_` envelope; the save doors' envelope
`VALIDATION_ERROR` / 400 with every entry in `issues`.
- **Publish door** — a new top-level `describe` in
`packages/runtime/src/domains/meta-object-extension-item.test.ts` (the
real-store rig: better-sqlite3 behind `ObjectQL`, the real protocol and
registry), with a writable module package that depends on the base: a
`type` change is refused at the save door (`VALIDATION_ERROR` / 400 at
`fields.name`, no row, no extender); a draft stored before the rule is
refused at publish and stays a draft; a display-only re-declaration
saves and publishes.
- **`os validate`** —
`packages/cli/test/validate-extension-redeclared-owner-field.test.ts`
(integration tier, spawns the CLI): an assembled artifact whose second
package re-declares the first's `amount` as `text` exits 1 with
`packages[1].manifest.objectExtensions[0].fields.amount`, `code:
VALIDATION_ERROR`, `changed: ['type']`; a label-only re-declaration
validates clean.
- **Install door** — a new top-level `describe` in
`packages/cloud-connection/src/marketplace-install-local-field-patch-targets.test.ts`
(the 2e harness, a real `SchemaRegistry`): a type change of the
installed owner's field is refused `422 VALIDATION_ERROR` with the
refusal in `details.refusals` and nothing registered, synced or written;
an added option value on an owner the same artifact delivers is refused
at `packages[1]…`; a display-only re-declaration installs.
- `validate-build-gate-parity.test.ts`: the new gate is classified in
`VALIDATE_ONLY_GATES`.
<details><summary>Ablation (one-time, not kept)</summary>
Committed first, then `node scripts/ablation-replace.mjs` on
`protocol-extension-redeclared-owner-field.ts`: anchor `if
(changed.length === 0) continue;` (x1) replaced by `if (changed.length
>= 0) continue;` (the judge refuses nothing); `pnpm --filter
@objectstack/metadata-protocol build`; `ablation-dist-preflight.mjs` saw
the marker in `dist/`.
- judge: 6 failed / 6 passed (every refusal case red)
- runtime rig: 2 failed / 20 passed (the save and publish refusals)
- install door: 2 failed / 9 passed (both refusals)
- `os validate`: 1 failed / 1 passed (the refusal)
Restored from `HEAD` (blob `e7ae83e11485` equals the HEAD blob, `git
diff HEAD` empty), rebuilt, `ablation-dist-preflight.mjs --absent`
passed over 24 built files; the runtime, install and `os validate` files
green again.
</details>
## Acceptance notes
- `os build` does not run the check yet (`VALIDATE_ONLY_GATES` row in
`validate-build-gate-parity.test.ts`), the same build-side gap the
field-patch target check carries; the install door refuses what the
build emits.
- `os validate` judges only an owner declared by another package of the
same artifact. A single-package module stack that extends an installed
base is judged at install.
- At the install door the installed owner's declaration carries the
platform columns the registry injects, so re-declaring one of those
(e.g. `owner_id`) is judged there and not at `os validate`.
- In a `.custom` pair, an unsuffixed `fields` key is refused first by
the `__c` judge, whose prescription already names `fieldPatches`.
## Verification
<details><summary>Affected packages (head 435f30c)</summary>
- `pnpm --filter @objectstack/metadata-protocol test`: Test Files 318
passed, 4 skipped; Tests 22466 passed, 26 skipped — exit 0
- `pnpm --filter @objectstack/cloud-connection test`: Test Files 70
passed; Tests 893 passed — exit 0
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: Test
Files 389 passed; Tests 5431 passed — exit 0
- `pnpm --filter @objectstack/cli exec vitest run --project integration
test/validate-extension-redeclared-owner-field.test.ts`: 2 passed — exit
0 (the rest of the integration layer is declared to CI)
- `pnpm --filter @objectstack/runtime exec vitest run
src/domains/meta-object-extension-item.test.ts`: 22 passed — exit 0
- typecheck: metadata-protocol, cli, cloud-connection, runtime — each
exit 0
</details>
<details><summary>Derived gates + lint (head 435f30c)</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 64 families. All 64 ran with exit 0;
`--ran` reconciliation: "64 derived famil(ies) accounted for — 64 run, 0
NOT-MEASURED".
- First pass: `check-plugin-teardown-shape --self-test`,
`check:dual-build-cjs-loads` and `check:i18n-coverage` exited 3
(prerequisites). Re-run after fetching the pinned commit and a
whole-tree `turbo run build` (exit 0): all three exit 0.
- Three families that ran while the ablation held its mutation
(`check:registry-log-declared`, `check:rest-log-spy-declared`,
`check:route-envelope`) were re-run on the restored tree: exit 0.
- `node scripts/check-commit-card-trailers.mjs --range
origin/main..HEAD`: exit 0.
- `pnpm lint`: exit 0.
</details>
<details><summary>Dogfood battery (the `(narrowing)` arm)</summary>
Head `435f30c7`, after the whole-tree build, under the lock: `pnpm
--filter @objectstack/dogfood test` — `VERDICT command-exit 0`, Test
Files 421 passed, 3 skipped (424); Tests 3513 passed, 36 skipped (3549);
no failures.
</details>
`origin/main` moved 10 commits since the branch point (`f4d568ba`); none
touches a file this PR changes, and `git merge-tree` merges clean.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4cx4CnMVtuDswCA9p2c)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f2ea193 commit a91ad42
12 files changed
Lines changed: 856 additions & 3 deletions
File tree
- .changeset
- packages
- cli
- src
- commands
- utils
- test
- cloud-connection/src
- metadata-protocol/src
- runtime/src/domains
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
84 | 87 | | |
85 | 88 | | |
86 | 89 | | |
| |||
754 | 757 | | |
755 | 758 | | |
756 | 759 | | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
757 | 789 | | |
758 | 790 | | |
759 | 791 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
262 | 262 | | |
263 | 263 | | |
264 | 264 | | |
265 | | - | |
266 | | - | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
267 | 268 | | |
268 | 269 | | |
269 | 270 | | |
270 | 271 | | |
271 | 272 | | |
272 | 273 | | |
273 | 274 | | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
274 | 281 | | |
275 | 282 | | |
276 | 283 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
14 | 19 | | |
15 | 20 | | |
16 | 21 | | |
| |||
227 | 232 | | |
228 | 233 | | |
229 | 234 | | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
0 commit comments