Skip to content

refactor(metadata-protocol): move the flow save's retired-spelling replay verdict out of protocol.ts into protocol-flow-save-replay.ts (#25467 cut 18) - #26317

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

objectstack-fleet[bot] merged 2 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 18 of the protocol.ts split (#24586 ruling A, recorded in 6270097840). Cuts 1–17 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797, #25939, #25972, #26014, #26104, #26168 and #26264. The claim is 6425711631. The scope is the cut 18 proposal of the cut 17 report (6423385238), as the server seat approved it in its dispatch order. The member passed the AST screen and moved unedited.

One ObjectStackProtocolImplementation member moves unedited into a new module, packages/metadata-protocol/src/protocol-flow-save-replay.ts (#23050 shape B):

protocol.ts loses 46 net lines (29,290 to 29,244). The cut is move-only, and the card stays open for later cuts.

Branch claude/project-thread-ybjm3h is the harness-pinned branch for this thread. It was restarted from main 0a1061a61c after cut 17 merged. The branch names no issue, so the claim comment is the identity record. It holds two commits:

  • the move commit e6ddd60ba6;
  • the merge of main 8faba89e69 (5a7ad6a7b2).

That merge touches no file of packages/metadata-protocol/: the package's src/ tree is 23f9f0cc at both ends. It does change the root tsconfig.json and two other packages' package.json, so the workspace was rebuilt in full and the package re-measured after it.

Serial with the file's in-flight PRs

Measured just before this PR was opened: of the 23 open PRs, three touch packages/metadata-protocol/src/. Each was checked with git merge-tree against this branch's head (5a7ad6a7b2).

#26138 edits seven protocol.*.test.ts files, none of which names the moved member, FLOW_FALLBACK_VERDICT_NOT_REPLAYED or the new module. Its merge-tree reports one conflict, in packages/spec/src/ui/visibility-alias-keys-retirement.test.ts, which #26138 has with main itself (a merge-tree against origin/main alone reports it too).

#26305 (head 1267e3cb2b) adds 26 lines to protocol.ts, at :27 (imports), :7145 and :7540. The merge-tree is clean (exit 0).

#26244 (draft, head c33b95d89f) edits protocol.ts (+5 / −2). Its hunks are the @objectstack/spec/contracts import lines at :166–167 and a host literal at :23178. The merge-tree conflicts in protocol.ts, at the import block only. #26244 rewrites lines 166–167. This cut rewrites the adjacent line 168, dropping ALL_CONVERSIONS from the @objectstack/spec import. Git refuses adjacent edits as one conflict. The resolution is the union of both sides:

import type { IFieldFileService, IPubSub } from '@objectstack/spec/contracts';
import { FIELD_FILE_SERVICE, RLS_MEMBERSHIP_RESOLVER_SERVICE } from '@objectstack/spec/contracts';
import { applyConversionsToFlow, applyConversionsToStoredItem, type ConversionNotice, type ConversionTodoNotice } from '@objectstack/spec';

The :23178 hunk merges clean. The same merge-tree also conflicts in packages/spec/src/type-alias-convention.pin.test.ts. That conflict is between #26244 and main itself: a merge-tree of #26244 with origin/main alone shows the same conflict. It is not this cut's.

Whichever PR lands second takes the three-line resolution above. The ALL_CONVERSIONS drop is owed, not optional: the moved body was its last reader in protocol.ts, so keeping it would leave an unused import.

The order's screen (private statics, new Error()

The member passed both checks:

  • No private-static read. The AST walk found no access on the ObjectStackProtocolImplementation identifier and none on this.constructor.
  • No error constructed. The body has no new Error( and no other error constructor, so no PRODUCERS row is owed. It has no try, no catch and no log line.

What moves

Member Base line Visibility Body lines AST tokens (params + return + body)
flowSaveReplaysRetired :6599 private async 55 642

The proposal said 55 body lines.

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

  • The class keeps the member with its name, docblock, signature and position. Its body now delegates, marked [#25467 cut 18].
  • Typing. FlowSaveReplayHost types its one member as the class member itself, ObjectStackProtocolImplementation['getOverlayRepo']. The function types its this as Pick<FlowSaveReplayHost, 'getOverlayRepo'>. The stub annotates its literal as ThisParameterType<typeof flowSaveReplaysRetired>. No member changes visibility.
  • Members read, measured by an AST walk over property and element accesses on this, with as casts, parentheses and ! stripped, and bare this checked too. The body reads getOverlayRepo only. It is handed over as a closure that calls the method on the protocol at call time. A test that sets protocol.getOverlayRepo = ... on the instance (protocol.driver-text-disclosure.test.ts does) still reaches the body.
  • Every other caller still calls the class method.

Module-level declarations, by reader

Measured by an AST walk over the body's identifiers, against protocol.ts's top-level declarations and import bindings. Property names are excluded.

Name Where protocol.ts has it Other readers in protocol.ts What this cut does
FLOW_FALLBACK_VERDICT_NOT_REPLAYED const at :3358 1 (the save door's flow fallback in saveMetaItem, base :20930) gains export in place; the new module imports it at runtime
ALL_CONVERSIONS the @objectstack/spec import, :168 0, so the moved body was its last reader leaves the import, and the new module imports it from @objectstack/spec
applyConversionsToFlow, ConversionNotice the same @objectstack/spec import yes stay; the new module imports them from there too
itemDiscriminator the @objectstack/metadata-core import yes stays; imported from there too
MetadataWriteFace exported type alias yes already exported; imported as a type

FLOW_FALLBACK_VERDICT_NOT_REPLAYED is not re-exported from index.ts, so the package's export table and .d.ts do not change (see below). The new module's runtime import of it from ./protocol.js follows the cycle that earlier modules already use (protocol-publish-batch.ts, protocol-stored-overlay-reads.ts). It reads the constant only at call time.

The one new statement in protocol.ts is the import of the new module.

The durability --list

check-durability-degradation-log-level exits 0 at both ends with identical output. Its --list is identical once [@:][0-9]+ is stripped: 159 lines, and no entry changes file. The body has no catch and makes no engine call. Its two reads are repo.headAt(...) on the overlay repository.

The published .d.ts

The series bar is ruling 6348891160, sorted-union identity, measured first.

Base 0a1061a61c and head e6ddd60ba6 were built with pnpm --filter @objectstack/metadata-protocol build against one dependency tree. The closure was built at head by a full pnpm build (119/119). For the base build, the package's src/ was swapped tree-only to base and restored from HEAD under a trap. Afterwards git diff HEAD was empty, git status was clean, and both touched blobs equalled HEAD's.

The readings were taken on the pre-merge tree and again after merging main (head 5a7ad6a7b2, closure rebuilt by a full pnpm build, 119/119). They 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. The member stays a private NAME; line, and the newly exported FLOW_FALLBACK_VERDICT_NOT_REPLAYED does not appear.
  • Normalised sha256: 46ccd7e5c4db673652513d1801ec9c9f06bf34a0a7fb6ba97fb2531ef0cc544b at both ends (80 string-literal union runs sorted, using the cut 12 normaliser).
  • The two builds differ where they should. index.js hashes 8732ec7d7e0d5cd5… at base and 74a6c78db7e9bcc6… at head. The .call(host site of flowSaveReplaysRetired occurs 0 times in the base bundles and once in the head bundles, ESM and CJS alike. function flowSaveReplaysRetired( appears 0 times at base and once at head.
  • Positive control on the head tree. private was dropped from flowSaveReplaysRetired on disk (restored from HEAD under a trap) and the package rebuilt. Both .d.ts files move to bd3b47c8b435f536… (normalised 164c324a9516551e…) and print flowSaveReplaysRetired(request: { name: string; item?: unknown; packageId?: string | null; …. So the bar can see a change on this member.
  • 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 the moved function, FlowSaveReplayHost nor FLOW_FALLBACK_VERDICT_NOT_REPLAYED appears.

No changeset: skip-changeset, per ruling 6348891160.

Proof of move-only

Body, by AST token. The compare reads the TypeScript compiler's AST leaf tokens, skipping JSDoc, over the parameters (less the added this), the return type and the body. It runs base protocol.ts at 0a1061a61c against the head module:

flowSaveReplaysRetired: base tokens 642, head tokens 642, differ 0, text-equal(ws-collapsed, comments incl.) true
TOTAL tokens=642 differ=0 members=1
CLASS members base=300 head=300 order-same=true byte-identical=299 stubs-with-identical-docblock+signature=1 other=[]
TOP base=246 head=247 byte-identical=244

Negative control. One token was changed in an in-memory copy of the head module (mode === 'draft' ? 'draft' : 'active' to mode !== 'draft' ? …, one occurrence):

MUTATED packages/metadata-protocol/src/protocol-flow-save-replay.ts "mode === 'draft' ? 'draft' : 'active'" -> "mode !== 'draft' ? 'draft' : 'active'"
flowSaveReplaysRetired: base tokens 642, head tokens 642, differ 1, text-equal(ws-collapsed, comments incl.) false
TOTAL tokens=642 differ=1 members=1
  • Class side. There are 300 members at base and at head, in the same order. The 299 that did not move are byte-identical to base, leading comments included. The stub sits at its base position and keeps its docblock and signature byte-identical (everything before the body's opening brace). The base signature spans lines ((request: { … }, mode: …): Promise<boolean> {). The module splits the two parameters onto their own lines. The token compare shows that this changes whitespace only.
  • Top level of protocol.ts. 244 of the 246 base statements are byte-identical, leading comments included. The other two are the @objectstack/spec import, which loses ALL_CONVERSIONS, and FLOW_FALLBACK_VERDICT_NOT_REPLAYED, which gains export. Head adds one statement, the import of the new module.
  • Size. +123 / −54 over 2 files, 177 changed lines. protocol-flow-save-replay.ts has 115 lines.

Readers of the moved lines

Every test and script that reads protocol.ts by path (70 files) was grepped for the member name and for the identifiers its body uses: ALL_CONVERSIONS, applyConversionsToFlow, FLOW_FALLBACK_VERDICT_NOT_REPLAYED, getOverlayRepo, headAt, excludeConversionIds, includeRetired, retiredFromLoadPath, and the local names. So were all test files in packages/ and ui/, and docs/qa/.

  • protocol.view-expansion-reconcile-call-sites.test.ts reads protocol.ts by path. It holds a census of stored-row WRITES (repo.put/delete/promoteDraft/restoreVersion(, raw sys_metadata engine writes). It also pins that a repository is bound only as const repo = this.getOverlayRepo();. The moved body binds repo that way and calls only repo.headAt(, a read. So the census loses no write, and no pin edit is owed. Six earlier seam modules already bind this.getOverlayRepo() outside protocol.ts on the same footing (protocol-drafts.ts, protocol-publish-batch.ts, protocol-save-door-refusals.ts and others). The pin passes at both ends, in the package suite.
  • No other by-path reader reads a moved body. protocol.driver-text-disclosure.test.ts and protocol.object-registry-write-through-spelling.test.ts match on getOverlayRepo, but as an instance override or call, not the moved text.
  • Behaviour through the class method, no edit owed. packages/rest/src/meta-flow-save-retired-spelling.test.ts names flowSaveReplaysRetired in its header and drives it through the save door. It was run at head after the full build: 1 file, 10 tests passed.
  • No hit in docs/qa/.
  • ADR anchors. check-adr-anchors exits 0 with identical output at both ends. The body cites [#23903, ruling B′], an issue citation that moves with it, and no ADR.
  • Engine-double contract (check-engine-double-contract). It exits 0 at both ends. The updateData/deleteData seams keep their entries at new line numbers (13976 → 13930, 14001 → 13955). productionFiles is 3,739 at base and 3,741 at head. Of the +2, one is the new file and one is the gitignored packages/spec/src/migrations/registry.ts that the spec build writes in the head tree.
  • 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 and check-durability-degradation-log-level. check-nul-bytes differs only in the scanned file count, +1. The base-end readings were taken in a clean detached worktree at 0a1061a61c.
  • Governed surface. check-governed-merges --branch claude/project-thread-ybjm3h finds 0 of 2 paths in the register, so the change is NOT governed. At 177 changed lines it is 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 exits 0 with no output. It covers the whole touched package, which has no lint script; these are the root lint script's flags.
  • packages/metadata-protocol suite, base and head, compared per test name and status. Each run used vitest --maxWorkers=2 with the JSON reporter under the shared verify lock, and both legs gave VERDICT command-exit 0. The runs are on the pre-merge tree; the merge changed no file of the package.
    • Base 0a1061a61c: 21,558 tests (21,539 passed, 0 failed, 19 skipped), 269 files.
    • Head e6ddd60ba6: 21,558 tests (21,539 passed, 0 failed, 19 skipped), 269 files.
    • None lost, none added, none changed status. The base leg is the package src/ swapped tree-only from 0a1061a61c (the new module removed) and restored from HEAD under a trap.
  • check-platform-checklist --changed-from origin/main on the merged tree: OK, with 1 anchored file in the change set, 29 anchors judged and 0 lost.
  • Derived gates. dispatch-gates --commands --repo objectstack-ai/objectstack derives 51 gates on 2 changed paths from the merged tree 5a7ad6a7b2; all 51 exit 0 after the full pnpm build (119/119). dispatch-gates --ran: 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The derivation flags the tree as behind main, but only for scripts/check-cli-test-child-env.mjs and scripts/sdui-manifest.sources/i18n.json; neither feeds one of these 51 families. Re-deriving against the newer main gives the same 51-line list, so main was not merged again.

Carry-forward

Unchanged from cut 17. These stay in protocol.ts:

  • 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 ensureObjectStorage.
  • The private-static readers: refusePackagedBaseRemoval, packagedBaseRefusal, runPublishSideEffects, promoteDraftForPublish, publishPackageDrafts and discardPackageDrafts.
  • The field writers: warnClosureReadUnavailable.

🤖 Generated with Claude Code

https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6

…play verdict out of protocol.ts (#25467 cut 18)

Move the body of flowSaveReplaysRetired unedited into
protocol-flow-save-replay.ts, behind a FlowSaveReplayHost that hands over
getOverlayRepo as a closure. The class keeps the method, its docblock and
declared signature; its body becomes a host literal plus a .call.
FLOW_FALLBACK_VERDICT_NOT_REPLAYED gains export in place, and
ALL_CONVERSIONS leaves protocol.ts' spec import with its last reader.

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 5 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 getOverlayRepo (symbol, a field of interface FlowSaveReplayHost), getOverlayRepo (literal, a string literal in flowSaveReplaysRetired; a string literal in getOverlayRepo))
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 3249e204ad1b74ccb23f8295ab5132759418f872 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3249e204ad1b74ccb23f8295ab5132759418f872

⚠️ 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 3249e204ad1b74ccb23f8295ab5132759418f872 → 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-26T21:08Z

Head 5a7ad6a7b2, report 6426921154 (on #25467), claim 6425711631. No contract review is owed, because there is no packages/spec/** diff and no narrowing.

item reading
Shape Draft, base main, Part of #25467 (cut 18), skip-changeset (no published behaviour moves), mergeable clean.
Files 2 files, +123/−54. flowSaveReplaysRetired moves into protocol-flow-save-replay.ts (#23050 shape B). The class member stays as a delegating stub whose host is a getOverlayRepo closure, so the one call site at the flow fallback is unchanged. Not governed.
Fix A pure move. Seat probe: of the 54 removed protocol.ts lines, 52 reappear in the new module, identical once the class indent is stripped. The other two are the order's: the @objectstack/spec import loses ALL_CONVERSIONS (measured by AST walk as its last reader), and FLOW_FALLBACK_VERDICT_NOT_REPLAYED gains export in place. The module imports that constant as a value from ./protocol.js, which is only read at call time; protocol-authoring-gate.ts is the precedent. Dev: AST compare 642 tokens, 0 differ (the negative control gives 1). .d.ts is raw-identical (08914e65…) before and after the merge, and the positive control moves it. Export tables are equal at 78 names.
Order questions Neither a private-static read nor an error constructor is in the body, so no PRODUCERS row is owed. The durability --list and the hand gates are identical at both ends. The reconcile-call-sites pin counts writes, and the moved body only reads (repo.headAt), so no pin edit is owed.
Serial git merge-tree is clean against #26305 (since merged). Against #26244 (draft) it conflicts only at the import block (:166–168). That resolution is a one-line union spelled out in the PR body, and it is #26244's to make when it next merges main.
Evidence 36 check runs succeeded and 16 were expected skips on this head, 0 failed. The suite ran 21,558 tests at both ends, with 0 lost, 0 added and 0 changed. Derived gates: 51 of 51 exit 0, and --ran reconciles.
Carried Cut 19 goes to the same thread once this lands: resolveWritePackageScope (about 40 lines, reads engine only).
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, now that every check on this head is green or an expected skip.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants