Repository navigation
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
|
…nd an unknown --object is refused (objectstack-ai#21662) Fixes objectstack-ai#21644 Clause-②: no A deployment-level flag is now written only by a full-scope run. `os migrate value-shapes` and `os migrate files-to-references` narrowed by `--object` apply their fixes, record no deployment flag, and say so. A full-scope `--apply` records the flag exactly as before. Across the family (`value-shapes`, `files-to-references`, `summary-nulls`, `duplicates`), an `--object` name the deployment does not declare is refused with `OBJECT_NOT_FOUND` before anything is read or written. The refusal names the unknown name and the declared objects. This follows triage ruling `5974774596`. `--apply --object` is not refused. ## Measured first (base `759dbe9ed3`) ### A1. The reach, at the public door (hypothesis confirmed) A throwaway SQLite project held three objects, one of them `os21644_site` with a `location` field. A served-shape boot seeded one clean row per object. Then one off-shape value was written past the write path: the site's `geo` stored as `{latitude, longitude}`. The fresh-datastore attestation had recorded both ADR-0104 flags as verified at birth, so the flag table was emptied first. - `os migrate value-shapes --json` exited 1, with `gatePassed: false` and `blocking: 1`. - `os migrate value-shapes --object os21644_sitee --apply --yes --json` (misspelled) exited **0**. It answered `gatePassed: true` with `scannedObjects: []`, and the `adr-0104-value-shapes` row read **verified** (`verified_at` set, `blocking: 0`). ### A2. The census, one row per command | command | `--object` | `--apply` records a deployment flag | where it is written (base) | unknown `--object` on base (measured) | | --- | --- | --- | --- | --- | | `value-shapes` | repeatable | `adr-0104-value-shapes` | the CLI: `recordDataMigrationRun` at `value-shapes.ts:221` | exit 0, `scannedObjects: []`; with `--apply`, the flag is recorded **verified** | | `files-to-references` | repeatable | `adr-0104-file-references`, then the column step's `columns_moved_at` | the producer: `runFilesToReferencesMigration` at `files-to-references-migration.ts:120`; the column stamp is `recordFileColumnMove` in the CLI (`files-to-references.ts:472`) | exit 0, both scans' `scannedObjects: []`; with `--apply`, the flag is recorded **verified**, **and the column step moved `os21644_product.image` and stamped `columns_moved_at`** | | `summary-nulls` | repeatable | none (its header: "No deployment flag, deliberately") | none | exit 0, `fields: []`, on a dry run and on `--apply` | | `duplicates` | single | none: no `--apply`, and it writes nothing | none | exit 0, `scanned: []`, `filter: { object: 'os21644_sitee' }` | A correctly spelled narrowed `files-to-references --apply` on base also recorded the flag verified and moved the column. Every scan draws its default candidates from the same registry: `options.objects ?? Object.keys(engine.getConfigs())` in `scanValueShapes`, `backfillFileReferences`, `verifyFileReferences` and `backfillSummaryNulls`, and `stack.allObjects()` for `collectScanTargets`. Each keeps only the candidates it covers, which is where an undeclared name was dropped. ### A3. The narrowed run - **CLI-recorded flag (`value-shapes`).** The flag write is skipped on a narrowed run, whether the run passes or fails. `--json` carries `flag: null` and `filter: { objects }` (`null` on a full-scope run, the shape `duplicates` already keeps). Both faces print one sentence: the run was narrowed, no deployment flag was recorded, and the command that records one is the same command without `--object`. - **Producer-recorded flag (`files-to-references`).** `runFilesToReferencesMigration` skips the write when it is given `objects`. This is the declared `service-storage` path only. Its `flag` result is `null` on a narrowed run. The CLI prints the same sentence and carries `filter`. - **The column step (`files-to-references`) does not run on a narrowed run.** The census row above is why. The step retypes every single-value media column in the database on the authority of the gate, and a narrowed gate vouches only for the named objects. Its stamp also requires a verified flag, which a narrowed run no longer records. Left running, a narrowed `--apply` would move columns and then fail to record the move. It now returns a stated skip, `narrowed_run`, and the human face says why. - **What "narrowed" means.** Any `--object` narrows, even a list that names every declared object. The flag is earned by the one spelling that means "every object", which is a run without `--object`. Treating a full list as full scope would need a second definition of "the whole deployment", checked against the registry of the moment, and that registry changes with the composition between two runs. The operator also gets one unambiguous prescription. - **Deviation from the dispatch wording ("skips it when `objects` is non-empty").** The producer treats **any** `objects` as narrowed, `[]` included. A scan handed `[]` walks nothing (`[] ?? …` is `[]`). A non-empty test would therefore record a verified flag over an empty scan, the card's own defect at the producer's API. A unit pin holds this. - The prompts and closing lines that promised a flag on a narrowed run now say it records none. ⛔ `--apply --object` is not refused, and the full-scope write is unchanged. ### A4. Unknown `--object` - **Checked against the registry the command's own boot resolved, before the scan.** For `value-shapes`, `files-to-references` and `summary-nulls` that registry is `Object.keys(engine.getConfigs())`. For `duplicates` it is the names of `stack.allObjects()`. These are the same sets the scans draw from, so the refusal and the scan judge one population. There is no `packages/objectql` edit and no scanner edit. - **The refusal is objectstack-ai#21643's.** It is `objectNotFoundError` from `@objectstack/core`: `code: 'OBJECT_NOT_FOUND'`, `status: 404`, and `object` naming the first unknown name. Its message names every unknown name and the declared objects, sorted. There is no new error code. `value-shapes`, `files-to-references` and `summary-nulls` answer `{ error, code }`, as `unmapped-columns` does. `duplicates` keeps its own error shape, `{ error: 'report_failed', detail, code }`: its catch now passes `errorCodeFields` through. - **The list is the declared set, not the covered subset.** Computing the covered subset for `value-shapes` needs `isScannableValueShapeField`, which `@objectstack/objectql` does not export, and that package is fenced. The declared set is also exactly the accept set. A declared object the command has nothing to check on is accepted, because an empty answer about a real object is true. On the fixture boot the list is 12 names, platform objects included. - **Clause-②: no stands as the claim declared it.** A misspelled name moves from exit 0 to exit 1, which is the ruled correction of a wrong answer. Every declared name and `--apply --object` are still accepted. ## Changes - `packages/cli/src/utils/migrate-object-scope.ts` (new): `refuseUndeclaredObjects`, `isNarrowedRun` and `narrowedFlagNote`, shared by the four commands. - `packages/cli/src/commands/migrate/value-shapes.ts`: refuses an unknown name, skips the flag on a narrowed run, adds `filter`, and adjusts the narrowed prompt and closing lines. - `packages/cli/src/commands/migrate/files-to-references.ts`: refuses an unknown name, adds the `narrowed_run` column-step skip, adds `filter`, and adjusts the narrowed prompt and closing lines. - `packages/cli/src/commands/migrate/summary-nulls.ts` and `duplicates.ts`: refuse an unknown name. `duplicates`' error document carries the error's `code`. - `packages/services/service-storage/src/files-to-references-migration.ts`: skips the flag write when given `objects`. - `content/docs/deployment/cli.mdx`: one paragraph under "Data migrations" (`--object` narrows, an unknown name is refused, only a full-scope run records a flag), and the two `--object` example comments. - `.changeset/21644-narrowed-apply-flag.md`: `@objectstack/cli` patch and `@objectstack/service-storage` patch, `Clause-②: no`. `packages/objectql`, `packages/platform-objects`, `packages/spec`, every other `service-storage` path, and `content/docs/releases/` are untouched. ## Pins - **`object-scope.integration.test.ts`** spawns the CLI against SQLite, one database copy per run, and is one enumeration over the census (`FAMILY`). - A narrowed `--apply` (`value-shapes`, `files-to-references`): exit 0, `flag: null`, `filter: { objects }`, no flag row, and the note on stderr naming the full-scope command. - A full-scope `--apply`: the flag recorded verified, in the document and in the row. - `summary-nulls` and `duplicates`: no flag row, narrowed or not, as before. - A narrowed `--apply` after an earned flag leaves that row byte-equal. - `files-to-references` narrowed: `columnMove: null` and `columnsMovedAt: null`. Its full-scope control moves `os21644_product.image` and stamps it. - Unknown `--object`, on all four: exit 1 and `OBJECT_NOT_FOUND`, naming the name and the declared objects. The one document is the refusal and no report, no flag row is written, and the app rows are unchanged. The human face exits 1 and names it. - The measured repro. Control: the full-scope scan sees `blocking: 1` and exits 1. The misspelled `--object --apply` exits 1 with `OBJECT_NOT_FOUND`, and the flag stays unrecorded. Spelled right, the narrowed run finds the value, exits 1, and still records no flag. - **`migrate-object-scope.test.ts`** (unit): the envelope (`code`, `status`, `object`), every unknown name named once, the declared list sorted, the empty-registry message, the accepted cases, and what `isNarrowedRun` treats as narrowed (an empty list and a full list both narrow). - **`files-to-references-migration.test.ts`** (`service-storage`, beside the producer): - a narrowed apply converts and records no flag; - a narrowed failing apply records nothing; - a narrowed apply leaves an earned flag row equal; - `objects: []` records nothing. ## Reverse verification (implementation committed first; all three legs re-run at the final head `fe988c20f0`) Each leg ran through `node scripts/ablation-replace.mjs` in wrap mode, under a script trap that restores from `HEAD`. The spawned CLI loads its commands from `src/` through `bin/run-dev.js`. In the first round `packages/cli/dist` did not exist. In the final round it held a build of the unmutated source, and legs 1a and 2 still went red, which shows the spawned CLI read the mutated `src/`. The `service-storage` unit pin imports the producer from `src/`. Neither needed a rebuild. - **Leg 1a, the narrowed-run skip in the CLI** (`value-shapes.ts`): - The anchor `if (apply && !narrowed) {` became `if (apply) {`: anchor 1 to 0, replacement 0 to 1, blob `9f241dc2` to `d3a5c236`. - **3 red, 17 green.** Red: the `value-shapes` narrowed pin, the earned-flag-unchanged pin, and the spelled-right repro. Green: both full-scope controls (the column-step control among them), the `files-to-references` narrowed pin (its skip is the producer's), and every unknown-name pin. - Restored: blob `9f241dc2` equals `HEAD`, and `git diff HEAD` is empty. - **Leg 1b, the narrowed-run skip in the producer** (`files-to-references-migration.ts`): - The same anchor and replacement: anchor 1 to 0, blob `1aa9fea2` to `d4bb5002`. - **4 red, 6 green.** Red: all four narrowed pins (passing, failing, earned-flag-unchanged, and `objects: []`). Green: the six original pins, the full-scope apply among them. - Restored: blob `1aa9fea2` equals `HEAD`. - **A first round is recorded here because one of its readings was vacuous.** At `80e5eda6ea` this leg read 3 red and 7 green: the earned-flag-unchanged pin stayed green under the mutation, because the fake engine's rewrite landed in the same millisecond as the earned row. The pin now dates the earned row in the past (`fe988c20f0`), and the re-run is the reading above. - **Leg 2, the unknown-name refusal** (`migrate-object-scope.ts`): - The anchor `if (unknown.length === 0) return;` became `if (unknown.length >= 0) return;`: anchor 1 to 0, replacement 0 to 1, blob `b78a88b4` to `9286a990`. - **Unit: 3 red, 3 green.** Red: the three refusal cases. Green: the accepted cases and the two `isNarrowedRun` cases. - **Integration: 10 red, 10 green.** Red: all eight unknown-name pins (two per command, on all four), the human face, and the misspelled repro. Green: every narrowed and full-scope pin, and the repro's control. - Restored: blob `b78a88b4` equals `HEAD`. - In the first round, the `duplicates` "refused before anything was read" pin stayed green under this mutation: it asserted only on a key that report never carries. It now asserts that the one document is the refusal, which reds on all four commands. After all legs, `git diff HEAD` was empty and `git status --porcelain` was clean. ## Local verification (final head `fe988c20f0`, on base `759dbe9ed3`) `origin/main` was `759dbe9ed3` for the whole verification. Just before this PR opened, it gained four commits, `f40bb3217f` to `1a230548cf` (objectstack-ai#21649, objectstack-ai#21632, objectstack-ai#21648, objectstack-ai#21650). None of them touches this diff's paths (`packages/spec`, `metadata-protocol`, `service-automation`, `lint`, skills and docs references), so they were not merged in. CI runs on the merge ref. - **Builds.** The CLI's dependency closure (`turbo run build --filter=@objectstack/cli^...`) gave VERDICT 0. `@objectstack/service-storage` was rebuilt after the producer change (exit 0), and `@objectstack/cli` was built (exit 0). A repo build for the gate prerequisites gave VERDICT 0 (turbo: 72 tasks, 71 cached). - **`@objectstack/cli` typecheck** (`tsc --noEmit` plus `check:test-typecheck`): exit 0 at `80e5eda6ea`. No CLI file changed after that commit. - **`@objectstack/cli` unit project in full** at `80e5eda6ea`: - 255 of 257 files passed, with 3742 tests passed and 29 skipped (the two files below). - The other two files, `test/published-subpath-{console,hook-body}.pin.test.ts`, refused before testing because `packages/cli` was not built (their own prerequisite message). After the CLI build, both passed: 2 files, 29 tests. - **`@objectstack/service-storage`**: typecheck exit 0, and the full suite at `fe988c20f0` passed 41 files and 633 tests. - **`os migrate` integration pins on built packages**, at `80e5eda6ea`: - this PR's pin plus the absent-database roster: 2 files, 59 passed; - the one-shot family plus `duplicates.integration`: 2 files, 79 passed. - **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) derived 97 commands at `fe988c20f0`. - All 97 ran there, and each exited 0. - `--ran` with exit codes: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN. - An earlier round at `80e5eda6ea` had three gates answer `PREREQUISITE NOT MET` (exit 3): `check:skill-examples`, `check:dual-build-cjs-loads` and `check:i18n-coverage`. They read packages outside the CLI closure. The repo build cleared them. - **Full `pnpm lint`** (`eslint . --no-inline-config` over the whole repo): exit 0 at `fe988c20f0`, with nothing printed. - **No exported symbol was renamed or moved,** so the liveness-ledger anchor check had nothing to read. ## Acceptance notes - **A narrowed run's counterexample is not recorded.** The ruling says a narrowed `--apply` records no flag, so it records none even when it finds a violation. Such a counterexample is deployment-level evidence, since one off-shape value disproves "every value is on shape". The operator still gets exit 1 and the findings, and the next full-scope run closes the gate. This is an observation, not a filing. Carrier: none. - **The declared list includes platform objects.** It is 12 names on the fixture's lean boot, and a deployment that composes more plugins prints more. A long list in an error message is the price of naming the exact accept set. Carrier: none. - **A narrowed `value-shapes --apply` still takes the plain (DDL-performing) boot** even though it now writes nothing. That is unchanged, and the boot paragraph in the docs still describes it truthfully. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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