Repository navigation
Commit 15f808a
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9beNpM7b18QnbuwqgHvEwS)_
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb
## 维护者速读
- **改了什么:** 客户在定制包(`BASE.custom`)里声明覆盖的 mapping
现在真的生效(导入用的是改过的映射);定制包里任何可覆盖类型的覆盖都能删掉,删掉后基包原件回来(doc、mapping 以前报
403);给不可覆盖类型加 `overrides`
时,拒绝信息引用该类型自己的依据并给出一条走得通的出路;「全部元数据」里的类型瓷砖在只读包上显示「可定制」当且仅当 Studio
真的给了「定制」入口(flow、app 以前误显示「只读」)。
- **为什么改:** ADR-0146 D2 原文:定制包里的项替换基包的项,删掉它就回到基包的项;ADR-0146 的
revert(「恢复包原件」)就是删掉 `.custom` 里的覆盖项。旧行为对 doc/mapping 拒绝了这条路,mapping
覆盖保存发布后不起作用,拒绝文案一律叫人「新建同类」而有些类型根本不能新建。
- **风险与回滚:** 删除只对定制包自己的行放宽(点名
`?package=BASE.custom`);点名基包的删除仍是原答案,覆盖照旧生效(消融 A3 为证)。错误码与状态不变,无 spec
改动。回滚即撤 PR。
- **先看:**
`packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`(`isPairRowRemoval`
的放宽与
`excludedTypeRemedy`),`packages/metadata-protocol/src/protocol-package-bindings.ts`(`SEALED_ROW_SERVED_OVERRIDE_TYPES`
加
`mapping`),`ui/packages/app-shell/src/views/metadata-admin/PageShell.tsx`(`studioOffersCustomize`)。
- **两分钟验证:** `pnpm --filter @objectstack/objectql exec vitest run
src/protocol-custom-override-bare-slot-envelope.test.ts -t
30062`:每个可覆盖类型在两种内核上,删除定制包的覆盖后所有读都回到基包原件;点名基包的删除不动任何东西。
- **需要你做的:** 无,席位落地。第 3 项(翻译与覆盖自身 label 的优先级)不在本 PR,见 Acceptance notes。
Refs #30062 (items 1, 2, 4, 5)
Clause-②: no — ADR-0146 D2 ("an item in the customization package
replaces the base's item, and removing it brings the base's back") and
ADR-0146's revert ("Restore package original" = delete the overriding
item from `.custom`, so the base's document is served again) already
admit both the served mapping override and the pair's removal of its own
doc or mapping override; the 403 and the unserved row refused that text.
A removal naming the base keeps today's answer, and no accepted write is
refused that was accepted before.
Metadata-protocol: semantic
Skills-docs: follow-up #30229 — `skills/objectstack-platform/SKILL.md`'s
ADR-0146 paragraph says the read honours a pair's declared override
"today only for a view, dashboard, report, email_template, translation
or flow"; that list predates page, app, dataset, doc and book, and now
mapping, and it says nothing of the removal. `skills/**` is outside this
card's order, so the seat carries the edit.
## What changes
**Item 1 — a mapping override is served.**
`packages/metadata-protocol/src/protocol-package-bindings.ts`: `mapping`
joins `SEALED_ROW_SERVED_OVERRIDE_TYPES` (page, app, dataset, doc, book)
and `BORROWED_ENVELOPE_DECLINED_TYPES`. A mapping is sealed
(`allowEnvironmentOverlay: false`) and open for override (ADR-0146 D2,
no D9 condition), and its pair row was declined with every other stored
row of a name a managed package ships, so a saved and published mapping
override answered the base's body on every read. Now the by-name read
(no `?package` and `?package=BASE`), both lists, the layered read and
the pair's per-package layer serve it. The import door
(`@objectstack/rest` `import-mapping.ts:63`) and the connector pull
(`service-automation` `connector-pull.ts:244`) resolve a named mapping
through that package-less `getMetaItem`, so they get the pair's mapping.
The served arm was chosen, not "the save refuses loudly", so `Clause-②:
no` does not narrow. Comment-only updates in `protocol-lock-state.ts`,
`protocol-registry-hydration.ts` and `protocol.ts` name the sixth type.
**Item 2 — the pair's removal of its own override works for every
overridable type.** Measured: the doc and mapping 403 came from
`refusePackagedBaseRemoval` (`protocol-write-gates.ts`: a sealed managed
item on a kernel that does not merge the overlay at read), which
`deleteMetaItemUnaudited` skips only when `isPairRowRemoval` admits the
row, and `isPairRowRemoval` admitted only `CONDITIONS_DELIVERED =
{flow}`. `protocol-packaged-base-refusal.ts`: `isPairRowRemoval` now
admits the pair's row for every type `isOverridableMetadataType` opens
(`isOpenOverrideType`, so `action` still waits for D9); the pair check
is unchanged (the package the removal names must be the base's
customization package). The repository's delete gate then takes the
`runtime-only` intent the declared save wrote, which every overridable
type's `allowRuntimeCreate` admits. No spec flag moved. A removal naming
the base (`?package=BASE`) is untouched: on the host-config kernel it
answers `{ success: true, reset: false }` with "No row … remains" and
removes nothing; on the environment kernel a doc or mapping still
answers `403 NOT_OVERRIDABLE`.
**Item 4 — the All metadata tile agrees with the Customize door.**
`ui/packages/app-shell/src/views/metadata-admin/PageShell.tsx`: new
`studioOffersCustomize(type)` = overridable and offered by a Studio door
(`isStudioCustomizeType`: view, dashboard, report, app;
`AREA_FORM_EDITOR_TYPES`: email_template, translation; and the flow
header). On a read-only package `typeStateWord` reads it instead of
`allowEnvironmentOverlay`, so flow and app read 可定制, and doc, mapping,
page, dataset and book read 只读 until a door exists.
`StudioCustomizeControls.tsx` is not touched (seat ruling): a door added
to its list moves the tile with it.
**Item 5 — an excluded type's refusal names a remedy that works for
it.** `judgeOverrideWrite`'s `declared_type_not_overridable` sentence
cited ADR-0146 D6 and told every type to "create your own TYPE under a
new name". It now cites the type's own basis from
`OVERRIDE_TYPE_CLASSIFICATION` (`EXCLUSION_BASIS_CITATION`: D6 for the
security catalog and code-only types, D1 for a datasource, D9 for a
behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2
otherwise) and `excludedTypeRemedy` prescribes, in order: Setup ›
Datasources for a datasource; the object extension for a field
(`OBJECT_EXTENSION_ITEM_DOOR`); "create your own TYPE under a new name"
only where the registry admits a runtime create; the type's ADR-0126
Regime C prescription where it has a row
(`packagedBaseRegimePrescription`: a sharing rule names `POST
/api/v1/sharing/rules` and its switch); Setup › Queues for a queue;
otherwise "an X is defined only in package source, so it changes only
with a new version of 'BASE'". `articleFor` fixes "a api". Code
(`NOT_OVERRIDABLE`), status (403) and rule are unchanged, and the
permission sentence is byte-identical (pinned by `runtime`
`meta-object-extension-item-door-fit.test.ts:161`).
**Docs (head `5663983b`, added by the card thread after the CI Docs
Drift Check, comment 6648033732).**
`content/docs/concepts/metadata-lifecycle.mdx`: a declared override is
served for every type of `OVERRIDABLE_METADATA_TYPES` but `action`
(which waits for ADR-0146 D9's conditions); the import door and the
connector pull run the pair's mapping; a new bullet, removing the
override (`DELETE …?package=hotcrm.custom`, "Restore package original")
brings the base's back. `content/docs/api/metadata-api.mdx`: a
capability is not copied under a new name (its registry entry has
`allowRuntimeCreate: false`); it changes only with a new version of the
package that declares it, and an `overrides` on a type that takes none
is refused `403 NOT_OVERRIDABLE` with that type's own route. This PR
made the old sentences false, so they are fixed here.
## Tests
-
`packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts`:
`mapping` joins the sealed served types (by-name, lists, layered,
per-package, after a restart, with and without a metadata service). New
describe `[#30062] a pair's removal of its own override brings the
base's item back`, over view, dashboard, report, email_template, page,
app, dataset, doc, book, mapping, flow and translation, on both kernels:
(1) the removal naming the pair succeeds, the pair row is gone, and
every read serves the base's item again; (2) a removal naming the base
keeps main's exact answer per kernel and type, removes nothing, and the
override stays served.
- `packages/objectql/src/protocol-flow-custom-override.test.ts`: the old
flow-only admission pins (a mapping pair-row delete 403, a view delete
`override-artifact`) are rewritten to the new behaviour (removed;
`runtime-only`). They pinned the #28412 flow-only scope (contract review
6559358939), which item 2 supersedes.
-
`packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts`:
each of the 17 excluded non-D10 types cites its own basis, never reads
"a" before a vowel, names a create only where the registry admits one,
and the named remedies (sharing rule route and switch, Setup › Queues,
Setup › Datasources, object extension, new version of the base) appear
where they belong.
-
`packages/metadata-protocol/src/protocol-package-bindings.sealed-override.test.ts`:
the served set includes `mapping`.
-
`ui/packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx`:
for every overridable type plus permission, object and api, under both
overlay flags, the tile reads 可定制 exactly when a door offers Customize;
flow and app read 可定制, doc and mapping 只读, a writable flow 可新增; the flow
list header renders 可定制.
### Ablations
`scripts/ablation-replace.mjs` (wrap mode); for objectql legs the
mutated `metadata-protocol` was rebuilt and
`scripts/ablation-dist-preflight.mjs` proved the marker in `dist/`
before the run, then absent after restore. Each leg restored with blob
== HEAD and `git diff HEAD` empty.
| # | Mutation | Result |
|:--|:--|:--|
| A1 | `isPairRowRemoval` back to flow-only | 7 / 78 red: doc and
mapping pair removals on both kernels, the mapping pair-row delete, the
view intent |
| A2 | `mapping` out of `SEALED_ROW_SERVED_OVERRIDE_TYPES` | 6 / 71 red:
mapping served with and without a metadata service, mapping removal and
base-named control on both kernels |
| A3 | the pair check widened to admit a base-named removal | 2 / 71
red: doc and mapping, environment kernel, base-named removal |
| A4 | `typeStateWord` back to reading `allowEnvironmentOverlay` | 15 /
114 red: every per-type agreement row, flow/app, the flow header |
| A5 | the refusal back to the old "a TYPE … (ADR-0146 D6) … create your
own" sentence | 16 / 87 red: 14 per-type citation rows, the named-remedy
row, the code-only row |
## Acceptance notes
- **Item 3 (label precedence) is not here.** Ruled (6631250894): a
translation in the pair, then the override's own label, then the base's
translation. Measured on `origin/main` `b7075d99`: the rule that keeps
an override's own label over the base's translation exists only in
`translateView`, `translateDashboard`, `translateReport` and
`translateObject` (`packages/spec/src/system/i18n-resolver.ts`, the
`packagedBase` option), and `@objectstack/rest` wires
`PACKAGED_BASE_ACCESSORS` (`meta-item-read-gate.ts:2954`) for object,
dashboard, view and report only. `translatePage` (`:3393`),
`translateDataset` (`:2270`), `translateApp` (`:1756`) and
`translateBook` (`:2441`) take only `ResolveOptions` and apply the
catalog translation unconditionally, which is the card's page/dataset
defect. The ruling's first rank is unmet for every type: where the value
differs from the base's, the resolver keeps the authored value, so a
translation in the pair never gets in. Both halves change
`packages/spec/src/**`, so the seat splits the spec half to the spec
lane; the server half (`getPackaged*Base` accessors and REST table rows)
stays on #30062.
- **Interaction with #30063 and #30064 (in-flight `domain:ui` cards).**
Both add a door to `STUDIO_CUSTOMIZE_TYPES` (page and dataset). The new
agreement rows derive from that list, so they follow on their own. The
older #29705 pins in the same test file hard-code `page` 只读 (the tile at
`:129`, the list header at `:178`–`:186`); whichever of this PR and
#30063 lands second flips those to 可定制. #30064's claim names "All
metadata tile for datasets": after this PR the tile follows the door
with no extra change.
- **The flow-only removal scope of #28412 is superseded** for the pair's
own row only; a base-named removal is unchanged on both kernels and
pinned per type.
- **Translation** in the removal pins: the manifest door the harness
boots through registers no translation artifact (a code package's
translation file does, through the metadata plugin), so the base's
bundle is registered with `engine.registry.registerItem('translation',
…, BASE)`, and the removal pin reads the base's bundle back from there.
- Serial constraint: #30106 (`protocol-served-overrides.ts`, read path)
landed as PR #30207 (02:52Z) and is in `origin/main` `bb86ff96c`, merged
at head `c5033471`.
- **Merge-queue round (head `c5033471`).** Merged `origin/main`
`bb86ff96c` after #30063 (page door, `797f9257c`) and #30064 (dataset
door, `AREA_CUSTOMIZE_EDITOR_TYPES`, `6f095b9c1`) landed:
`studioOffersCustomize` also reads `AREA_CUSTOMIZE_EDITOR_TYPES`, and
`DirectoryPage.stateVocabulary-29705.test.tsx`'s 'a read-only package:
overlay types can be customized…', 'the view list header reads 可定制, the
page list header 只读' and 'typeStateWord: the mapping in every context'
now expect page 可定制 (filter count 隐藏只读 (4); the read-only control row is
`api`), per #30062 item 4 and ADR-0146 D2. This is the "whichever lands
second flips those" case noted above. Local: that file 114/114, the 6
files that read PageShell/DirectoryPage 150/150, app-shell type-check 0.
## Verification
Head `69812d8212` (base `b7075d99`, `origin/main` `474c030b` merged in
once, no conflict, no rebase). The affected closure was rebuilt after
the merge under the shared lock.
- `pnpm --filter @objectstack/metadata-protocol test`: 312 files passed
(4 skipped), 22 375 tests passed (26 skipped). `typecheck`: exit 0.
- `pnpm --filter @objectstack/objectql test`: 548 / 548 files, 10 910 /
10 910 tests. `typecheck`: exit 0.
- `@objectstack/runtime`
`src/domains/meta-object-extension-item-door-fit.test.ts`: 6 / 6 (it
pins the permission refusal sentence byte for byte).
- `ui`: the six test files that import `PageShell` or `DirectoryPage`
(`DirectoryPage.stateVocabulary-29705`, `.diagnosticsLinkTone-12768`,
`.environmentOverlay-12141`, `PageShell.narrowHeader-12306`,
`metadataAreaHost.customizations-28568`,
`metadataAreaHost.scope-28561`): 149 / 149. `turbo run type-check
--filter=@object-ui/app-shell`: 68 / 68 tasks. eslint on the two ui
files: 0 errors (one more `react-refresh/only-export-components`
warning, for the new `studioOffersCustomize` export beside the
already-exported `typeStateWord`).
- `pnpm lint`: exit 0. `node scripts/check-nul-bytes.mjs`: exit 0.
<details><summary>Gates: dispatch-gates --commands, each exit code
captured before any pipe</summary>
95 derived families at head `5663983b` (the docs commit added 29 docs
families and 7 doc-reading families): 94 exit 0, 1 NOT MEASURED.
`dispatch-gates --ran` exit 0. (At `69812d82`: 65 derived, 64 exit 0, 1
NOT MEASURED.)
- NOT MEASURED: `pnpm check:dual-build-cjs-loads` exit 3, "PREREQUISITE
NOT MET — this gate reads built output, and some package has no dist/"
(32 unbuilt packages outside this diff's closure); left to CI.
- `node scripts/check-plugin-teardown-shape.mjs --self-test` first
exited 3 on the shallow clone (pinned fixture commit unreachable); after
`git fetch origin 621a487…` it exits 0 (48 cases).
- `pnpm check:type-check-debt` first exited 3 (lock queue timeout,
nothing measured); re-run under the lock: exit 0, "0 raw tsc error(s)
total, none above its recorded number".
- Also run, outside the derivation: `check:authz-resolver`,
`check:error-code-casing` (both keep a roster under `packages/`),
`check:overlay-whitelist-table`, `check:changeset-fixed`: all exit 0.
</details>
Change set: 15 paths, +461 / −66 against `origin/main` (13 code and test
paths +448 / −61, plus the two docs pages).
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the maintainer brief
is Chinese by the seat's convention).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CaHjE2N5aEp4ogQnWY)_
<!-- signed: domain:server#5 · os-bill ·
session_01Utp6CYVV6fcAQrNTDQRvPb -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ef2ad31 commit 15f808a
15 files changed
Lines changed: 485 additions & 72 deletions
File tree
- .changeset
- content/docs
- api
- concepts
- packages
- metadata-protocol/src
- objectql/src
- ui/packages/app-shell/src/views/metadata-admin
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
109 | 112 | | |
110 | 113 | | |
111 | 114 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
135 | 140 | | |
136 | 141 | | |
137 | 142 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
401 | 401 | | |
402 | 402 | | |
403 | 403 | | |
404 | | - | |
405 | | - | |
406 | | - | |
407 | | - | |
408 | | - | |
409 | | - | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
410 | 410 | | |
411 | 411 | | |
412 | 412 | | |
| |||
755 | 755 | | |
756 | 756 | | |
757 | 757 | | |
758 | | - | |
759 | | - | |
| 758 | + | |
| 759 | + | |
760 | 760 | | |
761 | | - | |
| 761 | + | |
762 | 762 | | |
763 | 763 | | |
764 | 764 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | | - | |
154 | | - | |
| 153 | + | |
| 154 | + | |
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
33 | | - | |
| 34 | + | |
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
311 | | - | |
312 | | - | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
313 | 313 | | |
314 | 314 | | |
315 | 315 | | |
| |||
322 | 322 | | |
323 | 323 | | |
324 | 324 | | |
| 325 | + | |
| 326 | + | |
325 | 327 | | |
326 | 328 | | |
327 | 329 | | |
328 | 330 | | |
329 | 331 | | |
330 | 332 | | |
331 | | - | |
332 | | - | |
333 | | - | |
334 | | - | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
335 | 338 | | |
336 | 339 | | |
337 | 340 | | |
| |||
340 | 343 | | |
341 | 344 | | |
342 | 345 | | |
343 | | - | |
344 | | - | |
345 | | - | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
346 | 354 | | |
347 | 355 | | |
348 | 356 | | |
349 | 357 | | |
350 | | - | |
| 358 | + | |
351 | 359 | | |
352 | 360 | | |
353 | 361 | | |
| |||
0 commit comments