Repository navigation
fix(metadata-protocol)!: the direct metadata save refuses an object without its package namespace prefix - #24702
Conversation
…t its package namespace prefix
ADR-0129 D2 holds at both runtime doors that ask the rule: the direct
(publish-mode) saveMetaItem now refuses an object whose name lacks the
${namespace}_ prefix of the package it is written under, with the
NAMESPACE_PREFIX code and the authored prescription the publish-drafts
pre-flight already answers. One judge (objectNamespacePrefixViolation)
serves both doors.
Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD
Co-authored-by: Claude <noreply@anthropic.com>
…ows stored before the save door read the prefix rule The direct save now refuses an object whose name lacks its package's namespace prefix (ADR-0129 D2), so the ADR-0070 D4 duplicate fixtures stage those objects while the package declares no namespace and put the derived one back, keeping what each case pins. Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD Co-authored-by: Claude <noreply@anthropic.com>
…its package namespace prefix Also corrects the duplicatePackage comment that named the direct save as a door no prefix gate reads. Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD Co-authored-by: Claude <noreply@anthropic.com>
…x suite's pinned engine double Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD Co-authored-by: Claude <noreply@anthropic.com>
…ds the caller's bound Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD Co-authored-by: Claude <noreply@anthropic.com>
…tive-save-namespace-prefix # Conflicts: # packages/objectql/src/protocol-duplicate-package-names-in-step.test.ts
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 737a64fc3f9eaa8d1ff2b37cb8906df0e1344c2d && git checkout 737a64fc3f9eaa8d1ff2b37cb8906df0e1344c2d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d1173825b709dfd8ad4ef383757a5df0a2e82d2a afb7f1744793bb0d074a2a0163600eef3fcaa5c8 && git checkout -B drift-repro d1173825b709dfd8ad4ef383757a5df0a2e82d2a && git merge --no-ff afb7f1744793bb0d074a2a0163600eef3fcaa5c8
node scripts/docs-audit/affected-docs.mjs --json d1173825b709dfd8ad4ef383757a5df0a2e82d2a
|
…tive-save-namespace-prefix
…t its package namespace prefix The third door that makes an object live under a package now asks the same judge as the direct save and the publish-drafts pre-flight, on the package the promotion writes under, and refuses with NAMESPACE_PREFIX / 422 and the same sentence before anything is promoted. Claude-Session: https://claude.ai/code/session_01AHeUBbe6S3JRawcpbLejXD Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · ACCEPT ·
|
Contract reviewServed-tier: Face: narrowing ( ① Derived judgmentsAccept-set changes the diff implies, each judged against ADR-0129 D2 ("every object a package defines MUST be named namespace_shortName, platform-reserved sys_* exempt") and the ruling 6278955141 ("the refusal, not the ADR exemption; D2's MUST holds at the runtime save doors; in 18.0"):
Public surface: no export, no public method, no error code, no stored row shape, no REST route or payload key moves. The only observable change is the accept set at two doors, as declared. ② Semver level
FROM / TO / Fix: present and accurate for both newly refusing doors. FROM names the publish-mode save ( ADR-0087 marker: ③ Boundary flags
Implemented-by: VERDICT: PASS |
|
Adopted ·
Generated by Claude Code |
Fixes #24547
Clause-②: no (narrowing) (the active save door newly refuses an object whose name lacks its package's namespace prefix, which today it makes live; a narrowing owes one
CONTRACT_REVIEW_TIERreview of record before the queue)Ruling-ref: 6278955141 (the maintainer: 「同意」 — pull forward into 18.0 as a refusal). Surface amendment: 6280587888 (the per-item publish door: same rule, same code, same judge).
What changes
ADR-0129 D2: a package declares its prefix once (
manifest.namespace), and every object it defines is namednamespace_shortName. The package draft publish (publishPackageDrafts) refused an unprefixed object draft withNAMESPACE_PREFIX. The direct (publish-mode)saveMetaItemand the per-item publish (publishMetaItem) made the very same nameactiveunder the very same package. Now all three doors ask one judge and give one answer.packages/metadata-protocol/src/protocol.tsobjectNamespacePrefixViolation(name, packageId). It reads the package's declared namespace from the installed manifest and returnsvalidateObjectNamespacePrefix's own sentence, ornull. A package with no namespace, and a write bound to no package, are not judged.publishPackageDrafts' pre-flight now calls that judge. This is a pure refactor: same namespace read, same verdict, same code.saveMetaItemasks the same judge for anobjectin publish mode. It is judged against the package the save writes: the one it names, or the one a package-less publish save resolved into. On a violation it throwscode: 'NAMESPACE_PREFIX',status: 422, with the verbatim prescription.sourceset:migrate-stored, the flow-credential move).package-duplicate,package-unmanaged-import) are asked, like any author.promoteDraftForPublish, the per-item publish's promotion, asks the same judge for anobjectdraft onpromoteInto, the package the active row is written under. On a violation it throws the sameNAMESPACE_PREFIX/ 422 with the same sentence, beforerepo.promoteDraftwrites, so nothing is promoted and the draft stays a draft. The exemptions are the same: no package, no namespace,sys_*, and sealed managed content (as for the D9.9 refusal beside it).publishPackageDraftsalso promotes through here, but its pre-flight refuses the same name first.duplicatePackagethat called the direct save "a door no prefix gate reads" is corrected.NAMESPACE_PREFIXis already registered to@objectstack/metadata-protocolinERROR_CODE_LEDGER(packages/spec/src/api/error-code-ledger.zod.ts), and the publish pre-flight already stamps it. No ADR edit and nopackages/specchange..changeset/24547-active-save-namespace-prefix.md. It isminorand BREAKING, has FROM / TO / Fix lines covering both doors (the direct save and the per-item publish), theClause-②: no (narrowing)line andadr-0087: not-required (no-migration-prescription).check:adr-0087-registrationaccepts that disposition.Measured on the wire (fresh showcase on this branch, after the fix: the table at
3b94654c58; the per-item rows, and a re-read of the direct PUT and publish-drafts with the same answers, atafb7f17447)POST /api/v1/packagescom.v1.rolluprollup)PUT /api/v1/meta/object/v1_report?package=com.v1.rollup{"error":"Object 'v1_report' is missing the package namespace prefix. Rename it to 'rollup_v1_report' (namespace = 'rollup').","code":"NAMESPACE_PREFIX"};GET /api/v1/meta/object/v1_reportthen 404sharingModel(sibling refusal, for the envelope){"error":"object/rollup_no_owd failed author-time validation: …","code":"INVALID_METADATA","issues":[…]}PUT …/meta/object/rollup_v1_report?package=com.v1.rollup(prefixed)state: activePUT …/meta/object/v1_loose(no package)state: activePUT …/meta/object/v1_report?package=com.v1.rollup&mode=draftstate: draft(unchanged)POST /api/v1/packages/com.v1.rollup/publish-draftsoutcome: refused,failed[0].code: NAMESPACE_PREFIX, the same sentence (unchanged)POST /api/v1/meta/object/v1_report/publish?package=com.v1.rollup(per-item publish of that draft){"error":"Object 'v1_report' is missing the package namespace prefix. Rename it to 'rollup_v1_report' (namespace = 'rollup').","code":"NAMESPACE_PREFIX"}. ThenGET /api/v1/meta/object/v1_report404 andPOST /api/v1/data/v1_report404OBJECT_NOT_FOUND, and a following publish-drafts still lists the draft infailed[]rollup_v1_report, thenPOST …/meta/object/rollup_v1_report/publish?package=com.v1.rollupGET200The
/metaPUT door answers in the flat REST dialect, so the code is top-levelcode, besideerror. That is the same place the door putsINVALID_METADATA.Pins (
packages/metadata-protocol/src/protocol.active-save-namespace-prefix.test.ts, 8 cases)code: NAMESPACE_PREFIXandstatus: 422(the envelope, not a bare throw), the first sentence, the rename, and that no row is written.active.publishPackageDraftsrefuses it withNAMESPACE_PREFIX, andpublishMetaItemrefuses it too.failed[0].errorand the per-item refusal's message both equal the active door's message byte for byte, and the draft is still the only row.code: NAMESPACE_PREFIXandstatus: 422, the first sentence and the rename. Nothing is promoted and the draft stays a draft.Reproduced first, on the unfixed code: cases 1, 5 and 6 red (the active save answered
saved), cases 2–4 green.Ablation (
scripts/ablation-replace.mjs, on the committed fix):if (prefixViolation) {was changed toif (false && prefixViolation) {. The anchor went 1 → 0 and the blob 72a5cff9503e → 88f5360fcedf. Cases 1, 5 and 6 went red and 2–4 stayed green. Restore was proven: blob == HEAD andgit diff HEADempty. The direction was red, as expected. No dist build was needed: the suite imports./protocol.jsfrom source.Second ablation, the per-item publish branch, on the committed fix
afb7f17447.const prefixViolation = this.objectNamespacePrefixViolation(request.name, promoteInto);was changed toconst prefixViolation = false && this.objectNamespacePrefixViolation(request.name, promoteInto);. The anchor went 1 → 0 and the blob aecdeed0e1a2 → b1797c55d012.git diff HEADempty.Verification on the final head
afb7f17447(the per-item publish fix, on top of a merge oforigin/maind1173825b7throughos-regen-merge.sh)pnpm --filter @objectstack/metadata-protocol exec vitest run: 251 files passed (3 skipped), 20798 tests passed.pnpm --filter @objectstack/metadata-protocol typecheckandpnpm --filter @objectstack/objectql typecheck: exit 0 (check:test-typecheckOK, ledger unchanged).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 78 commands, all exit 0.--ranreconciliation: 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN.check:dual-build-cjs-loadsandcheck:lean-entry-closurefirst answered exit 3 (PREREQUISITE NOT MET: the fresh worktree had no full build yet). Re-run after the build: exit 0.3b94654c58(not re-run in this round), on the files that write objects under a package:package-first-authoring,flow-shipped-name-published-doorandinstall-local-unmanaged-modeall passed.wall-environment-metadata-refusalhad 2 failures, unrelated to this diff; see Acceptance notes.test:repo, which this diff does not touch, and the consumer packages' own suites outside the measured set below.File surface beyond the claim (declared)
packages/objectql/src/protocol-duplicate-package-{unprefixed-object,names-in-step,permission-field-keys}.test.ts. These ADR-0070 D4 duplicate fixtures staged an unprefixed object under a namespaced package through the direct save. That is the exact shape this change refuses, so without this changeTest Coregoes red. Each file now stages those objects with asaveStoredBeforeRulehelper, which hides the package's namespace for the one save and then restores it. That models a row stored before the door read the rule, so every case keeps what it pins. No assertion changed.scripts/engine-double-contract.pinned.json: thecheck:engine-double-contract --writeremedy for the new suite's pinned double. It adds 3 rows and loses 0.Shipped-tree exposure (measured)
examples/: 0. Code packages passdefineStack, and no test does a package-bound object save.packages/metadata-protocoltests: 0. The full suite is green.packages/objectqltests: 4 staging sites in 3 files, adjusted as above.com.probe.p/com.probe.q, from whichderiveNamespaceFromPackageIdderivesnull(a single-letter segment);crm_-prefixed names.Other doors that make an object live (enumerated; line numbers at
3b94654c58)publishMetaItem(throughpromoteDraftForPublish;POST /api/v1/meta/:type/:name/publish) is now judged too, per the seat's surface amendment 6280587888 on the card. At3b94654c58it still made the unprefixed object live: 200, thenGET200 and a data insert 201. Atafb7f17447it answers 422NAMESPACE_PREFIX(table above).saveMetaItem:duplicatePackage(:27916),importUnmanagedPackage(:28629), and the datasource external import (packages/services/service-datasource/src/plugin.ts:206, which writes no package and runs its own prefix check).revertCommit(:29484),rollbackToPackageCommit(:29975) androllbackMetaItem(:30046).loadMetaFromDb(:31403) hydrates stored rows at rest.installPackageregisters a manifest and makes no object live.Acceptance notes
packages/spec/src/kernel/namespace-prefix.tsheader nameMetadataManager.publishPackage()as the runtime chokepoint. That method (packages/metadata/src/metadata-manager.ts:1879) runs no object-name prefix check. The runtime doors that do arepublishPackageDraftsand, with this change,saveMetaItem. No edit here, because both files are outside this card's surface. Carrier: none.importUnmanagedPackagenow meets this refusal for a source package that declares no namespace and whose objects lack the prefix derived from its id. The refusal arrives mid-copy and the existing rollback undoes it, rather than at plan time. Not measured: no fixture has that shape. Carrier: none.packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts, the "thirteen write doors" case, 2 failures underisolatedandgroup. Its package publish of acrm_lead.*view is refused withlist-view-object-outside-closure. That is the list-view closure refusalorigin/mainbrought in, and nothing in this diff touches a view. The Dogfood Regression Gate does not run on PRs, so this surfaces onmain's push battery.For the contract-tier reviewer
source !== undefinedexemption (server-stated rewrites) is deliberate. The two package-copy faces are deliberately judged.request.packageIdafter the package-less publish address resolution. Case 5 pins that.promoteIntoand beforerepo.promoteDraft, with the same sealed-managed exemption as that refusal. Case 7 and the second ablation pin it.Generated by Claude Code