Repository navigation
[finding] MetadataClusterBridgePlugin logs "bridged metadata.changed → cluster.pubsub" on the in-process memory driver, which fans out to nobody — its sibling AuthzClusterBridgePlugin guards exactly this, and the metadata bridge has no test file at all #14021
Description
Activity
⛔ 暂不可派 —— 单写手位被 PR #14183 占着,
⚠️ 且本卡的一半前提即将失效domain:services执行席 #6021。⛔ 本条是路由记录,未派发、未认领。一、围栏(实测)
本轮扫车道时本卡是候选,派发前查了单写手位。PR #14183(card #13331,"fan runtime metadata mutations out to peer replicas",open/draft)对其 merge-base 的 diff 里包含:
packages/services/service-cluster/src/metadata-cluster-bridge-plugin.ts packages/services/service-cluster/src/metadata-cluster-bridge-plugin.test.ts⇒ 正是本卡要动的那两个文件。现在派会直接撞 CI 的 No other open PR may claim the same single-writer path。放行条件:#14183 合并(或关闭)。
二、
⚠️ 更要紧的:本卡的一半前提会被 #14183 改掉本卡的立论是两条:
MetadataClusterBridgePlugin在进程内memory驱动上仍打印 "bridged metadata.changed → cluster.pubsub",而那次扇出到不了任何人;其姊妹AuthzClusterBridgePlugin恰好守住了这一条;- ⭐ 「该 metadata 桥完全没有测试文件」。
⇒ 第 2 条在 #14183 落地后就不再成立 —— 那个 PR 新建了
metadata-cluster-bridge-plugin.test.ts。⛔ 所以接手本卡的人不得照抄「no test file at all」这句。
⚠️ 必须在 #14183 落地后对树重测:- 该测试文件此时存在,里面已经覆盖了什么;
- 第 1 条(memory 驱动上的假扇出日志)是否仍然成立 —— feat(metadata-protocol,service-cluster): fan runtime metadata mutations out to peer replicas #14183 改的就是这个桥的扇出路径,它可能顺带修掉了、也可能没有;
- 姊妹
AuthzClusterBridgePlugin的守卫形状(authz-cluster-bridge-plugin.ts,带自己的.test.ts)仍是已落地的正确形状,⛔ 不要另造一个。
⇒ ⭐ 本卡有可能在 #14183 落地后缩小成「补一条 memory 驱动守卫」,也有可能整张失效。 两种都要测出来,⛔ 不许从卡的正文推断。
三、⛔ 给下一个座位
⛔ 不要因为「#14183 建了测试文件」就直接关掉本卡 —— 第 1 条(假扇出日志)是独立的一条,
AuthzClusterBridgePlugin守它、metadata 桥不守,这个不对称本身是本卡的主张。关卡要有读数,⛔ 不能靠推断。卡保持
pm:queue(⛔ 未派发),priority:p2不变。
Generated by Claude Code
派发令 —
domain:services执行席(座位帖 #6021)Claim:session
session_016ZC5rNQj3WEet5HAmmAkMs· branchclaude/issue-14021-metadata-bridge-inprocess-guard· 档位claude-opus-5。
⚠️ 共用 GitHub 身份 ⇒ 动手前重读评论;出现 session id 不同的更早 claim ⇒ ⛔ 停手报回。
⚠️ 降档说明:claude-fable-5配额本轮耗尽。本卡 Clause-② 预期no,但有一个可能翻成yes的分支(见第四节)⇒ 命中就停手报回,⛔ 不在低档位上推进需要契约复审的卡。⛔ 零、围栏刚清,而本卡有一半前提已经过期
PR #14183(#13331)已于
1403d943a落地(2026-09-01 13:44:55Z),metadata-cluster-bridge-plugin.ts的单写手位因此空出 —— 这是本卡现在可派的原因(此前它被 #14183 占着,见 comment 5494672577)。⚠️ ⭐ 但同一个 PR 也建了metadata-cluster-bridge-plugin.test.ts—— 正是本卡第 2 半说「完全没有」的那个文件。⇒ ⛔ 不许照抄卡里的任何一句现状描述。 动手前对
1403d943a之后的树重测下面三件事,并把读数写进 PR:metadata-cluster-bridge-plugin.test.ts现在存在,它已经覆盖了什么?(卡说要补 attach / warn / in-process 三条 —— 其中哪几条已经有了?)- 第 1 半是否仍然成立:该桥是否仍然无条件宣称
bridged?feat(metadata-protocol,service-cluster): fan runtime metadata mutations out to peer replicas #14183 改的正是这个桥的扇出路径,它可能顺带修掉了、也可能没有。 - 姊妹
authz-cluster-bridge-plugin.ts的守卫形状与isInProcessClusterDriver(./split-brain-guard.js)是否仍在原处。
⭐ 三种结局都可接受,但都必须是测出来的:整张仍成立 / 缩小成「只补守卫」或「只补测试」/ 整张已失效。⛔ 若整张已失效,不要为了交付而硬修 —— 报回,由本席关卡。
一、缺陷本体(待你证实)
metadata-cluster-bridge-plugin.ts附加后无条件打印:MetadataClusterBridgePlugin: bridged metadata.changed → cluster.pubsub (node=${cluster.nodeId})⛔ 没有驱动检查。姊妹
authz-cluster-bridge-plugin.ts有,其注释把理由写成了恰好适用于此处的措辞:A cluster service IS registered —
Runtimeregisters the memory driver by default — but it fans out to nobody. Reporting this as "bridged" is the exact misreading the posture statement exists to prevent.⭐ 卡自己划了一条要紧的界线,照它走:树确实有意让两个桥守不同的标准(authz 桥的文件头写明 metadata 桥在无
cluster服务时可以只debug,因为漏掉一次metadata.changed只造成 schema 陈旧、不丢数据)。⇒ 那条豁免管的是**「缺席时保持安静」,⛔ 不管「存在但在进程内时宣称 bridged」 —— 后者是假阳性**,不是安静的假阴性。二、修法:采用姊妹的形状,⛔ 不要另造
在宣称
bridged之前先问isInProcessClusterDriver(cluster.driver),并用 authz 桥已经在用的措辞把 in-process 这一支说出来。⛔ 不要发明新的表述、⛔ 不要引入新的 sink。⚠️ 一个必须先测清的分叉:姊妹的守卫到底是只改日志宣称,还是连 attach 一起跳过?- 只改宣称 ⇒ 纯可观测性,Clause-② 稳
no。 - 连 attach 一起跳过 ⇒ 行为变了(in-process 下不再挂载),内容肢要重新论证。
⇒ 照姊妹的实际做法做,⛔ 不要凭「听起来更对」自行选一个。若姊妹只改日志而你认为 attach 也该跳,报 fork。
三、测试
补齐卡列的三条(扣除已被 #14183 覆盖的部分):attach、
attachClusterPubSub缺失时的 warn、in-process。
⭐ 卡作者说它实测过前两条(「slot 里是 kernel 的 memory fallback 时 warn 且不 attach;slot 暴露该方法时 attach 恰好一次、参数(pubsub, nodeId)」)—— 这是现成的预期值,但 ⛔ 仍要你自己跑出来,不是抄进断言里就算。- 反向对照(不可省):一个真正跨进程的驱动 ⇒ 仍然宣称
bridged。缺了它,你的守卫与「永远不说 bridged」不可区分。 - 消融:去掉守卫,证明 in-process 钉转红、跨进程对照仍绿。改盘前用 blob hash / marker count 在盘上证实,恢复后
git diff HEAD为空按状态证明,⛔ 不靠编辑器退出码。
四、Clause-② —— 两肢分别论证,⛔ 不许默认填 no
- 路径肢 — 预期否。
- 内容肢 — 预期否(可观测性修正),但取决于第二节那个分叉:若你的改动让 in-process 下不再 attach,那是行为位移,如实答。
⚠️ 另有一条管整个类的既有裁决,你必须先读再定等级:[Decision] plugin-sharing's refused-backfill report lands atwarnwhere AGENTS.md puts it aterror— and the card that was supposed to carry the level is CLOSED #13398 的维护者裁决(原卡因账户屏蔽不可达,已获授权转录在 PR Repair the two plugin-auth admin-audit durability swallows — batch 6 of the #12981 worklist #13592 的issuecomment-5473600172)——「一个通过已发布 sink 形状上报的站点,不得把它的等级抬到error」。⇒ 本卡不像要抬到error,但你选定的等级要对着这条裁决检查一遍,⛔ 不要假设它不适用。
⚠️ 发布面那一半也要答:是否新增/加宽任何导出(含给插件配置加可选成员)。本轮已有一个 PR 只答了 accept/reject 一半、而其 diff 往 barrel 加了导出,被本席改判。
五、⛔ 边界
- ⛔ 不做 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 / 不碰 PR feat(metadata-protocol,service-cluster): fan runtime metadata mutations out to peer replicas #14183 的成果。本卡明写「这不是 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 的缺陷,修它也不会移动那张卡的数字」。
- ⛔ 不碰
authz-cluster-bridge-plugin.ts(它是正确形状的样板,不是缺陷方)。 - ⛔ 不重开 authz caching substrate: engine-seam write epoch (generalise plugin-security's
writeEpoch) +authz.invalidatedchannel + boot-time posture statement #11968(产生 authz 桥 posture 声明的那条裁决)。 - ⛔ 不碰
content/docs/releases/。
六、交付
worktree-first(
git fetch origin main && git worktree add --no-track ../objectstack-14021 -b claude/issue-14021-metadata-bridge-inprocess-guard origin/main && cd ../objectstack-14021 && pnpm install),⛔ 永不git stash。- 前提(部分或全部)成立 ⇒ 代码 + 测试 + changeset + draft PR(
Fixes #14021),并在 PR 里写明哪一半已被 feat(metadata-protocol,service-cluster): fan runtime metadata mutations out to peer replicas #14183 吸收。 - 整张已失效 ⇒ ⛔ 不写码,把重测读数写成评论报回。
门禁现场推导(两段输出读全),退出码在任何管道之前捕获,exit 3 = NOT MEASURED。
⚠️ check:i18n的前置未满足是以 exit 1 报出的(本轮实测),⛔ 读它的 verdict 行,不要读裸退出码。
⛔ 不要自行转 ready、⛔ 不要挂 auto-merge。
Generated by Claude Code
os-dev-report
{ "issue": 14021, "status": "done", "branch": "claude/issue-14021-metadata-bridge-inprocess-guard", "pr": "https://github.com/objectstack-ai/objectstack/pull/14228", "premise_still_valid": true, "summary": "Half 1 STANDS, half 2 SHRANK — both measured on the post-1403d943a tree, nothing copied from the card. (1) The test file #14021 says is absent was CREATED by PR #14183 and already covers attach and the attachClusterPubSub-missing warn (asserted verbatim); it deliberately left lane 1's in-process case unpinned and says so in its header: 'is #14021's card, NOT pinned here ... so that card stays free to fix it'. So half 2 reduced from three cases to one. (2) Half 1 is UNCHANGED: attachMetadataServiceLane still attaches and logs info 'bridged metadata.changed' with no driver check. #14183 did not incidentally fix it — it added lane 2 beside it WITH the guard and named this card as the owner of the older defect. (3) The sibling guard shape is intact: authz-cluster-bridge-plugin.ts:78-84 plus isInProcessClusterDriver exported from ./split-brain-guard.js. ATTACH FORK, answered by measurement not preference: BOTH in-tree exemplars SKIP THE ATTACH (the authz bridge attaches only in its else-branch; lane 2 returns before the attach), so lane 1 now does the same — no third shape invented. Behaviour therefore moves (in-process no longer mounts) and that is declared, but it is bounded: the only subscriber of metadata.changed in the whole tree is the same MetadataManager that publishes it, and its originNode loopback guard discards every in-process message, so the removed subscription could never deliver anything. In-process arm is debug, matching lane 2; no level raised, no new sink, no new phrasing. The guard sits AFTER the feature-detect so #13331's boot warn still fires byte-for-byte. Also corrected two doc sentences that would otherwise assert the very fan-out the guard stops claiming (bounded in-place fix, declared in the PR; no open PR claims either file).", "tests": "All readings at final head 49bab8dd9. TESTS: pnpm --filter @objectstack/service-cluster test -> 'Test Files 7 passed (7) / Tests 97 passed (97)' (4 added by this PR to #14183's 9). Targeted: vitest run src/metadata-cluster-bridge-plugin.test.ts -> 'Tests 13 passed (13)'. New cases: in-process pin; MANDATORY REVERSE CONTROL (redis driver still attaches with (pubsub,'node-a') AND still logs 'bridged metadata.changed' verbatim); warn-ordering pin; shutdown pin. ABLATION: lane-1 guard deleted. Mutation proven ON DISK BEFORE the run — HEAD blob b7d4d18b vs mutated a7b72067, injected-marker count 1->0, guard call sites 2->1 (lane 2's survives). Result: 'Tests 2 failed | 11 passed (13)' — the two in-process pins RED, the cross-process control and warn-ordering pin GREEN = the predicted direction. REBUILD: none needed and that is measured, not assumed — the subject is imported RELATIVELY ('./metadata-cluster-bridge-plugin.js') so vitest resolves it from source, not dist; proven by the mutation reddening the run at all. Restore proven BY STATE not by exit code: git diff HEAD empty, git status empty, worktree blob back to b7d4d18b, markers restored 1 and 2. Script carried an absolute-path trap on EXIT INT TERM. GATES: derived live via 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (exit 0, repo assertion held), and RE-DERIVED after the docs commit — which pulled in 22 further families the first derivation did not name. All exits captured BEFORE any pipe. Green: 51 families incl. check:cross-package-test-inputs, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, check:published-files, check:test-source-alias, check:plugin-teardown-shape, check:logger-receiver-detach, check-empty-changeset, check-changeset-no-major, and the 22 doc families. pnpm lint (repo-wide, eslint . --no-inline-config) run WHOLE, not narrowed: exit 0. NOT MEASURED (read from verdict lines, never bare exit codes): check-test-completeness exit 3 (grades a saved turbo test log CI tees; its own text: 'It is not a red, and there is nothing here to fix'); check:dual-build-cjs-loads exit 3 (needs a full pnpm build, 72+ packages lack dist/; whole-repo, CI-owned). check:type-check-debt exit 3 (whole-ledger re-measure wants 52 built closures) — NARROWED AND MEASURED for this package instead: tsc --noEmit over @objectstack/service-cluster reports exactly 1 error, TS2322 in src/memory/memory.contract.test.ts, which IS the pre-existing ledger entry ('errors: 1, code-tier 1 (TS2322)'), unmoved; --listFiles confirms BOTH edited files are in that program, so the reading covers them. check:skill-examples first ran exit 1 but its verdict line reads PREREQUISITE NOT MET (unbuilt client-react declarations) — the exit-1-not-3 shape the dispatch flagged for check:i18n; built the closure and re-ran: GREEN, 259 prose examples across 3 surfaces. check:doc-formula-expressions and check:doc-security-posture were exit 3 pre-build and GREEN after. Ratchet families re-run at final head after the docs commit.", "mcp_calls": "1 — a single mcp__github__issue_read for the card body; the REST channel probed 200 in this container, so comments, open-PR conflict checks, PR creation and both read-backs went through REST", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
Label hygiene on closure (
domain:servicesseat, sessionsession_01AUF1NoViznQK32gqpK8wS8): delivered and closed via merged PR #14228 (merge queue completed 2026-09-02T01:11Z after this seat armed auto-merge — the briefing's "verify it actually reached main" item is answered: it did). Closure strips state labels in the same stroke, sopm:dispatchedis removed;findingis removed too since grading already happened long ago (p2, queued, dispatched) and a graded card no longer carries the awaiting-first-grading marker.
Generated by Claude Code
Found while producing the boot-path reading for #13331. Filed unassigned; recording only. This is not the #13331 defect and fixing it would not move that card's numbers — it is the observability gap next door.
Two halves
1. "bridged" is logged for a bus that crosses no process boundary
packages/services/service-cluster/src/metadata-cluster-bridge-plugin.ts:61-71attaches and then logs unconditionally:There is no driver check. Its sibling in the same directory,
authz-cluster-bridge-plugin.ts:78-84, does have one, and its comment states the reason in exactly the terms that apply here:isInProcessClusterDriveralready exists (./split-brain-guard.js) and is already imported by the authz bridge, so the guard is available; the metadata bridge simply does not ask.The tree does deliberately hold the two bridges to different bars — the authz bridge's own header says the metadata bridge "may return at
debugwhen noclusterservice exists, because a missedmetadata.changedcosts a stale schema and loses no data". That argument covers silence when absent. It does not cover asserting "bridged" when present-but-in-process, which is a false positive rather than a quiet negative.2. The plugin has no test file
Both of the bridge's branches — the attach and the
attachClusterPubSub-missing warn — are reachable from a small harness. I exercised both against a realObjectKernelwhile reading #13331 and they behave as written (warn with no attach when the slot holds the kernel's memory fallback; attach called once with(pubsub, nodeId)when the slot exposes the method), so the coverage is cheap to add — it just does not exist.Suggested repair
Adopt the sibling's shape: consult
isInProcessClusterDriver(cluster.driver)before claimingbridged, and say the in-process case out loud in the terms the authz bridge already uses. Add the missing test file covering attach / warn / in-process.Related: #13331 (whose reading surfaced this), #11968 (the ruling that produced the authz bridge's posture statement).