Skip to content

Commit b399cb8

Browse files
fix(spec): the owner-stamp build door judges a packages[] artifact's top level the way the boot does (#29303)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9J4iPJLcsx6kVWHxTPk6cwZ)_ Fixes #28695 Clause-②: yes (widening) — the build door stops refusing shapes the boot accepts: a foreign-stamped top-level seed dataset, `datasets` entry or job of a `packages[]` artifact, and a top-level copy of a body's view container spelled with another binding. The one shape it now refuses that it accepted before (a top-level list-bound container no body declares, wearing a foreign stamp, while a body carries any list-bound container) is one the boot already refused, and the pending build-door changeset for item stamps already declares it refused. Metadata-protocol: added (a foreign `_packageId` on a top-level `data`, `datasets` or `jobs` item of a `packages[]` artifact, and on a top-level copy of a body's view container, is accepted at the build doors as it is at boot) ## 维护者速读 - **改了什么**:`defineStack` / `os validate` / `os build` 对多包制品(`packages[]`)顶层"残余项"的 `_packageId` 判定,改成和启动时 `unclaimedTopLevel` 完全同一套认领规则;另补 30 个逐集合的构建侧钉子和一行文档。 - **为什么改**:构建门和启动门对"哪些顶层项算残余"判法不一:顶层 seed `data`、`datasets`、`jobs` 构建拒、启动收;视图容器按名字认领,换一种绑定写法就误判(两个方向都有)。 - **风险与回滚**:只动 spec 里一个私有校验函数,无导出、无键变化;回滚即 revert 本 PR。唯一新拒的形状启动本来就拒。 - **先看**:`packages/spec/src/stack.zod.ts`(`isResidualTopLevelItem` 与 `TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL`)、`packages/metadata/src/owner-stamp-residual-build-boot.test.ts`(两门等价钉)、`.changeset/28695-owner-stamp-residual-walk.md`。 - **两分钟验证**:`pnpm --filter @objectstack/metadata exec vitest run src/owner-stamp-residual-build-boot.test.ts`,应见 27 个用例全绿(在 main 上是 11 红)。 - **需要你做的**:无,席位落地(落地前需一次契约复审档复核,由席位转交分诊)。 ## What changes **S1: the residual walk.** `applyCollectionItemOwnerStampGate` (`packages/spec/src/stack.zod.ts`) judges a `packages[]` stack's top level through a new private predicate, `isResidualTopLevelItem`, that mirrors the boot's residual rule (`unclaimedTopLevel` / `artifactItemSlot` / `claimedArtifactSlots`, `@objectstack/metadata`), which spec cannot import: - top-level `data`, `datasets` and `jobs` are never residual (`TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL`). The boot's rule walks only the collections the metadata door registers as items, and these three are not among them; `AppPlugin` seeds, consumes and schedules them off the stack whole, judging no stamp. A package body's items of these collections are still judged at both doors; - an aggregated view container's slot is the object it binds to (`deriveViewContainerObject`: own `object`, then `list.data.object`, then `form.data.object`, then `name`), and a body's container also claims each view it expands into (`expandViewContainer`). Any other item's slot is its `name`; an item with no slot is not judged, since no door registers it. The single-package branch and the body judgement are unchanged. **Measured against the card's mechanism assumption.** Seed `data` held: refused at build, accepted at boot. **Picklists did not hold:** the boot's rule lists a residual picklist, and the metadata door registers it under `manifest.id` and refuses a foreign stamp on it (`registerUnclaimedTopLevel` → `registerArtifactBodyCollections`, measured in the new metadata pin). Skipping picklists would have made the build door looser than the boot, so the build door keeps judging them. Measuring the whole list showed two more collections in the `data` class: `datasets` and `jobs`, which the boot's rule does not walk either. **S3: build-side pins key by key.** `stack-collection-item-owner-stamp.test.ts` gains one pin per member of `OWNER_STAMP_JUDGED_COLLECTIONS`, iterated from the constant, with a fixture table typed as a `Record` keyed by `OwnerStampJudgedCollection` (a new member does not compile until it has a fixture) and a test holding the table's keys equal to the constant. Each pin requires the unstamped stack to parse, then exactly one custom issue at `KEY.INDEX._packageId` for a foreign stamp, and none for a stamp equal to `manifest.id`. **The equality pin.** `packages/metadata/src/owner-stamp-residual-build-boot.test.ts` is where both rules are reachable. For every judged collection, a foreign-stamped top-level item no body declares: the build door raises the issue exactly when `unclaimedTopLevel` lists the item, and `MetadataPlugin.registerUnclaimedTopLevel` refuses exactly those. A 5 × 5 matrix of body/top-level view container spellings holds the same equality. **S4: docs.** One row in the doors table of `content/docs/deployment/validating-metadata.mdx` for the owner-stamp refusal. The connectors failure-mode row (`content/docs/automation/connectors.mdx`) was read and left as is: its sentence ("an entry carrying a `_packageId` other than its own package's is refused at the install") is about the install door, and it is true there. ## Tests - New tests only. No existing test was edited (no forced test edits). - `packages/spec/src/stack-collection-item-owner-stamp.test.ts`: +31 (S3, 30 collections + table/constant equality) and +6 (S1 residual walk). - `packages/metadata/src/owner-stamp-residual-build-boot.test.ts`: new, 27 tests. ## Acceptance notes - S2 (`apps` at the residual door) rode #28697, as ruled; `packages/objectql/src/plugin.ts` is untouched. - The pending changeset `.changeset/28627-item-owner-stamp-build-door.md` still says "a top-level item no body declares judged against `manifest.id`" without the `data` / `datasets` / `jobs` exception. Not edited here (editing another PR's pending changeset reds Check Changeset by design); this PR's own changeset states the exception, and both ship in the same release notes. - A foreign `_packageId` on a top-level `data`, `datasets` or `jobs` item of a `packages[]` stack is now judged at no door. Nothing reads a stamp on those three today (they are applied or scheduled straight off the stack), so it is noted here, not filed. ## Verification <details><summary>S1 red on main, green after (spec)</summary> `src/stack-collection-item-owner-stamp.test.ts` with the S1 suite on main's gate: `Tests 5 failed | 43 passed (48)` (data, datasets, jobs, both container cases). After the fix: `Tests 48 passed (48)`. </details> <details><summary>Equality pin red on main, green after (metadata)</summary> `stack.zod.ts` restored from main (`47ee57fe`), restore proved by blob hash and an empty `git diff HEAD`: `Tests 11 failed | 16 passed (27)`, first divergence `datasets: build verdict == unclaimedTopLevel lists it (false): expected true to be false`. On the head: `Tests 27 passed (27)`. </details> <details><summary>Ablation: an item schema that stops spreading MetadataProtectionFields reds a pin</summary> `node scripts/ablation-replace.mjs` removed `...MetadataProtectionFields` from `PositionSchema` (anchor 1 → 0, blob `5ca91b5a` → `75b8039e`): `× positions: one foreign-stamped item raises the gate's issue …`, `Tests 1 failed | 47 passed (48)`. Restored, blob equal to HEAD, `git diff HEAD` empty. </details> <details><summary>Consumer suites</summary> All on head `2562bce2`, each under `os-verify-lock` (`VERDICT command-exit 0` unless named). - `pnpm --filter @objectstack/spec test`: `Test Files 790 passed (790)`, `Tests 23407 passed | 1 todo`. - `pnpm --filter @objectstack/spec typecheck`: exit 0. `check:generated`: exit 0 (nothing stale). - `pnpm --filter @objectstack/metadata test`: `Test Files 68 passed (68)`, `Tests 956 passed` (the `unclaimedTopLevel` consumer). `typecheck`: exit 0. - `@objectstack/cli` `vitest --project unit`: `370 passed`, 2 files failed first on a missing `packages/cli/dist` (a prerequisite: the round built only cli's dependencies). After building the workspace, those two files (`published-subpath-console.pin`, `published-subpath-hook-body.pin`): `Tests 29 passed (29)`. - `@objectstack/cli` `vitest --project integration test/collection-item-owner-stamp-validate-door.test.ts` (the `os validate` door of this gate): `Tests 2 passed (2)`. - Dogfood: n/a, no `!` in the title and no `(narrowing)` arm. </details> <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `2562bce2`: 105 families, all run after the last commit. - 102 exit 0 on the first pass. 3 exit 3 (prerequisite): `check-plugin-teardown-shape --self-test` (pinned commit missing from the shallow clone; fetched, then exit 0), `spec check:skill-examples` and `check:dual-build-cjs-loads` (workspace dist missing; after `turbo run build --filter='./packages/**'`, both exit 0). - `dispatch-gates --ran`: `105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED (a DERIVED zero)`. - `check-changeset-no-major.mjs --base origin/main --event` with this body: `LEVEL AXIS: … clause-② yes (widening) and metadata-protocol added`, exit 0. - Local-only `check:dev-prereqs`: exit 1 on machine state (35 `ui/` packages have no dist in this container), not on this diff. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [ ] 3. No formatter ran; `pnpm lint` (repo-wide, CI-owned) not run locally. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: `check:generated` is clean (nothing to regenerate). - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmQPQBHfN1JR1ZSJUUa)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0138641 commit b399cb8

5 files changed

Lines changed: 458 additions & 14 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
fix(spec): the owner-stamp build door judges a `packages[]` artifact's top level the way the boot does
6+
7+
`defineStack`, `os validate` and `os build` refuse a collection item whose body `_packageId` names a package other than the one that ships it. For a release artifact (`packages[]`), the top-level item no body declares is the stack's residual, judged against `manifest.id`. Which top-level items are residual is now read the way the boot reads it (`unclaimedTopLevel`, `@objectstack/metadata`), so the build door refuses a residual's stamp exactly when the boot does:
8+
9+
- top-level seed `data`, `datasets` and `jobs` are never judged as residual items: the boot applies, consumes and schedules them straight off the stack, files none of them as residual items and judges none of their stamps. A package body's items of these collections are still judged at both doors;
10+
- a top-level view container is claimed by the object it binds to (its own `object`, else its list's or form's `data.object`), however the binding is spelled, and a body's container also claims every view it expands into. A top-level copy of a body's container written the other way round is no longer judged against the artifact's id, and a top-level container binding another object through its list alone is no longer passed over as a copy;
11+
- a top-level item with no registry slot (no `name`, and for a view no binding) is not judged, since no door registers it.
12+
13+
Clause-②: yes (widening) — the build door stops refusing three shapes the boot accepts: a foreign-stamped top-level seed dataset, `datasets` entry or job of a `packages[]` artifact, and a top-level copy of a body's view container spelled with another binding. The one shape it now refuses that it accepted before, a top-level list-bound container no body declares wearing a foreign stamp while a body carries any list-bound container, is one the boot already refused and the pending build-door refusal for collection item stamps already declares refused ("a top-level item no body declares judged against `manifest.id`").
14+
15+
Metadata-protocol: added (a foreign `_packageId` on a top-level `data`, `datasets` or `jobs` item of a `packages[]` artifact, and on a top-level copy of a body's view container, is accepted at the build doors as it is at boot)

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
509509
| Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, seed replay/state safety, capability references, liveness | ✓ | ✓ | ✓ | — |
510510
| Package docs — flatness, prefixes, links (ADR-0046) | ✓ | ✓ | ✓ | — |
511511
| Undeclared authoring keys — every metadata collection (#3786) and the stack's own top-level keys (#4167) | ✓ | ✓ | — | — |
512+
| Collection item owner stamps — an item whose body `_packageId` names a package other than the one that ships it: the stack's `manifest.id` (none without a `manifest`); in a `packages[]` artifact each body's own `id`, and `manifest.id` for a top-level item no body declares, except seed `data`, `datasets` and `jobs`, which the boot never files from the top level. The boot and the install refuse the same stamp when they register the item (ADR-0010, ADR-0130 D4) | ✓ | ✓ | — | — |
512513
| Declared-unique scope — a bare `unique: true` index, a field-level and index-level double declaration, legacy organization composites (ADR-0120 D5) | ✓ | ✓ | ✓ | — |
513514
| Naming, labels, the rest of the data-model best-practice sweep, i18n coverage | — | — | ✓ | — |
514515
| Emits `dist/objectstack.json` | — | ✓ | — | — |
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [ADR-0010 · ADR-0130 D4] The build door and the boot agree on which
5+
* top-level items of a `packages[]` stack are its RESIDUAL, so the owner-stamp
6+
* judge refuses a residual's foreign `_packageId` at both doors or at neither.
7+
*
8+
* The boot's answer is {@link unclaimedTopLevel} (this package), and the
9+
* metadata door registers exactly those items under the stack's `manifest.id`
10+
* (`MetadataPlugin.registerUnclaimedTopLevel`), judging each stamp. The build
11+
* door is the stack schema's refinement (`applyCollectionItemOwnerStampGate`,
12+
* `@objectstack/spec`), which cannot import this package and so mirrors the
13+
* rule. This file is where both are reachable, and it holds the mirror equal:
14+
*
15+
* - collection by collection over `OWNER_STAMP_JUDGED_COLLECTIONS`, one
16+
* foreign-stamped top-level item that no body declares: the build door
17+
* raises the judge's issue exactly when `unclaimedTopLevel` lists the item,
18+
* and the metadata door refuses exactly those;
19+
* - view containers, body shape by top-level shape: a container is claimed by
20+
* the object it binds to however the binding is spelled.
21+
*/
22+
23+
import { describe, it, expect } from 'vitest';
24+
import { composeStacks, defineStack, ObjectStackDefinitionSchema } from '@objectstack/spec';
25+
import { OWNER_STAMP_JUDGED_COLLECTIONS, type OwnerStampJudgedCollection } from '@objectstack/spec/kernel';
26+
import { MetadataPlugin, unclaimedTopLevel } from './plugin.js';
27+
28+
const CRM = 'com.probe.crm';
29+
const ORDERS = 'com.probe.orders';
30+
const FOREIGN = 'com.probe.foreign';
31+
32+
const manifest = (id: string, namespace: string) => ({ id, name: namespace, namespace, version: '1.0.0', type: 'module' as const });
33+
34+
const PROBE_OBJECT = {
35+
name: 'probe_thing',
36+
label: 'Thing',
37+
sharingModel: 'private',
38+
ownership: 'user',
39+
fields: { title: { type: 'text', label: 'Title' } },
40+
};
41+
const OTHER_OBJECT = { ...PROBE_OBJECT, name: 'probe_other', label: 'Other' };
42+
43+
const probeFlow = {
44+
name: 'probe_flow',
45+
label: 'Flow',
46+
type: 'autolaunched',
47+
nodes: [
48+
{ id: 'start', type: 'start', label: 'Start' },
49+
{ id: 'end', type: 'end', label: 'End' },
50+
],
51+
edges: [{ id: 'e1', source: 'start', target: 'end' }],
52+
};
53+
const probeDataset = {
54+
name: 'probe_ds',
55+
label: 'Dataset',
56+
object: 'probe_thing',
57+
dimensions: [{ name: 'title', field: 'title', type: 'string' }],
58+
measures: [{ name: 'row_count', aggregate: 'count' }],
59+
};
60+
const probeQueue = { name: 'probe_queue', label: 'Queue', members: [] };
61+
const listOf = (object: string) => ({ type: 'grid', data: { provider: 'object', object }, columns: ['title'] });
62+
63+
/**
64+
* One spec-valid item per judged collection, and what else the stack needs for
65+
* it to resolve. Keyed by the constant's member type, so a collection added to
66+
* the constant does not compile here until it has a fixture.
67+
*/
68+
const JUDGED_FIXTURES: Record<OwnerStampJudgedCollection, { item: Record<string, unknown>; context?: Record<string, unknown> }> = {
69+
objects: { item: { ...PROBE_OBJECT, name: 'probe_extra', label: 'Extra' } },
70+
apps: { item: { name: 'probe_app', label: 'App', navigation: [{ id: 'nav_1', type: 'object', objectName: 'probe_thing', label: 'Thing' }] } },
71+
picklists: { item: { name: 'probe_pick', label: 'Pick', options: [{ value: 'aa', label: 'A' }] } },
72+
actions: { item: { name: 'probe_go', label: 'Go', type: 'script', target: 'go' } },
73+
views: { item: { object: 'probe_thing', list: listOf('probe_thing') } },
74+
pages: { item: { name: 'probe_page', label: 'Page', type: 'app', regions: [] } },
75+
dashboards: { item: { name: 'probe_dash', label: 'Dash', widgets: [] } },
76+
reports: {
77+
item: { name: 'probe_report', label: 'Report', type: 'summary', dataset: 'probe_ds', rows: ['title'], values: ['row_count'] },
78+
context: { datasets: [probeDataset] },
79+
},
80+
datasets: { item: probeDataset },
81+
flows: { item: probeFlow },
82+
webhooks: {
83+
item: { name: 'probe_webhook', label: 'Webhook', object: 'probe_thing', triggers: ['create'], url: 'https://example.com/hook' },
84+
context: { requires: ['webhooks', 'realtime'] },
85+
},
86+
jobs: { item: { name: 'probe_job', label: 'Job', schedule: { type: 'interval', intervalMs: 60000 }, body: { language: 'js', source: 'return 1;', capabilities: [] } } },
87+
positions: { item: { name: 'probe_pos', label: 'Pos' } },
88+
permissions: { item: { name: 'probe_perm', label: 'Perm', objects: {} } },
89+
capabilities: { item: { name: 'probe.export', label: 'Export' } },
90+
sharingRules: { item: { name: 'probe_share', label: 'Share', object: 'probe_thing', type: 'criteria', condition: 'true', sharedWith: { type: 'team', value: 'team_ops' } } },
91+
queues: { item: probeQueue },
92+
assignmentRules: {
93+
item: { name: 'probe_route', object: 'probe_thing', entries: [{ name: 'to_queue', assignTo: { type: 'queue', value: 'probe_queue' } }] },
94+
context: { queues: [probeQueue] },
95+
},
96+
agents: { item: { name: 'probe_agent', label: 'Agent', role: 'Assistant', instructions: 'Triage the request.' } },
97+
tools: { item: { name: 'probe_tool', label: 'Tool', description: 'tool', parameters: { type: 'object', properties: {} } } },
98+
skills: { item: { name: 'probe_skill', label: 'Skill', description: 'skill', instructions: 'x', tools: [] } },
99+
apis: {
100+
item: { name: 'probe_api', description: 'api', path: '/api/v1/apps/probeorders/p1', method: 'GET', type: 'flow', target: 'probe_flow' },
101+
context: { flows: [probeFlow] },
102+
},
103+
hooks: { item: { name: 'probe_hook', label: 'Hook', object: 'probe_thing', events: ['beforeInsert'], handler: () => {} } },
104+
mappings: { item: { name: 'probe_map', label: 'Map', targetObject: 'probe_thing', fieldMapping: [{ source: 'a', target: 'title' }] } },
105+
analyticsCubes: { item: { name: 'probe_cube', title: 'Cube', description: 'cube', sql: 'probe_thing', measures: {}, dimensions: {} } },
106+
connectors: { item: { name: 'probe_conn', label: 'Probe', type: 'api', provider: 'openapi', providerConfig: { spec: './specs/billing.json' } } },
107+
emailTemplates: { item: { name: 'probe_mail', label: 'Mail', subject: 'Hi', bodyHtml: '<p>Hello</p>' } },
108+
docs: { item: { name: 'probe_doc', label: 'Doc', content: '# Doc' } },
109+
books: { item: { name: 'probe_book', label: 'Book', groups: [] } },
110+
data: { item: { object: 'probe_thing', records: [] } },
111+
};
112+
113+
/** Two packages composed as `os build` does; the artifact's own id is the orders module's. */
114+
function artifactOf(crmBody: Record<string, unknown> = {}): Record<string, unknown> {
115+
return composeStacks([
116+
defineStack({ manifest: manifest(CRM, 'probecrm'), objects: [PROBE_OBJECT, OTHER_OBJECT], ...crmBody } as never, { strict: false }),
117+
defineStack({ manifest: manifest(ORDERS, 'probeorders') } as never),
118+
], { manifest: 'preserve' }) as unknown as Record<string, unknown>;
119+
}
120+
121+
/** The artifact with `items` appended to the top level of `key`, and the index the first lands at. */
122+
function withTopLevel(artifact: Record<string, unknown>, key: string, items: unknown[], context: Record<string, unknown> = {}) {
123+
const stack: Record<string, unknown> = { ...artifact, ...context };
124+
const existing = Array.isArray(stack[key]) ? (stack[key] as unknown[]) : [];
125+
stack[key] = [...existing, ...items];
126+
return { stack, at: existing.length };
127+
}
128+
129+
/** The build door: does the parse raise an issue at `<key>.<at>._packageId`? */
130+
function buildJudges(stack: unknown, key: string, at: number): boolean {
131+
const result = ObjectStackDefinitionSchema.safeParse(stack);
132+
if (result.success) return false;
133+
return result.error.issues.some((i) => i.path.join('.') === `${key}.${at}._packageId` && i.code === 'custom');
134+
}
135+
136+
/** The boot's residual rule: does `unclaimedTopLevel` list the item at `<key>[at]`? */
137+
function bootListsResidual(stack: unknown, key: string, at: number): boolean {
138+
return unclaimedTopLevel(stack)?.items.some((entry) => entry.field === key && entry.index === at) === true;
139+
}
140+
141+
/** The metadata door: does the residual registration refuse on a stamp? */
142+
async function bootRefusesStamp(stack: unknown): Promise<boolean> {
143+
try {
144+
await MetadataPlugin.registerUnclaimedTopLevel(
145+
{ register: () => undefined, warn: () => undefined },
146+
stack,
147+
{ door: 'config', label: 'probe' },
148+
);
149+
return false;
150+
} catch (e: any) {
151+
return e?.code === 'VALIDATION_ERROR' && Array.isArray(e.issues) && e.issues.some((i: any) => i?.path === '_packageId');
152+
}
153+
}
154+
155+
describe('[ADR-0010 · ADR-0130 D4] a residual\'s foreign stamp: refused at the build door exactly when the boot files it as residual', () => {
156+
it('the fixture table covers exactly the judged collections', () => {
157+
expect(Object.keys(JUDGED_FIXTURES).sort()).toEqual([...OWNER_STAMP_JUDGED_COLLECTIONS].sort());
158+
});
159+
160+
it('collection by collection over OWNER_STAMP_JUDGED_COLLECTIONS', async () => {
161+
const notResidual: string[] = [];
162+
for (const key of OWNER_STAMP_JUDGED_COLLECTIONS) {
163+
const { item, context } = JUDGED_FIXTURES[key];
164+
// CONTROL: the unstamped stack parses, so a missing issue below is the gate's verdict.
165+
const plain = withTopLevel(artifactOf(), key, [{ ...item }], context);
166+
const control = ObjectStackDefinitionSchema.safeParse(plain.stack);
167+
expect(control.success, `${key} CONTROL: ${control.success ? '' : JSON.stringify(control.error.issues.slice(0, 2))}`).toBe(true);
168+
169+
const { stack, at } = withTopLevel(artifactOf(), key, [{ ...item, _packageId: FOREIGN }], context);
170+
const listed = bootListsResidual(stack, key, at);
171+
expect(buildJudges(stack, key, at), `${key}: build verdict == unclaimedTopLevel lists it (${listed})`).toBe(listed);
172+
expect(await bootRefusesStamp(stack), `${key}: the metadata door refuses what it lists`).toBe(listed);
173+
if (!listed) notResidual.push(key);
174+
}
175+
// The reading, so the equality above is not vacuous: the three
176+
// collections the boot reads off the stack whole, never as items.
177+
expect(notResidual.sort()).toEqual(['data', 'datasets', 'jobs']);
178+
});
179+
});
180+
181+
describe('[ADR-0010 · ADR-0130 D4] a view container is claimed by the object it binds to, at both doors', () => {
182+
const shapes: Record<string, Record<string, unknown>> = {
183+
'own object': { object: 'probe_thing', list: listOf('probe_thing') },
184+
'list binding only': { list: listOf('probe_thing') },
185+
'form binding only': { form: { type: 'simple', data: { provider: 'object', object: 'probe_thing' }, sections: [{ fields: ['title'] }] } },
186+
'another object, list binding only': { list: listOf('probe_other') },
187+
'another object, own object': { object: 'probe_other', list: listOf('probe_other') },
188+
};
189+
const cases = Object.keys(shapes).flatMap((body) => Object.keys(shapes).map((top) => [body, top] as const));
190+
191+
it.each(cases)('body container (%s) × top-level container (%s), stamped foreign', async (body, top) => {
192+
const artifact = artifactOf({ views: [shapes[body]] });
193+
const { stack, at } = withTopLevel(artifact, 'views', [{ ...shapes[top], _packageId: FOREIGN }]);
194+
const listed = bootListsResidual(stack, 'views', at);
195+
expect(buildJudges(stack, 'views', at)).toBe(listed);
196+
expect(await bootRefusesStamp(stack)).toBe(listed);
197+
// Claimed exactly when both bind the same object.
198+
expect(listed).toBe(body.startsWith('another') !== top.startsWith('another'));
199+
});
200+
});

0 commit comments

Comments
 (0)