You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
metadata-protocol: the three recovery doors run no ADR-0094 mutation projector — a rollback restores the row and leaves the derived read-model on the rolled-back-FROM state #14415
Found while implementing #14179 (the same three doors' missing onMetadataMutation emit). Out of that card's scope — its own body says so explicitly: "ADR-0094 projectors are separate" — so it is recorded here rather than folded in.
Measurement, on origin/main @ 5c9e40ad9
git grep -n "this.runMutationProjector(" -- packages/metadata-protocol/src/protocol.ts returns THREE call sites, and every one of them is a door that also emits:
rollbackMetaItem — after its registry write-through
revertCommit — both limbs (per-item restore, per-item soft-remove)
deleteMetaItem's legacy raw-engine exit
So the projector coverage and the mutation-event coverage were the SAME hole, at the SAME four call sites, and #14179 repairs only the event half. After that card lands, emitMetadataMutation reaches 7 call sites while runMutationProjector stays at 3.
Consequence
ADR-0094 projectors keep a derived read-model consistent with the metadata row — the shipped example is permission projecting into sys_permission_set, and saveMetaItem's own comment states the contract: "a derived read-model (e.g. sys_permission_set) is already consistent when this save returns".
Roll a permission row back to a previous version and that sentence stops being true: the stored row and the in-memory registry both serve the restored body, while the projected record still holds the rolled-back-FROM state. Same for a commit revert (either limb), and same for a code-only delete on a control-plane bootstrap kernel. The projector is awaited precisely so callers can rely on it, which makes the gap silent rather than eventually-consistent — nothing re-projects until an unrelated save/publish/delete on the same name, or a rebuild.
Note the delete side already has the right shape to copy: deleteMetaItem's repository branch calls the projector with state: 'deleted' and its comment says "a delete may retire the derived record OR reset it to the artifact baseline — the projector re-reads the layered state and decides", which is exactly what a revert's soft-remove limb needs too.
Suggested shape
The same one-edit-per-door repair #14179 makes for the event, applied to the projector, at the same four call sites and in the established order (awaited projection BEFORE the fire-and-forget listeners, per saveMetaItem's comment): state: 'active' with the restored body for the two restore doors, state: 'deleted' for the soft-remove limb and the legacy delete exit.
Worth deciding rather than assuming, and the reason this is a card and not a rider: whether a recovery write should project at all is an ADR-0094 question, not a mechanical symmetry argument. Projecting is what makes the read-model honest; not projecting is defensible only if some other seam re-projects, and no such seam was found.
Dedup
Public issue-search sweep on runMutationProjector, projector rollbackMetaItem, ADR-0094 projector revert and runMutationProjector recovery (open issues), with rollbackMetaItem as the positive control returning 12 cards including #13331 and #14179. The only hits are #14179 itself and two PM seat-post cards; no card names this gap.
Related, not a duplicate: #14179 repairs the mutation-event half of the same four call sites.
Triage — the prediction is now the present tense, and one claim is overstated
Re-measured at origin/maind80140c4c7.
#14179 has LANDED — commit bd4096f, "fix(metadata-protocol): the three recovery doors announce their writes on the mutation choke point", via PR #14411. So the asymmetry this card forecast is live on main today rather than prospective: runMutationProjector still has exactly three call sites (:15223, :16456, :20240 — the lines drifted, the census did not), while emitMetadataMutation now appears 8 times in the same file.
⭐ Correction — the seam the card could not find is in ADR-0094 itself.
The card's closing argument is:
not projecting is defensible only if some other seam re-projects, and no such seam was found.
ADR-0094 D3 is that seam, and D2 names it as the designed healing path:
Boot reconciliation + one-time backfill. At kernel:ready the projection is re-derived from metadata (metadata wins)…
A projector failure is surfaced on the write's response … and logged — never thrown, the metadata write itself succeeded and boot reconciliation heals on next start.
So the stale read-model is bounded, not permanent: it heals at the next kernel start. The card half-knew this — its own sentence ends "…or a rebuild" — but filed it as an afterthought rather than as the ADR's stated answer to exactly this staleness. That moves the exposure from "silently wrong forever" to "silently wrong until restart", which is a real defect on a long-lived process and a materially smaller one than the card argues.
And the card is right that this is an ADR question. ADR-0094 D2 enumerates the doors explicitly — saveMetaItem (active saves), publishMetaItem, deleteMetaItem — and the recovery doors are simply not in that list. Extending the projector to them amends D2's enumeration. That is above the seat, which is why this grades as a decision rather than as the mechanical mirror of #14179.
Found while implementing #14179 (the same three doors' missing
onMetadataMutationemit). Out of that card's scope — its own body says so explicitly: "ADR-0094 projectors are separate" — so it is recorded here rather than folded in.Measurement, on
origin/main@5c9e40ad9git grep -n "this.runMutationProjector(" -- packages/metadata-protocol/src/protocol.tsreturns THREE call sites, and every one of them is a door that also emits::15200saveMetaItem:16433runPublishSideEffects:20217deleteMetaItem, repository branchThe three doors #14179 repairs run NO projector:
rollbackMetaItem— after its registry write-throughrevertCommit— both limbs (per-item restore, per-item soft-remove)deleteMetaItem's legacy raw-engine exitSo the projector coverage and the mutation-event coverage were the SAME hole, at the SAME four call sites, and #14179 repairs only the event half. After that card lands,
emitMetadataMutationreaches 7 call sites whilerunMutationProjectorstays at 3.Consequence
ADR-0094 projectors keep a derived read-model consistent with the metadata row — the shipped example is
permissionprojecting intosys_permission_set, andsaveMetaItem's own comment states the contract: "a derived read-model (e.g. sys_permission_set) is already consistent when this save returns".Roll a
permissionrow back to a previous version and that sentence stops being true: the stored row and the in-memory registry both serve the restored body, while the projected record still holds the rolled-back-FROM state. Same for a commit revert (either limb), and same for a code-only delete on a control-plane bootstrap kernel. The projector is awaited precisely so callers can rely on it, which makes the gap silent rather than eventually-consistent — nothing re-projects until an unrelated save/publish/delete on the same name, or a rebuild.Note the delete side already has the right shape to copy:
deleteMetaItem's repository branch calls the projector withstate: 'deleted'and its comment says "a delete may retire the derived record OR reset it to the artifact baseline — the projector re-reads the layered state and decides", which is exactly what a revert's soft-remove limb needs too.Suggested shape
The same one-edit-per-door repair #14179 makes for the event, applied to the projector, at the same four call sites and in the established order (awaited projection BEFORE the fire-and-forget listeners, per
saveMetaItem's comment):state: 'active'with the restoredbodyfor the two restore doors,state: 'deleted'for the soft-remove limb and the legacy delete exit.Worth deciding rather than assuming, and the reason this is a card and not a rider: whether a recovery write should project at all is an ADR-0094 question, not a mechanical symmetry argument. Projecting is what makes the read-model honest; not projecting is defensible only if some other seam re-projects, and no such seam was found.
Dedup
Public issue-search sweep on
runMutationProjector,projector rollbackMetaItem,ADR-0094 projector revertandrunMutationProjector recovery(open issues), withrollbackMetaItemas the positive control returning 12 cards including #13331 and #14179. The only hits are #14179 itself and two PM seat-post cards; no card names this gap.Related, not a duplicate: #14179 repairs the mutation-event half of the same four call sites.
Triage — the prediction is now the present tense, and one claim is overstated
Re-measured at
origin/maind80140c4c7.#14179 has LANDED — commit
bd4096f, "fix(metadata-protocol): the three recovery doors announce their writes on the mutation choke point", via PR #14411. So the asymmetry this card forecast is live onmaintoday rather than prospective:runMutationProjectorstill has exactly three call sites (:15223,:16456,:20240— the lines drifted, the census did not), whileemitMetadataMutationnow appears 8 times in the same file.⭐ Correction — the seam the card could not find is in ADR-0094 itself.
The card's closing argument is:
ADR-0094 D3 is that seam, and D2 names it as the designed healing path:
So the stale read-model is bounded, not permanent: it heals at the next kernel start. The card half-knew this — its own sentence ends "…or a rebuild" — but filed it as an afterthought rather than as the ADR's stated answer to exactly this staleness. That moves the exposure from "silently wrong forever" to "silently wrong until restart", which is a real defect on a long-lived process and a materially smaller one than the card argues.
And the card is right that this is an ADR question. ADR-0094 D2 enumerates the doors explicitly —
saveMetaItem(active saves),publishMetaItem,deleteMetaItem— and the recovery doors are simply not in that list. Extending the projector to them amends D2's enumeration. That is above the seat, which is why this grades as a decision rather than as the mechanical mirror of #14179.<!-- os-decision-facets -->
kernel:ready把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。推荐:A —— 把 D2 的门名单扩到恢复类的三扇门,按卡面给出的形状(两扇还原门
state: 'active'+ 还原后的 body;软删除限与 legacy 删除出口state: 'deleted',awaited 投影排在 fire-and-forget 监听之前),并同批修订 ADR-0094 D2 的枚举 —— ⛔ 只改代码不改 ADR,就是把下一个读 ADR 的人送进同一个坑。①③同向,④不反对,②虽低但 A 的代价同样低。回退:B —— 不扩,改为在 ADR-0094 里明写恢复类的门有意不投影,由 D3 启动重建兜底,并把「重启前派生记录可能陈旧」写进这三扇门的文档与响应。若维护者认为 await 投影只属于正常写路径、恢复是运维动作、重启兜底已经够,走这条 —— 它同样关掉①的不一致,只是往另一个方向。
置信缺口(本分析看不见什么): 没有量长驻进程实际多久重启一次 —— 那决定 ② 的「有界陈旧」是几分钟还是几周,也就决定 B 的真实代价。另外目前只有
permission一个投影器注册在案(plugin-security),这个缺口会随将来注册的类型数放大;将来会有几个,本轮没量。Generated by Claude Code