Skip to content

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

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-21639-view-container-collision
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-21639-view-container-collision

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

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:engine seat 1 under claim 5974259402 (file surface corrected by 5974270195), branch claude/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.

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 merging origin/main (head d5101d1831). objectui was read at the old pin 89cad75d55 and at the new pin ab1879721595, which origin/main moved to while this ran.

Census input Evidence A second container of one object, a container under another container's expanded name, or a container under a shipped view item's name, on purpose?
#13407's authoring path: the platform checklist's live view-authoring item studio-authoring.view-authoring-live (P1, revision 2) Step 1 saves ONE container, qa_repair_asset_views = { object: 'repair_asset', list, form }, on a runtime-authored object. No other checklist item stores a container on repair_asset. #13407's own repro (a container bound to note under another name, per PR #21637's census) is one container too. No.
#21412's P2 / P2b (seat answer 5961930912) One container, lead_views, bound to crm_lead. Pinned in the #21412 block of the same test file, still green. No.
#21334's arm (rulings 5946423948, 5955628428) It expands under the container's own name, OBJECT.CONTAINER_NAME and OBJECT.CONTAINER_NAME.KEY, never a name the owning package ships. Pinned as two allowed rows below. No.
Studio's metadata editor re-save (objectui at both pins) Creators write view items: the metadata-admin createBuildBody (anchors.ts:291, "Emit a canonical ViewItem"), the spec create seed for view (metadata-create-seeds.ts:62, viewKind: 'list'), and the flat configs of data-objectstack createView and setViewConfig. Re-savers save the loaded body under the name it carries: ResourceEditPage (:1477), the Interfaces pillar's StudioDesignSurface (:2475, new at ab1879721595), and updateView. No creator writes a container. A re-saver writes a colliding container only when the store already holds the collision, and that re-save is now refused until its body stops colliding.
Package duplication (duplicatePackage, a writer through this door, outside the ruled set) Throwaway probe, deleted afterwards. A container pone_extra_views in com.example.pone, bound to crm_lead (outside the package, shipped by no code package), duplicated into com.example.ptwo. At BASE: copied, and the object door's crm_lead.default became the copy's, wearing _packageId: com.example.ptwo. The source package's view was silently replaced. At HEAD: failed[] carries this refusal, naming pone_extra_views, and the source's view stays. Yes, as a byproduct; not on purpose, as this dev reads it. The copy is meant to be an independent base, and the collision is the defect itself. Raised as an open question in the report.
migrateStoredMetadata It re-saves rows already stored, inside a try that records outcome: 'failed'. Not a creator.
The in-repo AI author The MCP tools in packages/mcp/src/mcp-http-tools.ts have no metadata write. skills/objectstack-ui teaches defineView in source. No. The cloud AI author is outside this repository and the ruled set: NOT MEASURED.
Packaged containers and stored rows Source registrars never reach this door. The example apps seed no sys_metadata view rows. Hosted tenants: NOT MEASURED. n/a

The attribution half: does a live overlay path depend on a package-less container row taking a shipped item's package?

  • Callers. runtimeViewContainerPackage is called by expandRuntimeViewContainer (and by the new overlaidShippedContainerViewNames). expandRuntimeViewContainer is called by expandStoredViewContainers (the list read, the by-name read's resolveRowlessExpandedView, and this predicate), by hydrateExpandedViewItems (the registry), and by the predicate itself.
  • The overlay that does take a package is unchanged and pinned (CONTROL): a package-less row under the package's own container name. That is the path behind the checklist's packaged-display-class-direct-edit designer overlay.
  • No writer stores a package-less container under a view item's name. Studio's creators write view items, the save door now refuses the shape, and the restore doors and draft promotion re-write only bodies already stored.
  • Verdict: no live overlay path depends on it, so the ruling's attribution lands.

Every accept-set change at saveMetaItem, type view

Each row covers publish and draft mode, both scopes and both kernels. "Stored container" means one in the caller's selection.

Input Before (BASE 7b07749f05) After
A container bound to another object, under a name a stored container expands Accepted: stored, and registered on an unscoped kernel. The sibling's view under that name was served by neither door, and the by-name read answered the raw container. Refused VALIDATION_ERROR / 400, naming the stored container. Nothing is stored or registered.
An unbound container under such a name Same as above. Same as above.
A container whose expansion takes a name a stored container already expands: a second container of one object whose bare list takes OBJECT.default, under a free name or under the object's name Accepted. The one read last replaced the other's view on both doors. Refused, naming the stored container.
A container under the name of a view item a package ships: package-less, organization-scoped, or bound to a writable package Accepted. The packaged view was no longer listed on the object door, and by-name answered the raw container. Package-less, the row also took the shipping package, so its bare list replaced OBJECT.default on both doors with that package's _packageId. Refused, naming the package.
An overlay of a package's own container whose member takes the name of a view item the package ships on its own Accepted. The member replaced that packaged view on both doors. Refused, naming the package.
A container under its own expanded name (#21558), or under a name another stored container of the same object expands (#21620) Refused VALIDATION_ERROR / 400. Refused, with the same envelope. The prescription now comes first, and the own-expansion arm no longer prescribes a save under a name a stored container holds.
A form-only container under a registered view item's name Refused VALIDATION_ERROR / 400 under #21558 or #21620. Under any other registered name: 422 from the identity stamp. Every shape this predicate covers is refused VALIDATION_ERROR / 400 first, before the stamp.
Everything else Unchanged. Unchanged.

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:

  • On that package's object it expands under its own name (showcase_task.showcase_task.in_progress), with no _packageId and no default.
  • The packaged views it used to replace (showcase_task.default, or showcase_task.edit for a listViews.edit member) are served again on both doors (pinned).
  • The row still takes its own name's slot on both doors. That is the read doors' name-keyed overlay, out of surface and unchanged.

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_local and unscoped) and both scopes, unless the row names one scope.

  • A refused row asserts:
    • code and status;
    • the save name, the colliding name and the owner;
    • the owner and the view-item prescription inside the first 500 characters;
    • that no stored row's name is ever prescribed as a name to save under;
    • in publish and in draft mode, that nothing is stored or registered;
    • that the owner's view still answers on both doors.
  • An allowed row asserts that it saves, and that each named view answers on both doors.
  • The structural tests fail when:
    • a row is neither refused nor allowed, or is both;
    • a ruled shape has no row;
    • a cell of the predicate has no refused row.
# Shape Verdict Owner named / reason
1 A container under a name its own expansion produces (#21558's own-expansion name) REFUSED its own expansion
2 The same, while a stored container holds its object's name REFUSED its own expansion; the prescription names that container
3 A container of the same object under a name a stored container expands (#21620's sibling) REFUSED the stored container crm_lead
4 A container bound to another object under that name ((1)'s other-object container) REFUSED the stored container crm_lead
5 An unbound container under that name ((1)'s unbound container) REFUSED the stored container crm_lead
6 A second container of one object, free name, bare list on OBJECT.default ((2)'s second container default) REFUSED the stored container crm_lead
7 A container under its object's name, after a free-named container took OBJECT.default REFUSED the stored container lead_other_views
8 A package-less container under a shipped view item's name (#21638's shipped item name) REFUSED the package com.example.showcase
9 A writable-package container under a shipped view item's name REFUSED the package com.example.showcase
10 An overlay of the package's container whose new member takes a view item the package ships on its own REFUSED the package com.example.showcase
11 A view item under a stored container's expanded name (allowed: #21510's sanctioned override) ALLOWED a view item is not a container: it is that name's sanctioned override
12 A view item under a shipped view item's name ALLOWED the sanctioned override of the packaged view by name
13 A container under its object's name (allowed) ALLOWED the container contract's own name (ADR-0017 §3.2), and nothing it expands is served elsewhere
14 A container's own re-save ALLOWED the row under the save name is the row this save replaces
15 A container under a name of its own beside a sibling, with names the sibling does not expand ALLOWED the census writers' shape, colliding with nothing
16 An overlay of the package's own container, by its own name ALLOWED ADR-0005: the row stands in for the shipped container and its views
17 A package-less container on another package's object ALLOWED #21334's arm: its names derive from its own name
18 The same, in a writable package ALLOWED #21334's arm, bound to its own package
19 Under another organization's container's expanded name (organization scope) ALLOWED the caller's selection decides, as it does for the readers
20 An environment-wide container under an organization's container's expanded name (environment scope) ALLOWED the caller's selection decides; reading every organization's rows at an environment-wide save would be a cross-tenant read

The PM's mechanism hypotheses

Tests

All readings are at HEAD d5101d1831 unless named otherwise.

  • Premise.
    • Method: the new pins, run against BASE's protocol.ts (blob 56bc12dce760), restored by a trap-guarded script. Restore proof: blob e5765e5ba0f9 equal to HEAD at 0af0f28e49, and git diff HEAD empty. The command: vitest run src/view-container-runtime-expansion.test.ts -t '#21639|#21638'.
    • Result: 40 failed, 50 passed (231 skipped).
    • Red:
      • the 7 newly refused shapes × 2 scopes × 2 kernels (28), each failing on the save is refused;
      • row 2 × 4, where BASE's arm reads "Save the container under its object's name, 'crm_lead'" while a stored container holds crm_lead;
      • the 8 attribution pins, where the packaged label and config were replaced.
    • Green: rows 1 and 3, every allowed row, the 2 structural tests, and the 4 attribution controls.
  • New pins: 90.
    • The enumeration block: 2 structural tests + 76 row cells.
    • The #21638 attribution block: 2 at-rest cases + 1 control, × 2 scopes × 2 kernels.
  • The file: 321 passed (231 pre-existing + 90).
  • The package: pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 gives Test Files 209 passed, 3 skipped (212); Tests 3463 passed, 19 skipped (3482); VERDICT command-exit 0.
  • Typecheck: pnpm --filter @objectstack/metadata-protocol typecheck (tsc --noEmit) exits 0, and tsc --noEmit --listFiles includes the test file.
  • Downstream sample.
    • Direction: consumers (downstream) of @objectstack/metadata-protocol.
    • Build: against dist/ rebuilt at d5101d1831 by turbo run build --filter='@objectstack/dogfood^...' --filter='@objectstack/metadata-protocol...' --concurrency=2: 63 of 63 tasks. The built dist/index.js carries 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) and view-container-default-form.
    • All pass. The rest of the downstream run is CI's.
  • Over REST, measured with a throwaway dogfood probe on the booted showcase, deleted afterwards:
    • PUT /api/v1/meta/view/showcase_task.in_progress with a container answers 400 VALIDATION_ERROR, and the object door still serves showcase_task.in_progress as "In Progress" from com.example.showcase;
    • a second container on one runtime object answers 400;
    • a view item under the shipped name answers 200.

Reverse verification

The change was committed first. Both legs ran from committed e5ac5cf14a, the same protocol.ts blob as HEAD, since the later merge touched nothing under metadata-protocol. Each leg was a trap-guarded script around scripts/ablation-replace.mjs --delete.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, at d5101d1831 derives 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-test and release-pending-publish --self-test;

  • check:objectui-changeset and check:pm-changeset-deadline-census.

  • All 64 exited 0. pnpm check:dual-build-cjs-loads first exited 3: PREREQUISITE NOT MET, because 8 packages had no dist/. It exited 0 after those were built through the lock.

  • check:dual-build-cjs-loads and check:type-check-debt ran through the verify lock.

  • --ran: "64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN".

  • check-adr-0087-registration accepts the changeset's not-required (no-migration-prescription) disposition.

  • Lint, a declared narrowing.

    • Measured: eslint --no-inline-config --format json over the 2 changed .ts files reports 2 files, 0 errors and 0 warnings, with no "file ignored" message. The changeset .md has no matching ESLint configuration.
    • No untouched file can change verdict: type-aware linting is off. eslint.config.mjs:328 says so, and --print-config shows parserOptions.project and projectService both null.
    • Repo-wide pnpm lint is CI's.
  • Branch state: origin/main was merged twice, f30588ceb7 and then d5101d1831. Neither moved a byte under packages/metadata-protocol.

Acceptance notes


Generated by Claude Code

claude added 9 commits October 3, 2026 22:59
…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>
…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>
@github-actions github-actions Bot added the size/l label Oct 4, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 7 documentable anchor(s).

15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/api/error-catalog.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/api/error-handling-client.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/api/error-handling-server.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/automation/jobs.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/automation/webhooks.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/data-modeling/drivers.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/deployment/validating-metadata.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/cluster.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/authorization.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/protocol/kernel/error-handling.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/protocol/objectql/types.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/ui/forms.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/releases/v17/17-5.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))
  • content/docs/releases/v17/17-6.mdx (via VALIDATION_ERROR (literal, a string literal in viewContainerNameCollisionRefusal))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 15fe567c9c74e088684944094aa686b9bd3b386c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 12468018e89ae330cf1119538c72b97b1908066b — the merge of head d5101d18311ef0fd7dc5daa08c1264ce06ca545c into base 15fe567c9c74e088684944094aa686b9bd3b386c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 15fe567c9c74e088684944094aa686b9bd3b386c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #21648 at head d5101d1831 (#21639, with #21638 folded in)

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-04T00:05Z. The os-dev report is on #21639. Judged against GitHub and the branch, not against the report.

  • Shape: draft, base main, assignee os-project-manager.
  • Scope: 3 files, +746/-183: protocol.ts, the pin file and the changeset. check-governed-merges reports NOT governed.
  • The diff, read (non-comment lines):
  • Prescriptions, checked against the family rule. Each message names the other owner, and none prescribes a save under a name another stored row holds.
    • The own-expansion arm keeps "save under its object's name" only when no stored row holds that name. Otherwise it names that container, or a name of its own.
    • Deviation 1 (prescription first) — accepted. Ordering the prescription before the explanation is a measured fix: the first REST probe received the shipped-arm prescription cut at the 500-character bound (CLIENT_MESSAGE_MAX). A pin holds the owner and the view-item arm inside the first 500 characters.
  • The enumeration pin is the card's acceptance: 20 rows, each either refused (with its owner) or allowed (with its reason).
    • Structural tests fail on a row that is neither or both, on a ruled shape with no row, and on a predicate cell with no refused row.
    • Row 7 is accepted on the ruling's literal rule: the object-named container is refused after a free-named container took OBJECT.default. Only a row stored before this change can produce it, because row 6 now refuses the free-named one. The message names that container.
  • Open question (package duplication) — the seat answers A. Package duplication can copy a container whose object is outside the copied package into a second container of that object. That is a byproduct, not a writer saving one on purpose, so (2)'s stop condition does not fire.
    • Before this change, the copy silently took the source's view, which ADR-0126 rules out.
    • It now lands in failed[] with the refusal and its remedy.
    • B would re-open the precedence order the ruling forbids. C is a new duplication behaviour and a new card if anyone asks for it.
    • The changeset states it.
  • Clause-②: no (narrowing) — accepted. There is no path leg and nothing widens. The attribution change restores packaged views a row used to hide, which is not a new accepted input. No contract review is owed.
  • Changeset, checked against the diff:
    • minor, the BREAKING banner, and adr-0087: not-required (no-migration-prescription) with the census facts, including duplication.
    • "One rule" matches the predicate's order and arms.
    • Each "before and after per shape" bullet matches an enumeration row: 4 and 5, 6 and 7, 8 and 9, 10, and 1 to 3.
    • "What still saves" matches rows 11 to 20.
    • "Rows stored before this change" matches the attribution branch: a row named after a shipped item expands under its own name with no package, and the packaged views come back.
    • "The fix" carries the family's arms.
  • Evidence:
    • Premise at BASE: 40 of the 90 new pins red, in exactly the newly refused cells, row 2's old prescription and the 8 at-rest attribution pins.

    • Suites: the package passes 3463 of 3463, typecheck exits 0, and the downstream objectql, rest and dogfood samples are green.

    • REST probes on the booted showcase: a 400 for a container under a shipped name and for a second container, and a 200 for a view item under the shipped name.

    • Reverse verification, two legs:

      • the predicate's throw ablated: 104 refusal pins red across all four families, and every allowed row and control green;
      • the attribution guard ablated: exactly the 8 at-rest pins red.

      Both restores were proved by blob equality.

    • Gates: dispatch-gates --ran reconciles 64 of 64.

  • CI on d5101d18, at this read: 12 check runs are in progress, and check-expected-skips is NOT MEASURED. The seat lands only once every check is green or an expected skip.

Out-of-scope, noted, not filed:


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 00:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 00:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit eea82af Oct 4, 2026
42 of 43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21639-view-container-collision branch October 4, 2026 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment