Skip to content

refactor(metadata-protocol): move the package door save refusal and the origin-gated removal verdict out of protocol.ts into protocol-packaged-base-refusal.ts (#25467 cut 13) - #25972

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ybjm3h
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ybjm3h

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Part of #25467
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6270097840

Cut 13 of the protocol.ts split (#24586 ruling A, recorded in 6270097840). Cuts 1–12 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797 and #25939. The claim is 6409240079; the scope is the cut 13 proposal of the cut 12 report (6408535757), as the server seat approved it in its dispatch order, narrowed by one member (below).

Two ObjectStackProtocolImplementation members move unedited into a new module, packages/metadata-protocol/src/protocol-packaged-base-refusal.ts (#23050 shape B): the package door's save refusal refusePackagedBaseOverride and the origin-gated removal verdict originGatedRemovalRefusal. protocol.ts loses 61 net lines (29,681 to 29,620). The cut is move-only, and the card stays open for later cuts.

The scope narrowed: refusePackagedBaseRemoval stays in the class

The proposal (my cut 12 report) named three members and said none reads a private-static member. That reading was wrong for one of them: it grepped the one-line spelling ObjectStackProtocolImplementation.NAME, and refusePackagedBaseRemoval breaks that spelling across two lines (ObjectStackProtocolImplementation then .mergesOverlayAtRead(request.type)). mergesOverlayAtRead is private static, and the Release's stay list keeps the private-static readers in the class, which this order's rulings hold. So refusePackagedBaseRemoval stays, unedited, and the re-measure below walks the AST (a property access on the class identifier), not the text. The two members that move read no static member at all.

Does packagedBaseRefusal ride along? No. It is not a small private helper: it is a public member (the /automation domain of @objectstack/runtime calls it), it reads the private static isPackagedBaseRefusal, and it calls refusePackagedBaseRemoval, which stays. It stays in the class and is handed to the moved originGatedRemovalRefusal as a closure.

Branch claude/project-thread-ybjm3h is the harness-pinned branch for this thread, restarted from main after cut 12 merged. It names no issue, so the claim comment is the identity record. It holds the move commit ed0ca2d57 and the pin commit 1c09db420, both on main 7a04ba288d (cut 12's merge efe35983f8 and #25935 are in its history), and the merge of main 59d9181ed (b1ac1d8a9). That merge brought six commits, none of which touches packages/metadata-protocol/ (the package's src/ tree is 761912e9 at both ends of the range).

Serial with the file's in-flight PRs. Measured just before this PR was opened: of the 14 open PRs, one touches packages/metadata-protocol/src/: #25941 edits metadata-diagnostics.ts and adds its memo test, disjoint from protocol.ts. #25826 is no longer open. The release PR #25640's file list could not be paged through this container's proxy; at cut 11 it touched only the package's CHANGELOG.md and package.json. No open PR edits protocol.ts.

What moves

Member Base line Visibility AST tokens (params + return + body)
refusePackagedBaseOverride :16370 private 136
originGatedRemovalRefusal :16534 private 43

(refusePackagedBaseOverride's 72 body lines are mostly its record comment; comments are not AST tokens, and the comparison below checks them as text.)

The shape (#23050 shape B, as cuts 1–12 apply it)

  • The class keeps every member with its name, docblock, signature and position. Each body is now one host literal plus a .call, marked [#25467 cut 13].
  • Typing. PackagedBaseRefusalHost types every member as the class member itself (an indexed access on ObjectStackProtocolImplementation). Each moved function types its this as the Pick of the host over exactly the members its body reads. Each stub annotates its literal as ThisParameterType of typeof fn. No cast, no generic member (so the plain .call spelling), and no member changes visibility.
  • Members read, measured. refusePackagedBaseOverride reads isSealedManagedItem and isWritablePackage; originGatedRemovalRefusal reads packagedBaseRefusal. Each goes as a closure on the protocol. No field is read, so there is no getter.
  • Every other caller still calls the class method: saveMetaItem and packagedBaseRefusal call refusePackagedBaseOverride; deleteMetaItem calls originGatedRemovalRefusal.

Module-level declarations, by reader

  • None gains export, none moves. The bodies read no module-level declaration of protocol.ts. They read SysMetadataRepository (its static readOnlyBaseOverrideError), managedItemSealedSentence and isOriginGatedType, which the new module imports from where protocol.ts imports them (./sys-metadata-repository.js, ./packaged-base-regime.js). The new module's only import from protocol.js is a type import, so it adds no runtime cycle.
  • Imports in protocol.ts. One name loses its last reader and is dropped: isOriginGatedType from ./packaged-base-regime.js (at base: the import, the body of originGatedRemovalRefusal, and a {@link} in its docblock, which stays as prose). The one new statement is the import of the new module.

The published .d.ts

The series bar is ruling 6348891160, sorted-union identity, measured first. Base 7a04ba288d and head were built with pnpm --filter @objectstack/metadata-protocol build against one dependency tree (the closure built at head by a full pnpm build, 119/119, then the package's src/ swapped tree-only to base for the base build and restored from HEAD under a trap; git diff HEAD empty, git status clean and all three touched blobs equal to HEAD's after). Measured twice: on the pre-merge tree (base 7a04ba288d, head 1c09db420) and after the merge (merged base, head b1ac1d8a9, closure rebuilt by a full pnpm build, 119/119); the readings below hold for both.

  • Raw sha256: 08914e65f936a2b031c3f3f215481a97f53d132eefd869e4f42d928947ebe173 at base and at head, on both dist/index.d.ts and dist/index.d.cts, 775,874 bytes each. Raw identity holds, so sorted-union identity holds too. Both members stay private NAME; lines.
  • Normalised sha256: 46ccd7e5c4db673652513d1801ec9c9f06bf34a0a7fb6ba97fb2531ef0cc544b at both ends (80 string-literal union runs sorted; this series' cut 12 normaliser).
  • The two builds differ where they should: index.js hashes 53d93de18be521db… at base and 4d3c4c7364445db0… at head; each of the two functions' .call(host sites occurs 0 times in the base bundles and once in the head bundles (ESM and CJS alike).
  • Positive control on the head tree (on disk, restored from HEAD under a trap, hash equal to HEAD's after): private dropped from originGatedRemovalRefusal and the package rebuilt. Both .d.ts files move to fec45eed6afc2fff…, so the bar can see a change on these members.
  • Export tables. The ESM and CJS bundles, loaded at runtime from the package, each export the same 78 names at base and head; the sorted list hashes to 9f18a0177ac736ed… on all four. Neither moved function nor the host type appears.

No changeset: skip-changeset, per ruling 6348891160.

Proof of move-only

  • Bodies, by AST token (the TypeScript compiler's AST leaf tokens, JSDoc skipped, over parameters less the added this, return type and body): 179 tokens compared, 0 differ. The parameter, return-type and body text, comments included, is also equal with whitespace collapsed, for both. Negative control: one changed token in refusePackagedBaseOverride's body (namedBase && to namedBase ||, on an in-memory copy) makes the check report differ=1 on that member and 0 on the other.
  • Class side. 300 members at base and at head, in the same order. The 298 that did not move are byte-identical to base, leading comments included. Each of the 2 stubs sits at its base position and keeps its docblock and signature byte-identical (everything before the body's opening brace).
  • Top level of protocol.ts. 240 of the 241 base statements other than the class are byte-identical, leading comments included. The other one is the ./packaged-base-regime.js import, which loses isOriginGatedType. Head adds one statement, the new module's import.
  • Size. +157 / −75 over 3 files, 232 changed lines (the order's estimate was 250–320 for three members; the narrowed cut is smaller). protocol-packaged-base-refusal.ts has 140 lines.

Readers of the moved lines

Every test and script that reads protocol.ts by path, and every test file in packages/ and scripts/, was grepped for the three proposed member names and the host name; so was docs/qa/, which cites methods by name.

  • protocol.packaged-base-refusal.test.ts (lines 9, 225, 231), protocol.package-door-before-gates.test.ts (line 7) and objectql engine-security-catalog-package-door.test.ts (line 13) name refusePackagedBaseOverride / refusePackagedBaseRemoval in comments only; none reads protocol.ts by path or asserts on the bodies' text. Verdict: no edit owed; all three pass at head (see Verification).
  • docs/qa/platform-checklist/areas/access-security.json cites packages/metadata-protocol/src/protocol.ts#refusePackagedBaseOverride as an anchor (line 1517) and in prose (line 1459). The method keeps its name in protocol.ts, and check-platform-checklist --changed-from origin/main judges it: OK, 1 anchored file in the change set, 29 anchors judged, 0 lost.
  • No other hit outside CHANGELOG.md entries.
  • Bracketed-refusal pin (PRODUCERS): refusePackagedBaseOverride's one new Error( moves with its body, so the new module gets a row, 'protocol-packaged-base-refusal.ts': 1, the module's own count (the order's "two" counted the one in refusePackagedBaseRemoval, which stays). protocol.ts goes from 72 to 71 such sites, still above its floor of 5. Control: with that throw changed to new TypeError( (on disk, restored from HEAD under a trap, hash equal to HEAD's after; on-disk counts read first: 0 new Error(, 1 new TypeError(), the new row fails with "protocol-packaged-base-refusal.ts no longer constructs errors here — this pin is scanning the wrong file: expected 0 to be greater than or equal to 1" (1 failed, 14 passed); restored, 15/15 pass.
  • ADR anchors and decision letters. check-adr-anchors exits 0 at both ends: 85 anchored files, 58,860 citations, files 8,259 → 8,260, and 1,847 → 1,843 decision-letter citations. The four that leave the anchored file are comments in refusePackagedBaseOverride's body: three ADR-0131 D6 and one ADR-0070 D1. The protocol.ts anchor entry's four paragraphs (ADR-0029 D9.8/D9.9 object hydration, ADR-0119 atomic batches, ADR-0005 appendix (c) view storage, ADR-0126 §2 / §3 flow regime) do not describe the package door, so the new module gets no entry (disclosure only, as cuts 8 and 12 disclosed theirs). ADR-0005, which the moved comments also cite, stays referenced from protocol.ts (the gate's "every governing ADR still referenced" holds).
  • Durability checker: check-durability-degradation-log-level exits 0 at both ends with identical output, and its --list is identical once [@:][0-9]+ is stripped (159 lines): the moved bodies hold no try/catch.
  • Engine-double contract (check-engine-double-contract): exits 0 at both ends. The updateData/deleteData seams keep their entries at new line numbers (14083 → 14085, 14108 → 14110). productionFiles is 3,707 at base and 3,709 at head: +1 is the new file, +1 is the gitignored packages/spec/src/migrations/registry.ts, which the spec build writes in the head tree and the clean base tree had not built.
  • Identical output at both ends: check-adr-symbol-anchors, check-system-context-census, check-tenant-audit-census, check-query-options-erasure-ratchet, check-filter-alias-parity. check-nul-bytes differs only in the scanned file count, +1. The base-end readings were taken in a clean detached worktree at 7a04ba288d.
  • Governed surface. check-governed-merges --branch claude/project-thread-ybjm3h: 0 of 3 paths hit the register, NOT governed; 232 changed lines, under the 3,000-line human-merge bar.

Verification

  • tsc --noEmit in packages/metadata-protocol exits 0 before and after the merge. pnpm exec eslint --no-inline-config packages/metadata-protocol (the whole touched package; it has no lint script, these are the root lint script's flags) exits 0, no output.
  • packages/metadata-protocol suite, base and head, compared per test name and status (vitest --maxWorkers=2, JSON reporter, each run under the shared verify lock, VERDICT command-exit 0 both legs), on the pre-merge tree (the merge changed no file of the package):
    • Base 7a04ba288d: 21,537 tests (21,518 passed, 0 failed, 19 skipped), 267 files.
    • Head 1c09db420: 21,538 tests (21,519 passed, 0 failed, 19 skipped), 267 files.
    • None lost, none changed status, one added: the new PRODUCERS row's case. The base leg is the package src/ swapped tree-only from 7a04ba288d (the new module removed from the tree) and restored from HEAD under a trap.
  • Cross-package pin named in prose. objectql engine-security-catalog-package-door.test.ts at head b1ac1d8a9, under the verify lock: 1 file, 18/18 passed, VERDICT command-exit 0. The two metadata-protocol pins named above ran inside the package suite and pass at both ends.
  • Derived gates. dispatch-gates --commands --repo objectstack-ai/objectstack derives 59 gates on 3 changed paths, the same list before and after the merge; all 59 exit 0 on the merged tree b1ac1d8a9, after the full pnpm build and with the plugin-teardown fixture commit fetched first. dispatch-gates --ran: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.

Acceptance notes

  • refusePackagedBaseRemoval and refusePackagedBaseOverride now live in two files, though their docblocks describe them as twins (one predicate, isSealedManagedItem). The removal twin can follow only once private-static readers can move (the stay list's reason); until then the class docblocks, which did not move, still name each other with {@link}.
  • The proposal's static-read screen was a text grep and missed a two-line spelling; this cut's screen walks the AST. The cut 14 proposal in the report was screened the same way.

Carry-forward

Unchanged from cut 12: public members whose return type is inferred, the bodies kept by a reader (probeRecord, getMetaItemCached, findReferencesToMeta, packagelessAddress, refuseUnmintableMetaType, metaTypeNamespaceExists, viewContainerNameCollisionRefusal with overlaidShippedContainerViewNames, reportPageRequiresAbsentAtLoad, the 7 applyRegistryWriteThrough callers, readCodeLayerForCarryForward), and the private-static readers (now named among them: refusePackagedBaseRemoval and packagedBaseRefusal, besides runPublishSideEffects, promoteDraftForPublish, publishPackageDrafts and discardPackageDrafts) stay in protocol.ts.


Generated by Claude Code

… the origin-gated removal verdict out of protocol.ts (#25467 cut 13)

Move-only. The bodies of refusePackagedBaseOverride and
originGatedRemovalRefusal move into the new
protocol-packaged-base-refusal.ts (host shape B: protocol-bound closures for
isSealedManagedItem, isWritablePackage and packagedBaseRefusal). Both
members keep their name, docblock and signature in the class as stubs that
delegate. refusePackagedBaseRemoval stays in the class: it reads the private
static mergesOverlayAtRead. isOriginGatedType leaves protocol.ts' import with
its last reader. Nothing re-exports the new module, so the published surface
does not move.

Claude-Session: https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6
Co-authored-by: Claude <noreply@anthropic.com>
…e door's throw site (#25467 cut 13)

refusePackagedBaseOverride's one `new Error(` moved with its body into
protocol-packaged-base-refusal.ts, so the scan gains that module's row with
its own count as the floor, as cuts 1, 3, 5, 6, 7, 10 and 12 added theirs.

Claude-Session: https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/concepts/metadata-lifecycle.mdx (via NOT_OVERRIDABLE (literal, a string literal in refusePackagedBaseOverride))
  • content/docs/deployment/environment-variables.mdx (via NOT_OVERRIDABLE (literal, a string literal in refusePackagedBaseOverride))
  • content/docs/permissions/administrator-guide.mdx (via NOT_OVERRIDABLE (literal, a string literal in refusePackagedBaseOverride))
  • content/docs/permissions/authorization.mdx (via NOT_OVERRIDABLE (literal, a string literal in refusePackagedBaseOverride))
  • content/docs/permissions/permission-sets.mdx (via NOT_OVERRIDABLE (literal, a string literal in refusePackagedBaseOverride))
  • content/docs/ui/public-data-collection.mdx (via NOT_OVERRIDABLE (literal, a string literal in refusePackagedBaseOverride))

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

  • content/docs/releases/v17/17-6.mdx (via NOT_OVERRIDABLE (literal, a string literal in refusePackagedBaseOverride))
  • content/docs/releases/v17/17-7.mdx (via NOT_OVERRIDABLE (literal, a string literal in refusePackagedBaseOverride))

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 0c3722882633d2941810df0ab542280407958f3f → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 0c3722882633d2941810df0ab542280407958f3f

⚠️ 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 0c3722882633d2941810df0ab542280407958f3f → 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 · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-26T07:01Z

Head b1ac1d8a94, report 6410166697, claim 6409240079.

item reading
Shape draft, base main, Part of #25467, Clause-②: no and Metadata-protocol: none line-initial, skip-changeset.
Files (3, +157/−75) new protocol-packaged-base-refusal.ts, the two delegating stubs plus import in protocol.ts, one PRODUCERS row in protocol.bracketed-refusal-opener-absence.test.ts. No packages/spec/**, no governed path (check-governed-merges --pr 25972), 232 changed lines.
Scope narrowed (accepted) two members moved, refusePackagedBaseOverride and originGatedRemovalRefusal. refusePackagedBaseRemoval stays: the seat re-read it on the head, its body calls the private static mergesOverlayAtRead split across two lines (protocol.ts 16415), so the stay-list ruling keeps it; packagedBaseRefusal stays (public, reads a private static). One PRODUCERS row, not two, follows.
Shape B ThisParameterType host literals with protocol-bound closures (isSealedManagedItem, isWritablePackage; packagedBaseRefusal), .call(host, …); names, docblocks and signatures stay in the class.
Evidence AST 179 tokens, 0 differ; negative control differ = 1; .d.ts raw identity at both ends; 78 export names identical; suite 0 lost / 0 changed / +1 (the new row, whose control reds); 59/59 derived gates exit 0 (report).
Landing ready + auto-merge through the relay once every check on this head is green or an expected skip. Cut 14 (collectBatchPendingDeclarations + applySeedBodies, report's sizing) is the next claim.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants