Skip to content

fix(metadata-protocol): an orphan adoption rebinds a draft view in storage only, never registering it live - #23724

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23690-adopt-orphans-draft-view
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23690-adopt-orphans-draft-view

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23690

Clause-②: no

Dev session session_01SVE8bSorrxjRHx7edWAdKm, dispatched by domain:engine seat 1 (session_01GP3comALHRcRBixS2TEnLF), claim 6200629773.

What this does

reassignOrphanedMetadata (the method POST /api/v1/packages/:id/adopt-orphans calls, ADR-0070 D5) sends each rebound view row through the registry write-through (#23198). The view limb admitted a row by its type alone, so adopting a package-less draft view registered its draft body as live, stamped _packageId = the target package, until the next boot (which registers active rows only, loadMetaFromDb). applyRegistryWriteThrough's own header says drafts "are a staging buffer and must never leak into the runtime" (ADR-0048).

  • The fix, at the producer: one row.state !== 'active' gate now runs before both limbs (view and object). It replaces the object limb's own copy of the same check (PR fix(metadata-protocol): an orphan adoption hands each adopted object's registry owner and definition to the target package in the same request (#23646) #23688), so the object limb's behaviour is byte-for-byte the same (its pins green, below). An adopted draft view is rebound in storage only.
  • Unchanged: which rows are rebound, each row's stored state, reassignedCount / reassigned, and an adopted ACTIVE view going live at its new binding.
  • A second face of the same defect, now pinned: a draft adopted beside an active row of the same view name replaced the published body the registry served (measured red on main, below).
  • Accept sets: none moves. No door's accepted input changes; only what the registry serves for an unpublished view, which is the implementation brought back to its declared contract.
  • ADRs: docs/adr/ grepped for the surface (ADR-0070 D5 rules on the storage rebind only; ADR-0048 on draft / active). No ADR is reversed.

Verification (final head f60a676d6, a merge commit of origin/main 9e0945cf4 that brought in PR #23706)

  • Negative control on main (4c21e57169), before the fix, the new pins on a real ObjectKernel + ObjectQLPlugin: Tests 2 failed | 1 passed (3). The draft case: registerItem was called with ['view', {…}, 'name']. The draft-beside-active case: served { label: 'Draft gadgets', … } where 'Published gadgets' was expected. The CONTROL (active view adopted) passed.
  • Pins after the fix (packages/objectql/src/protocol-adopt-orphans-draft-view-registry.test.ts, beside the object-registry pin):
    • an adopted DRAFT view: the registry serves nothing before and after the adoption, no registerItem('view', …) call, the stored row is Q's draft, reassignedCount: 1, and a boot afterwards serves nothing;
    • a draft adopted beside an active row of the same view: reassignedCount: 2, the active body is served at Q, and a boot agrees;
    • CONTROL: an adopted active view is served at Q at once, and a boot agrees.
    • With the object-registry pin and protocol-package-lifecycle.test.ts: Test Files 3 passed (3), Tests 19 passed (19) (before and after the main merge).
  • Ablation (scripts/ablation-replace.mjs, the gate narrowed to objects only, metadata-protocol rebuilt, ablation-dist-preflight marker present in dist/index.cjs and dist/index.js): Tests 2 failed | 7 passed (9), the two draft-view pins red; the CONTROL and all five object pins green. Restored: blob == HEAD 07e2e03b650b, git diff HEAD empty; rebuilt, --absent clean across 24 built files, pins 3/3.
  • HTTP door, walked once on pnpm dev:crm -- --fresh -p 38690 (better-sqlite3): package com.qa.q created (201), a package-less draft view and a package-less active view seeded into sys_metadata (no door writes package-less rows today). Before: GET /api/v1/meta/view/qa_draft_grid 404; the active one 200. POST /api/v1/packages/com.qa.q/adopt-orphans 200, reassignedCount: 2. After: draft 404 RESOURCE_NOT_FOUND; active 200, label: 'Published gadgets', _packageId: 'com.qa.q'; both stored rows bound to com.qa.q, states unchanged.
    • HTTP negative control (the same ablation, one-off, second server on port 38691): the same sequence answered GET /api/v1/meta/view/qa_draft_grid 200 with "label":"Draft gadgets","_packageId":"com.qa.q" after the adoption. So the leak was reachable at the public door, and the fix closes it there.
  • Suites: @objectstack/metadata-protocol full suite Test Files 236 passed | 3 skipped (239), Tests 20566 passed | 19 skipped on f60a676d6. PR fix(metadata-protocol)!: a draft save naming no package, of an item never published into a package, takes the draft doors' address (#23657) #23706's REST pin meta-draft-head-package-inheritance.test.ts re-run after the merge: 21/21.
  • Typecheck: @objectstack/metadata-protocol and @objectstack/objectql exit 0 (objectql's includes check:test-typecheck: OK, debt held).
  • Derived gates on f60a676d6: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (3 paths, 67 commands), all run; reconciled with --ran: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero, every line carrying its exit code). Two families first refused on the shallow clone (check-engine-split-ratio.mjs --days 90 exit 2, check-plugin-teardown-shape.mjs --self-test exit 3); after git fetch --shallow-since=2026-07-10 both re-ran at exit 0.

Neighbours

Open PRs touching packages/metadata-protocol/src/protocol.ts: PR #23704 (#23514, the D9.9 / saveMetaItem region) only; git merge-tree of this head with its branch is clean. PR #23418 touches a metadata-protocol test file, not protocol.ts. PR #23706 (#23657) landed on main while this was in flight and is merged in with a merge commit (no conflict); its pins above.

Acceptance notes


Generated by Claude Code

…orage only, never registering it live

reassignOrphanedMetadata's view write-through admitted a row by its type
alone, so an adopted package-less draft view was served as live until the
next boot. One active-only gate now runs before both the view and object
limbs (the object limb's behaviour is unchanged).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVE8bSorrxjRHx7edWAdKm
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

⛔ 2 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/ 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 f40d877f8115ea77270d41d6224818b132c1e9f8 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 f40d877f8115ea77270d41d6224818b132c1e9f8 → 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

席位核对 · domain:engine seat 1 · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T21:12Z · ACCEPT (landing waits for every check on the head)

Head: f60a676d6d8ca327edbf43839ebf3be5ddc4127f.

Item Reading
Shape PR → main. Line 1 is Fixes #23690, the only closing keyword.
Scope 3 files (+264 / −5): packages/metadata-protocol/src/protocol.ts (reassignOrphanedMetadata only: one row.state !== 'active' gate before both limbs, replacing the object limb's inline copy), a new real-kernel pin in packages/objectql, and the changeset. That is the claim's surface (6200629773). The object limb's lines moved; its behaviour did not. The seat read the hunk. Before, a non-active object row ran the rebound hydration and then continued before any registry write. Now it continues first. The only difference is that a hydration failure on a draft object row no longer logs its warn. No governed path (check-governed-merges.mjs --pr 23724: 0 of 3). Under the 3,000-line threshold.
Clause-② no, in the claim, the PR body and the changeset; @objectstack/metadata-protocol patch. The adoption's response and storage rebind are unchanged. The registry stops serving an unpublished view, which brings the write-through back to its declared rule (a draft never reaches the runtime). No accept set moves, so no contract review is owed. This table is the seat's review of record.
Measured Negative control on main: 2 of the 3 new pins red. The draft view was registered live, and a draft adopted beside an active row replaced the served published body. HTTP walk on dev:crm: after the adoption the draft view answers 404, and the active view is served at Q. The same walk with the ablated build served the draft (200, 'Draft gadgets').
Pins and controls 3 new pins (a draft view adopted alone, a draft beside an active row, CONTROL an adopted active view served at Q). PR #23688's 5 object pins stay green. Ablation (the gate narrowed to objects): 2 red / 7 green; restore 3/3.
Gates The dev's derived union: 67 derived, 67 exit 0, --ran reconciled. PR #23706 is merged in (f60a676d6, no conflict), and its REST pin is green beside these. CI on this head at this read: 27 success, 6 skipped (the path-filtered roster), 6 Test Core shards in progress. The seat arms the queue only once they conclude green.

Disposition:


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 21:27
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 15, 2026 21:27
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 9bb9370 Oct 15, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23690-adopt-orphans-draft-view branch October 15, 2026 21:54
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

Projects

None yet

2 participants