Skip to content

feat(runtime): the customizations read serves the nav contributions other packages add to a base's apps - #29742

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-f3xzh7
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-f3xzh7

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #28722

Ruling-ref: 6566915346
Clause-②: no
Metadata-protocol: none

维护者速读

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, packages: an admin cannot disable a type:module sub-package of an installed app (422 WRITABLE_PACKAGE_REQUIRED), and a disabled package's nav contributions stay in the app #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 feat(spec): the customizations read's contributions declare a nav kind, with an entry shape per kind #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.
Red first, then green
  • 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.
Ablations (each through scripts/ablation-replace.mjs, restored to the HEAD blob 21dfef8a1015, git diff HEAD empty; runtime reads packages.ts from src, so no rebuild between legs)
  • 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.
Local readings (final head `58810e41`)
  • Merged origin/main twice through scripts/pm/os-regen-merge.sh: at fc558923, and at 3d5933a9 after feat(spec,runtime): a package export carries its namespace and dependencies, so a reinstalled .custom pair keeps both (#29617) #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 fix(runtime): the base export serves the base's own document while a .custom override of it is live #29687, which is packages: a published dashboard or report override removes the item from the base's export, where ADR-0146 D8 item 3 says the base export is unchanged #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).

Acceptance notes

Verification

pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-customizations.test.ts → 22 passed.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQwxbYJ6AF6nA1jMoub4rQ

…red on main

Co-Authored-By: Claude <noreply@anthropic.com>
readBaseContributions reads the navigation-contribution registry the served
app's fold reads, for each app registered under the base: one entry per item
with an id, the contribution's group when it names one, the disabled-package
skip the fold applies, and the base and pair excluded through the shared
stamp predicate. The spec nav arm drops its planned marker; docs regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
…n merge

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 61 of 218 client-bound route-ledger rows — the other 157 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 157: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 173 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3eeb81d19495b9e4064685fc9662936a834f017c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d04d248aada870f93cab5b09545762c35d47c8a5 — the merge of head be2875456d3eb4a0f32bfaf0021cc62a0d57b1c9 into base 3eeb81d19495b9e4064685fc9662936a834f017c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d04d248aada870f93cab5b09545762c35d47c8a5 && git checkout d04d248aada870f93cab5b09545762c35d47c8a5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3eeb81d19495b9e4064685fc9662936a834f017c be2875456d3eb4a0f32bfaf0021cc62a0d57b1c9 && git checkout -B drift-repro 3eeb81d19495b9e4064685fc9662936a834f017c && git merge --no-ff be2875456d3eb4a0f32bfaf0021cc62a0d57b1c9

node scripts/docs-audit/affected-docs.mjs --json 3eeb81d19495b9e4064685fc9662936a834f017c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 58810e4198ab2518cc3019dc266e85825387da23
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts + packages/runtime/src/domains/packages.ts, packages/spec/src/api/package-lifecycle.test.ts, packages/runtime/src/domains/packages-customizations.test.ts, packages/objectql/src/registry.ts (appNavContributions / getAppNavContributions / applyNavContributions / listItems / isPackageDisabled / registerItem), packages/objectql/src/engine.ts (manifest navigationContributions registration), packages/metadata-core/src/code-artifact-provenance.ts (isCodeArtifactBody), packages/spec/authorable-surface/api.json, packages/spec/variant-docs.json, ui/packages/app-shell/src/views/studio-design/packageCustomizations-io.ts

Reviewer: an at-tier subagent adopted by this seat. Inputs: card #28722 and every comment, ruling 6566915346 (#27928 letter E), the PR body, file list and net diff at the head, the head's check-runs (none failed at review time; Test Core / Test UI / type checks / builds still in progress, read again by the seat before landing).

① Derived judgments

  1. Accept set of GetPackageCustomizationsResponseSchema unchanged: every zod hunk is a describe(...) string, docblock or line comment; no z.* structure moves. Right.
  2. The read now emits nav entries: a producer catching up to the arm feat(spec): the customizations read's contributions declare a nav kind, with an entry shape per kind #29401 (0c640141, spec minor, Clause-②: yes (widening), Metadata-protocol: added) already published under the declared-ahead pattern. Clause-②: no is right for this slice; the ruling's widening sentence was spent by the spec slice.
  3. Metadata-protocol: none right: authorable-surface/api.json keys the member by path with no describe text; variant-docs.json key kind:field|flow|nav unchanged; nothing an author writes is touched.
  4. Ruling's "no key on any nav item; BaseNavItemSchema stays strict" held: no nav item schema touched; readBaseContributions reads item.id only.
  5. readBaseContributions's exported return type unchanged; the dropped Exclude was a local alias.
  6. The spec pin flip is a true gate flip (marker present → absent on arm, member, schema); the name-required/separator and no-tracker-id pins stay.

② Semver level

Changeset: @objectstack/runtime patch + @objectstack/spec patch, Clause-②: no, Metadata-protocol: none, identical to the PR body. Kind none takes patch on every moved package (spec describe text ships in dist; runtime serves new entries). Right.

③ Boundary flags

④ Design judgment

package-lifecycle.zod.ts against ruling 6566915346 E and its first consumer readBaseContributions:

  • Same registry (MUST 1): getAppNavContributions(app) and applyNavContributions read the same appNavContributions map by bare app name; no join on names.
  • Disabled package not listed (MUST 2): the same isPackageDisabled(contribution.packageId) predicate the fold asks, per read; the disabled test holds agreement with getApp('showcase').navigation across disable/enable.
  • No key on any nav item (MUST 3): nothing written.
  • Base, pair, missing package and the sys_metadata sentinel excluded via codePackageOf/isCodeArtifactBody; the engine stamps manifest contributions with the package id, so read and fold judge the same stamp.
  • Fold/read agreement: empty items, id-less separators, contributed groups (listed once), undeclared groups (named) all pinned; ordering matches the member describe; the answer parses through the published schema.
  • Pre-existing, mirrored, not this PR's: appNavContributions is emptied only by clear(), so an uninstalled package's contributions keep folding and the read lists them too (agreement, as the ruling requires).

Findings:

No must-fix.

Implemented-by: claude/project-thread-f3xzh7
Reviewed-by: session_01JQwxbYyp6wSRcywGFK2L4A

VERDICT: PASS

…d-ahead members

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: be2875456d3eb4a0f32bfaf0021cc62a0d57b1c9
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts + content/docs/references/api/package-lifecycle.mdx

Delta review by the same at-tier reviewer as record 6594600155: one commit be287545 on 58810e41, 2 files, +10/−8, both the file-header paragraph (zod lines 41–46) and its rendered copy in the reference mdx.

① Derived judgments

  • Accept set and served surface unchanged: the zod hunk is inside the leading file comment; no describe(...), z.* or export moves. Clause-②: no, Metadata-protocol: none stand.
  • The header's claim is now true on its own head: exactly two live [PLANNED — not yet served] describes remain, PackageExportManifestSchema.withheldMaps (line 248) and PackageExportBundleSchema (line 352), and the paragraph names both. Should-fix 1 of 6594600155 is answered.
  • Nothing else moved: git diff --stat 58810e41 be287545 lists only the two files.

② Semver level

Unchanged: @objectstack/runtime patch, @objectstack/spec patch. Comment text only; no changeset change owed.

③ Boundary flags

④ Design judgment

The zod header now agrees with the file's own markers. The regenerated mdx paragraph is the zod comment verbatim, and the mdx still carries both [PLANNED — not yet served] describes. No must-fix, no should-fix.

Implemented-by: claude/project-thread-f3xzh7
Reviewed-by: session_01JQwxbYyp6wSRcywGFK2L4A

VERDICT: PASS

os-warren pushed a commit that referenced this pull request Nov 3, 2026
…ent export as declared ahead

The nav arm is served by #28722's server slice (#29742); the paragraph now
names PackageExportBundleSchema (#28914) alone. Reference doc regenerated.

Claude-Session: https://claude.ai/code/session_01JQwxbYXRMR5S5B7JSmKkV7
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-warren · session_01JQwxbYyp6wSRcywGFK2L4A

维护者速读

席位核对

ACCEPT · domain:server#3 · head be2875456d3eb4a0f32bfaf0021cc62a0d57b1c9 · 2026-11-03T04:15Z · seat review of record at TIER_DEFAULT for the non-hits files; the packages/spec face reviewed at CONTRACT_REVIEW_TIER (PASS 6594600155 on 58810e41, delta PASS 6595009388 on this head). Not governed: check-governed-merges --pr 29742 NOT governed, 307 lines.

  1. Shape: draft, base main, Fixes #28722 (Wanted 3 is the card's last open item; Wanted 1–2 recorded earlier on the card), Clause-②: no, Metadata-protocol: none; body and changeset agree (@objectstack/runtime patch + @objectstack/spec patch). 7 files, +240/−67.
  2. Premise measured: on main the customizations read listed no nav entry while the fold served contributed items; the flipped spec pins and the new runtime tests were red on main's code (dev report 6594471167), green on this head.
  3. Gates on this head: check-runs pinned to be287545: 65 total, 57 success, 8 skipped, 0 failed (Lint & Repo Gates and TypeScript Type Check success); mergeable_state clean. Dev on this head: 104/104 derived gates exit 0, --ran 0 unrun, check:generated 0, spec 23792 tests passed; on 58810e41: pnpm lint 0, runtime 531 files green.
  4. Prose checked against the diff: the changeset's "Every item … that declares an id, contributed to an app the base ships, is a nav entry" (readBaseContributions skips a non-string or empty id, and an app the base does not ship), "A disabled package's items are not listed … and they come back when it is enabled again" (isPackageDisabled per read; the disable/enable test), "The base's own items and its <base>.custom pair's are not contributions" (codePackageOf drops baseId/pairId), "field and flow entries come first … then nav by app, group (no group first), name and package" (the sort) and "The schema accepts exactly what it accepted before" (comment and describe text only, per the contract review). The two .mdx hunks are generated from the zod text. Each holds.
  5. Out of scope: the Console drawing card belongs to ui seat 4 per the ruling's Execution line ("ui seat 4 files the drawing card") → notified at MERGED. Pre-existing and mirrored, not this PR's: appNavContributions is emptied only by clear() (the read agrees with the fold). The header's inherited "ADR-0049" cite for the declared-ahead pattern is not this PR's claim. Skills-docs: not owed (Metadata-protocol: none).
  6. Landing: not governed, under 3000 lines ⇒ the seat lands it (pr_ready + automerge_enable). Serial: feat(runtime): a package export names each connector map it withheld; install-local names each as incomplete #29746 (packages: a package export marks each withheld connector header/env map, so an import knows it is incomplete (#28640 ruling 3A) #29342) edits the same zod header and mdx; the second lander resolves it to name PackageExportBundleSchema alone. At MERGED: landing record on runtime: a served read naming which package contributes to an installed base — extension fields, flows on its objects, navigation entries — so Studio can draw contributed items greyed with their source (ADR-0029 D7, ADR-0146 D3) #28722 (closed by Fixes), pm:dispatched removed, ui seat 4 notified, card thread released.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 04:17
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 3, 2026 04:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 8d208a4 Nov 3, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-f3xzh7 branch November 3, 2026 04:40
os-warren pushed a commit that referenced this pull request Nov 3, 2026
…he reference after merging main

With #29742 (nav arm), #29755 (whole-deployment export) and this change
(withheldMaps) all served, no member of package-lifecycle.zod.ts is
declared ahead of its producer.

Claude-Session: https://claude.ai/code/session_01JQwxbYXRMR5S5B7JSmKkV7
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant