Skip to content

feat(#536): run completion for saccade_track (CC-536-01-01) - #562

Merged
raylei50653 merged 5 commits into
mainfrom
feat/536-completion
Oct 10, 2026
Merged

raylei50653 merged 5 commits into
mainfrom
feat/536-completion

Conversation

@raylei50653

Copy link
Copy Markdown
Owner

摘要

#536 Completion 切片(CC-536-01-01),依 owner 授權從 main@f7d4e1e8 起。僅供審查,不要 merge。 契約狀態與 #550 checkpoint 的回寫留到 merge 並通過主線驗證之後。

要求 實作
Run identity 參數解析後產生 run_id(getrandom 128 bit,32 hex),stderr 第一行 saccade_track: run_id <id>;journal 與 report 都帶它。參數被拒時不產生 run id,也不建立任何檔案(與既有 rejected 檢查一致)
Exclusive lock <out>/saccade_track.lock 上非阻塞 flock(LOCK_EX|LOCK_NB),持有到 process 結束;衝突時 exit 2,不寫 journal、不刪檔。lock 檔不刪除
Journal <out>/saccade_track.journal.json,format saccade.native_track_journal/v1:state running/failed/complete;依 argv 順序的每個 sequence 狀態 pending/written,含 txt 與 trace 的 sha256;report {path, sha256};failure {sequence|null, message};identity {"level": null}
Output publication txt、trace、report、journal 都是寫 temp(.<name>.<run_id>.tmp)→ fsync → rename → fsync 目錄。journal 只在 rename 之後才改成 written
Commit point 全部 sequence → report → state=complete(最後一次寫入)
順序 建立 <out>(含不存在的父目錄)→ lock → 以 temp+rename 安裝新 journal → 之後才作廢本輪的 --report、<out>/<seq>.txt、<trace>/<seq>/detector.bin。新 journal 與 lock 在建立任何東西之前就先排除在作廢清單外:輸出路徑若撞到它們,會在 <out> 建立前被拒
Report saccade.native_track_report/v3 = v2 加 run_id 與 identity。v2 語義不變;native_track_parity.py 改釘 v3,並依 caller rule 判讀 journal
相容性 MOT txt 位元組不變(GPU 7/7 與 PR-C2 正式 run 的 hash 完全相同,見下);saccade_track_measurement 的介面與 surface 不變

程式碼:shipping/{include/saccade_shipping,src}/run_completion.*(CUDA-free,只用 std filesystem、POSIX、strict_json、sha256),track_driver.hpp 的 begin_run / run_sequences 透過 RunCompletion::run 提交;兩個 entrypoint 的 main 持有 std::optional<RunCompletion>,錯誤處理寫入 failed。

負控制

CUDA-free 協定測試:tests/native/test_shipping_run_completion.cpp,181 checks,跑在既有的 shipping-config-loader ctest job,沒有改 workflow。每個 case 都在 fork 出來的子 process 執行真正的 RunCompletion::run,所以 kill 是真的 SIGKILL、第二個 run 是真的第二個 process。kill point 只在測試 target 以 SACCADE_RUN_COMPLETION_TEST_FAULTS 編入;shipping 與 measurement build 會把它們編譯成空。

要求的負控制 case 結果
失敗重跑 failed_rerun 新 run_id、failed,failure.sequence 是失敗的那個、message 相同;前面的 sequence 已 written,內容是重跑的位元組;後面的 sequence pending,舊 txt 與 trace 已刪除;<out> 內其他檔案未動
舊 report 殘留 failed_rerun、killed_in_* 位於 <out> 外的舊 report 已作廢;舊 run_id 不再命名任何東西
同目錄雙 process lock_busy 第二個 process exit 2;所有路徑的名稱、位元組、mtime 都不變;journal 保留持有者的 run_id;持有者被 SIGKILL 後由 kernel 釋放 lock
sequence 中途失敗 failed_rerun(throw)、killed_in_sequence(SIGKILL) journal 分別是 failed/running;被中斷的 sequence 為 pending,沒有 txt
rename 成功、journal 未更新 killed_after_rename txt 是本輪的完整輸出,但仍是 pending,caller rule 不算它。另有 killed_before_rename(只有 temp 檔)與 killed_after_report(report 已在,但不是 complete)
<out> 原本不存在 fresh 多層父目錄都會建立,complete,所有 hash 相符,不留下 temp 檔
新 journal 不被清理 killed_in_first_sequence、collisions 作廢之後 journal 仍在,所有 sequence pending;--report 指向 journal、lock、<seq>.txt、trace 檔,或經 .. 別名指向它們時,都在 <out> 建立前被拒;--report 位置是目錄時 run 失敗,目錄保留

Mutation 檢查(本機,非 CI;raw 輸出在 results/536_completion/mutation_006007d3/):12 個 mutant,每個至少讓一項檢查失敗:journal 在 rename 之前寫、直接寫入正式路徑、不取 lock、不作廢、保留舊 report、complete 寫在 report 之前、作廢在 journal 之前、作廢時刪掉新 journal、拿掉碰撞檢查、fail 不記 sequence、lock 衝突時仍寫 journal、建構子失敗不記錄。「作廢時刪掉新 journal」一開始沒被抓到(下一次 commit 會重寫 journal),所以加了 killed_in_first_sequence(commit 006007d3)。每個 mutant 後都以 git checkout 還原,並比對 sha256 確認。

Reader↔writer:tests/unit/test_native_track_parity.py 以 --keep 跑上述測試,再用 native_track_parity 的 committed_sequences/journal_problems 判讀 writer 真正留下的檔案;另有合成 journal 的 fail-closed 測試,涵蓋錯誤 run_id、running/failed、pending 但檔案存在、sha 不符、trace/report 被替換、identity 升級。

真 binary、不需 GPU(tests/unit/test_saccade_track_completion_cli.py,以及修改後的 test_saccade_track_schedule_cli.py;未 build 時 skip):run_id 是第一行且每次不同(兩個 entrypoint 都測);用外部 fcntl.flock 佔住 <out> 時 exit 2,所有檔案不變;--report 指向 journal 時在 <out> 建立前被拒;失敗重跑會刪除舊 report 與本輪的舊 txt,其他 sequence 的 txt 保留。

Runtime 證據(GPU,本機,非 CI)

results/536_completion/runtime_006007d3/(gitignored),commit 006007d3,工作樹乾淨(前後都是),全部在 gpu0 lease 下依序執行(run.sh)。op library aa84cccd… 前後都等於 attestation。binary 是 build-release/shipping/(-DSACCADE_WITH_OPENCV=OFF,PR-C2 的 arch list)。

步驟 結果
本 commit 的 anchor + oracle-rows exit 0
parity,shipping、double buffer、7 序列、含 trace,--against PR-C2 正式 run(full_276659a8/parity_host) EXACT(detector/mot_txt/graph_captures),against.identical=true:txt、trace hash 與 graph 計數完全相同。report v3,identity {"level": null},journal 沒有 problem
measurement none --against 上一列 EXACT,identical
measurement --schedule serial --against PR-C2 serial EXACT,identical
7 個負控制(6 個 mutation + ref_edit) 7/7 CAUGHT
surface 檢查 shipping binary 不含任何禁止字串(23 項,exit 0);measurement binary 被抓到(exit 1,對照組)
completion R1 complete(MOT17-09、02) exit 0,第一行是 run_id,complete,沒有 problem
R2 失敗重跑,同一個 <out>(第二個 sequence MOT17-99-SDP 不存在) exit 2,failed,failure.sequence=MOT17-99-SDP;MOT17-09 已 written,位元組與 R1 相同;舊 report 已刪除;MOT17-02(不在本輪)未動
R3 第二個 process 在持有者執行中啟動 exit 2,「in use by another run」;它的 run_id 沒有出現在 journal 或 report;持有者 exit 0,沒有 problem,txt 與 R1 相同
R4 第一個 sequence written 之後 SIGKILL running;只有 MOT17-09 committed;沒有 MOT17-02 txt,也沒有 report

CI 與 runtime coordinate

shipping/** 屬於 decision_relevant,所以 implementation 軸會移動(290 → 292 檔)。只有這支 PR 時 pytest 會被 6 個 coordinate-bound contract test 擋下,這是預期的閘門,不是功能壞掉。Republication 依 runbook §3.2 走 stacked PR(base = 本 branch),連結見下方留言;其餘軸不變,probe 重新抓取(behavior 2dabed0b,與先前相同;equivalence 仍是 unproven)。

本 PR 不做的事

沒有 Preflight/Gate A:hash 與輸入檢查仍在 GPU 初始化之後(G2)。沒有 Identity integration:identity.level 一律是 null,沒有 expected_source_verified,也沒有 binding 清單(CC-536-01-02)。不改模型、numerics、MOT 行為、parity 標準或 required CI workflow。發行維持 local-only,#547 不變。不新增狀態轉換語義(#537),也不新增 contract→check 文件(#541)。

Known limits

  1. Package entrypoint 沒有重新 pin。 shipping/entrypoint_pin.json 仍是 PR-C2 的 bytes,所以以 SACCADE_SHIPPING_ENTRYPOINT 打包的 tree 仍是沒有 completion 的舊行為。shipping/package/README.txt 也沒有改:tree 內的 README 必須與 source 位元組相同,而在 pin 更新之前描述 completion 會不正確。連帶的,對 pinned package 跑 native_track_parity --native-from,現在會得到 UNRESOLVED(v2 report,沒有 journal)。卡片要求把作廢的行為變更寫進 package README,本 PR 只寫在 CLI 說明,README 等 re-pin 時再改。重新 pin(含 README 與正式 run)需要另一次授權。
  2. <out> 以外的 --report/--trace 不受 lock 保護。現有保護:每個 run 的 temp 檔名不同,加上 rename,所以不會出現寫到一半的檔案;report 帶 run_id;journal 記錄 report 與每個 trace 的 sha256。只有 journal 記錄、而且 hash 相符的檔案才算數(已有測試)。用不同 <out>、但共用 report 或 trace 的兩個並行 run 不會被排除,後 rename 的那個勝出,另一個會被 hash 不符偵測到。
  3. kill 會留下 .<name>.<run_id>.tmp,不會自動清理(作廢只處理本輪的正式路徑)。
  4. 有呼叫 fsync,但沒有測斷電 durability;網路檔案系統或 WSL 掛載的 Windows 磁碟上的 flock/rename 沒有驗證(本次證據在 WSL2 ext4)。
  5. F3:任何時點的 auditor exit 127 都會讓 journal 停在 running。
  6. 行為變更:torn config、缺 lineage,以及(measurement build 的)不合法 mutation 名稱,現在 <out> 會留下 lock 與 failed journal(以前不會建立 <out>);test_saccade_track_schedule_cli.py 的斷言已據此修改。
  7. CI 只涵蓋 CUDA-free 協定測試與合成 journal 的 reader 測試;真 binary 的 CLI 測試與 reader↔writer 交叉檢查在 CI 會 skip(沒有 build),只在本機跑。

🤖 Generated with Claude Code

raylei50653 and others added 3 commits October 9, 2026 23:08
Run id on stderr's first line; non-blocking exclusive flock on <out>;
journal saccade.native_track_journal/v1 (running/failed/complete, per
sequence pending/written with sha256) installed by temp+rename before
this run's earlier report / txt / trace files are removed; txt and trace
temp+rename, journal written after each; report then state=complete as
the only commit point. Report format v3 (v2 + run_id + identity level
null); native_track_parity pins v3 and judges the journal by the caller
rule. CUDA-free protocol test with real SIGKILL kill points and a second
process; real-binary CLI tests without a GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutant that removed the new journal during the cleanup survived the
earlier cases (the next commit rewrote it); this case observes the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Archive the outgoing canonical (implementation 3d85835ce4814d72, the #552
review-fix publication) and promote a fresh capture from the controlled
host (build/runtime_identity, Release, --run-probe --require-complete) at
the implementation head 006007d.

Axes:
* implementation MOVED 3d85835ce4814d72 -> 6848dfaa31137ba2; files 290 -> 292.
  - Added (2): shipping/include/saccade_shipping/run_completion.hpp,
    shipping/src/run_completion.cpp (run id, <out> lock, journal,
    temp -> rename, the complete commit point; CC-536-01-01).
  - Changed (4): shipping/CMakeLists.txt (saccade_shipping_completion and its
    CUDA-free test), shipping/tools/track_driver.hpp (commits through
    RunCompletion; report v3), shipping/tools/saccade_track.cpp and
    shipping/tools/saccade_track_measurement.cpp (begin_run; failed journal
    on a caught error).
  - Removed: none.
* environment, decision_surface, identity_semantics, runtime_inputs:
  unchanged.
* probe: re-run fresh, not reused. Behavior 2dabed0b, equal to the previous
  publication. The build witness moved because build/runtime_identity was
  reconfigured (cmake -S . -B) before the build. Probe equality is not an
  equivalence claim; equivalence stays unproven.

check_runtime_identity_staleness.py --mode attested: exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@raylei50653

Copy link
Copy Markdown
Owner Author

Runtime-coordinate republication (stacked): see the PR above whose base is feat/536-completion.

@raylei50653

Copy link
Copy Markdown
Owner Author

Stacked republication: #563.

@raylei50653

Copy link
Copy Markdown
Owner Author

CI on 006007d3: 7/8 checks passed. shipping-config-loader ran saccade_shipping_run_completion_test and it passed (7/7 ctest). pytest failed on exactly 4 coordinate-bound tests: test_research_lock ×2 and test_runtime_identity_staleness ×2. They report implementation moved and was not republished, 3d85835c → 6848dfaa; the other 5210 tests passed. #563 resolves that lag; CI does not run on #563 because its base is not main. Locally, with #563 applied and the tree clean, the coordinate-bound contract tests give 150 passed.

raylei50653 and others added 2 commits October 10, 2026 09:54
…ads the run id from line 1 only

#562 review P2/P3 (reader and tests only; the shipping implementation
coordinate is unchanged).

P2: journal_problems() parsed the journal with a bare json.loads, so a
journal that exists but is not JSON raised instead of returning problems
(the run would not be judged UNRESOLVED through the diagnostic contract).
It now returns a problem for an unreadable / non-JSON journal, a top level
that is not an object, and `sequences` that is not a list of objects; a
`report` that is not an object counts as unrecorded. committed_sequences()
returns [] for the same malformed `sequences`.

P3: invocation_run_id() accepted a unique run_id line anywhere in the log.
It now takes the run id only from the log's first line (the writer's
first stderr line; the log has no stdout before it) and still refuses a
log with any other run_id line, of either entrypoint.

Negative controls: 8 corrupt journal texts, 6 malformed `sequences`, 3
malformed `report`, and leading-line / second-entrypoint run id logs; 18
of them fail on the previous reader. The 10 saccade_track.log files of the
local GPU runs (results/536_completion/runtime_006007d3) all still yield
their run id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore(provenance): republish the runtime coordinate for #536 Completion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant