Repository navigation
Commit ba0084e
feat(spec): one override declaration for code and Studio, with the .custom pairing helpers and the closed overridable-type list (ADR-0146) (#28467)
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Fixes #28404
Clause-②: yes (widening)
Metadata-protocol: added
## 维护者速读
- **改了什么**:spec 里新增「覆盖声明」`overrides: { package, basedOn }`,加在 13
种可覆盖的元数据上;另加 `.custom` 配对包的命名、命名空间、依赖范围规则,可覆盖类型的封闭清单,`__c`
扩展字段后缀和基础字段可改的显示属性白名单。
- **为什么改**:开发者在仓库里写 `hotcrm.custom`,管理员在 Studio
里定制,两边要写出同一份声明,后面的写入关、读取、导出和 `os validate` 才能读同一样东西,不会各自拼一套「这一项覆盖了
HotCRM 的哪一项」。
- **风险与回滚**:只是新增可选字段、常量和纯函数,没有任何入口开始按它放行或拒绝;现有元数据不受影响。回滚就是 revert 本 PR。
- **先看**:① `packages/spec/src/kernel/customization-package.ts` 的
`OVERRIDE_TYPE_CLASSIFICATION`(哪些类型能覆盖,有 4 个待您定);②
`customizationNamespace`(基础命名空间超过 13 个字符就没有配对命名空间);③
`packages/spec/src/data/field-customization.ts` 的白名单。
- **两分钟验证**:`pnpm --filter @objectstack/spec exec vitest run
src/kernel/override-declaration.test.ts
src/kernel/customization-package.test.ts
src/data/field-customization.test.ts`,预期三个文件全绿。
- **需要你做的**:无。下面 "For the maintainer" 的 7 条您已同意(2026-10-31
20:44Z「同意」);契约复审 PASS、CI 全绿后由席位落地。
## For the maintainer
ADR-0146 does not decide these. **Ruled 2026-10-31 20:44Z: the
maintainer agreed to all seven as built (「同意」)**, together with the
follow-ups recorded after the list:
1. **Namespace past 13 characters.** `customizationNamespace(baseNs)`
answers `<base_ns>_custom`; a base namespace longer than 13 characters
gets no pair namespace (`{ ok: false, reason: 'base_namespace_too_long'
}`) and the ensure door will refuse that base with a sentence. No
truncation, no hashing. Nothing in the repository is affected today (the
longest base namespace is `feature_flags`, 13).
2. **`seed`** — runtime-creatable and shipped by packages, so D2's first
sentence admits it, but ADR-0126 §3 puts it outside ("one-shot").
Classified `excluded`, basis `adr_0126_one_shot` (ruled).
3. **`datasource`** — the same reading (ADR-0126 §3: "origin-gated"); D2
names it on neither side. Classified `excluded`, basis
`setup_owned_connection` (ADR-0146 D1 gives datasources to Setup;
ADR-0126 §3 origin-gated; ruled). The card's census listed four
undecided types and missed this one: its registry entry is the only
multi-line entry, which a line-wise reading cannot see (the registry's
own comment warns about this).
4. **`tool`** and **`skill`** — behavioural (ADR-0126 §3 pre-charts them
for Regime C), with no ledger switch and no D9-style conditions written
for them. Classified `excluded`, basis `behavioural_switch_first`:
`action`'s path, the ledger switch first and an override door later
(ruled).
5. **`external_catalog`** — decided by D2's first sentence (packages
never ship one), classified `excluded`, basis `not_package_shipped`.
Listed so you can confirm the reading.
6. **Prerelease base versions.**
`customizationDependencyRange('2.0.0-beta.1')` answers `^2` (the major
of the numeric core, the ADR's words read literally). The ADR-0141 D2
install check cannot read a prerelease installed version, so a pair
whose base sits at a prerelease is re-installable once the base reaches
a release of that major.
7. **`basedOn` is required inside the block.** The ADR's shape names
both keys; the PR requires both to be non-empty strings when `overrides`
is present.
Follow-ups the ruling also agreed:
- No row of `OVERRIDE_TYPE_CLASSIFICATION` stays `undecided`; the basis
value is removed and a test pins that no row carries it (commit
`5d79f48d2f`).
- `basedOn` records the **base package's `manifest.version`** the
override was taken from, not a history version of the item; its
`.describe()` says so. The ADR-0146 D2 wording is aligned in the #28390
text amendment.
- A separate low-priority card: `os lint` / `os validate` warn when a
manifest namespace is longer than 13 characters (that package cannot be
customized).
## What changes
- **`overrides: { package, basedOn }`**
(`packages/spec/src/kernel/override-declaration.zod.ts`,
`OverrideDeclarationFields`): one closed field set, spread on the 13
types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`: `view`
(the ViewItem record and the flattened overlay; not the `defineView`
container, see the acceptance notes), `dashboard`, `report`,
`translation` (the item), `email_template`, `page`, `app`, `dataset`,
`doc`, `book`, `mapping`, `flow`, `action`. It is a separate spread, not
part of `MetadataProtectionFields`: that envelope is spread into
excluded types and is what registration and clone strip as
loader-stamped state. `package` refuses a `.custom` id. `basedOn`'s
`.describe()` says it is recorded and never judged.
- **Pairing helpers**
(`packages/spec/src/kernel/customization-package.ts`):
`CUSTOMIZATION_PACKAGE_SUFFIX`, `customizationPackageId`,
`customizationBasePackageId`, `isCustomizationPackage`,
`isCustomizationPairOf` (ruling A: only the paired `<base>.custom`
declares), `customizationNamespace`, `customizationDependencyRange`.
Each derivation answers `{ ok: true, value }` or `{ ok: false, reason
}`. Dotted ids are covered. The namespace answer is judged against
`ManifestSchema.shape.namespace` and the id against
`MANIFEST_ID_PATTERN`, so the helpers restate neither grammar.
- **The closed type list**: `OVERRIDE_TYPE_CLASSIFICATION` is total over
`MetadataType` at compile time and pinned to
`DEFAULT_METADATA_TYPE_REGISTRY` in both directions by a test. `object`
is `extend_only`; `permission`, `position`, `capability` are excluded
(D6); the code-only types are excluded; `flow` and `action` carry their
D9 conditions as data. `OVERRIDABLE_METADATA_TYPES`,
`getOverrideTypeClassification`, `isOverridableMetadataType` read it.
- **`__c` and the display whitelist**
(`packages/spec/src/data/field-customization.ts`):
`EXTENSION_FIELD_SUFFIX`, `isExtensionFieldName`,
`BASE_FIELD_DISPLAY_WHITELIST` = field `label`, `description`,
`inlineHelpText`, `placeholder`, and option `label`, each measured
against D10's test. Only the class D10 names; other display-only keys
may pass the same test and enter by decision.
- **The per-field patch slot on `ObjectExtensionSchema` rides N-23a.** A
slot that the merge (`mergeObjectDefinitions`) does not apply would be
an authorable key that nothing reads, so it lands together with that
merge.
- Liveness rows (`planned`) for `overrides` on the 12 ledgers that walk
it, and the count shards; the `llms.txt` schema counts (kernel 31, total
213); regenerated spec artefacts.
- The served `/meta/types` property counts in
`packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts`
move by one on the seven overridable types that suite pins (`action`,
`app`, `dashboard`, `dataset`, `flow`, `page`, `report`); the excluded
types keep theirs (seat commit `e55a4e7c6b`, after `Test Core (7/8)`
went red on `e0253ce45c`).
- Ledger rows in two existing pins the key reaches: a root `omit` row
for `overrides` on the eight types with a hand-written metadata form
(`metadata-form-zod-reconciliation.test.ts`; Studio writes the
declaration on a customization, so no hand-filled control is offered
while it is `planned`), and a `noReader` row for `flow.overrides` in the
flows translation face.
## Tests
- `packages/spec/src/kernel/override-declaration.test.ts`: the shape
accepts and refuses (missing, empty, unknown key, `.custom` package);
every overridable type keeps `overrides` through its metadata schema
(the save door's parse) and refuses a malformed block; the excluded
types with a create seed refuse the key by name; `defineStack()`
(strict) keeps it on all 11 item collections; the export's `viewItems:`
channel keeps it for a ViewItem and an overlay.
- `packages/spec/src/kernel/customization-package.test.ts`: table tests
for the id helpers (dotted ids, a pair of a pair, a dependent package
that is not the pair), the namespace (13 and 14 characters, pinned
against the grammar), the range, and the type list pinned to the
registry.
- `packages/spec/src/data/field-customization.test.ts`: every whitelist
member exists on `FieldSchema` (or its option schema) and takes text;
none is in the structural list, and every structural key named is a real
`FieldSchema` key.
-
`packages/runtime/src/domains/packages-export-override-declaration.test.ts`:
`GET /packages/<base>.custom/export` carries `overrides` on a flow and a
view item and strips provenance; both re-parse with the block intact.
- Full `@objectstack/spec` suite on `e0253ce45c`: 865 files passed,
24491 tests passed, 1 todo. An earlier run named three existing pins the
key reaches (the form reconciliation, the flows translation face, the
description renderer's level-1 count); they are recorded in their
ledgers above and the module header opens at level 2.
- Reverse check: with the spread removed from `flow.zod.ts`, the strict
`defineStack()` in the first file refuses `overrides` on `flows.0` and
the file goes red; restored from `HEAD`.
## Acceptance notes
- **`view` via `defineStack()` and `translation` via the stack.** The
stack authors views as `defineView` containers (`views:`) and
translations as locale bundles (`translations:`); both are aggregates
that are expanded or merged into items at load. The declaration is on
the item shapes the `view` and `translation` metadata types parse, so it
survives the metadata save and the export, but a repository author
cannot write it through those two stack collections today. Whether a
container-level declaration fans out to its expanded items is a loader
decision for the read and Studio cards, not taken here.
- **The export skips `emailTemplates`** (`assemblePackageManifest`
excludes it with `datasources`), so an overriding email template in
`.custom` does not travel through `GET /packages/:id/export` today.
Pre-existing; the effective-read card carries it.
- The `app` schema now carries the landing refinement from main
(#28449); the override spread sits inside it, resolved in the merge.
- No `view` liveness row: the `view` ledger governs the `defineView`
container (`ViewSchema`), which does not carry the key.
## Verification
Independent review: none run (quota); the PR waits for the seat's
contract-review hand-off.
**Gates** (head `e0253ce45c`): `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` derives 112 commands. All
112 ran in one pass and exited 0, each exit code recorded before any
pipe. `--ran` reconciliation: 112 derived, 112 run, 0 not measured, 0
unrun. The full farm is CI's.
Also run:
- `pnpm --filter @objectstack/spec check:generated`: all 14 artefacts
current after regeneration on each merge.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
- `pnpm --filter @objectstack/runtime typecheck`: exit 0.
- The runtime export test: 3 of 3 passed, on `0f9936f445`. The later
merge brought only the app landing, components and docs.
Earlier passes found two real fixes, both in this PR: the `llms.txt`
schema counts, and the three ledger pins. Gates that refused with exit 3
because a prerequisite build was missing were re-run after the build and
are not counted as passes.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2bba35a commit ba0084e
89 files changed
Lines changed: 1945 additions & 25 deletions
File tree
- .changeset
- content/docs/references
- api
- automation
- data
- kernel
- system
- ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- metadata-protocol/src
- runtime/src/domains
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- liveness
- state-counts
- src
- automation
- data
- kernel
- system
- ui
- skills
- objectstack-ai/references
- objectstack-automation/references
- objectstack-data/references
- objectstack-ui/references
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| 132 | + | |
132 | 133 | | |
133 | 134 | | |
134 | 135 | | |
| |||
186 | 187 | | |
187 | 188 | | |
188 | 189 | | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
189 | 197 | | |
190 | 198 | | |
191 | 199 | | |
| |||
249 | 257 | | |
250 | 258 | | |
251 | 259 | | |
| 260 | + | |
252 | 261 | | |
253 | 262 | | |
254 | 263 | | |
| |||
789 | 798 | | |
790 | 799 | | |
791 | 800 | | |
| 801 | + | |
792 | 802 | | |
793 | 803 | | |
794 | 804 | | |
| |||
1098 | 1108 | | |
1099 | 1109 | | |
1100 | 1110 | | |
| 1111 | + | |
1101 | 1112 | | |
1102 | 1113 | | |
1103 | 1114 | | |
| |||
1162 | 1173 | | |
1163 | 1174 | | |
1164 | 1175 | | |
| 1176 | + | |
1165 | 1177 | | |
1166 | 1178 | | |
1167 | 1179 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
| 109 | + | |
109 | 110 | | |
110 | 111 | | |
111 | 112 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| |||
107 | 108 | | |
108 | 109 | | |
109 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
110 | 118 | | |
111 | 119 | | |
112 | 120 | | |
| |||
264 | 272 | | |
265 | 273 | | |
266 | 274 | | |
| 275 | + | |
267 | 276 | | |
268 | 277 | | |
269 | 278 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
60 | 61 | | |
61 | 62 | | |
62 | 63 | | |
| |||
77 | 78 | | |
78 | 79 | | |
79 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
80 | 88 | | |
81 | 89 | | |
82 | 90 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
525 | 525 | | |
526 | 526 | | |
527 | 527 | | |
| 528 | + | |
528 | 529 | | |
529 | 530 | | |
530 | 531 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
204 | 204 | | |
205 | 205 | | |
206 | 206 | | |
207 | | - | |
| 207 | + | |
208 | 208 | | |
209 | 209 | | |
210 | 210 | | |
| |||
225 | 225 | | |
226 | 226 | | |
227 | 227 | | |
| 228 | + | |
228 | 229 | | |
229 | 230 | | |
230 | 231 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
| 371 | + | |
371 | 372 | | |
372 | 373 | | |
373 | 374 | | |
| |||
0 commit comments