fix(metadata-protocol): one collision predicate at the save door — a stored view container never takes a name already served from elsewhere (#21639, #21638) - #21648
Conversation
…tainer at the save door; a package-less container row named after a shipped view item belongs to no package (WIP) Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…container collision predicate, and the package-less container row's attribution Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
… name as the container, never as a name to save under; #21558's pins name the shipping package Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
… asked positively by the save door's shipped arm and the container row's package attribution Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…t narrows nothing it does not hold Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ate, and the package-less container row's attribution Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ew-container-collision
…wner and its prescription inside the 500-character wire bound, the explanation after Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ew-container-collision
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 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 12468018e89ae330cf1119538c72b97b1908066b && git checkout 12468018e89ae330cf1119538c72b97b1908066b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 15fe567c9c74e088684944094aa686b9bd3b386c d5101d18311ef0fd7dc5daa08c1264ce06ca545c && git checkout -B drift-repro 15fe567c9c74e088684944094aa686b9bd3b386c && git merge --no-ff d5101d18311ef0fd7dc5daa08c1264ce06ca545c
node scripts/docs-audit/affected-docs.mjs --json 15fe567c9c74e088684944094aa686b9bd3b386c
|
ACCEPT — PR #21648 at head
|
Fixes #21639
Fixes #21638
Clause-②: no (narrowing)
#21638 carries its own claim on this branch (5975022936), as the Closing-Target Claim Guard requires for every card a PR closes. Triage folded it into #21639's claim.
The runtime save door's accept set narrows: one predicate now refuses a stored view container whose save name, or any name its expansion produces, is a name already served from elsewhere. Nothing widens. A package-less container row stored under the name of a view item a package ships now belongs to no package, as triage's fold of #21638 rules. Dispatched by
domain:engineseat 1 under claim 5974259402 (file surface corrected by 5974270195), branchclaude/issue-21639-view-container-collision. Triage's ruling 5973827435 and its fold 5973838571 are implemented as written.What changed
All of it is in
packages/metadata-protocol/src/protocol.ts, the claimed surface. The producer is the save door itself, so nothing moved to another package.viewContainerNameCollisionRefusalreplaces finding(metadata-protocol): the runtime save door accepts a view container saved under one of its own expanded names, and after #21510's rule no door answers a view item for that name #21558'scontainerOwnExpansionNameRefusaland The runtime save door accepts a view container saved under a name ANOTHER container expands to; the object door then lists the second container's list in place of the first's default, and no door answers a view item under the saved name #21620'scontainerSiblingExpansionNameRefusalat the same place insaveMetaItem: aftersavedItemNameRefusal, beforenormalizeViewMetadata.expandRuntimeViewContainerwith the request's package binding, the binding the row is stored under. The body judged is the authored one with the door's ownnamestamp applied first.readActiveOverlayRowsselects through the readers' gateorganizationIdForMetaRead, with no package filter. Each is parsed bystoredOverlayEntriesand expanded byexpandStoredViewContainerswith its own binding. The row stored under the save name is left out, because it is the row this save replaces. Thesame objectqualifier is gone, as ruling (1) B says, and so is the now-unusedderiveViewContainerObjectimport.lookupArtifactItemanswers, and the artifact must carryviewKind(isShippedViewItem, a new module-level predicate). One exception: the views of the shipped container this row overlays by its own name (overlaidShippedContainerViewNames). ADR-0005 keys an overlay by its own name, so that container's views are the row's own to replace.VALIDATION_ERROR/ 400, with no new code. Every message names the other owner (the stored container, the shipping package, or "its own expansion") and gives the family's prescription. That is: add the view as a member of the container that owns the name, or save a view item under the name. For a shipped name: save a view item under it, or a name or key of its own. ⛔ No arm prescribes a save under a name another stored row holds. finding(metadata-protocol): the runtime save door accepts a view container saved under one of its own expanded names, and after #21510's rule no door answers a view item for that name #21558's own-expansion arm used to say "Save the container under its object's name" even when a stored container held that name. It now names that container to add the view to.CLIENT_MESSAGE_MAX). The first REST probe of this branch received the shipped-arm prescription cut mid-word, so every message now front-loads the owner and the prescription. The pin asserts both inside the first 500 characters (rest/meta: the /references 501 back-loads its ADR-0110 D3 prescription, so the #5423 bound silently cuts it off for long object names #17584's ordering rule).runtimeViewContainerPackage. A package-less row whose same-named shipped artifact is a view item (viewKindset) now answers no package: "A container row is not an overlay of the item, so it is attributed to no package." A package-less row under a shipped container's name keeps that container's package, as before. A bound row is unchanged.Census, taken first (the ruling's stop conditions)
(2): does a live writer save a second container of one object on purpose? Readings are at BASE
7b07749f05, re-read after mergingorigin/main(headd5101d1831). objectui was read at the old pin89cad75d55and at the new pinab1879721595, whichorigin/mainmoved to while this ran.studio-authoring.view-authoring-live(P1, revision 2)qa_repair_asset_views={ object: 'repair_asset', list, form }, on a runtime-authored object. No other checklist item stores a container onrepair_asset. #13407's own repro (a container bound tonoteunder another name, per PR #21637's census) is one container too.lead_views, bound tocrm_lead. Pinned in the#21412block of the same test file, still green.OBJECT.CONTAINER_NAMEandOBJECT.CONTAINER_NAME.KEY, never a name the owning package ships. Pinned as two allowed rows below.createBuildBody(anchors.ts:291, "Emit a canonical ViewItem"), the spec create seed forview(metadata-create-seeds.ts:62,viewKind: 'list'), and the flat configs ofdata-objectstackcreateViewandsetViewConfig. Re-savers save the loaded body under the name it carries:ResourceEditPage(:1477), the Interfaces pillar'sStudioDesignSurface(:2475, new atab1879721595), andupdateView.duplicatePackage, a writer through this door, outside the ruled set)pone_extra_viewsincom.example.pone, bound tocrm_lead(outside the package, shipped by no code package), duplicated intocom.example.ptwo. At BASE: copied, and the object door'scrm_lead.defaultbecame the copy's, wearing_packageId: com.example.ptwo. The source package's view was silently replaced. At HEAD:failed[]carries this refusal, namingpone_extra_views, and the source's view stays.migrateStoredMetadatatrythat recordsoutcome: 'failed'.packages/mcp/src/mcp-http-tools.tshave no metadata write.skills/objectstack-uiteachesdefineViewin source.sys_metadataview rows. Hosted tenants: NOT MEASURED.The attribution half: does a live overlay path depend on a package-less container row taking a shipped item's package?
runtimeViewContainerPackageis called byexpandRuntimeViewContainer(and by the newoverlaidShippedContainerViewNames).expandRuntimeViewContaineris called byexpandStoredViewContainers(the list read, the by-name read'sresolveRowlessExpandedView, and this predicate), byhydrateExpandedViewItems(the registry), and by the predicate itself.packaged-display-class-direct-editdesigner overlay.Every accept-set change at
saveMetaItem, typeviewEach row covers publish and draft mode, both scopes and both kernels. "Stored container" means one in the caller's selection.
7b07749f05)VALIDATION_ERROR/ 400, naming the stored container. Nothing is stored or registered.listtakesOBJECT.default, under a free name or under the object's namelistreplacedOBJECT.defaulton both doors with that package's_packageId.VALIDATION_ERROR/ 400.form-only container under a registered view item's nameVALIDATION_ERROR/ 400 under #21558 or #21620. Under any other registered name: 422 from the identity stamp.VALIDATION_ERROR/ 400 first, before the stamp.Rows already stored. They keep their bytes, and no row is re-saved. A package-less container row stored under a shipped view item's name now reads as belonging to no package:
showcase_task.showcase_task.in_progress), with no_packageIdand no default.showcase_task.default, orshowcase_task.editfor alistViews.editmember) are served again on both doors (pinned).A new save of a row in a refused shape, a re-save included, is refused until its body stops colliding. Delete stays open.
The enumeration pin (the card's acceptance)
view-container-runtime-expansion.test.ts, block#21639. Each row runs on both kernels (env_localand unscoped) and both scopes, unless the row names one scope.codeandstatus;crm_leadcrm_leadcrm_leadlistonOBJECT.default((2)'s second container default)crm_leadOBJECT.defaultlead_other_viewscom.example.showcasecom.example.showcasecom.example.showcaseThe PM's mechanism hypotheses
7b07749f05.saveMetaItemcalledcontainerOwnExpansionNameRefusaland thencontainerSiblingExpansionNameRefusal, both beforenormalizeViewMetadataand bothVALIDATION_ERROR/ 400. Both are replaced by the one predicate.OBJECT.default, it is (2)'s shape and refused (row 7).lookupArtifactItem, the registry's artifact read, which never answers a tenant-authored row, holding an artifact withviewKindset.sys-metadata-repository.package-writability.test.ts's ADR-0005 overlay preservation pin. That fixture's artifact stub is neither a container nor a view item. The predicate is now positive, and that pin is green.sourceorwriteFace, somigrateStoredMetadataandduplicatePackagerecord the refusal as the row's failure. Duplication was measured (the census row above).Tests
All readings are at HEAD
d5101d1831unless named otherwise.protocol.ts(blob56bc12dce760), restored by a trap-guarded script. Restore proof: blobe5765e5ba0f9equal to HEAD at0af0f28e49, andgit diff HEADempty. The command:vitest run src/view-container-runtime-expansion.test.ts -t '#21639|#21638'.the save is refused;crm_lead;#21638attribution block: 2 at-rest cases + 1 control, × 2 scopes × 2 kernels.pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2gives Test Files 209 passed, 3 skipped (212); Tests 3463 passed, 19 skipped (3482);VERDICT command-exit 0.pnpm --filter @objectstack/metadata-protocol typecheck(tsc --noEmit) exits 0, andtsc --noEmit --listFilesincludes the test file.@objectstack/metadata-protocol.dist/rebuilt atd5101d1831byturbo run build --filter='@objectstack/dogfood^...' --filter='@objectstack/metadata-protocol...' --concurrency=2: 63 of 63 tasks. The builtdist/index.jscarries the new predicate, and the old method names are gone from it.objectql, 11 files, 229 tests:protocol-meta,protocol-view-identity-overlay,protocol-org-overlay-registry-gate,protocol-commit-history,protocol-packaged-view-base,protocol-save-meta-repo-path,protocol-save-meta-repo-path-real-engine,view-container-divergent-name-registrars,view-container-name-refusal,engine-nested-plugin-view-expansion,metadata-validation-sweep.rest, 1 file, 7 tests:public-form-routes.stored-row.dogfood, 2 files, 7 tests:view-container-cross-package-default(PR fix(metadata-protocol): a bare-list view container on another package's object expands under its own name #21430's pin over REST) andview-container-default-form.PUT /api/v1/meta/view/showcase_task.in_progresswith a container answers 400VALIDATION_ERROR, and the object door still servesshowcase_task.in_progressas "In Progress" fromcom.example.showcase;Reverse verification
The change was committed first. Both legs ran from committed
e5ac5cf14a, the sameprotocol.tsblob as HEAD, since the later merge touched nothing undermetadata-protocol. Each leg was a trap-guarded script aroundscripts/ablation-replace.mjs --delete.if (containerCollision) throw containerCollision;.f853b383e1b2tobe22198e5e1f. Predicted direction: red.<object>.default, are accepted with no diagnostic #21639 refused rows (40), finding(metadata-protocol): the runtime save door accepts a view container saved under one of its own expanded names, and after #21510's rule no door answers a view item for that name #21558's showcase block (24), The runtime save door accepts a view container saved under a name ANOTHER container expands to; the object door then lists the second container's list in place of the first's default, and no door answers a view item under the saved name #21620's block (36) and finding(metadata-protocol): the runtime save door accepts a view container saved under one of its own expanded names, and after #21510's rule no door answers a view item for that name #21558's runtime-object block (4).f853b383e1b2, equal to HEAD, andgit diff HEADempty. Both the tool and the script's trap proved it.if (isShippedViewItem(overlaid)) return undefined;.f853b383e1b2to55cc79455d20. Predicted direction: only the at-rest pins turn red../index.js, the source.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, atd5101d1831derives 64 families. That is the dispatch lead's 56 plus the 8 the changeset adds:check-adr-0087-registration×2;check-empty-changeset×2;release-rehearsal-clone --self-testandrelease-pending-publish --self-test;check:objectui-changesetandcheck:pm-changeset-deadline-census.All 64 exited 0.
pnpm check:dual-build-cjs-loadsfirst exited 3: PREREQUISITE NOT MET, because 8 packages had nodist/. It exited 0 after those were built through the lock.check:dual-build-cjs-loadsandcheck:type-check-debtran through the verify lock.--ran: "64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN".check-adr-0087-registrationaccepts the changeset'snot-required (no-migration-prescription)disposition.Lint, a declared narrowing.
eslint --no-inline-config --format jsonover the 2 changed.tsfiles reports 2 files, 0 errors and 0 warnings, with no "file ignored" message. The changeset.mdhas no matching ESLint configuration.eslint.config.mjs:328says so, and--print-configshowsparserOptions.projectandprojectServiceboth null.pnpm lintis CI's.Branch state:
origin/mainwas merged twice,f30588ceb7and thend5101d1831. Neither moved a byte underpackages/metadata-protocol.Acceptance notes
rollbackMetaItem,revertCommitand the draft promotion do not run the predicate, as for finding(metadata-protocol): the runtime save door accepts a view container saved under one of its own expanded names, and after #21510's rule no door answers a view item for that name #21558 and The runtime save door accepts a view container saved under a name ANOTHER container expands to; the object door then lists the second container's list in place of the first's default, and no door answers a view item under the saved name #21620. A draft saved before its sibling existed can be promoted into a collision.dist/index.d.tsswaps two private member lines for two:viewContainerNameCollisionRefusalandoverlaidShippedContainerViewNames. No public member or exported type changes..changeset/21639-view-container-name-collision.md:'@objectstack/metadata-protocol': minor,Clause-②: no (narrowing), the BREAKING banner, the ADR-0087 dispositionnot-required (no-migration-prescription)written from this census, and a before/after per shape.Generated by Claude Code