Repository navigation
feat(spec)!: strict defineStack judges a packages[] stack's namespace prefixes per body and its top level by the residual rule - #33122
Conversation
… 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
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxbMr8S6rSezDVowtd
📓 Docs Drift Check3 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
Coarse fallback — 171 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
|
domain:spec#2 · os-tesla · session_01L9qibvfX5bawZKtfa6u7j9 Contract reviewServed-tier:
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; ① Derived judgments
② Semver level
Prose faces against the code at head:
③ Boundary flags
④ Design judgmentHolds. The namespace judge now reads a No must-fix, no should-fix. Nits:
Implemented-by: VERDICT: PASS |
|
domain:spec#2 · os-tesla · session_01L9qibvfX5bawZKtfa6u7j9 维护者速读
席位意见:可以落地。 席位核对ACCEPT · seat
|
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 asresolveArtifactCollectionsoutput) is accepted by strictdefineStack, where it was refused. Narrowed: apackages[]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.namecarries its own package'snamespaceprefix) is unchanged, and no skill or docs page teaches handing apackages[]stack todefineStack; what moves is which namespace strictdefineStackjudges 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,没有第二份实现。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 条全绿,其中一条是「两个命名空间的组合制品被接受」,一条是「残余对象前缀不对被拒(422STACK_NAMESPACE_PREFIX_INVALID)」。What changes
validateNamespacePrefix(the strictdefineStackpass,packages/spec/src/stack.zod.ts) now reads its prefix findings from a newnamespacePrefixErrors, which judges each object against the namespace of the package the load path registers it under:packages[]: unchanged. Every top-level object is judged againstmanifest.namespace, and the finding isvalidateObjectNamespacePrefix's own sentence, byte for byte.packages[](ADR-0130 D4): each body's objects are judged against that body's ownnamespace, the finding prefixedPackage 'ID' (packages[J].manifest.objects[I]):. A top-level object that no body declares is the residual, registered undermanifest.idby the boot's residual rule (unclaimedTopLevel,@objectstack/metadata), and is judged againstmanifest.namespace, prefixedTop-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.isResidualTopLevelItem(bodies), the mirror ofunclaimedTopLevelthat 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/metadatais read, not changed.namespaceis not judged (the rule's legacy skip); the namespace/object-name clash half ofvalidateNamespacePrefix(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/specminor, 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 namespacescrmandsvccomposed undermanifest: 'preserve':svcmanifest, with anti-vacuity (crm_accountat the top level);codeSTACK_NAMESPACE_PREFIX_INVALID,status422,issuesexactly the located finding; a residual the manifest's namespace prefixes is accepted;packages[]gives the exact finding and the exact message (header, line, hint) it gave onmain.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 namespacesvc).Measured on
origin/main2c98485d(which carries #33074; the order's64551047is its parent and lacks the shared residual reading) before the fix, with a probe through the real reader: the round trip was refusedSTACK_NAMESPACE_PREFIX_INVALID(Object 'crm_account' is missing the package namespace prefix … (namespace = 'svc'));unclaimedTopLevelof 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 objectcrm_strayin thesvcbody 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 readblob == HEAD (bf975a2fe1a7) and git diff HEAD is empty.stack.zod.tscrm_account); runtime: 1 failed (round trip)stack.zod.tsrestored toorigin/main2c98485d(blobf5dc7819, checked againstgit rev-parse origin/main:…), tests unchangedpackages[]finding and message are byte-identical tomain); runtime: round trip failedBoth suites resolve
@objectstack/spectosrc/(stack.test.tsimports./stack.zod;packages/runtime/vitest.config.tsaliases the package tosrc), so no dist rebuild was owed per leg.Acceptance notes
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: apackages[]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 withoutpackages[]; 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.packages[]stack to strictdefineStack. HotCRM (4f0ffd8, read-only) builds each package with strictdefineStack(all five declarenamespace: 'crm') and reads the composed suite throughresolveArtifactCollectionsintest/helpers/composed-stack.tswithout re-judging it. The consumer suites below are the empirical half.defineStackhint after the findings still reads "Every object.name must be${manifest.namespace}_${shortName}"; on apackages[]stack the namespace is the body's, which each located finding names. Left as is so the no-packages[]message stays byte-identical.ruling|Ruling-ref|maintainer|⛔):stack.zod.ts68 hits file-wide, none inside the edited region (validateNamespacePrefixand the newnamespacePrefixErrors,:2540–2670);:1846, :1855, the retiredstack.test.ts2 hits (type: pageblock), untouched;artifact-collections.public-reader.test.tsnone. No ruling sits on the edited lines.defineStackjudges apackages[]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 throughresolveArtifactCollections, with this PR and its merge sha. That is the landing seat's card to file, not this PR's.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 nostack.zod.tsin their files).da607a7f), noted and not pushed for:validateObjectNamespacePrefixalso refuses the legacy__form, so a body object such ascrm_a__bis newly refused too. Same rule, code and status.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).namespacePrefixErrorsre-spells the two-lineentry.manifestwalk the clash half repeats, and labels packages throughsingleAppPackageLabel, a helper named for the other rule.${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 mergingorigin/maine997113c: #33110, #33080, #33078, none on this diff's files) unless named, afterpnpm install --frozen-lockfileandpnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' --filter @objectstack/cli build, every heavy command underscripts/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. (At1dbbe63dit named onetest-typecheck-debt.jsonarrival, a TS2488 on the new body pin's spread; fixed in6112dbb9, not ledgered.)pnpm --filter @objectstack/spec test: Test Files 840 passed (840), Tests 26832 passed, 1 todo.typecheck(at6112dbb9): exit 0.dispatch-gatesdoes not derive them), at6112dbb9, the merge touching none of their sources:@objectstack/metadatatest (theunclaimedTopLevelload path): Test Files 76 passed (76), Tests 2204 passed.@objectstack/metadata-protocoltest: Test Files 337 passed, 7 skipped (344); Tests 22799 passed, 36 skipped.@objectstack/coretest: Test Files 146 passed (146), Tests 3369 passed.@objectstack/cliunit (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 unbuiltpackages/cli/dist(PREREQUISITE, the pins' own guard), and after building the cli they passed atda607a7f(2 files, 29 tests). The integration layer is CI's (no spawn entry or integration file in the diff).@objectstack/runtimeartifact-collections.test.ts+artifact-collections.public-reader.test.ts: Test Files 2 passed (2), Tests 26 passed; runtimetypecheck: exit 0.Clause-②carries a(narrowing)arm):pnpm --filter @objectstack/dogfood testatda607a7f, 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).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack→ 91 families (same list at6112dbb9andda607a7f), 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-anyexit 3 PREREQUISITE;check:dts-closureexit 1 naming two packages mid-build;check:dual-build-cjs-loadsexit 3) and all six exited 0 when rerun after the build settled.node scripts/check-plugin-teardown-shape.mjs --self-testexit 0 aftergit fetch origin 621a487607881c66b2899b7e3477115229a156b4 --depth=1. Reconciliation--ranwith 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).Generated by Claude Code