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

Description

@os-musk

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:

lineenclosing methodprojectoremit
:15200saveMetaItemyesyes
:16433runPublishSideEffectsyesyes
:20217deleteMetaItem, repository branchyesyes

The three doors #14179 repairs run NO projector:

  1. rollbackMetaItem — after its registry write-through
  2. revertCommit — both limbs (per-item restore, per-item soft-remove)
  3. 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.

<!-- os-decision-facets -->

  • ① 项目长远合理性(权重 ≥50%,领起推荐) —— ADR-0094 的核心不变量是一句话:「投影器是这条记录的唯一写者」。它点名了三扇门。而恢复类的门(回滚、还原提交、legacy 删除出口)改的是同一行元数据,却不在名单里 —— 同一个不变量,一半的门守着、一半的门不守。长远终态只有两种自洽形态:名单覆盖所有会改这行元数据的门,或者 ADR 明说恢复类的门有意不投影、由启动重建兜底。①指向把名单补全,而不是让「哪些门算数」这件事继续靠读代码才知道。
  • ② 实际业务拉动 —— 未量到有客户撞上,而且陈旧是有界的:D3 的启动重建会在下次 kernel:ready 把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。
  • ③ 防 AI 犯错 —— 最响的一棱:把一个 permission 回滚到上一版,元数据行和内存注册表都给出还原后的内容,派生记录却停在回滚前的状态,没有任何报错 —— 运维以为权限已经回滚了,实际派生面还是旧的。⚠️ 但要诚实:metadata-protocol: rollbackMetaItem / revertCommit registry write-throughs never emit onMetadataMutation — boot-cached rebinds and (post-#13331) cluster fan-out miss recovery writes #14179 落地后事件面已经在这四个点广播了,订阅者能看见「写发生过」;缺的是被 await 的投影,不是全无信号。
  • ④ 创业阶段不扩散 —— 补全名单不引入任何新概念:同一个已注册的钩子,在四个已存在的调用点各跑一次。不是 declare-and-maintain,是让已声明的东西在它本就该生效的地方生效。

推荐: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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      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

      Description

      @os-musk

      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:

      lineenclosing methodprojectoremit
      :15200saveMetaItemyesyes
      :16433runPublishSideEffectsyesyes
      :20217deleteMetaItem, repository branchyesyes

      The three doors #14179 repairs run NO projector:

      1. rollbackMetaItem — after its registry write-through
      2. revertCommit — both limbs (per-item restore, per-item soft-remove)
      3. 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.

      <!-- os-decision-facets -->

      • ① 项目长远合理性(权重 ≥50%,领起推荐) —— ADR-0094 的核心不变量是一句话:「投影器是这条记录的唯一写者」。它点名了三扇门。而恢复类的门(回滚、还原提交、legacy 删除出口)改的是同一行元数据,却不在名单里 —— 同一个不变量,一半的门守着、一半的门不守。长远终态只有两种自洽形态:名单覆盖所有会改这行元数据的门,或者 ADR 明说恢复类的门有意不投影、由启动重建兜底。①指向把名单补全,而不是让「哪些门算数」这件事继续靠读代码才知道。
      • ② 实际业务拉动 —— 未量到有客户撞上,而且陈旧是有界的:D3 的启动重建会在下次 kernel:ready 把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。
      • ③ 防 AI 犯错 —— 最响的一棱:把一个 permission 回滚到上一版,元数据行和内存注册表都给出还原后的内容,派生记录却停在回滚前的状态,没有任何报错 —— 运维以为权限已经回滚了,实际派生面还是旧的。⚠️ 但要诚实:metadata-protocol: rollbackMetaItem / revertCommit registry write-throughs never emit onMetadataMutation — boot-cached rebinds and (post-#13331) cluster fan-out miss recovery writes #14179 落地后事件面已经在这四个点广播了,订阅者能看见「写发生过」;缺的是被 await 的投影,不是全无信号。
      • ④ 创业阶段不扩散 —— 补全名单不引入任何新概念:同一个已注册的钩子,在四个已存在的调用点各跑一次。不是 declare-and-maintain,是让已声明的东西在它本就该生效的地方生效。

      推荐: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

      Activity

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

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          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

          Description

          @os-musk

          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:

          lineenclosing methodprojectoremit
          :15200saveMetaItemyesyes
          :16433runPublishSideEffectsyesyes
          :20217deleteMetaItem, repository branchyesyes

          The three doors #14179 repairs run NO projector:

          1. rollbackMetaItem — after its registry write-through
          2. revertCommit — both limbs (per-item restore, per-item soft-remove)
          3. 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.

          <!-- os-decision-facets -->

          • ① 项目长远合理性(权重 ≥50%,领起推荐) —— ADR-0094 的核心不变量是一句话:「投影器是这条记录的唯一写者」。它点名了三扇门。而恢复类的门(回滚、还原提交、legacy 删除出口)改的是同一行元数据,却不在名单里 —— 同一个不变量,一半的门守着、一半的门不守。长远终态只有两种自洽形态:名单覆盖所有会改这行元数据的门,或者 ADR 明说恢复类的门有意不投影、由启动重建兜底。①指向把名单补全,而不是让「哪些门算数」这件事继续靠读代码才知道。
          • ② 实际业务拉动 —— 未量到有客户撞上,而且陈旧是有界的:D3 的启动重建会在下次 kernel:ready 把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。
          • ③ 防 AI 犯错 —— 最响的一棱:把一个 permission 回滚到上一版,元数据行和内存注册表都给出还原后的内容,派生记录却停在回滚前的状态,没有任何报错 —— 运维以为权限已经回滚了,实际派生面还是旧的。⚠️ 但要诚实:metadata-protocol: rollbackMetaItem / revertCommit registry write-throughs never emit onMetadataMutation — boot-cached rebinds and (post-#13331) cluster fan-out miss recovery writes #14179 落地后事件面已经在这四个点广播了,订阅者能看见「写发生过」;缺的是被 await 的投影,不是全无信号。
          • ④ 创业阶段不扩散 —— 补全名单不引入任何新概念:同一个已注册的钩子,在四个已存在的调用点各跑一次。不是 declare-and-maintain,是让已声明的东西在它本就该生效的地方生效。

          推荐: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

          Activity

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

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              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

              Description

              @os-musk

              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:

              lineenclosing methodprojectoremit
              :15200saveMetaItemyesyes
              :16433runPublishSideEffectsyesyes
              :20217deleteMetaItem, repository branchyesyes

              The three doors #14179 repairs run NO projector:

              1. rollbackMetaItem — after its registry write-through
              2. revertCommit — both limbs (per-item restore, per-item soft-remove)
              3. 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.

              <!-- os-decision-facets -->

              • ① 项目长远合理性(权重 ≥50%,领起推荐) —— ADR-0094 的核心不变量是一句话:「投影器是这条记录的唯一写者」。它点名了三扇门。而恢复类的门(回滚、还原提交、legacy 删除出口)改的是同一行元数据,却不在名单里 —— 同一个不变量,一半的门守着、一半的门不守。长远终态只有两种自洽形态:名单覆盖所有会改这行元数据的门,或者 ADR 明说恢复类的门有意不投影、由启动重建兜底。①指向把名单补全,而不是让「哪些门算数」这件事继续靠读代码才知道。
              • ② 实际业务拉动 —— 未量到有客户撞上,而且陈旧是有界的:D3 的启动重建会在下次 kernel:ready 把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。
              • ③ 防 AI 犯错 —— 最响的一棱:把一个 permission 回滚到上一版,元数据行和内存注册表都给出还原后的内容,派生记录却停在回滚前的状态,没有任何报错 —— 运维以为权限已经回滚了,实际派生面还是旧的。⚠️ 但要诚实:metadata-protocol: rollbackMetaItem / revertCommit registry write-throughs never emit onMetadataMutation — boot-cached rebinds and (post-#13331) cluster fan-out miss recovery writes #14179 落地后事件面已经在这四个点广播了,订阅者能看见「写发生过」;缺的是被 await 的投影,不是全无信号。
              • ④ 创业阶段不扩散 —— 补全名单不引入任何新概念:同一个已注册的钩子,在四个已存在的调用点各跑一次。不是 declare-and-maintain,是让已声明的东西在它本就该生效的地方生效。

              推荐: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

              Activity

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

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  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

                  Description

                  @os-musk

                  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:

                  lineenclosing methodprojectoremit
                  :15200saveMetaItemyesyes
                  :16433runPublishSideEffectsyesyes
                  :20217deleteMetaItem, repository branchyesyes

                  The three doors #14179 repairs run NO projector:

                  1. rollbackMetaItem — after its registry write-through
                  2. revertCommit — both limbs (per-item restore, per-item soft-remove)
                  3. 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.

                  <!-- os-decision-facets -->

                  • ① 项目长远合理性(权重 ≥50%,领起推荐) —— ADR-0094 的核心不变量是一句话:「投影器是这条记录的唯一写者」。它点名了三扇门。而恢复类的门(回滚、还原提交、legacy 删除出口)改的是同一行元数据,却不在名单里 —— 同一个不变量,一半的门守着、一半的门不守。长远终态只有两种自洽形态:名单覆盖所有会改这行元数据的门,或者 ADR 明说恢复类的门有意不投影、由启动重建兜底。①指向把名单补全,而不是让「哪些门算数」这件事继续靠读代码才知道。
                  • ② 实际业务拉动 —— 未量到有客户撞上,而且陈旧是有界的:D3 的启动重建会在下次 kernel:ready 把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。
                  • ③ 防 AI 犯错 —— 最响的一棱:把一个 permission 回滚到上一版,元数据行和内存注册表都给出还原后的内容,派生记录却停在回滚前的状态,没有任何报错 —— 运维以为权限已经回滚了,实际派生面还是旧的。⚠️ 但要诚实:metadata-protocol: rollbackMetaItem / revertCommit registry write-throughs never emit onMetadataMutation — boot-cached rebinds and (post-#13331) cluster fan-out miss recovery writes #14179 落地后事件面已经在这四个点广播了,订阅者能看见「写发生过」;缺的是被 await 的投影,不是全无信号。
                  • ④ 创业阶段不扩散 —— 补全名单不引入任何新概念:同一个已注册的钩子,在四个已存在的调用点各跑一次。不是 declare-and-maintain,是让已声明的东西在它本就该生效的地方生效。

                  推荐: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

                  Activity

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

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      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

                      Description

                      @os-musk

                      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:

                      lineenclosing methodprojectoremit
                      :15200saveMetaItemyesyes
                      :16433runPublishSideEffectsyesyes
                      :20217deleteMetaItem, repository branchyesyes

                      The three doors #14179 repairs run NO projector:

                      1. rollbackMetaItem — after its registry write-through
                      2. revertCommit — both limbs (per-item restore, per-item soft-remove)
                      3. 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.

                      <!-- os-decision-facets -->

                      • ① 项目长远合理性(权重 ≥50%,领起推荐) —— ADR-0094 的核心不变量是一句话:「投影器是这条记录的唯一写者」。它点名了三扇门。而恢复类的门(回滚、还原提交、legacy 删除出口)改的是同一行元数据,却不在名单里 —— 同一个不变量,一半的门守着、一半的门不守。长远终态只有两种自洽形态:名单覆盖所有会改这行元数据的门,或者 ADR 明说恢复类的门有意不投影、由启动重建兜底。①指向把名单补全,而不是让「哪些门算数」这件事继续靠读代码才知道。
                      • ② 实际业务拉动 —— 未量到有客户撞上,而且陈旧是有界的:D3 的启动重建会在下次 kernel:ready 把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。
                      • ③ 防 AI 犯错 —— 最响的一棱:把一个 permission 回滚到上一版,元数据行和内存注册表都给出还原后的内容,派生记录却停在回滚前的状态,没有任何报错 —— 运维以为权限已经回滚了,实际派生面还是旧的。⚠️ 但要诚实:metadata-protocol: rollbackMetaItem / revertCommit registry write-throughs never emit onMetadataMutation — boot-cached rebinds and (post-#13331) cluster fan-out miss recovery writes #14179 落地后事件面已经在这四个点广播了,订阅者能看见「写发生过」;缺的是被 await 的投影,不是全无信号。
                      • ④ 创业阶段不扩散 —— 补全名单不引入任何新概念:同一个已注册的钩子,在四个已存在的调用点各跑一次。不是 declare-and-maintain,是让已声明的东西在它本就该生效的地方生效。

                      推荐: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

                      Activity

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

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          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

                          Description

                          @os-musk

                          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:

                          lineenclosing methodprojectoremit
                          :15200saveMetaItemyesyes
                          :16433runPublishSideEffectsyesyes
                          :20217deleteMetaItem, repository branchyesyes

                          The three doors #14179 repairs run NO projector:

                          1. rollbackMetaItem — after its registry write-through
                          2. revertCommit — both limbs (per-item restore, per-item soft-remove)
                          3. 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.

                          <!-- os-decision-facets -->

                          • ① 项目长远合理性(权重 ≥50%,领起推荐) —— ADR-0094 的核心不变量是一句话:「投影器是这条记录的唯一写者」。它点名了三扇门。而恢复类的门(回滚、还原提交、legacy 删除出口)改的是同一行元数据,却不在名单里 —— 同一个不变量,一半的门守着、一半的门不守。长远终态只有两种自洽形态:名单覆盖所有会改这行元数据的门,或者 ADR 明说恢复类的门有意不投影、由启动重建兜底。①指向把名单补全,而不是让「哪些门算数」这件事继续靠读代码才知道。
                          • ② 实际业务拉动 —— 未量到有客户撞上,而且陈旧是有界的:D3 的启动重建会在下次 kernel:ready 把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。
                          • ③ 防 AI 犯错 —— 最响的一棱:把一个 permission 回滚到上一版,元数据行和内存注册表都给出还原后的内容,派生记录却停在回滚前的状态,没有任何报错 —— 运维以为权限已经回滚了,实际派生面还是旧的。⚠️ 但要诚实:metadata-protocol: rollbackMetaItem / revertCommit registry write-throughs never emit onMetadataMutation — boot-cached rebinds and (post-#13331) cluster fan-out miss recovery writes #14179 落地后事件面已经在这四个点广播了,订阅者能看见「写发生过」;缺的是被 await 的投影,不是全无信号。
                          • ④ 创业阶段不扩散 —— 补全名单不引入任何新概念:同一个已注册的钩子,在四个已存在的调用点各跑一次。不是 declare-and-maintain,是让已声明的东西在它本就该生效的地方生效。

                          推荐: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

                          Activity

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

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              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

                              Description

                              @os-musk

                              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:

                              lineenclosing methodprojectoremit
                              :15200saveMetaItemyesyes
                              :16433runPublishSideEffectsyesyes
                              :20217deleteMetaItem, repository branchyesyes

                              The three doors #14179 repairs run NO projector:

                              1. rollbackMetaItem — after its registry write-through
                              2. revertCommit — both limbs (per-item restore, per-item soft-remove)
                              3. 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.

                              <!-- os-decision-facets -->

                              • ① 项目长远合理性(权重 ≥50%,领起推荐) —— ADR-0094 的核心不变量是一句话:「投影器是这条记录的唯一写者」。它点名了三扇门。而恢复类的门(回滚、还原提交、legacy 删除出口)改的是同一行元数据,却不在名单里 —— 同一个不变量,一半的门守着、一半的门不守。长远终态只有两种自洽形态:名单覆盖所有会改这行元数据的门,或者 ADR 明说恢复类的门有意不投影、由启动重建兜底。①指向把名单补全,而不是让「哪些门算数」这件事继续靠读代码才知道。
                              • ② 实际业务拉动 —— 未量到有客户撞上,而且陈旧是有界的:D3 的启动重建会在下次 kernel:ready 把投影重新推导出来(元数据为准)。所以这不是「永久错」,是「撑到下次重启」。低拉动。
                              • ③ 防 AI 犯错 —— 最响的一棱:把一个 permission 回滚到上一版,元数据行和内存注册表都给出还原后的内容,派生记录却停在回滚前的状态,没有任何报错 —— 运维以为权限已经回滚了,实际派生面还是旧的。⚠️ 但要诚实:metadata-protocol: rollbackMetaItem / revertCommit registry write-throughs never emit onMetadataMutation — boot-cached rebinds and (post-#13331) cluster fan-out miss recovery writes #14179 落地后事件面已经在这四个点广播了,订阅者能看见「写发生过」;缺的是被 await 的投影,不是全无信号。
                              • ④ 创业阶段不扩散 —— 补全名单不引入任何新概念:同一个已注册的钩子,在四个已存在的调用点各跑一次。不是 declare-and-maintain,是让已声明的东西在它本就该生效的地方生效。

                              推荐: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

                              Activity

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

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions