Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# L7: Native drain decisions before whole-Goal migration

This slice removes the second interpretation of proved shadow history from the
Python drainer and repairs byte-change detection before checkpoint effects.
It supports the existing whole-Goal sequence: capture legacy writes, drain the
candidate, qualify exact lineage, fence legacy writers, migrate the reviewed
snapshot and verify canonical consumers. Recovery of a committed transaction
must not become another mutation or a provider-promotion approval.

The accompanying [TS checkpoint](../typescript-control-plane-migration-v0/2026-09-23-shadow-drain-planning.md)
records ownership, lock ordering and transport limits. Real public CLI crash
recovery uses a disposable copy of a complete source population; File and SQLite
canonical readback is checked independently. No active Goal, writer fence or
registry is promoted by this validation. These checks do not substitute for a
reviewed cohort migration, sustained mixed-writer qualification or SQLite D2.

The remaining **5–8 cohesive PR packages** estimate is still conditional:

| Delivery package | Estimate | Decisive remaining outcome |
| --- | --- | --- |
| L2/L3 caller adoption and executor fences | 1–2 | Every shipped caller uses the qualified owner and rejects stale writers. |
| L5/D1 consumer and projection closure | 1 | CLI, packaged clients and projections read canonical state consistently. |
| L6 SQLite D2 | 1–2 | Contributor-owned capacity, crash, restore and genuinely elapsed sustained-run evidence. |
| L7 continuity plus L8 whole-Goal migration | 1–2 | Mixed-writer continuity, reviewed cohort migration and fenced export/rollback. |
| L9 defaults and bounded Python retirement | 1 | New-Goal defaults, onboarding, compatibility guidance and deletion of writers whose final callers have migrated. |

This PR improves L7 and retires a concrete Python rule group; it does not close
one of those complete packages by itself. The required at-least-ten-day SQLite
soak cannot be replaced with more synthetic transactions. PostgreSQL remains a
separate service/credential/tenant and operational qualification path, while the
provider-neutral promotion and canonical-read contracts remain reusable.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# L7:整 Goal 迁移前的原生 drain 决策

本阶段删除 Python drainer 对已证明历史的第二次解释,并修复游标副作用之前的
字节变化识别。它服务于既有整 Goal 路径:捕获旧写入、排空候选、验证精确谱系、
围住旧 writer、迁移已审阅快照、核对 canonical 消费端。
恢复已提交事务不能变成再次写入,也不等于获得 provider 晋升批准。

配套 [TS 检查点](../typescript-control-plane-migration-v0/2026-09-23-shadow-drain-planning.zh-CN.md)
记录规则归属、锁顺序及传输限制。真实 CLI 崩溃恢复使用完整源人口的可丢弃副本,
另行核对 File/SQLite canonical 读回。验证不晋升活跃 Goal、不改它的 writer fence
或 registry,也不能替代经过审阅的 cohort 迁移、持续混合写资格或 SQLite D2。

剩余 **5–8 个完整 PR 包**仍是条件估算:

| 交付包 | 数量 | 尚需可观察结果 |
| --- | --- | --- |
| L2/L3 调用方接入与执行围栏 | 1–2 | 所有已发布调用方使用合格所有者,拒绝陈旧 writer。 |
| L5/D1 消费与投影闭环 | 1 | CLI、打包客户端及投影一致消费 canonical 状态。 |
| L6 SQLite D2 | 1–2 | 既有贡献者负责的容量、崩溃、恢复及真实经过时间的持续验证。 |
| L7 连续性与 L8 整 Goal 迁移 | 1–2 | 混合 writer 连续性、审阅后的 cohort 迁移及带围栏导出/回滚。 |
| L9 默认值与有限 Python 退役 | 1 | 新 Goal 默认、接入流程、兼容指引,以及最后调用方迁走后删除旧 writer。 |

本 PR 推进 L7,并实际退役一组 Python 规则,但不因此把完整交付包机械减一。
SQLite 至少十天的 soak 需要真实经过时间,不能用更多合成事务替代。
PostgreSQL 仍有独立的服务、凭据、租户及运行资格路径;provider 无关的晋升与
canonical 读取合同可复用。
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# T3: One owner for receipt-proven shadow drain recovery

The migration gap was duplicate recovery authority: TS validated the complete
File shadow lineage, then Python received its transaction history and separately
interpreted receipt continuity, cursor anchors, replay budgets and commit ACKs.
`coordination/shadow_drain_plan.ts` now owns those decisions. Its existing drain
caller sends filesystem identities and raw-byte digests; TS obtains history from
the native verifier and returns checkpoint, reclamation and pending-entry plans.
Historical transactions and full projections no longer cross this drain RPC.

The owner is the existing coordination runtime-shadow boundary, with the built-in
File shadow provider. There is no new capability, extension, configuration, CLI
flag or persistent format. The internal request/result v0 pair ships together.
`authority-shadow drain`, inline post-write drain and their existing CLI feedback
consume the same planner; frontend configuration does not change because this
is neither a new setting nor a new user action.

Python retains the source-specific adapter: source readback, primary/maintenance
locks, filesystem observation, byte revalidation, durable cursor writes and
unlink. It releases the maintenance lock before native commit, then reacquires
it for exact ACK readback. The read-only planner does not take that lock again.
A plan is not an unlocked deletion permit: inventory, cursor and bytes must
still match under both locks before effects.

The related semantic repair moves complete raw-byte revalidation before cursor
writes. A formatting-only JSON change after proof formerly escaped decoded-object
comparison: unlink failed, but the cursor could already have advanced. Now the
checkpoint and residue remain untouched in that case. The existing reclamation
byte check is shared with pre-checkpoint validation, not copied.

Characterization covers receipt identity, interleaved partition sequences,
no-op applied markers, corrupt tails outside the budget, marker-only residue,
ACK mismatch and process crash recovery. Real File tests retain the full native
and legacy production-scale Todo populations. A synthetic 4,000-receipt proof
exceeds 2 MiB while its empty-backlog plan remains under 2 KiB; this tests response
amplification, not long-history provider capacity.

This retires Python's history/proof interpretation, not all legacy source codecs.
The existing native 10,000-transaction verification ceiling and verifier cost are
unchanged. Response size depends on pending/recovered entries rather than all
settled history. Large backlog admission and sustained qualification remain their
existing boundaries. See the paired [authority checkpoint](../shared-goal-authority-state-provider-v0/2026-09-23-shadow-drain-recovery.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# T3:影子写入恢复由一个语义所有者决策

原来的重复是:TS 已验证完整 File 影子历史,Python 又接收全部事务历史,
独立解释回执连续性、游标锚点、恢复预算与提交 ACK。现在由
`coordination/shadow_drain_plan.ts` 统一决策;Python 只传文件身份与原始字节摘要,
TS 从现有原生验证器取得历史,返回游标、清理与待提交计划。
历史事务和完整投影不再跨越这条 drain RPC。

所有者仍是 coordination runtime-shadow,provider 仍是内置 File 影子存储。
没有新增 capability、扩展、配置、CLI 参数或持久格式;内部 v0 请求与响应随包配套发布。
`authority-shadow drain`、写入后的内联 drain 和现有 CLI 反馈共用该计划。
这不是新增设置或用户操作,所以无需新增前端配置入口。

Python 保留旧源适配:源数据读回、主写锁与维护锁、文件观测、字节复核、
持久游标写入及删除。调用原生 commit 前释放维护锁,核对 ACK 时重新加锁;
只读 planner 不再次获取该锁。计划不能当作脱离锁的删除许可:执行前仍须
在两个锁内核对完整目录、游标及原始字节。

相邻语义修复是把完整字节复核放到游标写入之前。证明后若 JSON 只改排版,
旧的解码对象比较无法识别;删除虽会失败,游标却可能先被推进。
现在这种情况下游标和残留文件都不改变。删除与游标前复核共用字节检查,
没有复制另一套规则。

验证覆盖身份、交错分区序号、无操作的应用摘要、预算外损坏尾部、仅 marker 残留、
ACK 不一致与进程崩溃恢复。真实 File 测试保留 native/legacy 复杂 Todo 全量人口。
合成 4,000 条回执的证明超过 2 MiB,空积压计划小于 2 KiB;这证明响应不再随
完整历史放大,不代表已取得长历史存储容量资格。

本次退役 Python 的历史/证明解释,不删除全部旧源 codec。原生验证器现有
10,000 笔事务上限与验证成本不变;响应随待处理/恢复条目增长,仍有边界。
大积压接纳和持续运行资格继续由原边界负责。参见配套的
[authority 检查点](../shared-goal-authority-state-provider-v0/2026-09-23-shadow-drain-recovery.zh-CN.md)。
36 changes: 20 additions & 16 deletions examples/shared-goal-authority-e2e/mutants.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,9 @@ def command(self) -> list[str]:
'? "Todo update cannot edit another claim owner\'s work"',
'? "Update rejected"')),),
'tests/control_plane/test_shadow_observable_native_e2e.py::test_canonical_argument_intent_and_atomic_claim[disabled]'),
Case('cursor_baseline_digest', ((COORDINATION + 'local_authority_shadow_adapter.py', replacement(
' return None if marker is None else marker["partition_digest"]',
''' head = transaction["projection"]
return partition_digest({"handoff_mode": head["handoff_mode"], "todos": head["todos"]}
if self._partition == TODO_PARTITION else {"leases": head["leases"]})''')),),
Case('cursor_baseline_digest', ((COORDINATION + 'shadow_drain_plan.ts', replacement(
'last_partition_digest: partitionDigest(last, r.partition)',
'last_partition_digest: view.head_digest')),),
'tests/control_plane/test_shadow_cursor_recovery_e2e.py::test_abandoned_cursor_survives_all_consumers[2-0-todos]'),
Case('qualification_baseline_digest', ((COORDINATION + 'runtime_shadow.ts', replacement(
'const digest = marker === null ? null : (marker as JsonObject).partition_digest;',
Expand All @@ -122,8 +120,10 @@ def command(self) -> list[str]:
(COORDINATION + 'runtime_shadow.ts', replacement(
'if (digest !== cursor.last_partition_digest) throw new ShadowLineageError("outbox_cursor_unproved");',
'// DELIBERATE MUTANT: accept any syntactically valid cursor digest.')),
(COORDINATION + 'local_authority_shadow_adapter.py', replacement(
' or self._cursor_digest(anchor) != cursor["last_partition_digest"]\n', ''))),
(COORDINATION + 'shadow_drain_plan.ts', replacement(
' anchor.provider_revision === r.cursor.last_provider_revision &&\n'
' partitionDigest(anchor, r.partition) === r.cursor.last_partition_digest,\n',
' anchor.provider_revision === r.cursor.last_provider_revision,\n'))),
'tests/control_plane/test_shadow_cursor_recovery_e2e.py::test_forged_applied_digest_holds_every_consumer_without_rewriting_bytes[True-todos]'),
Case('lineage', ((COORDINATION + 'local_authority_shadow.ts', replacement(' requireLineage(entry.capture_lineage_id === binding.capture_lineage_id, "stale_generation");', ' // DELIBERATE MUTANT: omit active lineage validation.')),),
'tests/control_plane_ts/local_authority_shadow_outbox.test.ts', 'self-consistent foreign'),
Expand All @@ -148,12 +148,13 @@ def command(self) -> list[str]:
record,
)'''
CASES.extend([
Case("receipt_bytes", ((COORDINATION + "local_authority_shadow_adapter.py", replacement(
" expected = receipt.get(key)",
" expected = outbox.raw_bytes_digest(path.read_bytes())")),),
Case("receipt_bytes", ((COORDINATION + "shadow_drain_plan.ts", replacement(
"entry[key] === null || entry[key] === rc[key]",
"entry[key] === null || entry[key] === entry[key]")),),
"tests/control_plane/test_shadow_drain_adversarial.py::test_raw_residue_mismatch_preserves_every_file_before_any_cleanup"),
Case("cursor_regression", ((COORDINATION + "local_authority_shadow_adapter.py", replacement(
"last_seq=len(history),", "last_seq=1,")),),
Case("cursor_regression", ((COORDINATION + "shadow_drain_plan.ts", replacement(
"{last_seq: history.size, last_entry_id: last.operation_id",
"{last_seq: 1, last_entry_id: last.operation_id")),),
"tests/control_plane/test_shadow_drain_e2e.py::test_public_primary_maps_one_to_one_to_receipts_and_replays_idempotently"),
Case("early_committed", ((COORDINATION + "local_authority_shadow_outbox.py", replacement(
PREPARED_WRITE, PREPARED_WRITE + '''
Expand Down Expand Up @@ -181,8 +182,8 @@ def command(self) -> list[str]:
"const matched = true;")),),
LADDER_ROW + "[s2c2.parity_divergent_detects_foreign_edit]"),
Case("replay_counted_as_delivery", ((COORDINATION + "local_authority_shadow_adapter.py", replacement(
' self._result.replayed += 1\n self._result.no_op += int(receipt["no_op"])',
' self._result.delivered += 1\n self._result.no_op += int(receipt["no_op"])')),),
" self._result.replayed += 1\n",
" self._result.delivered += 1\n")),),
LADDER_ROW + "[s2c2.sigkill_mid_drain]"),
])

Expand Down Expand Up @@ -252,8 +253,11 @@ def apply(source: str) -> str:
" return # DELIBERATE MUTANT: allow another goal to bypass source authority.\n resolved_source = state_file.resolve(strict=False)")),),
"tests/control_plane/test_shadow_writer_variant_e2e.py::test_other_goal_cannot_write_a_protected_goal_source_via_state_override[active_capture]"),
Case("cleanup_hides_verified_commit", ((COORDINATION + "local_authority_shadow_adapter.py", replacement(
" self._record_view(view)\n self._reconcile(transactions, delivered_entry_id=entry.entry_id)",
" self._reconcile(transactions, delivered_entry_id=entry.entry_id)\n self._record_view(view)")),),
" if self._result.cursor_before is None:\n"
" self._result.cursor_before = view.get(\"cursor\")\n"
" self._record_view(view)\n",
" if self._result.cursor_before is None:\n"
" self._result.cursor_before = view.get(\"cursor\")\n")),),
"tests/control_plane/test_shadow_drain_adversarial.py::test_cleanup_permission_failure_reports_verified_commit_and_recovers[before_commit]"),
Case("native_update_maintenance", ((COORDINATION + "local_authority_runtime.ts",
remove_native_update_maintenance),),
Expand Down
Loading
Loading