Skip to content

Commit 8d208a4

Browse files
feat(runtime): the customizations read serves the nav contributions other packages add to a base's apps (#29742)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8KcviDs8Y3wWmPMX818Nmzo)_ Fixes #28722 Ruling-ref: 6566915346 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`GET /packages/:id/customizations` 的 `contributions` 现在真正返回 `nav` 条目:别的已安装包往本包应用里加的导航项,每条写明是哪个包加的;spec 里 `nav` 臂去掉 [PLANNED — not yet served] 标记。 - 为什么改:裁决 E(6566915346,「同意」):Studio 要把别的包加进 HotCRM 菜单的条目灰显并标出来源,spec 协议已在 #29401 落地,这是服务端那一半(#28722 第三项)。 - 风险与回滚:只读接口多出 `nav` 条目,形状 spec 早已声明;读的是菜单折叠用的同一个注册表,停用包的条目不列,和菜单一致;导航项本身不加任何键;回滚即撤掉此 PR。 - 先看:`packages/runtime/src/domains/packages.ts`(`readBaseContributions` 新增的 nav 段与排序)、`packages/runtime/src/domains/packages-customizations.test.ts`(新测试)、`packages/spec/src/api/package-lifecycle.zod.ts`(去标记)。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-customizations.test.ts`,预期 22 个测试全部通过。 - 需要你做的:无,席位落地。 ## What changes The customizations read now serves the `nav` kind of `contributions` that the spec declared ahead of its producer in #29401 (ruling 6566915346, letter E). - **`readBaseContributions`** (`packages/runtime/src/domains/packages.ts`) reads the navigation-contribution registry (`getAppNavContributions`), the one the served app's fold (`applyNavContributions`) reads, for every app registered under the base (`listItems('app', baseId)`). It writes one entry per item of a contribution's `items` that declares an `id`: `{ kind: 'nav', app, group?, name, packageId }`. `group` is the contribution's `group` when it names one, including a group the app does not declare (the fold relocates those items to the top level, and the spec arm says the group is still named). A separator with no `id` is not an entry, and a contributed group's children ride inside it. - **The fold's rules, not a copy of them.** A disabled package's contribution is skipped with the fold's own predicate (`isPackageDisabled`, #28117), asked per read. The base and its pair are excluded through the stamp predicate the `field` and `flow` kinds already share (`codePackageOf`), which also drops a package-less registration and the `sys_metadata` sentinel. Nothing is read from or written to any nav item, and no client-side join on names is involved. - **Order**, as the member describe declares: the `field` and `flow` entries first, ordered as before. Then the `nav` entries, by app, group (no group first), name and package. `counts.contributions` counts every entry. - The forced `Exclude` type annotation #29401 added is gone. The local types are the member's own arms. - **Spec** (`packages/spec/src/api/package-lifecycle.zod.ts`): the `nav` arm's describe drops `[PLANNED — not yet served]`, and so do the member describe, the schema describe, the file header and the schema docblock. The docblock keeps the ruling's MUSTs: the same registry, no disabled package, no key on any nav item. The schema accepts exactly what it accepted before. The planned-marker pin in `package-lifecycle.test.ts` is flipped in the same change: the arm, the member and the schema now carry no `PLANNED` / `not yet served`. - Regenerated `content/docs/references/api/package-lifecycle.mdx` (`gen:docs`, the only artifact `check:generated` named stale). `content/docs/api/metadata-api.mdx` now names the `nav` kind as served. - Changeset `.changeset/28722-customizations-serve-nav-contributions.md`: `@objectstack/runtime` patch and `@objectstack/spec` patch (the describe text ships in `dist`). The authorable-surface shard did not move: `check:generated`, including `check:authorable-surface`, exits 0 with no `packages/spec/authorable-surface/**` change. So the declaration is `Metadata-protocol: none`, and `Clause-②: no` because the arm was already declared. ## Why #28722 Wanted 3: Studio needs one served answer naming the package behind each navigation entry another package adds to an installed base's app, so it can draw those entries greyed with their source (ADR-0029 D7, ADR-0146 D3, #27356 row 20). The spec slice (#29401) declared the shape. This serves it from the registry the menu fold reads. ## Tests `packages/runtime/src/domains/packages-customizations.test.ts`. The fixture's base now ships an app with a group, and the other base ships one of its own. The card's third package also contributes a nav item through its manifest `navigationContributions`. The pair contributes a nav item to the base's app, and so does the base itself. - The card's test, updated: the third package is named for its field, its flow **and its nav item**, and neither the base's nor the pair's nav item is listed (count 3). The environment-flow test's expected list gains the nav entry. - New: one entry per item with an id, with its group (including an undeclared group), no-group entries first, field and flow entries before nav. A separator is not an entry, a contributed group's child is not an entry, and the answer parses through `GetPackageCustomizationsResponseSchema`. - New: a disabled package's items are not listed and re-enabling restores them. AGREEMENT: at each step, the listed names equal the ids the served app (`registry.getApp`) carries beyond the base's and the pair's own. - New: a contribution to an app the base does not own is not the base's. Another base's read lists the one made to its app, and one made to an app no package declares is nobody's. - New: a package-less contribution and one stamped with the `sys_metadata` sentinel are not listed. - `packages/spec/src/api/package-lifecycle.test.ts`: the three planned-marker pins flipped. <details><summary>Red first, then green</summary> - Test commit `8b347c02` on base `744211fc` (`packages.ts` unchanged): `vitest run src/domains/packages-customizations.test.ts` exit 1, 6 failed and 16 passed of 22. Every failure is an AssertionError on missing `nav` entries. - Spec pin flip against the unchanged `package-lifecycle.zod.ts`: exit 1, 3 failed and 50 passed of 53. With the zod edit: exit 0, 53 of 53. - Implementation `66293723`: runtime suite file exit 0, 22 of 22. </details> <details><summary>Ablations (each through scripts/ablation-replace.mjs, restored to the HEAD blob 21dfef8, git diff HEAD empty; runtime reads packages.ts from src, so no rebuild between legs)</summary> - The disabled-package skip deleted: 1 red (the disabled test). - `codePackageOf` replaced by the raw `packageId`: 6 red (base, pair, sentinel and package-less items appear). - `listItems('app', baseId)` replaced by `listItems('app')`: 2 red (the not-owned test, and another base's read). - The item-id guard deleted: 1 red (the separator becomes an entry). - The `group` spread deleted: 2 red. The first attempt passed an empty `--replacement`, which the tool refused before running anything (replacement count 0 to 0). It was rerun with `--delete`. </details> <details><summary>Local readings (final head `58810e41`)</summary> - Merged `origin/main` twice through `scripts/pm/os-regen-merge.sh`: at `fc558923`, and at `3d5933a9` after #29651 touched both `package-lifecycle.zod.ts` and `packages.ts` (no conflicts). The second merge took main's side of `content/docs/references/api/package-lifecycle.mdx`, which was then regenerated from the merged tree (`gen:schema` via the spec build, then `gen:docs`) in `a4c5be41`. A third merge at `0eb97fa4` (`58810e41`) took in #29687, which is #29649's change to `packages.ts` (seat 4's serial PR). It merged with no conflicts and no regeneration owed. Every reading below was retaken at `58810e41` unless it names another head. - Builds, under os-verify-lock: `turbo run build --filter='@objectstack/runtime...'` VERDICT command-exit 0 (34 tasks) at `58810e41`. `turbo run build --filter='./packages/**'` VERDICT 0 (78 tasks) was the prerequisite for `check:skill-examples` and `check:dual-build-cjs-loads`, which exit 3 with no built output. - `packages/spec` `check:generated`: before regeneration it named exactly `check:docs` stale. After `gen:docs` it exits 0, including `check:authorable-surface` with no shard change, which is the `Metadata-protocol: none` reading. One run at `a4c5be41` reported `check:test-typecheck` as `tsc … failed without diagnostics` when started without `NODE_OPTIONS`. Rerun alone with `--max-old-space-size=4096` it is OK (52 files, 240 errors, 131 pinned signatures held), and the final `check:generated` exits 0. - `pnpm --filter @objectstack/spec test`: exit 0, Test Files 803 passed (803), Tests 23782 passed, 1 todo (head `9540d333`). `src/api/package-lifecycle.test.ts` at `58810e41`: 57 passed. - `pnpm --filter @objectstack/spec typecheck`: exit 0. - `pnpm --filter @objectstack/runtime typecheck`: exit 0 at `58810e41`. The first run at `9540d333` exited 1 only because `@objectstack/driver-turso` had no `dist` (TS2307 in an untouched test file). Once that package was built, it exits 0. - `pnpm --filter @objectstack/runtime test` under the lock, at `58810e41`: VERDICT command-exit 0, Test Files 531 passed (531), Tests 8529 passed, 19 skipped. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `58810e41` (and before at `a4c5be41`, same result): 104 commands, all 104 exit 0 (stdin closed, exit captured before any pipe). `--ran`: 104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. In the first round at `9540d333`, three commands exited 3 on prerequisites: `check-plugin-teardown-shape --self-test` (shallow clone; its fixture commit was fetched, then 48 cases pass), and `check:skill-examples` and `check:dual-build-cjs-loads` (no built output; exit 0 after the build above). - `pnpm lint`: exit 0 at `58810e41`. - Not run locally: `test:repo` (CI's Test Core), and full dogfood (no `!` in the title, no narrowing arm). </details> ## Acceptance notes - Serial: #29342 (seat 5) edits the same file header paragraph of `package-lifecycle.zod.ts` that lists the declared-ahead members. This PR leaves only `withheldMaps` there, so whichever lands second takes the other's sentence. #29649 (seat 4) touches `ownDocumentsOf` in `packages.ts`, a region disjoint from this one. #29342 had not landed at the final merge. #29649 landed as #29687 and is merged in (`58810e41`). - Observation, not filed: for a disabled base, `listItems('app', baseId)` already hides the base's app, so the read lists no `nav` entry, while `field` entries on its objects are still listed. Nothing is served to contribute to in that state, and the docblock says so. ## Verification `pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-customizations.test.ts` → 22 passed. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [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: its generated artifacts are regenerated and committed. - [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. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbYJ6AF6nA1jMoub4rQ)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbYJ6AF6nA1jMoub4rQ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4ab2e7e commit 8d208a4

7 files changed

Lines changed: 240 additions & 67 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat(runtime): the customizations read serves the `nav` contributions: the navigation items other packages add to a base's apps
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
`GET /packages/:id/customizations` now lists, in `contributions`, the navigation items other installed packages add to the base's apps, each naming the package that adds them. Studio can draw those menu entries greyed with their source, as it already can for contributed fields and flows (ADR-0029 D7 as amended by ADR-0146 D3).
12+
13+
- **One entry per item.** Every item of a package's `navigationContributions` entry that declares an `id`, contributed to an app the base ships, is a `nav` entry: `{ kind: 'nav', app, group?, name, packageId }`. `group` is the group the contribution targets, when it targets one. A separator without an `id` is not an entry, and a contributed group's children ride inside it.
14+
- **The same answer the menu shows.** The entries come from the registry the served app's navigation fold reads, never a join on names. A disabled package's items are not listed, as the fold drops them, and they come back when it is enabled again. The base's own items and its `<base>.custom` pair's are not contributions, and no nav item gains a key.
15+
- **Order and counts.** The `field` and `flow` entries come first as before, then the `nav` entries by app, group (no group first), name and package. `counts.contributions` counts them all.
16+
17+
The spec's `nav` arm drops its `[PLANNED — not yet served]` marker, and the member's descriptions no longer say the kind is unserved. The schema accepts exactly what it accepted before.
18+
19+
Nothing to do for existing metadata. A client that read `contributions` and assumed only `field` and `flow` entries now also receives `nav` entries, which the declared schema already allowed.

‎content/docs/api/metadata-api.mdx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,8 @@ never overridden; it is copied under a new name. What the reads then serve is in
110110
`POST /packages/hotcrm/ensure-customization` creates the pair when absent (Studio calls
111111
it on the first change), and `GET /packages/hotcrm/customizations` lists its overrides,
112112
extensions and switched-off items in one read, beside `contributions`: the fields (kind `field`)
113-
and flows (kind `flow`) other installed packages contribute to `hotcrm`'s objects, each entry
114-
naming its package. Its `nav` kind (a navigation item another package contributes to a
115-
`hotcrm` app) is declared and not yet served.
113+
and flows (kind `flow`) other installed packages contribute to `hotcrm`'s objects, and the
114+
navigation items (kind `nav`) they contribute to `hotcrm`'s apps, each entry naming its package.
116115

117116
**Body**: `{ type: "object", name: "account", item: { ... } }`
118117
**Response**: `{ success: true }`

‎content/docs/references/api/package-lifecycle.mdx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ declared a VERSION rollback against the live COMMIT-rollback path;
4343
`RollbackToPackageCommitResponseSchema` below is the true contract,
4444
authored after that retirement per the ruling's sequencing (3A).
4545

46-
Two members are not transcriptions but DECLARED AHEAD OF THEIR PRODUCER
47-
(ADR-0049), each describe opening `[PLANNED — not yet served]` until the
48-
change that serves it drops the marker: the `nav` arm of
49-
`GetPackageCustomizationsResponseSchema.contributions` (#27928) and
46+
Two declarations are not transcriptions but DECLARED AHEAD OF THEIR
47+
PRODUCER (ADR-0049), each describe opening `[PLANNED — not yet served]`
48+
until the change that serves it drops the marker:
5049
`PackageExportManifestSchema.withheldMaps` (#28640 ruling ③A, served by
51-
#29342). Their own docblocks say what is not served yet.
50+
#29342) and `PackageExportBundleSchema` (`GET /packages/export`, #28914).
51+
Their own docblocks say what is not served yet.
5252

5353
<Callout type="info">
5454
**Source:** `packages/spec/src/api/package-lifecycle.zod.ts`
@@ -170,7 +170,7 @@ Per-package customization read request: the base package id, no query parameters
170170

171171
## GetPackageCustomizationsResponse
172172

173-
Per-package customization read: overrides, extensions and switched-off items of one installed base, and the fields, flows and (not yet served) navigation items other packages contribute to it.
173+
Per-package customization read: overrides, extensions and switched-off items of one installed base, and the fields, flows and navigation items other packages contribute to it.
174174

175175
### Properties
176176

@@ -180,7 +180,7 @@ Per-package customization read: overrides, extensions and switched-off items of
180180
| **overrides** | `{ type: string; name: string; member?: string; basedOn?: string; … }[]` | ✅ | Every ACTIVE item of the base's paired `<base>.custom` package that declares `overrides.package` = the base and that the effective read serves over the base's item of the same name: the rows the served-override predicate admits (a declaration from any other package, and an undeclared pair item, are not listed), minus the stored rows every read declines (a shipped flow name). Ordered by type, name and member. |
181181
| **extensions** | `{ object: string; kind: Enum<'field' \| 'validation' \| 'index'>; name?: string; fields?: string[] }[]` | ✅ | Every field, validation rule and index the paired `<base>.custom` package declares in an object extension (`objectExtensions`) of an object the base owns, one entry per member, as the extension declares it. Ordered by object, kind and name. |
182182
| **switchedOff** | `{ type: string; name: string }[]` | ✅ | Every activation-ledger row (`sys_metadata_activation`) that records the base as its package and `active: false`: the base's items this deployment switched off. Ordered by type and name. |
183-
| **contributions** | `({ kind: Enum<'field' \| 'flow'>; object: string; name: string; packageId: string } \| { kind: 'nav'; app: string; group?: string; name: string; … })[]` | ✅ | Every field, flow and navigation item another installed package contributes to the base, one entry per item and contributing package: the fields of every object extension of an object the base owns, from any package but the base and its pair; every flow registered under such a package whose trigger object the base owns (a flow of a disabled package is not served, so not listed); and, declared ahead of its producer and not yet served, every navigation item such a package contributes to an app the base owns. The entry shape is per `kind`: `object` for `field` and `flow`, `app` and an optional `group` for `nav`. Ordered: the `field` and `flow` entries first, by object, kind, name and package; then the `nav` entries, by app, group (an entry with no group first), name and package. |
183+
| **contributions** | `({ kind: Enum<'field' \| 'flow'>; object: string; name: string; packageId: string } \| { kind: 'nav'; app: string; group?: string; name: string; … })[]` | ✅ | Every field, flow and navigation item another installed package contributes to the base, one entry per item and contributing package: the fields of every object extension of an object the base owns, from any package but the base and its pair; every flow registered under such a package whose trigger object the base owns (a flow of a disabled package is not served, so not listed); and every navigation item such a package contributes to an app the base owns (a disabled package's are not listed). The entry shape is per `kind`: `object` for `field` and `flow`, `app` and an optional `group` for `nav`. Ordered: the `field` and `flow` entries first, by object, kind, name and package; then the `nav` entries, by app, group (an entry with no group first), name and package. |
184184
| **counts** | `{ overrides: integer; extensions: integer; switchedOff: integer; contributions: integer }` | ✅ | Each list's length, derived from the lists in this same answer. |
185185

186186
### Nested Shape: `GetPackageCustomizationsResponse.overrides[number]`
@@ -222,7 +222,7 @@ A field or flow another package contributes to one of the base's objects.
222222

223223
### Nested Shape: `GetPackageCustomizationsResponse.contributions[number][option 2]`
224224

225-
[PLANNED — not yet served] Declared ahead of its producer (ADR-0049): the read lists no `nav` entry yet. A navigation item another installed package contributes to an app the base owns, one entry per item of the contribution's `items` (a contributed group's children ride inside it and are not entries of their own), read from the same contribution registry the served app's navigation fold reads (ADR-0029 D7). An item that declares no `id` (a separator may omit it) has no `name` to give and is not an entry. A disabled package's contributions are not listed, as the fold drops them.
225+
A navigation item another installed package contributes to an app the base owns, one entry per item of the contribution's `items` (a contributed group's children ride inside it and are not entries of their own), read from the same contribution registry the served app's navigation fold reads (ADR-0029 D7). An item that declares no `id` (a separator may omit it) has no `name` to give and is not an entry. A disabled package's contributions are not listed, as the fold drops them.
226226

227227
| Property | Type | Required | Description |
228228
| :--- | :--- | :--- | :--- |

0 commit comments

Comments
 (0)