Files
FreeCADWasmDev/tools/check_continuation_state.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

126 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""Validate the persistent continuous-execution checkpoint.
This is intentionally read-only. It cannot start a model/agent turn; it gives
an external scheduler or the next resumed turn a deterministic continuation
decision without changing task state.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CHECKPOINT = ROOT / "docs/current/continuation-state.json"
TASK_STATE = ROOT / "docs/current/task-state.json"
MODULE_BATCH = ROOT / "docs/current/module-batch-manifest.json"
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--quiet", action="store_true")
parser.add_argument("--json", action="store_true", help="emit a machine-readable result")
args = parser.parse_args()
checkpoint = json.loads(CHECKPOINT.read_text(encoding="utf-8"))
task_state = json.loads(TASK_STATE.read_text(encoding="utf-8"))
active = task_state.get("active_task")
counts = task_state.get("counts", {})
errors: list[str] = []
if checkpoint.get("mode") != "continuous":
errors.append("continuation mode is not continuous")
if checkpoint.get("scope") != "full-project":
errors.append("continuation scope is not full-project")
allowed_scopes = {"all-task-cards-all-modules-all-required-y-gates"}
if checkpoint.get("implementationStrategy", {}).get("mode") == "feature-first-deferred-validation":
allowed_scopes.add("all-business-workflows-and-feature-routes")
if checkpoint.get("completionScope") not in allowed_scopes:
errors.append("completion scope is not the full project")
if checkpoint.get("noPartialCompletion") is not True:
errors.append("noPartialCompletion is not true")
if checkpoint.get("autoAdvance") is not True:
errors.append("autoAdvance is not true")
if checkpoint.get("mustContinue") is not True:
errors.append("mustContinue is not true")
if checkpoint.get("noFinalBeforeTerminate") is not True:
errors.append("noFinalBeforeTerminate is not true")
if checkpoint.get("phaseTransitionPolicy") != "immediate":
errors.append("phase transition policy is not immediate")
if checkpoint.get("confirmationPolicy") != "never_for_in_scope":
errors.append("confirmation policy is not never_for_in_scope")
if checkpoint.get("pausePolicy") != "never_between_phases":
errors.append("pause policy allows a phase pause")
if checkpoint.get("waitingAllowed") is not False:
errors.append("waitingAllowed must be false")
if checkpoint.get("awaitConfirmation") is not False:
errors.append("awaitConfirmation must be false")
if not isinstance(checkpoint.get("automaticActions"), list) or not checkpoint.get("automaticActions"):
errors.append("automaticActions is empty")
required_actions = {"source edits", "native builds", "asset sync", "context rotation", "ledger sync", "task claim", "single retry after timeout", "recovery-card creation"}
if not required_actions.issubset(set(checkpoint.get("automaticActions", []))):
errors.append("automaticActions is missing required in-scope actions")
allowed_pauses = {"missing credentials", "irreversible destructive action outside the requested repository scope", "new architecture decision that cannot be inferred from source"}
if set(checkpoint.get("pauseOnlyFor", [])) != allowed_pauses:
errors.append("pauseOnlyFor does not match the declared exception boundary")
if not checkpoint.get("termination"):
errors.append("termination policy is empty")
batch_policy = checkpoint.get("batchPolicy")
if not isinstance(batch_policy, dict):
errors.append("batchPolicy is missing")
else:
if batch_policy.get("mode") != "module+tier+family":
errors.append("batchPolicy mode is not module+tier+family")
if int(batch_policy.get("minRecords", 0)) < 2:
errors.append("batchPolicy minRecords must be at least 2")
if int(batch_policy.get("defaultRecords", 0)) < int(batch_policy.get("minRecords", 0)):
errors.append("batchPolicy defaultRecords is below minRecords")
if int(batch_policy.get("maxRecords", 0)) < int(batch_policy.get("defaultRecords", 0)):
errors.append("batchPolicy maxRecords is below defaultRecords")
if batch_policy.get("manifest") != "docs/current/module-batch-manifest.json":
errors.append("batchPolicy manifest is not the canonical module batch manifest")
if checkpoint.get("batchManifest") != "docs/current/module-batch-manifest.json":
errors.append("batchManifest is not the canonical module batch manifest")
if active and not MODULE_BATCH.exists():
errors.append("module-batch-manifest.json is missing for active task")
elif active and MODULE_BATCH.exists():
try:
batch = json.loads(MODULE_BATCH.read_text(encoding="utf-8"))
if batch.get("taskId") not in (None, active):
errors.append("module batch manifest taskId does not match active task")
if batch.get("status") == "ready":
selected = batch.get("selected") if isinstance(batch.get("selected"), dict) else {}
min_records = int(batch_policy.get("minRecords", 2)) if isinstance(batch_policy, dict) else 2
if int(selected.get("recordCount", 0)) < min_records:
if batch.get("singletonFallback") is not True:
errors.append("module batch manifest has fewer records than batchPolicy minRecords without singletonFallback")
if checkpoint.get("batchId") not in (None, batch.get("batchId")):
errors.append("continuation batchId does not match module batch manifest")
except (OSError, json.JSONDecodeError, TypeError, ValueError):
errors.append("module-batch-manifest.json is invalid")
if checkpoint.get("activeTask") != active:
errors.append(f"active task mismatch: checkpoint={checkpoint.get('activeTask')} ledger={active}")
if counts.get("in_progress", 0) != 1:
errors.append(f"expected exactly one in_progress task, got {counts.get('in_progress', 0)}")
phases = {"LOAD_STATE", "VALIDATE", "PREPARE_CONTEXT", "READ_MANIFEST", "IMPLEMENT_BATCH", "RECORD_RESULT", "ROTATE_CONTEXT", "SYNC_STATE", "PREPARE_NEXT", "CONTINUE"}
if checkpoint.get("phase") not in phases:
errors.append(f"invalid continuation phase: {checkpoint.get('phase')}")
if errors:
if args.json:
print(json.dumps({"schema": "freecad-web-continuation-check/v1", "ok": False, "activeTask": active, "errors": errors}, ensure_ascii=False))
return 1
if not args.quiet:
for error in errors:
print(f"CONTINUATION_STATE_INVALID: {error}")
return 1
if args.json:
print(json.dumps({"schema": "freecad-web-continuation-check/v1", "ok": True, "activeTask": active, "phase": checkpoint.get("phase"), "mustContinue": True, "noFinalBeforeTerminate": True, "resumeCommand": checkpoint.get("resumeCommand", "")}, ensure_ascii=False))
elif not args.quiet:
print(f"CONTINUATION_STATE_READY: active={active} phase={checkpoint.get('phase')} mustContinue=true noFinalBeforeTerminate=true")
print(f"RESUME_COMMAND: {checkpoint.get('resumeCommand', '')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())