Repository navigation
fix(metadata-protocol): the save door refuses a view container saved under a name another stored container of the same object expands to (#21620) - #21637
Conversation
…under a name another stored container of the same object expands to A second container stored under a name a sibling container expands to became that name's own row, so the sibling's expansion no longer filled it: the object door listed nothing under the name and the by-name read answered the raw container. The save door now refuses that save with VALIDATION_ERROR / 400, judged by the readers' own row selection and expansion, before the identity stamp. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…pansion name refusal Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ntainer-named-after-object
📓 Docs Drift CheckThis PR changes 1 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 8c2545abd0208fcdf8a3cda333c1c7a163af1f2a && git checkout 8c2545abd0208fcdf8a3cda333c1c7a163af1f2a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5b5e83f446bde0bf6e13db304b9f07f115635704 13736a50f64ea4a3ce6b76fa8e0b1f5b847bf912 && git checkout -B drift-repro 5b5e83f446bde0bf6e13db304b9f07f115635704 && git merge --no-ff 13736a50f64ea4a3ce6b76fa8e0b1f5b847bf912
node scripts/docs-audit/affected-docs.mjs --json 5b5e83f446bde0bf6e13db304b9f07f115635704
|
REWORK — PR #21637 at head
|
…sibling container, never a save under its name The refusal told the author to save the container under its object's name. In the card's own pair that name holds the sibling container, so following the arm literally replaced the sibling's row and dropped the view the refusal keeps serving. It now names the stored container that expands the name and prescribes adding the view as a member of it, or saving a view item under the expanded name. One pin per kernel; the changeset's fix carries the same two arms. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…sibling's name appears
The first spelling only asserted the message never says "under 'crm_lead'",
which the old wording ("under its object's name, 'crm_lead'") also
satisfied. Every occurrence of the sibling's name must now name it as the
container, or as the object a view binds to.
Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
ACCEPT — PR #21637 at head
|
Fixes #21620
Clause-②: no (narrowing)
The runtime save door's accept set narrows: a view container saved under a name that another stored container of the same object expands to is refused with
VALIDATION_ERROR/ 400. Nothing widens. Dispatched bydomain:engineseat 1 under claim 5973165628 (branchclaude/issue-21620-container-named-after-object).What changed
saveMetaItem(packages/metadata-protocol/src/protocol.ts), the method behindPUT /api/v1/meta/view/NAMEand the dispatcher's metadata save, gains one check:containerSiblingExpansionNameRefusal. It runs right after #21558'scontainerOwnExpansionNameRefusaland before the view identity stamp (normalizeViewMetadata). This is triage's pre-named fallback, the narrower check, not the broad one. The census below hit, so the broad check ("a container's name must equal its object's") was not written.viewrows thatreadActiveOverlayRowsselects for this caller, through the readers' gateorganizationIdForMetaReadand with no package filter. That is environment-wide rows plus the caller's organization's.storedOverlayEntriesand expanded byexpandStoredViewContainerswith its own package binding. So every member kind (a bare or namedlist,listViews,form,formViews), the expander's de-duplication, and metadata: a view container with a bare list on another package's object silently replaces that object's packaged default view on GET /meta/view?object= — while the by-name read still serves the original #21334's own-name arm on another package's object are judged where the readers place them.objectagainstderiveViewContainerObjectof the body. That is the one derivation the source registrars file a container under, newly imported from@objectstack/metadata/view-container.namestamp applied first (H3), before the identity patch. On an unscoped kernel the sibling's expansion is registered under the name, and aform-only container would otherwise take itsviewKindand reach the schema as a malformed view item.VALIDATION_ERROR/ 400, the same as the two name checks beside it. No new code, and the error-code ledger is untouched.saveMetaItem's view save door only.The census, taken first (the ruling's stop condition)
The ruling, as the seat reads it: if any writer or packaged container names a container other than after its object, do not write the broad check; record the hit; implement the narrower check. The census hit, decidably.
Readings at BASE
045b946256, with objectui at its pin89cad75d55:studio-authoring.view-authoring-live(P1, active, revision 2 of 2026-10-02)PUT /api/v1/meta/view/qa_repair_asset_views?mode=draftwith{ object: 'repair_asset', list, form }, on a runtime-authored object.PUT /api/v1/meta/view/NAMEof a container bound tonoteunder another name. #13407's repair taught the readers the container's ownobjectso that this shape serves.os_qa_shadow_probesaved onshowcase_task. Ruling 5946423948 allowed "expands under the container's own name". Seat answer 5955628428 rejected refusal at save because it "blocks a legitimate 'add views to a shipped object' path". PR #21430 pins it (46 cases plus a REST dogfood pin).packages/metadata/src/view-container-name.test.ts.buildViewConfigSaveBodyandviewEnvelope, ObjectDataPage throughcreateRuntimeMetadata, the metadata-admincreateBuildBody, and the flat configs ofdata-objectstacksetViewConfigandcreateView. Re-savers: the metadata-adminResourceEditPagesaves a body under the name it carries, anddata-objectstackupdateView's draft path merges onto the stored draft without reducing a container to itslist. Both re-save a container stored under a non-object name, under that name.packages/mcp/src/mcp-http-tools.tsare object, record and action tools: no metadata write. The publishedskills/objectstack-uiteachesdefineViewcontainers in source, with no top-levelname.defineView(call sites with an object-literal argument, outsidepackages/spec/srcand tests, and 0 with a top-levelname. The source registrars refuse a setnamethat disagrees with the derived object: the boot loop (engine.ts:7024), the artifact/HMR loader (plugin.ts:1192) andos validate(view-container-names.ts:101).sys_metadataview rows; the onesys_metadatamention is a comment in the showcase connectors. Hosted tenants: NOT MEASURED.H2, measured: what the broad check would have broken. A throwaway, trap-guarded probe planted the broad predicate at the save door at BASE, ran the full
metadata-protocolsuite, and was restored (blob8a8053c40c94equal to HEAD,git diff HEADempty). It gave 127 of 3342 tests red, every one carrying the probe's own message:resettable: trueand its layeredcodeis the hydrated expansion #21511 block (14), which reuse that harness's container.namecontradicts its row name and registers it under both keys; the two source registrars refuse the same document #21412's P2 and P2b (2).protocol.graft-folded-form-sections.test.ts(2,lead_viewsonlead) andsys-metadata-repository.package-writability.test.ts(1,case_grid).PR #21430's REST dogfood pin was not run under the probe (NOT MEASURED). With this PR it passes, 4 of 4. The pins that encode a ruled arm (#13407, #21334, #21412) are evidence for the stop condition, alongside the writers above. The probe's first attempt was refused by
ablation-replacebecause its replacement re-contained the anchor. Nothing ran, the tool restored, and the probe was re-spelled.Every accept-set change at
saveMetaItem, typeviewVALIDATION_ERROR/ 400. Nothing is stored or registered.name(the door stamps the save name).form-only container under a sibling's form-expanded name, on an unscoped kernel with an environment-wide sibling (the registry holds the sibling's expanded item under the name).INVALID_METADATA/ 422: the identity stamp copied that item'sviewKindonto the body.VALIDATION_ERROR/ 400 by this check, which now runs first.What still saves (pinned on both kernels and in both scopes):
Rows already stored in this shape keep their bytes and read as they do today. Measured with the check ablated, on both kernels and in both scopes:
crm_lead.defaulton both doors.A new save of such a row is refused, and delete stays open.
migrateStoredMetadataandduplicatePackagere-save stored rows through this door inside atrywhosecatchrecords the row:outcome: 'failed'with the refusal's text, or afailed[]entry. So they report such a row and never re-save it (H5, by construction: the check is not gated onsourceorwriteFace).The PM's mechanism hypotheses
045b946256:savedItemNameRefusal, thencontainerOwnExpansionNameRefusal, thennormalizeViewMetadata, allVALIDATION_ERROR/ 400. 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 check is kept as it is, and the new check sits after it. Every 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 pin holds its envelope and intent (the file is green).expandUnderOwnName's arm is a ruled writer path, so the broad check would have refused it.The foreseen follow-up: a container saved under the name of a view item a package ships
This was measured in-process on both kernels with a throwaway test, deleted afterwards. It is a different mechanism, so it is reported to the seat as a finding and not changed here. The save was
{ name: 'showcase_task.in_progress', object: 'showcase_task', list }asshowcase_task.in_progress: package-less and environment-wide, package-less and organization-scoped, and in a writable package.showcase_task.in_progress, and the by-name read answers the raw container.showcase_task.formreads the same.showcase_task.defaultreads the same from a writable package; package-less, 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 check refuses it.listthen expands toshowcase_task.defaultand replaces the packaged default on both doors, stamped_packageId: com.example.showcase.runtimeViewContainerPackage.Tests
Premise, measured at the fix's own pins with the new throw ablated (the measurement half of the reverse verification below), and confirmed by the throwaway door probe on both kernels and both scopes:
crm_lead.pipeline;crm_lead.defaultanswers the second container's list on both doors.With the fix: refused
VALIDATION_ERROR/ 400, and both doors answer the first container's views.New pins: 50, in a
#21620block inview-container-runtime-expansion.test.ts, inside metadata: a view container with a bare list on another package's object silently replaces that object's packaged default view on GET /meta/view?object= — while the by-name read still serves the original #21334's faithful-registry harness.env_localand unscoped) and per scope (environment-wide and organization-scoped):list,listViews,form,formViews);name, and in draft mode;form-only body;codeandstatus) and the two subjects it names. It also asserts that no row or draft is stored, that no container is registered under the name, and that the sibling's view still answers on both doors as the same item.The file: 231 passed (181 pre-existing plus 50), at
13736a50f6.The package, at
13736a50f6(patch round 1; round 0 read 3371 passed at5573152989):pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 209 passed, 3 skipped (212); Tests 3373 passed, 19 skipped (3392); exit 0.typecheck: exit 0.tsc --noEmit --listFilesincludes the test file.Downstream sample. Direction: consumers of
@objectstack/metadata-protocol, againstdist/rebuilt byturbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2(71 of 71 tasks). All pass; the rest of the downstream run is CI's.objectql:protocol-meta,protocol-view-identity-overlay,protocol-org-overlay-registry-gate,protocol-commit-history,view-container-divergent-name-registrars,engine-nested-plugin-view-expansionandmetadata-validation-sweep: 7 files, 189 tests.rest:public-form-routes.stored-row, 7 tests.dogfood: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 on the real showcase), 4 tests.The merge of
origin/main(719644794c) moves no byte underpackages/metadata-protocolor its dependency closure. So the suite, typecheck and ablation readings taken at5573152989read the same bytes.Reverse verification
The fix was committed first (
5573152989). A trap-guarded script then ranscripts/ablation-replace.mjs --deleteon the anchorif (siblingExpansionRefusal) throw siblingExpansionRefusal;.771b82e97372to1cb3741ebb3a.expected null to be an instance of Error(the save accepted). 1 answeredINVALID_METADATAinstead ofVALIDATION_ERROR(theform-only cell on the unscoped environment-wide kernel, the identity-stamp row in the table).git checkout HEAD -- ABS_PATHbrought the blob back to771b82e97372, equal to HEAD, andgit diff HEADwas empty. Both the tool and the script's own trap proved this.13736a50f6, two legs:56bc12dce760, equal to HEAD, withgit diff HEADempty../index.js, the source.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 64 families at719644794c. 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-test,release-pending-publish --self-test,check:objectui-changesetandcheck:pm-changeset-deadline-census.pnpm check:lean-entry-closurefirst exited 3 (PREREQUISITE NOT MET:objectql'sdist/was absent). It exited 0 after the workspace build.check:dual-build-cjs-loadsandcheck:type-check-debtran through the verify lock, after the build.--ran: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.13736a50f6: the same 64 families, all exit 0;--ran64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.check-adr-0087-registrationaccepts the changeset'snot-required (no-migration-prescription)disposition.eslint --no-inline-config --format jsonover the 2 changed.tsfiles reports 2 files, 0 errors and 0 warnings (no "file ignored" message). Type-aware linting is never enabled (eslint.config.mjs:327-328;--print-configshowsparserOptions.projectandprojectServiceboth null), so files this diff does not touch cannot change verdict. Repo-widepnpm lintis CI's.PUT /api/v1/meta/view/NAMEon a booted stack) is NOT MEASURED. The pins are in-process at the method that door calls, on both kernels, and the envelope is the one the same door's name refusals already carry through REST.Acceptance notes
The ruled predicate's "same object", read literally, leaves two measured shapes open. Both are accepted on both kernels and in both scopes, and the sibling's view is gone from both doors:
{ object: 'crm_account', list }saved ascrm_lead.pipeline);{ list }, whose derived object is its own name).Both are refused if the check drops "of the same object" and keys on the name alone. No writer in the census saves either shape, so that change would refuse nothing legitimate. It is raised to the seat as an open question, not taken here, because the ruling's words name the same object.
A second container under a free name still takes a sibling's expanded name. Two containers of one object whose expansions share a name (both bare
lists givecrm_lead.default) are both accepted, and the later one's view answers that name on both doors. This is the card's step-3 symptom without this save, measured on both kernels and in both scopes. It is reported to the seat as a finding.Scope and order:
Both are measured, and both are noted rather than filed.
Restore and publish doors.
rollbackMetaItem,revertCommitand the draft promotion do not run this check, 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. A draft or version stored before this change, or a draft stored before its sibling existed, can still be written back in this shape. Kept to the claimed surface.Cost. A save of a view container (only a container) reads the caller's active view rows once more, through the same overlay row cache the readers use. A view item pays nothing.
Declaration bytes.
dist/index.d.tsgains one private member line. No public member or exported type changes..changeset/21620-container-sibling-expansion-name.md:'@objectstack/metadata-protocol': minor,Clause-②: no (narrowing), the BREAKING banner, and the ADR-0087 dispositionnot-required (no-migration-prescription).Generated by Claude Code