Skip to content

fix(spec): the owner-stamp build door judges a packages[] artifact's top level the way the boot does - #29303

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-uhe0vw
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-uhe0vw

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
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

Verification

S1 red on main, green after (spec)

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).

Equality pin red on main, green after (metadata)

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).

Ablation: an item schema that stops spreading MetadataProtectionFields reds a pin

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.

Consumer suites

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.
Gates

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.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint (repo-wide, CI-owned) not run locally.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: check:generated is clean (nothing to regenerate).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Generated by Claude Code

…idual the way the boot does

The residual walk of applyCollectionItemOwnerStampGate now mirrors the
boot's unclaimedTopLevel: top-level data, datasets and jobs are never
residual items (no boot door files or judges them), and a view container
is claimed by the object it binds to however the binding is spelled, so a
copy spelled the other way is not judged and two list-bound containers are
two slots. Pins one foreign-stamped item per judged collection.

Claude-Session: https://claude.ai/code/session_01ADHkmQPQBHfN1JR1ZSJUUa
Co-authored-by: Claude <noreply@anthropic.com>
… to unclaimedTopLevel

Collection by collection over OWNER_STAMP_JUDGED_COLLECTIONS and view
container shape by shape: the stack schema raises a residual's foreign
_packageId exactly when unclaimedTopLevel lists the item and the metadata
door refuses it. Adds the owner-stamp row to the validating-metadata doors
table and the changeset.

Claude-Session: https://claude.ai/code/session_01ADHkmQPQBHfN1JR1ZSJUUa
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 168 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 e7d3ada2ef615a9e064014d0ae31ec68873e9a8d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ae561db9a9b847ae398892b662e1e242f7488e79 — the merge of head 2562bce2f75d368c7d8de963e40bd624a1091cbd into base e7d3ada2ef615a9e064014d0ae31ec68873e9a8d, 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 ae561db9a9b847ae398892b662e1e242f7488e79 && git checkout ae561db9a9b847ae398892b662e1e242f7488e79
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e7d3ada2ef615a9e064014d0ae31ec68873e9a8d 2562bce2f75d368c7d8de963e40bd624a1091cbd && git checkout -B drift-repro e7d3ada2ef615a9e064014d0ae31ec68873e9a8d && git merge --no-ff 2562bce2f75d368c7d8de963e40bd624a1091cbd

node scripts/docs-audit/affected-docs.mjs --json e7d3ada2ef615a9e064014d0ae31ec68873e9a8d

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: 2562bce2 · read 2026-11-02T11:58Z

维护者速读

席位意见:可落地,等契约复审。

席位核对 — ACCEPT · domain:spec#1 · head 2562bce2

  1. Shape: draft, base main, Fixes #28695, Clause-②: yes (widening) / Metadata-protocol: added at line start in the body and the changeset. Within claim 6575012012's surface (packages/objectql/src/plugin.ts untouched; the connectors row read and left, true at the install door). Not governed.
  2. Diff, read on the head: applyCollectionItemOwnerStampGate judges a packages[] top level through a private isResidualTopLevelItem that mirrors the boot's unclaimedTopLevel: data / datasets / jobs are never residual (TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL), and a view container's slot is its bound object (deriveViewContainerObject), with a body container also claiming its expanded views. No export or key changes.
  3. Premise, measured by the dev: seed data held. Picklists did not: the metadata door registers a residual picklist and refuses a foreign stamp, so the build door keeps judging them. datasets and jobs joined data. The seat accepts this as a premise correction inside the card's intent (build door == boot door).
  4. Seat open question, answered A: keep widening / added. The one newly refused shape (a foreign-stamped top-level list-bound container no body declares, while a body carries one) was already refused at boot, and the pending .changeset/28627-item-owner-stamp-build-door.md already declares that class refused. The contract review judges this call.
  5. Tests: new only (+37 spec, 27 metadata), no forced edits. Red on main / green on head for both suites; one ablation (PositionSchema without MetadataProtectionFields) reds a pin.
  6. Readings (dev, on 2562bce2): 105/105 derived gate families run, DERIVED zero; spec 790 files; metadata 68 files; cli unit and the os validate door integration test green. Check-runs on 2562bce2 at this reading: 57 success, 8 skipped, 0 failure, 0 running.
  7. Noted, not filed: a foreign _packageId on a top-level data / datasets / jobs item of a packages[] stack is now judged at no door. Nothing reads a stamp there today.
  8. Landing: one CONTRACT_REVIEW_TIER review (the published schema face), which the seat runs itself (claim line follows). Then the seat readies and queues.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 2562bce · 2026-11-02T11:58Z

@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2562bce2f75d368c7d8de963e40bd624a1091cbd
Local-runs: none
Scope: packages/spec/src/stack.zod.ts + packages/metadata/src/plugin.ts (ARTIFACT_FIELD_TO_TYPE, artifactItemSlot, claimedArtifactSlots, unclaimedTopLevel, registerArtifactBodyCollections, artifactItemOwnerStampRefusal, MetadataPlugin.registerUnclaimedTopLevel), packages/spec/src/ui/view-container-object.ts (deriveViewContainerObject), packages/spec/src/ui/view.zod.ts (isAggregatedViewContainer, expandViewContainer), packages/spec/src/kernel/collection-item-owner-stamp.ts (OWNER_STAMP_JUDGED_COLLECTIONS, the judge), packages/core/src/artifact-packages.ts (resolveArtifactPackageEntries), packages/objectql/src/plugin.ts and engine.ts (unclaimedTopLevelObjects / unclaimedTopLevelItems, registerApp's METADATA_ARRAY_KEYS loop and its seed data channel), packages/runtime/src/app-plugin.ts (registerUnclaimedTopLevel, the seed collector, collectBundleJobs) and packages/runtime/src/artifact-collections.ts, .changeset/28627-item-owner-stamp-build-door.md (pending on origin/main, so unreleased). The rest of the diff (both test files, the docs row, this PR's changeset) is the lane seat's ACCEPT at TIER_DEFAULT (comment 6576567803); the cited code was read, not reviewed.

Inputs: card #28695 (body, triage 6561043135 and 6572261416, claim 6575012012, os-dev-report 6575978908), PR #29303 (body, five-file list, git diff origin/main...2562bce2), and the check-runs on the head. Check-runs read on the head: 58 success, 12 skipped (opt-in harnesses, paths-filtered jobs and the relabel run's duplicates), 0 failure, 0 in progress once the second Check Changeset run finished; every one of the ten required contexts is success except Lint UI, which is skipped by its paths filter (no ui/ path in the diff). The check-run conclusions are the gate verdicts here; nothing was built, run or re-run.

① Derived judgments

Measured on the head's tree, the build door (applyCollectionItemOwnerStampGate) versus the boot's residual rule (unclaimedTopLevel):

  1. Top-level data, datasets, jobs of a packages[] artifact, foreign-stamped: refused on main, accepted on the head. Right. ARTIFACT_FIELD_TO_TYPE carries exactly these three judged collections nowhere (data by the recorded Six independent enumerations of the stack-collection set, none answerable to stack.zod.ts — one is mis-aimed and two still list retired kinds #6242 row 4(a) decision; datasets and jobs simply absent), so unclaimedTopLevel never lists them and registerUnclaimedTopLevel never judges them; the engine's registerApp runs on the bodies alone (resolveArtifactPackageEntries(...).map(manifest)) and its residual path reads the same unclaimedTopLevel answer; AppPlugin seeds this.collections.data and schedules collectBundleJobs(...) without calling the judge. Both doors now accept the shape; the body's items of these collections stay judged at both (engine METADATA_ARRAY_KEYS loop and seed channel, metadata door's artifactItemOwnerStampRefusal, build judgeBody on each body).
  2. A top-level copy of a body's view container spelled with another binding (body list.data.object vs top-level own object, or the reverse): refused on main, accepted on the head. Right. main keyed the claim on name ?? id ?? object, so two spellings of one slot were two keys; the head keys on deriveViewContainerObject, as artifactItemSlot does, so the slot is claimed and the boot's claimedArtifactSlots answers the same.
  3. A top-level list-bound container no body declares, binding another object, foreign-stamped, while a body carries a nameless container: accepted on main, refused on the head. Right, and it is the one narrowing in the diff. On main both containers collapsed onto the (unnamed) key, so the top-level one read as a copy; the boot filed it as a residual under manifest.id and refused the stamp. The head judges it. This is the shape ② is about.
  4. A top-level item with no registry slot (no name; for a view, no binding either): not judged on the head. Right. artifactItemSlot returns undefined for it, no door registers it, so no stamp is judged at boot either.
  5. Picklists stay judged as residuals. Right, against the card's S1 wording. ARTIFACT_FIELD_TO_TYPE.picklists is present, registerUnclaimedTopLevel registers a residual picklist under manifest.id through registerArtifactBodyCollections and refuses a foreign stamp there. Skipping it would have made the build looser than the boot. The card's mechanism assumption was wrong on this point and the dev measured it; the head is right.
  6. objects and apps on the top-level residual: judged at both doors. Right. The metadata door's artifactItemOwnerStampRefusal covers every member of OWNER_STAMP_JUDGED_COLLECTIONS (post objectql: an installed package's apps[] entry keeps a self-written _packageId (no #28534 refusal on the apps loop), so the per-package and disabled-package filters attribute the app to the package it names #28697), the engine's residual path judges residual objects and items (collectionItemOwnerStampRefusal in objectql/src/plugin.ts), and the build mirror walks the same constant. S2 is closed by objectql: an installed package's apps[] entry keeps a self-written _packageId (no #28534 refusal on the apps loop), so the per-package and disabled-package filters attribute the app to the package it names #28697 as triage ruled; nothing here reopens it.
  7. Single-package branch and body judgement unchanged. Right: judgeBody(config, manifest?.id, []) with no predicate on the packages-absent branch; each body judged against body.id as before.
  8. Public surface: none. TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL, residualSlotOf and isResidualTopLevelItem are module-private; the new expandViewContainer import is an existing root export; check:api-surface and check:authorable-surface are inside the green TypeScript Type Check job and the dev's check:generated had nothing stale. No authorable key moves.

② Semver level

@objectstack/spec: minor with Clause-②: yes (widening) / Metadata-protocol: added, identical in the PR body and the changeset; check-changeset-no-major --event accepted the pair (dev report) and both Check Changeset runs are green.

The level is right under either reading: a kind other than none on a package the diff moves takes at least minor, and added and narrowed both land there.

The arm and the kind. The diff, read against main, widens three accepted shapes (①.1, ①.2, ①.4) and refuses one it accepted (①.3). By the letter of the kind rule, one refused value is narrowed, and narrowed beats added. I judge yes (widening) / added right here, on this ground: the kind measures the change to what a metadata author can write against the package the author has, and the one refused shape is a member of the class the pending .changeset/28627-item-owner-stamp-build-door.md already declares refused in a no (narrowing) / narrowed entry with its BREAKING banner and ADR-0087 disposition ("a top-level item no body declares judged against manifest.id"). On main the gate failed to refuse that one member of its own declared class through the (unnamed) key collision; this PR makes 28627's declaration true for it. That changeset is still present on origin/main, so it is unreleased, and both entries are consumed by the same changeset version: the author's contract between the published 18.2.0 and the next release loses exactly what 28627's entry names, once. A second BREAKING entry for the same refusal would be a duplicate carrier, not a disclosure. The widening against main is real and is honestly declared as added, so the release notes carry it. The condition this rests on, that 28627 and this PR ship in one release, is visible in the tree (28627's changeset file present) and the entry's own prose states the one refused shape and why, so a reader can check it. Option A of the dev's open question stands; the dogfood battery is not owed.

③ Boundary flags

④ Design judgment

packages/spec/src/stack.zod.ts, against ADR-0010 / ADR-0130 D4, the boot's rule it mirrors, and its first consumers (defineStack, os validate, os build, the artifact door's parse).

The mirror holds, by construction, for every collection and container spelling, not only the pinned ones. I tried to break it along each axis the equality pin does not enumerate:

  • Body set. The build reads packages[j].manifest for every entry; the boot reads resolveArtifactPackageEntries(...), which parses each entry whole and REFUSES a malformed entry, a missing id or a duplicate (it never drops one), and the build gate runs on the parsed stack whose ArtifactPackageSchema has already accepted every entry. Same set; order differs and is irrelevant to a set.
  • Slot derivation. Both call the spec's own isAggregatedViewContainer and deriveViewContainerObject, in the same order (container ⇒ bound object; else name; else, for views, the derivation), and both treat an undefined slot as "not registered, not judged". residualSlotOf's String(name) differs only for a non-string name, which the parsed stack cannot carry.
  • Claims. Both key claims per collection; ARTIFACT_FIELD_TO_TYPE is injective, so the three extra fields the boot walks (objectExtensions, workflows, ragPipelines) claim types no judged collection looks up, and a body's container claims its object's slot plus every expandViewContainer name on both sides.
  • Owner. The boot's ownerId falls back to a top-level id that the strict stack schema does not declare; no manifest ⇒ undefined on both sides, and the judge refuses any stamp against no owner at both doors.
  • Parsed vs raw. Both production doors run the rule on a parsed stack (the artifact door parses with ObjectStackDefinitionSchema before registerUnclaimedTopLevel; the config door hands defineStack's output), and the new metadata pin additionally holds the equality with the boot reading the RAW stack, so a parse-time default cannot open a gap on the fixtures.
  • The one hand-copied fact (which judged collections the boot's map lacks) is pinned from the live map by the metadata test's notResidual equality, so a later ARTIFACT_FIELD_TO_TYPE change reds instead of silently diverging. That is the right place for the pin: the only package where both rules are reachable.

Placement. The predicate stays a private refinement on the schema, not a defineStack branch, so all five consumers judge alike; no runtime logic enters spec beyond the pure derivation already housed there (view-container-object.ts's header is explicit that the spec judges the same slot the registrars file under). Prime Directive 2 holds.

Findings.

  • should-fix — TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL's docblock attributes the consumption: "jobs are scheduled and datasets consumed by AppPlugin straight off the bundle". jobs is right (collectBundleJobs over resolveArtifactCollections). datasets is not: packages/runtime/src/app-plugin.ts reads no datasets collection (every datasets occurrence in that file is the seed-data collector), a body's datasets are registered and stamp-judged by the engine's registerApp (METADATA_ARRAY_KEYS includes datasets), and a top-level residual datasets item reaches no registrar at all. The rule the constant encodes is right and pinned; the sentence that explains it names the wrong mechanism, which is the kind of prose the next author reasons from. Correct it to what the tree does (datasets: body-registered by the engine, never filed from the top level), in this PR or the next edit of the file.
  • No must-fix. The two shapes the card asked about (S1) are measured against the live boot code and the measurement, not the card's wording, drives the constant; the S3 pins are iterated from the constant with a Record-typed fixture table and an equality test so a new member fails to compile before it is unpinned; the ablation reds a pin.

Implemented-by: claude/project-thread-uhe0vw
Reviewed-by: session_01ADHkmPi25XXKx92Xmg4viS

VERDICT: PASS

@objectstack-fleet objectstack-fleet Bot removed the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 2, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 12:14
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 12:14
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41445185458 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (6/16) — 失败步骤: Run the ui tests (shard 6/16)

    × a refused report save makes a reload and the pillar link ask; a save that lands clears it 1959ms
    FAIL   dom  packages/app-shell/src/views/studio-design/StudioDesignSurface.refusedLeafUnsaved-29113.test.tsx > a leaf edit whose save was refused guards every departure (objectstack#29113) > a refused
      ↳ 失败原因: AssertionError: expected false to be true // Object.is equality
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • packages/app-shell/src/views/studio-design/StudioDesignSurface.refusedLeafUnsaved-29113.test.tsx — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 2 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 2, 2026
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit b399cb8 Nov 2, 2026
74 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-uhe0vw branch November 2, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant