Repository navigation
refactor(metadata-protocol): move the post-persistence mutation fan-out out of protocol.ts into protocol-mutation-fan-out.ts (#25467 cut 25) - #26807
Conversation
…ut out of protocol.ts into protocol-mutation-fan-out.ts (#25467 cut 25) Move-only, shape B. runMutationProjector, notifyMutationListenersLocal, publishMetadataMutation and emitMetaItemPublished move, unedited, into one module. Each class member keeps its name, docblock and signature as a stub that hands the fields it reads over as getters. The callers stay in the class, so the recovery-doors call-site counts over protocol.ts do not move. Claude-Session: https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check15 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
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 09a37ebf972b9ab2756df9ed6e3ebb5ffe86d2ed && git checkout 09a37ebf972b9ab2756df9ed6e3ebb5ffe86d2ed
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c8583f80ac47177421c9c1fb71e5e33e431ad9cf 02bc68c6829dfde7fc3ae00cc96a60e83a41558e && git checkout -B drift-repro c8583f80ac47177421c9c1fb71e5e33e431ad9cf && git merge --no-ff 02bc68c6829dfde7fc3ae00cc96a60e83a41558e
node scripts/docs-audit/affected-docs.mjs --json c8583f80ac47177421c9c1fb71e5e33e431ad9cf |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Part of #25467 (cut 25). 2 files: protocol.ts and one new module for the claimed theme, protocol-mutation-fan-out.ts (runMutationProjector, notifyMutationListenersLocal, publishMetadataMutation, emitMetaItemPublished). skip-changeset (move-only). Clause-②: no, Metadata-protocol: none. |
| Move check (seat) | All 52 non-blank lines the PR removes from protocol.ts are found verbatim (indent-stripped) in the new module. The added lines in protocol.ts are the import with its header comment and four stubs, each through MutationFanOutHost with getters for the fields it reads (mutationProjectors, metadataMutationListeners, clusterPubSub and clusterNodeId, metaItemPublishedListeners). Every caller keeps calling the class members, so the recovery-doors call-site counts (7/2/7, declarations 1) hold. emitMetadataMutation and applyRemoteMetadataMutation stay. |
| Evidence | CI green on head 02bc68c682 (34 success, 16 skipped, 0 failed). Governed check: NOT governed, 231 changed lines. The report gives an AST compare of 374 tokens with 0 differ (each negative control moves only its own member), raw and sorted-union .d.ts identity (ruling 6348891160) with the positive control moving the hash, the suite at 21,635 tests per-test identical at base, head and merged, the read-seam census at 88 with an identical --list (it grew from 82 through main), and 46 of 46 derived gates. No open PR edits protocol.ts. |
| Disclosed | In the first layout raw .d.ts identity failed on comments only: tsc carried protocol.ts's :967 docblock into an inferred spec type, because the cut's import comment shifted it onto a reused node offset. The dev shortened that import comment by 16 characters, raw identity came back, and every head reading was re-run before push. The trap is latent on main and is written up in the report. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip. |
Generated by Claude Code
Requested by Leehom · project thread
Part of #25467
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6270097840
Cut 25 of the
protocol.tssplit (#24586 ruling A, recorded in6270097840). Cuts 1–24 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797, #25939, #25972, #26014, #26104, #26168, #26264, #26317, #26350, #26366, #26404, #26449, #26654 and #26704. The claim is6446194706. It takes the cut 25 proposal of the cut 24 report (6443136164), a themed group. All four members passed the AST screen and the read-seam census, and all four moved unedited.Four
ObjectStackProtocolImplementationmembers move unedited into one new module,packages/metadata-protocol/src/protocol-mutation-fan-out.ts(#23050 shape B). Together they are the post-persistence mutation fan-out:runMutationProjector: the registered projector for a just-persisted mutation (ADR-0094). It is awaited and never throws.notifyMutationListenersLocal: the local half ofemitMetadataMutation(Authored (Studio) hooks never execute their body — no bodyRunner on the metadata-service bind path #2588, runtime: TS-config boot registers a 'metadata' service without attachClusterPubSub — cross-node invalidation disabled; new object gives OBJECT_NOT_FOUND on non-writing replicas, never heals #13331). Each listener is isolated.publishMetadataMutation: the cluster half ofemitMetadataMutation(runtime: TS-config boot registers a 'metadata' service without attachClusterPubSub — cross-node invalidation disabled; new object gives OBJECT_NOT_FOUND on non-writing replicas, never heals #13331). It publishes the mutation's address to peers, fire-and-forget, and skips drafts.emitMetaItemPublished: the per-item publish listeners (per-item publish(POST /meta/:type/:name/publish)不广播 metadata:reloaded:flow 发布后在存活 kernel 上永不绑定;view drafts 对按项发布还不可见 #10219), awaited and isolated.protocol.tsloses 25 net lines (29,152 to 29,127). The cut is move-only, and the card stays open for later cuts.Branch
claude/project-thread-ybjm3his the harness-pinned branch for this thread. It was restarted frommain0f3d068cf1(after cut 24's merge). It names no issue, so the claim comment is the identity record. It holds the move commit62c90d6199and one merge ofmain,02bc68c682(13 commits). The merge touches no file ofpackages/metadata-protocol/. The package'ssrc/tree is91361684at the move commit and at the merge.Serial with the file's in-flight PRs
Measured just before this PR was opened, against the merged head
02bc68c682: of the 26 open PRs, none editsprotocol.ts, and none names any of the four members,MutationFanOutHostor the new module. Four touch other files ofpackages/metadata-protocol/src/:locale#26769: one test file.migrations/files.disabledReasonbesidedisabled#26786: one test file.Each one's
git merge-treeis clean against this head.mainmoved three commits after the merge: #26787, #26788 (dogfood tests) and #26796 (create-objectstack). None touches this package,git merge-treewith this head is clean, and the derivation found nothing stale, somainwas not merged again.The order's screen (private statics,
new Error(, the read-seam census, the call-site counts)All four members passed:
ObjectStackProtocolImplementationidentifier and none onthis.constructor.new Error(, so noPRODUCERSrow is owed.check-durability-degradation-log-level --listwas taken on the base and again on the cut. Both read88 read seam(s); the base count has grown from 82 since cut 24, throughmain. The lists are identical once[@:][0-9]+is stripped. The full readings are under "The durability--list".protocol.recovery-doors-emit-mutation.test.tsandprotocol.recovery-doors-mutation-projector.test.tsreadprotocol.tsby path and count its text. The counts at base and head:protocol.tsthis.emitMetadataMutation(private emitMetadataMutation(this.notifyMutationListenersLocal(private notifyMutationListenersLocal(this.runMutationProjector(private async runMutationProjector(The callers that are counted stay in the class:
emitMetadataMutation(local, then cluster),applyRemoteMetadataMutation(local only), the seven projecting doors and the per-item publish door. That test file also slicesapplyRemoteMetadataMutation's body by text, and that body does not move. Both files pass at both ends, in the package suite.What moves
runMutationProjector:7493mutationProjectors(field)notifyMutationListenersLocal:7563metadataMutationListeners(field)publishMetadataMutation:7675clusterPubSub,clusterNodeId(fields)emitMetaItemPublished:7864metaItemPublishedListeners(field)protocol-mutation-fan-out.tshas 152 lines.The shape (#23050 shape B, as cuts 1–24 apply it)
The class keeps all four members. Each keeps its name, docblock, signature and position. Each body now delegates, marked
[#25467 cut 25].Every member read is a field, so each is handed over as a getter.
MutationFanOutHosttypes each field as the class member itself (ObjectStackProtocolImplementation['…']). Each function types itsthisas thePickof what it reads, and each stub annotates its literal asThisParameterType<typeof …>.attachMetadataMutationPubSub/detachMetadataMutationPubSubreassignclusterPubSubandclusterNodeId, and the getter sees the current values, exactly as the body did. This is the field hand-over cuts 20, 23 and 24 used.None of the four reads another, so no closure is owed.
No member changes visibility.
Every caller still calls the class method. The sites (base) are:
this.runMutationProjector(at:21364,:22976,:26599,:26718,:27073,:27923and:28121.this.notifyMutationListenersLocal(at:7550and:7787.this.publishMetadataMutation(at:7551.this.emitMetaItemPublished(at:22394.None of them changes.
Module-level declarations, by reader
Measured by an AST walk over each body's identifiers, against
protocol.ts's top-level declarations and import bindings (property names excluded):protocol.tshas itprotocol.tsMETADATA_MUTATION_CLUSTER_CHANNELconst,:5926attachMetadataMutationPubSub, and docblocks)./protocol.jsClusterMetadataMutationPayloadattachMetadataMutationPubSub)MetadataMutationEvent,MutationProjectionOutcome,MetaItemPublishedEventNo import of
protocol.tsloses its last reader, and no declaration gainsexport.The published
.d.ts: one emitter trap, met and avoidedThe series bar is ruling
6348891160: sorted-union identity, measured first, with raw identity reported when it holds. Both hold for this cut, as pushed.Disclosed: the first layout of this cut failed the bar, and only through comments. The cut's first import comment was 16 characters longer. With it, the head
.d.tsmoved to0105198c6b324117…(782,358 bytes; normalisedc211e183dfe9c5e8…). The diff against base was one hunk: an inferred type from@objectstack/specprinted asgeneratedBy /** … */ ? /** … */ : string | undefinedinstead ofgeneratedBy?: string | undefined;. Both comments wereprotocol.ts's own:967docblock ("Zod schemas used to validate overlay items…"), which has nothing to do with that type.removeComments; sha99dc518ed3a640f9…for both). So no type moved.protocol.ts's text at the same offset. The cut's added import comment had shifted that docblock to start at exactly that offset (50,150).index.js8ef90e4aa9cbf4b4…both ways).main: any edit that moves the docblock onto that offset would trigger it. Diff the.d.tshunk before blaming a change for it.Base
0f3d068cf1and head62c90d6199were built withpnpm --filter @objectstack/metadata-protocol buildagainst one dependency tree. The closure was built at head by a fullpnpm build(116/116). For the base build, the package'ssrc/was swapped tree-only to base (the new module removed) and restored fromHEADunder a trap. Afterwardsgit diff HEADwas empty,git statuswas clean, and both touched blobs equalledHEAD's.310d157f1455a7b76c3d9a60e905ac5e07debb23ec4a6a615b510870341bdbecat both ends, ondist/index.d.tsanddist/index.d.cts. The normaliser sorted 80 string-literal union runs. Sorted-union identity holds.724eaa784128c37a7e4b59d85e21212ba6378a5b07279853c97828cd8368e932at both ends, 777,143 bytes. Raw identity holds. It is also the hash cut 24 measured, since the base is cut 24's merge.02bc68c682(closure rebuilt by a fullpnpm build, 116/116): the same raw and normalised sha256, and the sameindex.js/index.cjshashes, as the pre-merge head.index.jshashes0cb9bc1754636851…at base and8ef90e4aa9cbf4b4…at head (index.cjs40c568bf03d0672b…→65ffd5926831fb5d…).….call(hostspellings occurs 0 times in the base bundles and once in the head bundles (ESM and CJS alike).function …(declaration goes 0 → 1.privatewas dropped fromrunMutationProjectoron disk (restored fromHEADunder a trap) and the package rebuilt. Both.d.tsfiles move toc996c4705a6c30f8…(normalised5db8ebd53a1498a3…, which differs from the bar's310d157f…). So the bar can see a change on these members.9f18a0177ac736ed…on all of them. None of the four functions, and notMutationFanOutHost, appears.No changeset:
skip-changeset, per ruling6348891160.The durability
--listcheck-durability-degradation-log-levelexits 0 at both ends with identical output:88 read seam(s)at both ends. Its--listis identical once[@:][0-9]+is stripped (164 lines, and no entry changes file).try(runMutationProjector, and the two listener loops). Each guards a callback, not a call in the gate's read vocabulary (find/findOne/count).warn, as before. No seam enters or leaves the census.88 read seam(s).Proof of move-only
Bodies, by AST token. The compare reads the TypeScript compiler's AST leaf tokens, with JSDoc skipped, over the parameters (less the added
this), the return type and the body. It compares baseprotocol.tsat0f3d068cf1against the head module:Negative controls, one per member. Each changed one token in an in-memory copy of the head module, and only the mutated member moved (differ 1; the other three stayed 0):
protocol.ts. All 254 base statements are byte-identical, leading comments included. Head adds one statement, the new module's import.Readers of the moved lines
The tests and scripts that read
protocol.tsby path were grepped for the four member names, the fields they read, and the bodies' log text:mutation projector failed,metadata-mutation listener failed,cluster metadata-mutation publish failed,post-publish re-bind announce FAILED,partitionKey. These are the files that nameprotocol.tsand callreadFileSync. So were the test files inpackages/,docs/qa/and the@objectstack/specliveness ledgers.By path, two readers name the members, and neither reads a moved body. They are the two recovery-doors tests above. They count call sites and declarations, which stay, and their table holds at both ends. The emit-mutation test also names
publishMetadataMutationin a comment.Behaviour through the class method, no edit owed. These drive the four members through the protocol:
protocol.cluster-mutation-fanout.test.ts,mutation-listeners.test.tsand both recovery-doors tests (metadata-protocol).plugin-publish-announce.test.ts,plugin-authored-hooks.test.tsandplugin-authored-actions.test.ts(objectql).The metadata-protocol ones pass at both ends, in the package suite.
The liveness ledgers. No row points at any of the four. Carried forward:
manifest.json'sdependenciesrow →protocol.ts#resolveWritePackageScope(cut 19), andseed.json→protocol.ts#applySeedBodies(cut 14).check:livenesschecks only that the#symbolis named in the file. Re-pointing them is apackages/spec/**edit, outside this card.ADR anchors.
check-adr-anchorsexits 0 at both ends.Engine-double contract (
check-engine-double-contract): exits 0 at both ends.updateData/deleteDataseams keep their entries at new line numbers (13863 → 13838, 13888 → 13863).productionFilesis 3,798 at base and 3,800 at head. The +2 are the new file and the gitignoredpackages/spec/src/migrations/registry.tsthat the spec build writes in the head tree, as in cuts 20–24.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-parityandcheck-durability-degradation-log-level.check-nul-bytesdiffers only in the scanned file count, +1. The base-end readings were taken in a clean detached worktree at0f3d068cf1.Verification
tsc --noEmitinpackages/metadata-protocolexits 0 at head and on the merged tree.pnpm exec eslint --no-inline-config packages/metadata-protocolexits 0 with no output (the whole touched package, with the rootlintscript's flags).packages/metadata-protocolsuite, base and head, compared per test name and status. Each leg ran vitest--maxWorkers=2with the JSON reporter under the shared verify lock.0f3d068cf1: 21,635 tests (21,616 passed, 0 failed, 19 skipped), 272 files.62c90d6199: 21,635 tests (21,616 passed, 0 failed, 19 skipped), 272 files.src/swapped tree-only from0f3d068cf1(the new module removed) and restored fromHEADunder a trap.02bc68c682gives the same 21,635 tests, per test identical to the pre-merge head (0 lost, 0 added, 0 changed).check-platform-checklist --changed-from origin/main(merged tree): OK. 1 anchored file is in the change set, 29 anchors were judged, and 0 were lost.check-governed-merges --branch claude/project-thread-ybjm3hfinds 0 of 2 paths in the register (13 surfaces), so the PR is NOT governed. It has 231 changed lines, under the 3,000-line human-merge bar.dispatch-gates --commands --repo objectstack-ai/objectstackderives 46 gates on 2 changed paths from02bc68c682, with no stale-tree finding. All 46 exit 0 after the fullpnpm build(116/116).dispatch-gates --ranreports 46 derived, 46 run, 0 NOT-MEASURED and 0 UNRUN, and exits 0.check-durability-degradation-log-level --liston02bc68c682:88 read seam(s).Carry-forward
These stay in
protocol.ts:protocol.tsread-seam entry ofcheck-durability-degradation-log-level --list, as the guarded wrapper or the guarding member, stays until the gate follows imported wrappers or a ruling accepts the loss.findEnvironmentRows,packagelessAddress,findServedOverlayRow,foldStoredCollection,applyObjectRegistryMutation,unmanagedImportCollisions,rollBackUnmanagedImport,readRevertableCommitandstoredFlowBindingAgrees.rethrowUnlessMetadataStoreUnprovisionedcannot move at all.probeRecord,getMetaItemCached,findReferencesToMeta,refuseUnmintableMetaTypeandmetaTypeNamespaceExists.viewContainerNameCollisionRefusalwithoverlaidShippedContainerViewNames, andreportPageRequiresAbsentAtLoad.applyRegistryWriteThroughcallers (among themapplyRemoteMetadataMutation),readCodeLayerForCarryForwardandensureObjectStorage.emitMetadataMutation, a counted call site.refusePackagedBaseRemoval,packagedBaseRefusal,runPublishSideEffects,promoteDraftForPublish,publishPackageDraftsanddiscardPackageDrafts.warnClosureReadUnavailableandresolveSduiManifest.🤖 Generated with Claude Code
https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6