Skip to content

feat(spec): the customizations read's contributions declare a nav kind, with an entry shape per kind - #29401

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-845vdn
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-845vdn

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #27928

Part of #28722 (its Wanted 3, the server slice that serves the nav kind, stays open).

Ruling-ref: 6566915346
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — a served response member no author writes, and its nav kind is not served yet, so there is nothing to teach; the API docs page names contributions in this diff and the server slice that serves the kind owns the rest

维护者速读

  • 改了什么:spec 里 GET /packages/:id/customizations 的 contributions 加第三种 nav(别的包往本包应用里加的导航项),条目形状按种类分:字段/流程条目照旧带 object,导航条目带 app、可选 group、导航项 id(name)和 packageId。
  • 为什么改:裁决 E(6566915346,「同意」):Studio 要把别的包加进 HotCRM 分组的导航项灰显并标出来源,服务端 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 第三项要先有这份协议。
  • 风险与回滚:纯加宽,今天服务端发的字段/流程条目原样解析;nav 臂标了 [PLANNED — not yet served],服务端还不发;运行时只改了一处本地类型标注(编译所需,行为不变);回滚就是撤掉这个 PR。
  • 先看:packages/spec/src/api/package-lifecycle.zod.ts(contributions 的两臂和排序说明)、packages/spec/src/api/package-lifecycle.test.ts(新测试和 PLANNED 钉)、packages/runtime/src/domains/packages.ts(一行类型收窄)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/package-lifecycle.test.ts,预期 35 个测试全部通过。
  • 需要你做的:无,席位落地。

What changes

The customizations read's contributions member gains the nav kind the ruling binds (letter E): a navigation item another installed package's navigationContributions add to an app the base owns. Declared ahead of its producer (ADR-0049): the runtime lists no nav entry until the server slice of #28722 (Wanted 3) reads the navigation-contribution registry.

  • GetPackageCustomizationsResponseSchema.contributions (packages/spec/src/api/package-lifecycle.zod.ts) is now an array of z.discriminatedUnion('kind', …) with two strict arms:
    • the field / flow arm: kind, object, name, packageId, the same members and describes as before, so it accepts exactly what the member accepted (CONTROL tests); the arm gains a one-line describe;
    • the nav arm: kind: 'nav', app (the base-owned app), an optional group (the group the contribution targets, as declared), name (the nav item's id) and packageId. Its describe opens [PLANNED — not yet served] and names ADR-0049; the tracker ids are in an adjacent // comment only.
  • The member describe names the three kinds, the per-kind shape, and an order: the field and flow entries first, in their existing order (object, kind, name, package), then the nav entries by app, group (an entry with no group first), name and package. counts.contributions still counts items.
  • The schema docblock says "exactly three kinds", spells the per-kind shape, and carries a DECLARED AHEAD OF ITS PRODUCER (ADR-0049) paragraph stating what the producer MUST do when it serves the kind: read the same appNavContributions registry the navigation fold reads, never join by name, list no disabled package's contributions, and add no key to any nav item. The schema-level describe names the third kind as not yet served.
  • Regenerated: content/docs/references/api/package-lifecycle.mdx (the union renders as two nested shapes), and the strictness ledger count docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md (486 to 487, the new strict arm).
  • Docs: content/docs/api/metadata-api.mdx names the contributions list beside overrides, extensions and switched-off items, and says the nav kind is declared and not yet served.
  • Changeset .changeset/27928-customizations-contributions-nav-kind.md: @objectstack/spec minor, with both declaration lines.
  • packages/spec/variant-docs.json: a governed entry for the new union (key kind:nav), bound to content/docs/api/metadata-api.mdx, which names the list and its nav kind. The gate's walker reads only literal arms, so the field / flow arm's z.enum is not in the key.
  • Forced consumer edit: packages/runtime/src/domains/packages.ts readBaseContributions now types its local entries as the contributions entry type with the nav arm excluded (a TypeScript Exclude over { kind: 'nav' }). Without it, pnpm --filter @objectstack/runtime typecheck exits 2 (TS2339 object on the union, twice, and TS7053 on the kindOrder index, twice). Type-only: the producer lists the same field and flow entries, and reading the nav registry stays the server slice's. No runtime changeset: the edit is a type annotation and a comment inside a non-exported function, so no published byte moves.

No liveness row. Measured: packages/spec/liveness/ governs metadata types (its README: the source of truth is the metadata-type registry), and no ledger names this API response or its member (git grep over packages/spec/liveness finds neither). The declared-not-served channel for a wire schema is the describe marker, pinned in the test.

Why

The ruling (6566915346, letter E, the maintainer's 「同意」) serves the contributed navigation entries as a third kind of the landed contributions member, so Studio can draw them greyed with their package (#27356 row 20) from one served read, never a client-side join on names. The server slice needs this declaration first.

Tests

  • packages/spec/src/api/package-lifecycle.test.ts, new: CONTROL, a field and a flow entry parse and are preserved; CONTROL, a field entry still requires object; a nav entry with its group parses and is preserved; a nav entry without a group parses and gains none; a nav entry carrying object is refused (unrecognized_keys); a field entry carrying app and a flow entry carrying group are refused (unrecognized_keys); a nav entry without app, name or packageId is refused (invalid_type at that path); an unknown kind is refused (invalid_union at kind); the nav arm's describe opens the planned marker, names ADR-0049 and carries no tracker id; the member says the kind is not yet served and the field / flow arm carries no marker.
  • No existing test was edited.
Local readings (head `3bdb4133`; the last commit touches only `packages/spec/variant-docs.json`, which no test reads, so the test readings from `b6afae25` stand)
  • Build and regen: pnpm --filter @objectstack/spec build under os-verify-lock, VERDICT command-exit 0. check:generated then exit 1 naming two stale artifacts (check:docs, check:strictness-ledger); check:generated --fix VERDICT 0 regenerated exactly those two, committed with the source. check:generated without fix: exit 0 in the gate run below.
  • pnpm --filter @objectstack/spec test: VERDICT command-exit 0, Test Files 791 passed (791), Tests 23401 passed, 1 todo. src/api/package-lifecycle.test.ts alone: 35 passed (25 existing, 10 new).
  • pnpm --filter @objectstack/spec typecheck: VERDICT command-exit 0 (the test layer compiles; 131 pinned signatures held).
  • Consumer closure: turbo run build --filter='@objectstack/runtime^...' --filter='@objectstack/client^...', VERDICT 0 (36 tasks); then @objectstack/client and @objectstack/driver-turso built.
  • @objectstack/runtime typecheck: exit 2 before the forced edit (TS2339 object twice and TS7053 twice, all in readBaseContributions), exit 0 after it (test layer: 68 pinned signatures held). The red-then-green is also the reverse verification that the consumer reads the rebuilt spec declarations, not a cached copy.
  • @objectstack/runtime src/domains/packages-customizations.test.ts + src/dispatcher-plugin.package-customizations-mount.test.ts: Test Files 2 passed (2), Tests 19 passed.
  • @objectstack/client typecheck: exit 0 (it types the response through GetPackageCustomizationsResponse).
  • ui/packages/app-shell: turbo run build --filter='@object-ui/app-shell^...' VERDICT 0 (64 tasks), then pnpm type-check (both tsc programs) VERDICT 0. The four app-shell files that build a customizations answer (customizationMarks.test.ts, BuilderLanding.customizationCounts-28566.test.tsx, metadataAreaHost.customizations-28568.test.tsx, StudioDesignSurface.oneSurface-28572.test.tsx, run from ui/): Test Files 4 passed (4), Tests 34 passed. No non-test ui source reads contributions today.
  • Scoped lint: pnpm exec eslint --no-inline-config over the three changed .ts files, exit 0, no findings.
  • Ablation, nav-arm strictness: the nav arm's .strict() removed with scripts/ablation-replace.mjs (anchor 1 to 0, blob fd4561e7 to bbc05a90) in a side worktree under a trap: package-lifecycle.test.ts exit 1, 1 failed of 35 (a nav entry carrying object is refused). Restored: blob equal to HEAD fd4561e7, git diff HEAD empty, exit 0, 35 passed.
  • check-changeset-no-major.mjs --base origin/main --event with this body: exit 0, level axis yes (widening) + added. check-partof-closing-keyword.mjs with this body: exit 0.
  • Local-only families: check:skill-frame-freshness exit 0; check:dev-prereqs exit 1 because 8 of 106 workspace packages (ui examples and ui CLI among them) have no dist in this container, a machine-state reading and not a finding of this diff (packages/spec dist is fresh).
  • Main moved 17 commits after the branch point 2f7977cd, none touching this diff's files; not merged in before opening.
  • Dogfood battery: n/a, no ! in the title and no (narrowing) arm.
Gates (dispatch-gates --commands, 105 commands)
  • At the final head 3bdb4133: 105 derived commands, all 105 exit 0 (each with stdin closed, exit captured before any pipe). dispatch-gates --ran: 105 derived, 105 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN.
  • First round at b6afae25: 104 of 105 exit 0; check:variant-docs exit 1 ("undeclared discriminated union" for the new contributions entry), fixed in 3bdb4133 by a governed packages/spec/variant-docs.json entry bound to content/docs/api/metadata-api.mdx.
  • Before the gates: turbo run build --concurrency=2 --filter='./packages/**' under os-verify-lock, VERDICT 0 (78 tasks).
  • CI: the seat reads the PR's check-runs.

Acceptance notes

  • Three spellings the ruling left to this card are in the report's open questions: the order of nav entries, group as declared (named even when the app declares no such group), and one entry per item of the contribution's items (a contributed group's children are not entries of their own).
  • skills/objectstack-platform/SKILL.md describes the customizations read as listing "overrides, extensions and switched-off items" and has not named contributions since that member landed. A governed surface, not touched here; noted for the seat (carrier: the server slice that serves the kind).
  • The pending .changeset/28722-customizations-contributions.md says navigation entries are "not in this list yet", which is still true; it is not edited.
  • The runtime route-ledger note for this route names the fields and flows only, which is still what is served; the server slice extends it.

Verification

See the readings above. The PR stays a draft; the seat runs the contract review.

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 (scoped to the changed files locally; the repo-wide run is CI's).
  • 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

…ith a per-kind entry shape

GetPackageCustomizationsResponseSchema.contributions becomes a discriminated
union on `kind`: the field/flow entry (on an `object`, accepting exactly what
it accepted before) and a nav entry (on an `app`, with an optional `group`,
the nav item id as `name`, and `packageId`). The nav arm is declared ahead of
its producer and says so with a `[PLANNED — not yet served]` marker, pinned
in the test.

Claude-Session: https://claude.ai/code/session_01ADHkmQ28WWiNo3u94yGTPb
Co-authored-by: Claude <noreply@anthropic.com>
…eset, producer type narrowing

The package-lifecycle reference page and the api strictness count follow the
per-kind contributions entry. The metadata API page names the contributions
list and says the nav kind is not yet served. The runtime producer's local
entry type excludes the nav arm so it keeps compiling; it lists the same
field and flow entries as before.

Claude-Session: https://claude.ai/code/session_01ADHkmQ28WWiNo3u94yGTPb
Co-authored-by: Claude <noreply@anthropic.com>
…ledger

check:variant-docs names the new discriminated union of the customizations
read's contributions entry. It is bound to the API docs page that names the
list and its nav kind, as the flow registration verdict (also served, not
authored) is bound to its page.

Claude-Session: https://claude.ai/code/session_01ADHkmQ28WWiNo3u94yGTPb
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 2, 2026
@github-actions

github-actions Bot commented Nov 2, 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. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/variant-docs.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/variant-docs.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 a20dcae3f9beb3ade7a99d2d06a8be01c0f842cb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 05de8c9238d8cf7a43a02a2978ea37f1fda3faa6 — the merge of head 71941530252aa778086e2580e2e105513fe6563f into base a20dcae3f9beb3ade7a99d2d06a8be01c0f842cb, 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 05de8c9238d8cf7a43a02a2978ea37f1fda3faa6 && git checkout 05de8c9238d8cf7a43a02a2978ea37f1fda3faa6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a20dcae3f9beb3ade7a99d2d06a8be01c0f842cb 71941530252aa778086e2580e2e105513fe6563f && git checkout -B drift-repro a20dcae3f9beb3ade7a99d2d06a8be01c0f842cb && git merge --no-ff 71941530252aa778086e2580e2e105513fe6563f

node scripts/docs-audit/affected-docs.mjs --json a20dcae3f9beb3ade7a99d2d06a8be01c0f842cb

⚠️ 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:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract-review-claim: session_01ADHkmSduNGC2SZXszyrNut · head 3bdb413 · 2026-11-02T14:10Z

The lane seat reviews this head itself at CONTRACT_REVIEW_TIER (triage ruling 6575141496 on #28741 returned contract review to the lane): a widening of a served response schema (the customizations read's contributions gains a nav kind, declared ahead of its producer).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3bdb4133a47d965424795f4bd9ef6e6def322ddd
Local-runs: none
Scope: packages/spec/src/api/package-lifecycle.zod.ts + packages/spec/src/api/package-lifecycle.test.ts, packages/runtime/src/domains/packages.ts (readBaseContributions, the first consumer), packages/objectql/src/registry.ts (registerAppNavContribution, applyNavContributions, the registry the arm is declared against), packages/spec/src/ui/app.zod.ts (NavigationContributionSchema, BaseNavItemSchema, SeparatorNavItemSchema), packages/spec/variant-docs.json and packages/spec/scripts/check-variant-docs.mts (the gate that forced the ledger entry), ui/packages/app-shell/src/views/studio-design/packageCustomizations-io.ts and packages/client/src/index.ts (the other readers of the response), docs/adr/0049-no-unenforced-security-properties.md, packages/spec/src/shared/connector-auth.zod.ts (the declared-before-served precedent), packages/spec/liveness/README.md and the liveness ledgers, .changeset/28722-customizations-contributions.md and PR #28749's declaration lines (the precedent for this member)

Inputs read: card #27928 body and all eleven comments (ruling 6566915346 letter E; os-dev-report 6579232649), PR #29401 body, file list and net diff against the merge base 2f7977cd (main moved after the branch point but touched none of the diff's files), and the check-runs on the head as polled at review time. The rest of the diff (changeset, metadata-api.mdx, the regenerated reference page, the regenerated strictness count, the runtime type edit) is the lane seat's ACCEPT at TIER_DEFAULT; it was read for the claims the hits file makes, not reviewed.

① Derived judgments

  • Accept set, GetPackageCustomizationsResponseSchema.contributions[]: widened, nothing narrowed. On main the entry is one z.object({ kind: z.enum(['field','flow']), object, name, packageId }).strict(); on the head it is z.discriminatedUnion('kind', [that same strict shape, a strict nav shape]). Every body the old entry accepted is accepted by the first arm with the same members and describes; the CONTROL tests pin a field and a flow entry parse and are preserved and a field entry still requires object. A body with kind: 'nav' was refused and is now accepted. Right: Clause-②: yes (widening).
  • Per-kind strictness. Both arms are .strict(): a nav entry carrying object, a field entry carrying app, a flow entry carrying group are refused with unrecognized_keys; a nav entry missing app, name or packageId is refused with invalid_type at that path; an unknown kind is refused with invalid_union at kind. Each pinned. The strictness ledger count api/ 486 to 487 is the one new strict site. Right.
  • The nav arm's members against the registry it is declared on. registerAppNavContribution records { packageId, group?, priority, items } keyed by contribution.app (registry.ts:8119-8132); applyNavContributions skips a disabled package's contributions before the group lookup and appends to the group's children, or to the top level when the group is missing (8155-8197). The arm's app, optional group ("as the contribution declares it", named even when the app lacks the group), name (the item's id) and packageId are each a recorded fact of that registry; nothing is derived by a name join. The describe's MUSTs (same registry as the fold, no disabled package, no key on any nav item) restate ruling E. Right, with one gap in ④ (an id-less separator).
  • Order. Field and flow entries first by object, kind, name, package (exactly today's readBaseContributions sort, unchanged); then nav entries by app, group (none first), name, package. Deterministic without reading priority, which the entry does not carry; today's served order is unchanged. Right.
  • Public TS surface. GetPackageCustomizationsResponse['contributions'][number] becomes a union, so a reader of .object must narrow on kind first. In-repo readers: readBaseContributions (fixed with an Exclude over { kind: 'nav' }, type-only, the function is not exported, emitted JS and .d.ts unchanged); packages/client types the response and reads no member; ui/.../packageCustomizations-io.ts parses the whole answer and reads only counts; the four app-shell tests that build an answer set contributions: []. No ui/** consumer narrows on contributions[].object (asked by the brief; measured on the head, git grep over ui/). The changeset states the narrowing rule for external TS readers. Right. No @objectstack/runtime changeset is owed: no behaviour, JS or .d.ts byte moves (the source-map line shift the dev reports is not a published contract).
  • No key on any nav item; BaseNavItemSchema stays strict; no navContributors map. Confirmed in the diff (no app.zod.ts or protocol.zod.ts hunk). Right, per ruling E's third bullet.
  • counts.contributions unchanged (contributions.length). Right.
  • Generated artifacts. content/docs/references/api/package-lifecycle.mdx renders the union as two nested shapes; the strictness count regenerated; Lint & Repo Gates, Spec property liveness, Check Changeset, Type Check · consumer gates and Type Check · source gates are success on the head. Right.
  • Governed paths (asked by the brief). governedPathsIn over the PR's file list returns no row: packages/spec/variant-docs.json and docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md are not in GOVERNED_SURFACES (Tier H is docs/adr/**, .claude/**, skills/**, docs/NORTH-STAR.md, .github/workflows/**, AGENTS.md, CLAUDE.md), and Governed Surface Queue Guard is success. No separate approval is needed; the schema face alone makes the review owed, and this record is it.

② Semver level

  • Changeset .changeset/27928-customizations-contributions-nav-kind.md: @objectstack/spec: minor, carrying Clause-②: yes (widening) and Metadata-protocol: added, both line-initial; the PR body carries the same pair plus Skills-docs: none — REASON. Check Changeset and PR-body guards are success. Right.
  • Clause-②: yes (widening) matches ① (a new accepted arm, nothing refused that was accepted). Right.
  • Metadata-protocol: added is what ruling E binds verbatim and what the precedent PR feat(runtime): the per-base customizations read names the package behind each field and flow another package contributes #28749 declared for the same member (.changeset/28722-customizations-contributions.md: minor, yes (widening), added). Read against AGENTS.md's definition alone (a change to what a metadata author can WRITE) a served-only response member would read none; the ruling and the landed precedent decide it for this member, so the declaration and the minor level are right by ruling and precedent, recorded here so the pair is not re-litigated on the server slice. Nothing to escalate.
  • Only @objectstack/spec moves a published byte; the runtime edit is type-only inside a non-exported function. Right: one changeset.

③ Boundary flags

Dev report 6579232649, four open_questions, each answered:

  1. Order of nav entries — A (in the PR). Right. It keeps today's served order byte-for-byte, is deterministic from the entry's own members, and is spelled in the member describe. B would reorder served field/flow entries (a change for no reader); C needs priority, which the entry does not carry.
  2. group when the app lacks the group — A, as declared. Right. It is the registry's recorded fact (c.group), matches ruling E's wording ("the group when the contribution targets one"), and the describe says the served navigation appends those items at the top level anyway; Studio finds the item by its id.
  3. One entry per item of the contribution's items; a contributed group's children ride inside it — A. Right for what it decides (ruling E: one entry per item, the nav item's id as name; counts.contributions counts items). It leaves one case unspelled, ④ should-fix 1.
  4. Do app and group need the maintainer's approval again — A, no. Right: ruling 6566915346 (the maintainer's 「同意」) names app, group, name and packageId itself and says the per-kind shape is "spelled by the spec card". Not escalated.

Deviations (six), each answered:

  • No liveness row. Accepted. packages/spec/liveness/ roots on registered metadata kinds plus the SPEC_ONLY_SCHEMAS overrides (README; api.json governs the api endpoint metadata type, metadata_endpoints.json the REST server's construction argument); no ledger roots a package-lifecycle response, so there is no row to add and Spec property liveness is green. The declared-before-served channel for a wire schema is the describe marker pinned in a test, the same channel connector-auth.zod.ts clientSource ([PLANNED — not enforced]) and app.zod.ts nav dataSource ([PLANNED — not yet rendered]) use. The ruling's "liveness row" was a workload line, not a binding.
  • packages/spec/variant-docs.json outside the claim's file surface. Accepted. check:variant-docs reds on an undeclared discriminated union, so the entry is gate-forced; the path is not governed (①); the entry is GOVERNED rather than exempt and bound to content/docs/api/metadata-api.mdx, which names the list and its nav kind in the prose form the gate reads — the same disposition as the flow-registration-verdict entry. Its note records honestly that the key is kind:nav because the walker does not read a z.enum discriminator arm (see the out-of-scope item below).
  • Forced consumer edit in packages/runtime/src/domains/packages.ts. Accepted: the claim allows a narrowing-guard edit a typecheck forces; it is a local type alias plus a two-line comment, the producer lists the same field and flow entries and reads no nav registry (that stays 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 Wanted 3). No runtime changeset owed (①).
  • Main moved 17 commits after the branch point, not merged in. Accepted: git log 2f7977cd..origin/main over the diff's files is empty; CI runs on the merge ref.
  • No assignee, reviewer or PR-activity subscription. Accepted; the standing order forbids those writes; the seat reads CI.
  • Skills-docs: none — REASON. Accepted as to this diff (the nav kind is not served, so there is nothing to teach yet) and identical in form to PR feat(runtime): the per-base customizations read names the package behind each field and flow another package contributes #28749's line. The pre-existing gap it names — skills/objectstack-platform/SKILL.md describes the read as "overrides, extensions and switched-off items" and has not named contributions since feat(runtime): the per-base customizations read names the package behind each field and flow another package contributes #28749 — has now been "noted, not filed" by two PRs in a row. Escalated to the seat (not the maintainer): file the follow-up card now (skills/** is Tier H, so it is its own PR either way) rather than carry it to the server slice as a note a third time. Not a FAIL: it is outside this diff and Prime Directive chore: version packages #10 lets a docs gap ride as an acceptance note.

④ Design judgment

packages/spec/src/api/package-lifecycle.zod.ts, judged against ruling 6566915346 letter E, ADR-0029 D7 as amended by ADR-0146 D3, the registry the arm is declared on, and its first consumer readBaseContributions.

Holds. The union implements the ruling's four bullets exactly (①). The design was tried against: a field/flow body from today's runtime (accepted, preserved, CONTROL pins); a nav entry with and without group (accepted; no group invented); cross-kind key leakage in both directions (refused by strictness); a fourth kind (refused at kind); the first consumer's compile (the Exclude narrowing is sound: the field/flow arm does not extend { kind: 'nav' }, the nav arm does; the returned Contribution[] is assignable to the declared union array). The variant-docs entry and the hand-written doc hold the gate's prose form (`nav` on metadata-api.mdx).

The [PLANNED — not yet served] marking and its pin (asked by the brief). The marker follows the repo's declared-before-served convention ([PLANNED — not enforced] on clientSource, [PLANNED — not yet rendered] on nav dataSource and the record blocks), opens the arm's describe, names ADR-0049 in the text and carries no tracker id (the issue numbers live in the adjacent // comment and the docblock only, which check:doc-authoring allows). The pin in package-lifecycle.test.ts asserts the regex ^\[PLANNED — not yet served\] , the ADR id, no #NNN, that the member describe says "not yet served", and that the field/flow arm carries no PLANNED. The server slice drops the marker and the pin reds until it does — the right direction for a pin. No gate machine-reads [PLANNED (grep over scripts/**, packages/spec/scripts, packages/lint, .github/workflows), so the test is the whole enforcement, as it is for the two precedents. ADR-0049 is cited for its enforce-or-remove discipline rather than its security scope; the ADR's own amendment says the repo cites it "as the enforce-or-remove policy for spec-property retirement generally", and connector-auth.zod.ts already uses it the same way. Judged sound; one tightening below.

Should-fix 1 — an id-less contributed item has no name. NavigationContributionSchema.items is z.array(NavigationItemSchema) (app.zod.ts:1214), and NavigationItemSchema includes SeparatorNavItemSchema (app.zod.ts:1158), whose id is optional (app.zod.ts:844). The nav arm requires name: z.string() as "the nav item's id, as the contribution's items declare it", so a contributed separator without an id has no value the producer can serve and the spec does not say what happens to it. Nothing is served yet and nothing in the ruling's scenario (Studio greys an entry and labels its package) wants a separator listed, so this is not a FAIL, but a spec declared ahead of its producer is exactly where the rule belongs. Fix in this PR or before the server slice starts: one sentence on the nav arm's describe (an item with no id, a separator, is not an entry) and one pin in the test. Owner: the dev on this branch, or the seat as a line in #28722 Wanted 3's order.

Should-fix 2 — pin the schema-level "(not yet served)" too. The not-yet-served fact is written in three describes (the nav arm, the member, the schema's own .describe) plus the docblock, metadata-api.mdx and the variant-docs note. The test pins the first two only; the schema-level describe ("fields, flows and (not yet served) navigation items") can survive the server slice unnoticed and the regenerated reference page would then say the kind is unserved while it is served. One expect(schema.description).toContain('not yet served') beside the member pin closes it. Cheap; same PR or the server slice.

Out of scope, for the seat. check-variant-docs.mts reads a discriminator arm's variants as dd.values (literal) and never dd.entries (a zod 4 z.enum), so the ledger key for this union is kind:nav and would not change if field/flow grew or shrank — the ratchet the script's header promises ("adding or removing a variant CHANGES THE KEY") does not hold for enum arms. Pre-existing tooling gap, surfaced by this entry's honest note; a domain:spec tooling card, not this PR's.

Check-runs at review time. Lint & Repo Gates, Governed Surface Queue Guard, Live E2E UI, Spec property liveness, Check Changeset, PR-body guards, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Build UI Deps, Build Docs, Lint · ESLint and the scoped lints: success. Build Core, Test Core (1-8), Type Check · workspace (the job that carries check:generated, check:docs, check:variant-docs), Type Check UI, Test UI (1-16), Build & E2E UI: in_progress. This verdict is on the tree; in_progress is not a pass (AGENTS.md §7), so the seat lands only once every required context reads success.

Implemented-by: claude/project-thread-845vdn
Reviewed-by: session_01ADHkmSduNGC2SZXszyrNut

VERDICT: PASS

… schema describe's not-yet-served

Contract-review should-fixes: a contributed separator may omit its id, so
it has no value for the nav entry's required name and is not an entry; the
nav arm's describe says so and a test pins it. The schema-level describe's
"(not yet served)" is pinned beside the member pin, so the slice that
serves the kind cannot leave it behind.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 71941530252aa778086e2580e2e105513fe6563f
Local-runs: none

This is a DELTA record for the push after the PASS record 6579472483 (head 3bdb4133). The delta 3bdb4133...71941530 is one commit across 3 files, and it makes both should-fixes as asked. (1) The nav arm's describe in package-lifecycle.zod.ts gains one sentence: "An item that declares no id (a separator may omit it) has no name to give and is not an entry." A test pins that sentence and pins that a nav entry without name is refused at the entry schema. (2) A test pins the schema-level describe's "not yet served", beside the member pin, so the server slice (#28722 Wanted 3) has to drop all three markers together. The generated reference page carries the one changed describe line. No arm, member, ordering rule, changeset or consumer edit moved, so every judgment in 6579472483 (Clause-②: yes (widening), Metadata-protocol: added, spec minor) carries to this head unchanged.

Check-runs on 71941530: ci-failure.mjs GREEN, 65 check-runs, none failed.

Implemented-by: claude/project-thread-845vdn
Reviewed-by: session_01ADHkmSduNGC2SZXszyrNut

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Head-sha: 71941530252aa778086e2580e2e105513fe6563f · read 2026-11-02T15:04Z

维护者速读

  • 改了什么:spec 里 GET /packages/:id/customizations 的 contributions 加了第三种 nav,即别的包往本包应用里加的导航项。条目形状按种类分:字段/流程条目照旧带 object,导航条目带 app、可选的 group、导航项 id(name)和 packageId。没有 id 的导航项(例如分隔线)不算条目。
  • 为什么改:裁决 E(6566915346,「同意」)要求 Studio 把别的包加进 HotCRM 分组的导航项灰显并标出来源,服务端 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 第三项需要先有这份协议。
  • 风险与回滚:纯加宽,今天服务端发的字段/流程条目原样解析。nav 臂标了 [PLANNED — not yet served],服务端还不发。运行时只改了一处本地类型标注(编译所需,行为不变)。回滚就是撤掉这个 PR。
  • 先看:packages/spec/src/api/package-lifecycle.zod.ts(contributions 的两臂和排序说明)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/package-lifecycle.test.ts,预期全部通过。
  • 需要你做的:无,席位落地。

席位意见:可落地。契约复审在 3bdb413 判 PASS(6579472483),在现 head 的 delta 判 PASS,CI 全绿。

席位核对 — ACCEPT · domain:spec#2 · head 71941530

  1. Shape: draft, base main. The body carries Fixes #27928, Part of #28722, Ruling-ref: 6566915346, and at line start Clause-②: yes (widening) and Metadata-protocol: added; the changeset is @objectstack/spec minor. There are 8 files, +239/−31, inside claim 6577096435. Two files are gate-forced and declared: the variant-docs.json entry and the api strictness count. The runtime edit is a forced, type-only narrowing that the claim allows. No governed path (governedPathsIn returns no row, and Queue Guard is green).
  2. Premise measured: on main, contributions[].kind is z.enum(['field','flow']), so a nav entry is refused. On the head the discriminated union accepts a strict nav entry with and without group and refuses cross-kind keys in both directions, and the CONTROL pins keep field and flow entries byte-identical.
  3. Gates: ci-failure.mjs --sha 71941530… GREEN: 65 check-runs, none failed. The earlier Test UI red on 3bdb4133 came from shards that the push of 71941530 cancelled.
  4. Accepted deviations: no liveness row (the ledgers are per metadata type and no ledger roots this response); the variant-docs entry, keyed kind:nav (the walker gap is filed as spec tooling(check:variant-docs): the walker reads only literal discriminator arms, never a zod 4 z.enum arm, so a discriminatedUnion arm keyed by z.enum([...]) is ledgered under a key that does not move when its enum members change #29412); the runtime's index.js.map shift of two lines, while the JS and .d.ts are unchanged.
  5. Out of scope: the skills/objectstack-platform gap on contributions is filed as skills(objectstack-platform): the package customizations read is taught as "overrides, extensions and switched-off items"; its contributions member (field and flow since #28749, nav declared by #27928) is never named #29411. The variant-docs walker's blindness to z.enum arms is filed as spec tooling(check:variant-docs): the walker reads only literal discriminator arms, never a zod 4 z.enum arm, so a discriminatedUnion arm keyed by z.enum([...]) is ledgered under a key that does not move when its enum members change #29412. The producer is 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 Wanted 3, server seat 3's work, which flips the three [PLANNED] markers that the tests now pin. Skills-docs: none holds for this diff: the nav kind is not served yet.
  6. Landing: not Tier H and under 3000 lines, so the seat lands it with pr_ready and auto-merge. After merge, the seat writes a landing note on The served app navigation names the package that contributed each folded entry (#27356 row 20) #27928, removes pm:dispatched, and tells server seat 3 to take 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 Wanted 3.

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

Development

Successfully merging this pull request may close these issues.

The served app navigation names the package that contributed each folded entry (#27356 row 20)

1 participant