fix(metadata-protocol): the save door refuses a view container saved under a name its own expansion produces (#21558) - #21618
Conversation
…under a name its own expansion produces
A container such as { name: 'crm_lead.default', object: 'crm_lead', list }
saved as crm_lead.default is the stored row of a name its own bare list
expands to. Both read doors give a name with a row of its own that row and
never let an expansion fill it, and the object door never enumerates a
container, so no door answered a view item for the name. The save door now
refuses the shape with VALIDATION_ERROR / 400 and the prescription: save the
container under its object's name, or save a view item under the expanded
name. The predicate is the readers' own expansion (expandRuntimeViewContainer),
so every member kind and the expander's de-duplication are judged as the
readers place them. The read doors are unchanged.
Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
…n expanded name Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…ntainer-own-expansion-name
📓 Docs Drift CheckThis PR changes 1 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 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 783124618cf35c7bda447b1befd8552cb2eaba03 && git checkout 783124618cf35c7bda447b1befd8552cb2eaba03
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 440cd329a96a3b130ba9c2fdc048a241e7a019a0 079069661f0cd6607d5e59cb7030759ce85df9ee && git checkout -B drift-repro 440cd329a96a3b130ba9c2fdc048a241e7a019a0 && git merge --no-ff 079069661f0cd6607d5e59cb7030759ce85df9ee
node scripts/docs-audit/affected-docs.mjs --json 440cd329a96a3b130ba9c2fdc048a241e7a019a0
|
Contract reviewServed-tier: Rendered by an isolated at-tier reviewer and adopted by Inputs read:
① Derived judgmentsEvery accept-set and public-surface change the diff implies, judged against the head's source:
② Semver level
③ Boundary flagsThe dev's
Implemented-by: VERDICT: PASS |
Fixes #21558
Clause-②: yes (narrowing) — the runtime save door's accept set narrows: a view container saved under a name its own expansion produces is refused with
VALIDATION_ERROR/ 400. Nothing widens.What changed
saveMetaItem(packages/metadata-protocol/src/protocol.ts), the method behindPUT /api/v1/meta/view/NAMEand the dispatcher's metadata save, now refuses a view container saved under a name its own expansion produces. The card's case is{ name: 'showcase_task.default', object: 'showcase_task', list }saved asshowcase_task.default, the name its barelistexpands to. This implements triage's ruling 5966930701: refuse at the write door, with a named error and the prescription. ⛔ The read doors are not changed, and no stored row is re-saved.containerOwnExpansionNameRefusal, is called right after the name check placed at this door earlier (savedItemNameRefusal). It runs before the view identity stamp (normalizeViewMetadata).expandRuntimeViewContainer, whether the save name is one the container expands to. It does not copy the naming rule. So every member kind (a bare or namedlist,listViews,form,formViews) and the expander's de-duplicated names (_2) are judged where the readers place them. A container on another package's object expands under its own name (the arm from 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) and is never refused. A body with nonameis judged under the save name the door stamps on it.VALIDATION_ERROR/ 400, the same as the name check it sits beside. H4: the existing save-door name refusal uses this code, so no new code is minted and the error-code ledger is untouched.Every accept-set change at
saveMetaItem, typeviewVALIDATION_ERROR/ 400. Nothing is stored or registered.name(the door stamps the save name).form, saved under its own expanded name where the registry already holds a view item of that name.INVALID_METADATA/ 422. The identity stamp copied that item'sviewKindonto the body, so the schema saw a malformed view item.VALIDATION_ERROR/ 400 by this check, which now runs first.What still saves (the ruling's controls, pinned on both kernels and in both scopes)
Stored rows and the other writers through this door
migrateStoredMetadata(os migrate meta --stored) andduplicatePackagere-save stored rows through this door. For such a row they now record the refusal: migration as afailedrow with this reason, duplication as afailed[]entry. Neither re-saves it.Census (taken before the refusal was written)
At BASE
37442d4750, objectui at its pin89cad75d55:saveMetaItemare RESTPUT /api/v1/meta/view/NAMEand the dispatcher (runtime/src/domains/meta.ts:1424), which pass the caller's body through, plusmigrateStoredMetadataandduplicatePackage.runtime/src/domains/packages.ts:1583savesapponly, andruntime/src/domains/automation.ts:1628savesflowonly.app-shellObjectView.tsx:1471saves throughbuildViewConfigSaveBody, and:1530throughviewEnvelope.ObjectDataPage.tsx:381usescreateRuntimeMetadata. All three write a view item (viewKind: 'list').data-objectstackindex.ts:5522(setViewConfig) and:5811(createView) write flat view configs.updateViewreduces a container it reads to itslist(:5895).PublicFormsPage.tsx:229/301saves items listed bygetMetaItems, which never lists a container.createBuildBody(anchors.ts:291) emits a view item.ResourceEditPage.tsx:1477) saves a body under its ownname. It produces the refused shape only if an author types an expanded name into a container'sname.service-aiwas removed in21d4f8901b(the open edition is MCP-only, ADR-0025 S2), and the MCP tool list inmcp-http-tools.tshas no metadata write. The publishedskills/objectstack-uitells authors to writedefineViewcontainers in source. Those go through the source registrars, which refuse a containernamethat disagrees with its object. The cloud AI author is outside this repository: NOT MEASURED.defineView(sites inexamples/(crm 3, showcase 7, todo 2), and none carries a top-levelname.platform-objectscarries object-levellistViews, not view containers. Structurally, a source registrar files a container under its object and refuses anamethat disagrees with it, and an expanded name (OBJECT.KEY) is never the object's name. So a packaged container under its own expanded name cannot boot.sys_metadataview rows. In this repository's tests, no suite stores this shape: the fullmetadata-protocolsuite and the downstream samples below stay green with the refusal on. Hosted tenants: NOT MEASURED.Tests
37442d4750, pins present, refusal absent).vitest run src/view-container-runtime-expansion.test.ts -t '#21558'gave 27 failed / 9 passed:expected null to be an instance of Error, meaning the save was accepted. These are 16 member cells, 4 draft cells and 3 runtime-object cells.formcells answeredINVALID_METADATA/ 422 (the identity-stamp row in the table above).save=accepted objectDoor=[] byName=raw container. With the fix:save=refused VALIDATION_ERROR/400on both kernels.079069661f:pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 207 passed | 3 skipped (210), Tests 3248 passed | 19 skipped (3267),VERDICT command-exit 0.typecheck(tsc --noEmit):VERDICT command-exit 0.tsc --listFilesincludes the test file.view-container-runtime-expansion.test.ts:crm_lead.default,crm_lead.pipeline, the de-duplicatedcrm_lead.default_2, and an unnamed container, plus a control.@objectstack/metadata-protocol, built withturbo run build --filter='@objectstack/metadata-protocol...' --filter='@objectstack/objectql^...' --filter='@objectstack/rest^...', 24 tasks,VERDICT command-exit 0:protocol-meta,protocol-view-identity-overlay,protocol-org-overlay-registry-gateandprotocol-commit-history(4 files, 162 tests), plusmetadata-validation-sweep,view-container-divergent-name-registrarsandengine-nested-plugin-view-expansion(3 files, 27 tests).public-form-routes.stored-row(1 file, 7 tests).Reverse verification
The fix was committed first (
df4fcd4636). A trap-guarded script then ranscripts/ablation-replace.mjson the anchorif (ownExpansionRefusal) throw ownExpansionRefusal;:19953a79f484to96e1e0adefea.-t '#21558|PROBE'gave 28 failed / 10 passed: every refusal pin went red, and the 9 controls plus the probe stayed green. The direction is red, as predicted.git checkout HEAD -- ABS_PATHbrought the blob back to19953a79f484, equal to HEAD, andgit diff HEADwas empty. Both the tool and the script's own trap verified this../index.js(the source), not through a packageexports.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) at079069661fderived 64 families: the dispatch lead's 50, plus the ones this changeset and the test added.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: dists missing). After a workspace build it measured 106 entries in 66 packages and exited 0.--ran: 64 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, 0 warnings. The changeset.mdhas no matching ESLint configuration. Type-aware linting is never enabled (eslint.config.mjs:326-328, and--print-configshows noparserOptions.project), so files this diff does not touch cannot change verdict. Repo-widepnpm lintis CI's.PUT /api/v1/meta/view/NAMEon a booted stack): NOT MEASURED. The pins are in-process at the method that door calls, on both kernels.Acceptance notes
crm_leadwithlistViews.pipelineis stored, then{ name: 'crm_lead.pipeline', object: 'crm_lead', list }is saved ascrm_lead.pipeline. The save is accepted. The object door then lists nothing undercrm_lead.pipeline, and itscrm_lead.defaultbecomes the second container's list. The by-name read answers the raw container. The ruling covers only a name the container's own expansion produces.form.viewIdentityPatchleaveslist,listViewsandformViewscontainers alone, but not a container whose only member isform. Saved under the name of a registered view item, such a container takes that item'sviewKindand is refused 422 as a malformed view item. For its own expanded names this check now answers first. Under any other view item's name, that is the sibling shape above.rollbackMetaItem,revertCommitand the draft promotion do not run this check. A version or draft stored before this change can still be written back in this shape. A new draft in this shape can no longer be stored. Kept to the save door per the claimed surface.dist/index.d.tsgains one private member line. No public member or exported type changes.#21510 and #21511 are context only; this PR leaves both as they are.
Generated by Claude Code