Repository navigation
Commit e62dcc5
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XR1KAQEJUPapgHT2dsHnwQx)_
Fixes #28565
Clause-②: no
Metadata-protocol: semantic
Skills-docs: follow-up #28730
## 维护者速读
-
**改了什么**:对象扩展的「字段显示补丁」(`objectExtensions[].fieldPatches`,上一版只声明未生效)现在真的生效了:引擎注册扩展时带上它,注册表合并(`mergeObjectDefinitions`)在字段合并之后把白名单里的
label、description、帮助文字、占位符和选项 label
写到厂商字段上;字段类型、必填、选项取值不变。补丁指向目标对象没有的字段或选项值时,在市场安装口和 `os validate`
拒绝;启动时只记一次日志、不拦启动。
- **为什么改**:HotCRM 的 "Industry" 要在每个表单、列表和 API 读取里显示成
"Sector",而字段本身仍归厂商,厂商升级照样进来。指向不存在字段的补丁以前会悄悄什么都不做。
- **风险与回滚**:只影响写了 `fieldPatches`
的扩展;没写的扩展合并结果逐字节不变。租户自己改过的文字照旧优先(ADR-0029 D9.2a),两个扩展冲突时 priority 高者胜。回滚
= revert 本 PR。
- **先看**:① `packages/objectql/src/registry.ts` 的 `patchedField` /
`applyFieldPatches` 与 `tenantPatchedDisplayKeys`(合并与让位);②
`packages/metadata-core/src/extension-field-patch-targets.ts`(三处共用的目标检查);③
`packages/cloud-connection/src/marketplace-install-local-plugin.ts` 新增的
2e 步。
- **两分钟验证**:`pnpm --filter @objectstack/objectql exec vitest run
src/registry-extension-field-patches.test.ts`,预期 9 个测试全绿。
- **需要你做的**:无,席位落地。
## What changes
- **Carry** — `packages/objectql/src/engine.ts`: the manifest loop's
`extDef` carries `fieldPatches` beside `fields`, so the `'extend'`
registration hands the slot to the registry. Re-measured at `2678600d`:
still the only `'extend'` registration in `objectql`,
`metadata-protocol` and `runtime` (`git grep "'extend'"`, one call site,
`engine.ts:8996`).
- **Apply** — `packages/objectql/src/registry.ts`:
- `mergeObjectDefinitions` gains one step after the scalar overrides:
`applyFieldPatches` writes each `BASE_FIELD_DISPLAY_WHITELIST.field` key
onto the named field and each `BASE_FIELD_DISPLAY_WHITELIST.option` key
onto the option whose stored `value` the patch names. Keys are read from
the constant, so a widened whitelist widens the merge. The type of the
slot is the spec's own (`ObjectExtensionParsed['fieldPatches']`); there
is no second patch shape.
- D9.2a yield: `tenantPatchedDisplayKeys` computes, once over the fold's
base, which patched field/option keys the tenant authored away from the
packaged owner's value, with the same two "no opinion" edges as
`tenantAuthoredScalars` (no owner; base IS the owner by reference). The
comparison is `scalarOverridesPackagedBase` itself, called through a
one-line adapter because its exported key union names only the object
scalars and `message`.
- Priority: the fold already applies contributors in ascending
`priority`, so the higher one writes last, exactly as for `label`.
- Boot: `reportFieldPatchMisses` names, once per
object/package/field/option, a patch the fold could not apply, at
`warn`, worded as the observed state. Never a refusal: the fold runs on
every read while a boot is still registering (AGENTS.md *Startup
registry reads*), and the patch applies on the next read once the field
arrives.
- `subtractExtenderContributions` is untouched: patches are idempotent
over an already-folded base (the base carries the patched text, reads as
authored, and the patch yields to the same value), the way the scalars
are. Pinned by the "idempotent over a folded body" assertion.
- **By-name and layered reads** — no edit.
`protocol-object-extender-fold.ts` already calls
`registry.foldObjectExtendersOnto`, which runs
`foldExtendersOntoDefinition` → `mergeObjectDefinitions`, the one apply
implementation (measured; the order's assumption holds). The code layer
(`getArtifactItem('object')` → `resolveOwnerLayer`) goes through the
same fold.
- **Target check** —
`packages/metadata-core/src/extension-field-patch-targets.ts` (new):
`fieldPatchTargetMisses` (one extension against one target),
`fieldPatchTargetRefusals` (a manifest's `objectExtensions`, located)
and `fieldPatchTargetRefusalMessage`. Misses: a field the target lacks,
an option value its field lacks, and an option patch on a picklist-bound
field (its options are written over the field after the fold, so an
option patch there could never be served). `metadata-core` is the one
package all three readers already depend on. One call line each:
-
`packages/cloud-connection/src/marketplace-install-local-plugin.ts#handleInstall`,
new step **2e**, right after N-09's override write gate (2d) and before
the hot-register (3): `400 VALIDATION_ERROR`, every refusal in
`error.details.refusals`, nothing registered, written or synced.
Targets: one the same artifact delivers, else the registry's resolved
object.
- `packages/cli/src/commands/validate.ts`, new step **3a-iv** after the
override gate, over `packages/cli/src/utils/field-patch-targets.ts` (the
walk: objects across the composed stack and every `packages[]` entry).
- the registry fold's boot log (above).
- **Marker flip** — `packages/spec/src/data/object.zod.ts`:
`fieldPatches`' `.describe()` drops `[EXPERIMENTAL — not enforced]` and
cites `mergeObjectDefinitions`; its TSDoc, the `@example` comment and
the guidance code comment follow. No liveness ledger row exists for the
slot (`grep -l ObjectExtension packages/spec/liveness/*.json`: none).
`content/docs/references/data/object.mdx` regenerated (`gen:docs`, the
one artifact `check:generated` named stale). The spec test that pinned
the marker now pins its absence.
- **Docs** — `content/docs/data-modeling/object-extensions.mdx`:
`fieldPatches` in the contribution table, a "Relabelling a vendor field"
section, and the D9.2a callout extended.
- `packages/cli/test/validate-build-gate-parity.test.ts`: the new
validate gate is classified in `VALIDATE_ONLY_GATES` with its reason
(the closed roster requires it); the build-side gap is reported below.
- `.changeset/28565-field-patch-applied.md`: minor on `objectql`,
`metadata-core`, `cloud-connection`, `cli` and `spec` (the kind is
`semantic`; each package's `files[]` ships `dist`, which carries the
change after a build).
## Tests
All readings at head `043407e6` unless named. Red-on-base means the
change's source files restored to `2678600d` with the new tests kept.
| Test file | Head | Base / ablation |
|---|---|---|
| `packages/objectql/src/registry-extension-field-patches.test.ts` (9) |
9/9 | `registry.ts` + `engine.ts` at base: 8 red (the ninth is the
no-patch control). `engine.ts` alone at base (carry ablation): 1 red,
the `ObjectQL.registerApp` carry test. `reportFieldPatchMisses` call
removed: 1 red, the warned-once test. |
| `packages/metadata-core/src/extension-field-patch-targets.test.ts`
(10) | 10/10 | new module, absent on base |
|
`packages/cloud-connection/src/marketplace-install-local-field-patch-targets.test.ts`
(5) | 5/5 | step 2e call line removed: 4 red (every negative pin); the
lit control stays green |
| `packages/cli/src/utils/field-patch-targets.test.ts` (5) | 5/5 | new
module, absent on base |
| `packages/cli/test/validate-field-patch-targets.test.ts` (2, spawns
the CLI) | 2/2 | step 3a-iv call line removed: 1 red (the negative pin);
the control stays green |
| `packages/spec/src/data/field-customization.test.ts` | green | the
marker assertion reds on base (it pins the `EXPERIMENTAL` marker's
absence) |
Each ablation was restored and the file's blob checked equal to HEAD
before the next one.
Existing extension fold tests stay green: the full suites after the last
two main merges (whole-repo `pnpm build`, exit 0, then):
| Suite | Result |
|---|---|
| `@objectstack/objectql` | 515 files, 9915 tests, exit 0 |
| `@objectstack/metadata-core` | 22 files, 885 tests, exit 0 |
| `@objectstack/cloud-connection` | 66 files, 801 tests, exit 0 |
| `@objectstack/spec` `src/data` | 154 files, 4751 tests (1 todo), exit
0 |
| `@objectstack/cli` `unit` tier | 365 files, 5172 tests, exit 0 |
| `tsc --noEmit` objectql, cloud-connection, cli | exit 0 each |
| `eslint` over the 15 changed TS files | exit 0 |
After the final merge (`043407e6`, no file overlap with this diff) the
five new test files were re-run: all green. One run of the CLI trio
during that step exited 1 while other suites ran beside it; three
re-runs alone exited 0, and its log was not kept, so it is reported here
rather than explained.
**Derived gates** (`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, re-derived at head: identical list).
`--ran` verdict: **114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN**, exit
0. First pass: 109 exit 0 and five that did not. Each was traced to the
checkout, not the diff, and re-run green:
- `test cli unit` exit 1: `setup-integrations-order.pin` read a
`service-automation` dist from before #28651 came in with the merge;
rebuilt, green.
- `check-plugin-teardown-shape --self-test` exit 3: shallow clone; its
pinned fixture commit was fetched, then 48 cases passed.
- `check:skill-examples`, `check:dual-build-cjs-loads`,
`check:i18n-coverage` exit 3 (PREREQUISITE NOT MET: built output
missing); after a whole-repo `pnpm build`, exit 0 each.
## Acceptance notes
- **The protocol install primitive is not a door a patch can reach.**
The order rules a call line at
`protocol-package-install.ts#installPackage`; measured, its manifest is
parsed by `PackageInstallManifestSchema` (strict `ManifestSchema`),
which refuses `objectExtensions` as an unknown key before anything else
runs: `PackageInstallManifestSchema.safeParse({ id: 'com.acme.ext',
name: 'E', type: 'app', version: '1.0.0', objectExtensions: [...] })` →
`false`, "Unrecognized key(s) on this package manifest:
`objectExtensions`" (spec dist built from `2678600d`). A call line there
could never fire and could not be ablated, so none was written. The only
install door that delivers an extension today is `POST
/api/v1/marketplace/install-local` (including `os package install
<file>`, which posts there); the card's "Not here" names the future
runtime write door for an `object_extension` item (#28574).
- **`os build` does not run the target check.** It still emits an
artifact the install door then refuses; recorded as the
`VALIDATE_ONLY_GATES` row. Wiring it into `compile.ts` is outside the
card's three doors.
- **Option patches on picklist-bound fields are refused**, not applied:
the served options of such a field are the picklist's, written after the
fold (`finishServedObject`), so an option label patched in the fold
would be overwritten on every read.
- **`ObjectExtensionSchema.guidance.actions`** still reads "the merge
carries `fields`, `label`, `pluralLabel`, `description`, `validations`
and `indexes` only". It is a refusal sentence about `actions`, so it is
left as is under ruling 3 (only the slot's marker moves in
`packages/spec`); it now omits `fieldPatches` from that list.
- **Declaration lines** are the claim's, copied. The install and
validate refusals are new refusals of a key that was declared
`[EXPERIMENTAL — not enforced]`; the ruling records the kind as
`semantic`.
- #28564 (the `__c` judge at the same doors) has not landed at the time
of this PR; whichever lands second sits its call line beside the
other's.
## Verification
`pnpm --filter @objectstack/objectql exec vitest run
src/registry-extension-field-patches.test.ts` — 9/9 at head `043407e6`;
8 red with `registry.ts` and `engine.ts` restored to `2678600d` (the
ninth is the no-patch control).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5nSMgkteiemkoN31vf)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5493b88 commit e62dcc5
18 files changed
Lines changed: 1335 additions & 33 deletions
File tree
- .changeset
- content/docs
- data-modeling
- references/data
- packages
- cli
- src
- commands
- utils
- test
- cloud-connection/src
- metadata-core/src
- objectql/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 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| |||
76 | 77 | | |
77 | 78 | | |
78 | 79 | | |
79 | | - | |
| 80 | + | |
80 | 81 | | |
81 | 82 | | |
82 | 83 | | |
| |||
92 | 93 | | |
93 | 94 | | |
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 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
95 | 140 | | |
96 | 141 | | |
97 | 142 | | |
| |||
140 | 185 | | |
141 | 186 | | |
142 | 187 | | |
143 | | - | |
| 188 | + | |
| 189 | + | |
144 | 190 | | |
145 | 191 | | |
146 | 192 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
576 | 576 | | |
577 | 577 | | |
578 | 578 | | |
579 | | - | |
| 579 | + | |
580 | 580 | | |
581 | 581 | | |
582 | 582 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
81 | 84 | | |
82 | 85 | | |
83 | 86 | | |
| |||
719 | 722 | | |
720 | 723 | | |
721 | 724 | | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
722 | 754 | | |
723 | 755 | | |
724 | 756 | | |
| |||
| 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 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
237 | 237 | | |
238 | 238 | | |
239 | 239 | | |
240 | | - | |
241 | | - | |
242 | | - | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
| |||
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
254 | 259 | | |
255 | 260 | | |
256 | 261 | | |
| |||
0 commit comments