Skip to content

feat(spec)!: strict defineStack judges a packages[] stack's namespace prefixes per body and its top level by the residual rule - #33122

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

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3l8pov

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #33069

Clause-②: yes (narrowing) — one face widens and one narrows. Widened: a stack carrying packages[] whose top level repeats objects that package bodies of different namespaces declare (an additive artifact, such as resolveArtifactCollections output) is accepted by strict defineStack, where it was refused. Narrowed: a packages[] body whose object its own body's namespace does not prefix is refused, where it was accepted when the stack's top level held no copy of it or a copy the manifest's namespace prefixes.
Metadata-protocol: narrowed
Skills-docs: none — the taught rule (every object.name carries its own package's namespace prefix) is unchanged, and no skill or docs page teaches handing a packages[] stack to defineStack; what moves is which namespace strict defineStack judges each object of such a stack against.

维护者速读

  • 改了什么:严格模式 defineStack 判带 packages[] 的栈的命名空间前缀时,按加载路径的登记方式判:每个包体的对象对照该包体自己的命名空间;没有包体声明的顶层对象(残余)才对照栈 manifest 的命名空间。判残余用的是 feat(spec)!: strict defineStack judges a packages[] stack per body and counts only the top-level apps no body declares against its manifest #33074 已有的同一份 isResidualTopLevelItem,没有第二份实现。
  • 为什么改:原来把顶层合并后的所有对象都拿去比 manifest 的一个命名空间,多包、多命名空间的组合制品(resolveArtifactCollections 的输出)交回 defineStack 会被 STACK_NAMESPACE_PREFIX_INVALID 误拒;而包体里前缀写错的对象反而没人判。
  • 风险与回滚:一放一收(Clause-②: yes (narrowing))。收的一面只影响手搭的或 strict: false 构建的包体(经严格 defineStack 产出的包体早已按自己的命名空间判过)。实测本仓与 HotCRM 没有产品代码把带 packages[] 的栈交给严格 defineStack。不带 packages[] 的栈判法与报错原文逐字不变。回滚即 revert。
  • 先看:packages/spec/src/stack.zod.ts 的 namespacePrefixErrors(新函数,validateNamespacePrefix 改为调用它);packages/spec/src/stack.test.ts 新 describe;packages/runtime/src/artifact-collections.public-reader.test.ts 新 describe。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/stack.test.ts -t "namespace prefixes are judged per body",预期 4 条全绿,其中一条是「两个命名空间的组合制品被接受」,一条是「残余对象前缀不对被拒(422 STACK_NAMESPACE_PREFIX_INVALID)」。
  • 需要你做的:无,席位落地。

What changes

validateNamespacePrefix (the strict defineStack pass, packages/spec/src/stack.zod.ts) now reads its prefix findings from a new namespacePrefixErrors, which judges each object against the namespace of the package the load path registers it under:

  • No packages[]: unchanged. Every top-level object is judged against manifest.namespace, and the finding is validateObjectNamespacePrefix's own sentence, byte for byte.
  • packages[] (ADR-0130 D4): each body's objects are judged against that body's own namespace, the finding prefixed Package 'ID' (packages[J].manifest.objects[I]): . A top-level object that no body declares is the residual, registered under manifest.id by the boot's residual rule (unclaimedTopLevel, @objectstack/metadata), and is judged against manifest.namespace, prefixed Top-level objects[I], which no package body declares, is registered under package 'ID': . A top-level object a body declares is a copy of that body's object and is judged with the body only.
  • Shared reading, not a second one: which top-level object is residual is isResidualTopLevelItem(bodies), the mirror of unclaimedTopLevel that the owner-stamp gate and the single-app judge (feat(spec)!: strict defineStack judges a packages[] stack per body and counts only the top-level apps no body declares against its manifest #33074) already read. packages/metadata is read, not changed.
  • A package with no namespace is not judged (the rule's legacy skip); the namespace/object-name clash half of validateNamespacePrefix (ADR-0150 D6) is untouched.

Example finding (the residual pin): "Top-level objects[2], which no package body declares, is registered under package 'com.example.svc': Object 'zzz_orphan' is missing the package namespace prefix. Rename it to 'svc_zzz_orphan' (namespace = 'svc')."

ADR-0087. New semantic entry define-stack-packages-body-namespace-prefix-refused (packages/spec/src/migrations/entries/semantic/18.define-stack-packages-body-namespace-prefix-refused.ts), no D2 conversion (an object's name is its table and API name, only the author renames it). The changeset registers it.

Changeset. .changeset/33069-define-stack-packages-namespace.md, @objectstack/spec minor, both declaration lines, BREAKING banner with Before / Now / Unchanged / Fix.

No error code added: the refusal is the existing STACK_NAMESPACE_PREFIX_INVALID (422).

Tests

New top-level describe in packages/spec/src/stack.test.ts ("defineStack — a packages[] stack's namespace prefixes are judged per body, and its top level by the residual rule"), two packages of namespaces crm and svc composed under manifest: 'preserve':

  1. accepts the additive artifact (the reader's shape: both bodies' objects on the top level) under the svc manifest, with anti-vacuity (crm_account at the top level);
  2. refuses a residual top-level object no body's namespace and not the manifest's prefixes: code STACK_NAMESPACE_PREFIX_INVALID, status 422, issues exactly the located finding; a residual the manifest's namespace prefixes is accepted;
  3. refuses a body object its own body's namespace does not prefix (though another body's and the manifest's would), located at the body;
  4. CONTROL: a stack without packages[] gives the exact finding and the exact message (header, line, hint) it gave on main.

New top-level describe in packages/runtime/src/artifact-collections.public-reader.test.ts: defineStack(resolveArtifactCollections(composeStacks([crm app, svc module], { manifest: 'preserve' }))) passes, with anti-vacuity (both objects at the top level, manifest namespace svc).

Measured on origin/main 2c98485d (which carries #33074; the order's 64551047 is its parent and lacks the shared residual reading) before the fix, with a probe through the real reader: the round trip was refused STACK_NAMESPACE_PREFIX_INVALID (Object 'crm_account' is missing the package namespace prefix … (namespace = 'svc')); unclaimedTopLevel of the same artifact answered an empty residual (every top-level object is claimed by a body, so none is registered from the top level); a body object crm_stray in the svc body with no top-level copy was accepted.

Ablations (fix committed first at `1dbbe63d`, mutated through `scripts/ablation-replace.mjs`, restored to the HEAD blob)

Each mutation landed (anchor 1 → 0, blob bf975a2fe1a7 → a new blob) and each restore read blob == HEAD (bf975a2fe1a7) and git diff HEAD is empty.

# Mutation in stack.zod.ts Expected Observed
1 the residual filter dropped (every top-level object judged against the manifest, the old reading) additive pin red, runtime round-trip pin red spec: 2 failed (additive, and residual, which now also reports crm_account); runtime: 1 failed (round trip)
2 body objects not judged body pin red 1 failed (body)
3 residual judged never residual pin red 1 failed (residual)
main stack.zod.ts restored to origin/main 2c98485d (blob f5dc7819, checked against git rev-parse origin/main:…), tests unchanged additive, residual, body pins red; CONTROL green spec: 3 failed, control passed (the no-packages[] finding and message are byte-identical to main); runtime: round trip failed

Both suites resolve @objectstack/spec to src/ (stack.test.ts imports ./stack.zod; packages/runtime/vitest.config.ts aliases the package to src), so no dist rebuild was owed per leg.

Acceptance notes

  • Delta, measured (order step 1). Newly accepted: a packages[] stack whose top-level objects are copies of body objects prefixed by their own bodies' namespaces but not by the manifest's (the reader round trip of a multi-namespace suite). Newly refused: a packages[] body object its own body's namespace does not prefix, when the stack's top level holds no copy of it, holds a copy the manifest's namespace prefixes, or the manifest declares no namespace. Unchanged: every stack without packages[]; a residual top-level object is judged against the manifest's namespace exactly as before, now with a locator in front of the same sentence. A copy of a body object that the manifest's namespace does NOT prefix but its own body's does now passes; a mis-prefixed body object that used to be caught through its top-level copy is still refused, located at the body instead of the top level.
  • Producers: no first-party production code hands a packages[] stack to strict defineStack. HotCRM (4f0ffd8, read-only) builds each package with strict defineStack (all five declare namespace: 'crm') and reads the composed suite through resolveArtifactCollections in test/helpers/composed-stack.ts without re-judging it. The consumer suites below are the empirical half.
  • The defineStack hint after the findings still reads "Every object.name must be ${manifest.namespace}_${shortName}"; on a packages[] stack the namespace is the body's, which each located finding names. Left as is so the no-packages[] message stays byte-identical.
  • Ruling-surface grep (ruling|Ruling-ref|maintainer|⛔): stack.zod.ts 68 hits file-wide, none inside the edited region (validateNamespacePrefix and the new namespacePrefixErrors, :2540–2670); stack.test.ts 2 hits (:1846, :1855, the retired type: page block), untouched; artifact-collections.public-reader.test.ts none. No ruling sits on the edited lines.
  • ADR-0019 (governed, not edited). Its Status line's "Landed in spec" list is its enforcement log; after this lands it could name that strict defineStack judges a packages[] suite's namespace prefixes per package body (D4: an app owns a set of namespaces), so a suite whose packages declare different namespaces round-trips through resolveArtifactCollections, with this PR and its merge sha. That is the landing seat's card to file, not this PR's.
  • Serial: no open PR edits stack.zod.ts (search of open PRs naming it: 0; the two open spec-touching PRs, feat(objectql, core, metadata)!: tool names held per namespace, and the metadata service keeps each package's tool in its own (package, name) slot (ADR-0150 child B) #32895 and docs(spec): a hook's published body description says a hook body cannot send mail #33110, list no stack.zod.ts in their files).
  • Contract-review nits (record 6858193093, PASS at da607a7f), noted and not pushed for:
    • N1 · the declaration line lists two before-states of the newly refused body object; a third is missing (the stack manifest declares no namespace). The changeset's Before bullet and the delta note above cover it.
    • N2 · "its own body's namespace does not prefix" names one arm: validateObjectNamespacePrefix also refuses the legacy __ form, so a body object such as crm_a__b is newly refused too. Same rule, code and status.
    • N3 · pin gaps: N1's third state, N2's arm, a namespace-less body's copy, packages: [], and a multi-namespace suite with apps through the reader are not pinned. Pin 2's "refuses" half does not discriminate by verdict (the old rule refuses that stack too).
    • N4 · namespacePrefixErrors re-spells the two-line entry.manifest walk the clash half repeats, and labels packages through singleAppPackageLabel, a helper named for the other rule.
    • N5 · on a body finding the hint still reads "Every object.name must be ${manifest.namespace}_${shortName}" while the body's namespace is the one judged; left so the no-packages[] message stays byte-identical (the hint note above).

Verification

Readings at head da607a7f (after merging origin/main e997113c: #33110, #33080, #33078, none on this diff's files) unless named, after pnpm install --frozen-lockfile and pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' --filter @objectstack/cli build, every heavy command under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 on each unless named), exit codes captured before any pipe:

  • pnpm --filter @objectstack/spec check:generated: all 14 generated artefacts up to date. (At 1dbbe63d it named one test-typecheck-debt.json arrival, a TS2488 on the new body pin's spread; fixed in 6112dbb9, not ledgered.)
  • pnpm --filter @objectstack/spec test: Test Files 840 passed (840), Tests 26832 passed, 1 todo. typecheck (at 6112dbb9): exit 0.
  • Consumers of the changed schema door (run by hand; dispatch-gates does not derive them), at 6112dbb9, the merge touching none of their sources:
    • @objectstack/metadata test (the unclaimedTopLevel load path): Test Files 76 passed (76), Tests 2204 passed.
    • @objectstack/metadata-protocol test: Test Files 337 passed, 7 skipped (344); Tests 22799 passed, 36 skipped.
    • @objectstack/core test: Test Files 146 passed (146), Tests 3369 passed.
    • @objectstack/cli unit (vitest run --project unit): Test Files 414 passed of 416; the other two (published-subpath-console.pin, published-subpath-hook-body.pin) refused for an unbuilt packages/cli/dist (PREREQUISITE, the pins' own guard), and after building the cli they passed at da607a7f (2 files, 29 tests). The integration layer is CI's (no spawn entry or integration file in the diff).
    • @objectstack/runtime artifact-collections.test.ts + artifact-collections.public-reader.test.ts: Test Files 2 passed (2), Tests 26 passed; runtime typecheck: exit 0.
  • Full dogfood battery (Clause-② carries a (narrowing) arm): pnpm --filter @objectstack/dogfood test at da607a7f, lock VERDICT command-exit 0, Test Files 460 passed, 3 skipped (463); Tests 3874 passed, 42 skipped (3916); no failure (finished after this PR opened; also in the os-dev report, comment 6857790505).
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 91 families (same list at 6112dbb9 and da607a7f), each run with stdin from /dev/null. 85 exited 0 on the first pass; six read built output while a closure build was rewriting it (check:api-surface, check:dual-source-exports, check:entry-nameability, check:exported-any exit 3 PREREQUISITE; check:dts-closure exit 1 naming two packages mid-build; check:dual-build-cjs-loads exit 3) and all six exited 0 when rerun after the build settled. node scripts/check-plugin-teardown-shape.mjs --self-test exit 0 after git fetch origin 621a487607881c66b2899b7e3477115229a156b4 --depth=1. Reconciliation --ran with exit codes: 91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED (a DERIVED zero — all 91 recorded an exit code and none of them is 3).
  • CI: not yet run at opening; the contract review (6858193093) read 52 success, 11 skipped, 3 Test Core shards in progress, no failure at 15:59Z.

Generated by Claude Code

… prefixes per body and its top level by the residual rule

A packages[] stack is judged the way the load path registers its objects:
each package body's objects against that body's own namespace, and a
top-level object no body declares against the manifest's. A top-level copy
of a body's object is judged with that body only, so the reader round trip
of a suite whose packages declare different namespaces is accepted. A stack
without packages[] is judged unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxbMr8S6rSezDVowtd
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 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
  • 1 anchor(s) matched too much of the corpus to be a work list: /api/v1/data (route, 50 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 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 — 171 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 7c36f5240f506e8feed86272a16bd98eaeebaca8 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7c36f5240f506e8feed86272a16bd98eaeebaca8

⚠️ 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-tesla · session_01L9qibvfX5bawZKtfa6u7j9

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: da607a7fae1ced4ade70f575d65fc54d35977030
Local-runs: probe — the seat asked for the card's arm reproduced at merge-base and head and for the consumer suites of this schema (dispatch-gates misses them); run on a git archive export of the head tree, repo checkout untouched (git status empty), after a filtered pnpm install --frozen-lockfile and pnpm turbo run build --concurrency=2 --filter='@objectstack/cli...' --filter='@objectstack/runtime...' (64/64 tasks). Runs:

  • spec · vitest run --project local --maxWorkers=2 src/stack src/compose src/migrations src/api/error-code-ledger: 57 files, 1683 tests passed.
  • runtime · the nine test files that pair defineStack with a packages[] producer, artifact-collections.test.ts and artifact-collections.public-reader.test.ts among them: 9 files, 229 passed.
  • metadata · src/owner-stamp-residual-build-boot.test.ts (holds the residual mirror equal to unclaimedTopLevel), src/plugin-artifact-packages-attribution.test.ts, src/plugin-artifact-forward-conversion-retired-after.test.ts: 3 files, 43 passed.
  • metadata-protocol · src/protocol.install-manifest-id.test.ts: 19 passed. core · src/capability-composition.test.ts: 13 passed.
  • cli · unit project, the 15 unit files that pair defineStack with a packages[] producer (test/migrate-meta-composed.test.ts, src/utils/artifact-packages.test.ts among them): 202 passed. Integration project, 5 files (serve-config-boot-residual.integration, validate-per-package-authoring-parity, lint-per-package-authoring-parity, package-union-readers, union-fold-command-parity): 39 passed. The four e2e files and seven other integration files did not run.
  • Three throwaway probes, 54 shapes, each judged by head source and by the merge-base stack.zod.ts (blob f5dc7819) in one process; the judgments in ① cite them. Probe files removed afterwards.
  • New pins run against merge-base source: spec additive, residual and body pins red, control green (101 of 104 in stack.test.ts green); runtime round trip red. Head blob bf975a2f restored before the suites above.
    Scope: packages/spec/src/stack.zod.ts, packages/spec/src/migrations/entries/semantic/18.define-stack-packages-body-namespace-prefix-refused.ts (hits); packages/spec/src/stack.test.ts, packages/runtime/src/artifact-collections.public-reader.test.ts; the changeset and PR body (prose, sentence by sentence) + read at head: packages/metadata/src/plugin.ts (unclaimedTopLevel, claimedArtifactSlots, artifactItemSlot, ARTIFACT_FIELD_TO_TYPE, the artifact door's per-body loop), packages/objectql/src/plugin.ts (register, registerUnclaimedTopLevelObjects), packages/objectql/src/engine-registration.ts (registerPackageBody), packages/objectql/src/namespace-co-owner-names.ts (residualNamespacePackageOf), packages/core/src/artifact-packages.ts, packages/spec/src/kernel/namespace-prefix.ts, packages/spec/src/kernel/collection-item-owner-stamp.ts, packages/runtime/src/artifact-collections.ts, .changeset/32978-…, docs/adr/0019-app-as-consumer-unit.md (Status line), HotCRM 0009f58 (objectstack.config.ts, src/*/index.ts, test/helpers/composed-stack.ts)

Check-runs at review time (2026-11-13T15:59Z): 52 success, 11 skipped, 3 in progress (Test Core 1/8, 3/8, 8/8), no failure; Check Changeset and both Type Check gates success. Test Core is not yet a full verdict. main moved e997113c → 7c36f524 with no change to any path of this diff.

① Derived judgments

  • Card's arm, reproduced and fixed: defineStack(resolveArtifactCollections(composeStacks([crm app, svc module], { manifest: 'preserve' }))) is refused STACK_NAMESPACE_PREFIX_INVALID 422 at merge-base and accepted at head. Same for the reversed order, three namespaces, a second round trip, and a full suite of three packages that each carry an app, flow, hook and translation. Right.
  • The card's other producer, measured: a plain two-manifest preserve output has no top-level objects and was accepted at both. A preserve composition that stays additive (a manifest-less input beside two packages) was refused at merge-base for the bodies' copies and is accepted at head, the manifest-less objects judged as the residual. Right.
  • Narrowing arm, reproduced: a body object that fails under its own body's namespace was accepted at merge-base in three states (no top-level copy; a copy the manifest's namespace prefixes; a stack manifest with no namespace) and is refused at head, located at the body. Same for the manifest-id body, a strict: false body through composeStacks, and plugin-typed manifests. Right.
  • Whole delta (N = manifest.namespace): merge-base refuses ⇔ N is set and a top-level object fails under N. Head refuses ⇔ a namespaced body holds an object that fails under its own namespace, or N is set and a residual top-level object fails under N. Newly refused ⇔ a namespaced body holds an object failing under its own namespace and the old judge had nothing to refuse. Newly accepted ⇔ every top-level object failing under N is a body's copy and every body passes under its own namespace. Nothing else changes verdict; two edges are wider than the prose (N1, N2).
  • Newly accepted beyond the named example: the copy of an object of a body with no namespace (the rule's legacy skip), and the reader's output of a flat composition over a nested artifact. Both inside "a top-level object a body declares is judged with the body only". Right.
  • Refused at both, finding moves: a residual finding gains the Top-level objects[I] … locator (also under packages: []); a mis-prefixed body object once caught through its top-level copy is now reported at the body. Code, status, header and hint unchanged; the changeset's Now bullet states both locators.
  • Mirror of the load path: the metadata door registers each body's items from the body and of the top level only unclaimedTopLevel's items; the engine registers each body's objects with that body's own namespace (registerPackageBody) and the residual under the stack's namespace (residualNamespacePackageOf). In 14 probed shapes the judged residual equals unclaimedTopLevel. Right.
  • One implementation: namespacePrefixErrors asks isResidualTopLevelItem(bodies) with key objects, the predicate residualSingleAppFinding and the owner-stamp gate ask. No claim set or slot derivation is copied (N4). Right.
  • Unchanged: a stack without packages[] gives byte-identical code, status, issues and message (types app, module, plugin, adapter; one and several offenders; legacy __; sys_; no namespace; no manifest; map-form objects; the ADR-0150 D6 clash half). strict: false judges nothing at both. Right.
  • No export moves, no new error code, no schema shape change; no generated artefact is owed.
  • Exposure: none in non-test code. validateNamespacePrefix has one caller, strict defineStack. Every non-test defineStack( call in packages/**, examples/**, apps/** is a literal without packages:; the one non-test composeStacks( (examples/app-multi-package) exports its result directly. HotCRM 0009f58: five literal strict packages, all namespace: 'crm'; the preserve composition is exported directly.

② Semver level

Clause-②: yes (narrowing) + Metadata-protocol: narrowed is correct. @objectstack/spec minor matches the kind and the sibling entry (#33074). No runtime changeset owed (test-only). The semantic entry has the five SemanticMigration fields, its id equals the adr-0087: registered id and file name, and the BREAKING banner carries Before / Now / Unchanged / Fix. Skills-docs: none holds.

Prose faces against the code at head:

  • Changeset Before, Now, Unchanged, Fix: hold. Declaration line: N1, N2.
  • Entry replacement, reason, acceptanceCriteria: hold. surface: N2.
  • Docblocks of validateNamespacePrefix and namespacePrefixErrors: hold, with N5.
  • The new describe/it strings: hold (N3). The PR body's example finding matches the emitted text.

③ Boundary flags

  • open_questions: none.
  • Claim 6855480345 said yes / semantic and marked the level provisional; the PR and changeset say yes (narrowing) / narrowed. Both directions measured above.
  • File surface: five files, all inside the claim.
  • Measured on 2c98485d (child of the order's 64551047, the first main carrying the shared reading). Correct.
  • The PR body cites HotCRM 4f0ffd8; the review read 0009f58, same result.
  • ADR-0019's Status line ends at the single-app landings; governed text, correctly not edited.
  • PR author objectstack-fleet[bot].

④ Design judgment

Holds. The namespace judge now reads a packages[] stack as the load path registers it, through the residual mirror the owner-stamp gate and #33074's single-app judge already read, and leaves the authored route byte for byte. Holding the residual to manifest.namespace matches the engine's residual namespace holder. With this in, a multi-namespace suite's round trip reaches the single-app judge, which reads it per body (probe: accepted).

No must-fix, no should-fix. Nits:

  • N1 · the declaration line lists two before-states of the newly refused body object; a third is missing (the stack manifest declares no namespace). The Before bullet and acceptance notes cover it.
  • N2 · "its own body's namespace does not prefix" names one arm: validateObjectNamespacePrefix also refuses the legacy __ form, so a body object crm_a__b is newly refused too. Same rule, code and status.
  • N3 · pin gaps: N1's third state, N2's arm, a namespace-less body's copy, packages: [], a multi-namespace suite with apps through the reader. Pin 2's "refuses" half does not discriminate by verdict.
  • N4 · namespacePrefixErrors re-spells the two-line entry.manifest walk the clash half repeats, and labels through singleAppPackageLabel, a helper named for the other rule.
  • N5 · on a body finding the hint still reads "Every object.name must be ${manifest.namespace}_${shortName}" with the body's namespace; left so the no-packages[] message stays byte-identical.

Implemented-by: claude/project-thread-3l8pov
Reviewed-by: subagent of session_01L9qibvfX5bawZKtfa6u7j9

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibvfX5bawZKtfa6u7j9

维护者速读

  • 改了什么:严格模式 defineStack 遇到带 packages[] 的 stack 时,按包分别检查对象名前缀(每个包的对象对照它自己的命名空间),顶层没有被任何包声明的残留对象才对照 stack 的命名空间;不带 packages[] 的 stack 结果逐字节不变。
  • 为什么改:多个命名空间的多包产物交给严格 defineStack 时会被误拒(422 STACK_NAMESPACE_PREFIX_INVALID),而加载时本来就按各包自己的命名空间登记;和 spec: strict defineStack counts every top-level app of a packages[]-carrying stack against the stack's own manifest, while composeStacks and the load path count only the apps no body declares #32978 是同一类两扇门判法不一致。
  • 风险与回滚:Clause-②: yes (narrowing),Metadata-protocol: narrowed,spec minor,附 18 版语义迁移条目。收紧的只有一种形状:某个包里的对象不符合它自己包的命名空间(以前没人检查)。仓内代码、examples、HotCRM 都不受影响。回滚即 revert。
  • 先看:packages/spec/src/stack.zod.ts 的 namespacePrefixErrors(复用 isResidualTopLevelItem)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/stack.test.ts,预期全绿。
  • 需要你做的:无,席位落地。

席位意见:可以落地。

席位核对

ACCEPT · seat domain:spec#2 · head da607a7f · 2026-11-13T16:09Z

  1. Shape: Fixes #33069, draft, base main, 5 files, +224/−10; changeset @objectstack/spec minor; new semantic entry 18.define-stack-packages-body-namespace-prefix-refused. Clause-②: yes (narrowing), Metadata-protocol: narrowed, Skills-docs: none. Claim 6855480345 said yes / semantic and marked the level provisional; the dev measured it to narrowed, and record 6858193093 ③ checked both directions.
  2. Premise: PR feat(spec)!: strict defineStack judges a packages[] stack per body and counts only the top-level apps no body declares against its manifest #33074 (spec: strict defineStack counts every top-level app of a packages[]-carrying stack against the stack's own manifest, while composeStacks and the load path count only the apps no body declares #32978) merged with the shared residual reading. The card's arm reproduced at merge-base and fixed at head (record 6858193093 ①).
  3. Contract review: owed at CONTRACT_REVIEW_TIER (packages/spec/src/** non-test; narrowing). Record 6858193093 on this head da607a7f: PASS, no must-fix, no should-fix, nits N1–N5 (now in the PR body's Acceptance notes). Isolated subagent; it ran the consumer suites (spec, runtime, metadata, metadata-protocol, core, cli unit and integration) and 54 probe shapes at merge-base and head.
  4. Gates / check-runs: dev at da607a7f (report 6857790505): spec 840/840 files, cli unit 416/416, metadata, metadata-protocol, core and runtime artifact-collections green; 91/91 derived gates exit 0; full dogfood 460 passed, 3 skipped, 0 failed. CI on da607a7f: 71 runs, 58 success, 13 skipped, 0 failed. K = 2; clean merge with main.
  5. Accepted deviations and out-of-scope findings:
    • Nits N1–N5 (a third before-state missing from the declaration line, the legacy __ arm of the same rule, pin gaps, the re-spelled entry.manifest walk, the hint naming the body's namespace): Acceptance notes.
    • ADR-0019's Status line still ends at the single-app landings. It is governed text, so the seat files the follow-up after this lands.
  6. Landing: not governed (check-governed-merges --pr 33122), 234 changed lines. The seat readies the PR and arms auto-merge under the maintainer's standing words 「车道经理应该使用 pm-dispatch 技能审核和落地pr」. On MERGED: spec: strict defineStack refuses an additive packages[] artifact of differently-namespaced packages with STACK_NAMESPACE_PREFIX_INVALID, judging body objects against the stack manifest's namespace #33069 closes via Fixes; pm:dispatched comes off.

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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants