Skip to content

Commit e3087da

Browse files
authored
Merge pull request #3985 from wchwawa/codex/stage2c-correctness-rollback
fix(coordination): make Stage 2C capture and rollback recoverable
2 parents b8670eb + 507b9f1 commit e3087da

105 files changed

Lines changed: 14857 additions & 5217 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/python-tests.yml‎

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ jobs:
117117
# Without timing history least_duration alternates equal-weight tests.
118118
# Each runner retains the measured two-worker pool.
119119
run: >-
120-
python -m pytest -q -n 2
120+
python -m pytest -q -n 2 -m "not stage2c_e2e"
121121
--splits 2 --group ${{ matrix.shard }}
122122
--splitting-algorithm least_duration
123123
--durations=25 --durations-min=1
@@ -176,6 +176,70 @@ jobs:
176176
secrets:
177177
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
178178

179+
stage2c-correctness-e2e:
180+
runs-on: ubuntu-latest
181+
timeout-minutes: 30
182+
steps:
183+
- uses: actions/checkout@v7
184+
- uses: actions/setup-python@v6
185+
with:
186+
python-version: "3.11"
187+
cache: pip
188+
cache-dependency-path: tests/requirements-stage2c-linux-py311.txt
189+
- uses: actions/setup-node@v6
190+
with:
191+
node-version: "22.6"
192+
cache: npm
193+
- name: Install locked test and package build tools
194+
run: |
195+
python -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r tests/requirements-stage2c-linux-py311.txt
196+
npm ci --ignore-scripts
197+
- name: Build and verify the checked-out source package
198+
run: |
199+
python -m build --no-isolation --wheel --outdir .local/stage2c-source-install
200+
python - <<'PYTHON'
201+
import hashlib
202+
from pathlib import Path
203+
204+
directory = Path(".local/stage2c-source-install").resolve()
205+
wheels = list(directory.glob("*.whl"))
206+
if len(wheels) != 1:
207+
raise SystemExit("Expected exactly one wheel from the checked-out source")
208+
wheel = wheels[0]
209+
digest = hashlib.sha256(wheel.read_bytes()).hexdigest()
210+
(directory / "requirements.txt").write_text(
211+
f"loopx @ {wheel.as_uri()} --hash=sha256:{digest}\n", encoding="utf-8"
212+
)
213+
PYTHON
214+
python -m pip install --disable-pip-version-check --require-hashes --no-deps --no-index -r .local/stage2c-source-install/requirements.txt
215+
python -m pip check
216+
# Remove the generated source copy before pytest's normal discovery.
217+
python -c "import shutil; shutil.rmtree('build')"
218+
- name: Qualify real CLI, mixed writers, process death, and recovery
219+
env:
220+
LOOPX_SHADOW_COMPARISON_OUTPUT: .local/stage2c-observables
221+
run: python -m pytest -q -m stage2c_e2e --junitxml=stage2c-e2e.xml
222+
- name: Reject deliberate correctness regressions
223+
run: python examples/shared-goal-authority-e2e/mutants.py --output .local/stage2c-mutants
224+
- name: Build independently installed distributions
225+
run: python -m build --no-isolation
226+
- name: Qualify wheel outside the repository
227+
run: python examples/shared-goal-authority-e2e/installed.py --artifact dist/*.whl --report-json installed-wheel.json
228+
- name: Qualify sdist outside the repository
229+
run: python examples/shared-goal-authority-e2e/installed.py --artifact dist/*.tar.gz --report-json installed-sdist.json
230+
- name: Retain bounded acceptance evidence
231+
if: always()
232+
uses: actions/upload-artifact@v7
233+
with:
234+
name: stage2c-correctness-evidence
235+
include-hidden-files: true
236+
path: |
237+
stage2c-e2e.xml
238+
.local/stage2c-observables/
239+
installed-wheel.json
240+
installed-sdist.json
241+
.local/stage2c-mutants/
242+
179243
windows-powershell:
180244
runs-on: windows-latest
181245
timeout-minutes: 20

‎docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2268,9 +2268,14 @@ the unresolved gates remain prerequisites for a real promotion.
22682268
writer fence bound to that revision; provider-first `mutate` and
22692269
`todo_read` that never fall back to Markdown.
22702270
- The fence integration: every Python Todo mutation and every native task-lease
2271-
acquire, renew, transfer, and release checks the durable fence while holding
2272-
its own lock; an absent fence costs no runtime call; a present, unreadable,
2273-
or invalid fence fails closed.
2271+
acquire, renew, transfer, release, verify, and committed releasing
2272+
fence-close checks the durable fence while holding its own lock; an absent
2273+
fence costs no runtime call; a present, unreadable, or invalid fence fails
2274+
closed. A fenced write is rejected before its first side effect as a
2275+
validation-stage `permission_denied` with no receipt; the shared check owns
2276+
only the typed reason and the fence binding facts, and each caller adapter
2277+
renders one provider-neutral remediation from them and carries the check
2278+
result under `write_check`.
22742279
- The complete Todo read model: `loopx_todo_canonical_read_record_v0` publishes
22752280
a versioned field manifest, and the TypeScript projection rejects a
22762281
replacement that drops fields already present on a stored record.

‎docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,8 +1799,11 @@ authority 语义与各 provider 的物理 retention 策略。本文档不实现
17991799
writer fence 的 `coordination.local_authority.promote`;永不回退到 Markdown 的
18001800
provider-first `mutate` 与 `todo_read`。
18011801
- fence 集成:每个 Python Todo mutation 与每个 native task-lease
1802-
acquire/renew/transfer/release 都在自己的锁内检查持久 fence;fence 不存在时零运行时
1803-
调用;fence 存在但不可读或无效时 fail closed。
1802+
acquire/renew/transfer/release/verify 以及已提交的释放型 fence-close 都在自己的锁内
1803+
检查持久 fence;fence 不存在时零运行时调用;fence 存在但不可读或无效时 fail closed。
1804+
被 fence 拒绝的写在第一个副作用之前以 validation 阶段的 `permission_denied`
1805+
返回且不产生回执;共享检查只拥有类型化原因与 fence 绑定事实,各调用方 adapter
1806+
据此渲染同一条 provider 中立的 remediation,并把检查结果放在 `write_check` 下携带。
18041807
- 完整 Todo read model:`loopx_todo_canonical_read_record_v0` 发布带版本字段 manifest;
18051808
TypeScript projection 拒绝 replacement 丢弃既有记录中已经存在的字段。
18061809

‎examples/shared-goal-authority-e2e/README.md‎

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -84,23 +84,33 @@ confined to the `bindings` block nulls every binding, marks
8484
`summary.privacy_violations`, which no flag relaxes. Evidence therefore
8585
carries counters, cursors, outcome tokens, and sha256 prefixes only.
8686

87-
## Test seams later PRs must provide
88-
89-
The pending `s2c2.*` rows will be implemented against these seams; a Stage 2C
90-
parity PR that does not expose them cannot be ladder-verified:
91-
92-
- a drain lock file at `<runtime>/authority-shadow/outbox/<goal>/drain` so the
93-
ladder can hold the drain window with `loopx.file_lock.exclusive_file_lock`
94-
exactly as it holds `<runtime>/authority-shadow/file/<goal>/observation`
95-
today, then SIGKILL a writer before or during drain;
96-
- one file per outbox entry under `<runtime>/authority-shadow/outbox/<goal>/`
97-
with a prepared-then-committed marker, so pending entries are countable and
98-
a rollback with pending entries is observable from disk;
99-
- `drain` and `verify` product commands that emit JSON with `drained_count`,
100-
`cursor_before`, `cursor_after`, `parity_verdict`, and the source and
101-
candidate digests, so parity-equal and foreign-edit rows can assert on
102-
typed fields rather than prose;
103-
- the same commands must resolve the runtime root the way `todo` and
104-
`task-lease` do (`effective_runtime_root`), so the one-lineage guarantee that
105-
`s2c1.dual_runtime_root_consistency` proves for the observation hooks also
106-
holds for drain and verify.
87+
## Bounded outbox correctness and the pending ladder
88+
89+
The independently runnable [correctness suite](correctness.md) covers the
90+
`file_outbox_v1` capture lineage, strict cursor recovery, mixed writers,
91+
source fences, and recoverable bootstrap/rollback. It uses real CLI and native
92+
processes, the production `FileAuthorityStore`, and process death at persistence
93+
boundaries. [Installed-package E2E](installed.py) repeats the public lifecycle
94+
outside the checkout for both wheel and sdist. [Negative controls](mutants.py)
95+
deliberately remove correctness checks in disposable source copies.
96+
97+
These checks do not change the nine pending `s2c2.*` ladder declarations above.
98+
Sustained production parity, the migration/growth gates, and promotion remain
99+
separate obligations. A bounded qualification result reports
100+
`sustained_parity_verdict=not_evaluated`.
101+
102+
Future ladder rows must use the actual product interfaces:
103+
104+
- `authority-shadow drain` for receipt-verified replay and cursor recovery;
105+
- `coordination-shadow inspect / qualify / read-candidate` for comparison,
106+
bounded historical qualification, and a qualified read from that same head;
107+
- `coordination-shadow rollback` with an exact revision or unfinished bootstrap
108+
operation selector, followed by explicit rebootstrap;
109+
- the stable management lock outside the goal outbox for scheduling a management
110+
boundary. Rollback moves the entire goal outbox, so a lock inside that directory
111+
cannot coordinate it. The tests retain scheduling-only barriers around real
112+
persistence calls rather than depending on the retired drain lock.
113+
114+
There is no `verify` command or generic `reset`. The independent v0 observation
115+
store and its runtime-root override behavior retain their original scope; its
116+
historical evidence is not an outbox qualification receipt.

0 commit comments

Comments
 (0)