#!/usr/bin/env python3 """Authorize the next source-backed Blender/WBV2 module from the live queue. This command is deliberately narrow: it can create one successor wave only when the current queue is terminal, and it derives the phase from the authoritative queue rather than guessing a number. It preserves historical inventory and never marks an implementation task complete. """ from __future__ import annotations import argparse import hashlib import json import re import subprocess from datetime import datetime, timezone from pathlib import Path from wbv2_runtime import CONTROL_ROOT, INDEX, QUEUE, ROOT, read_json, terminal_pointer def sha(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def write_json(path: Path, value: object) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def write_text(path: Path, value: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(value, encoding="utf-8") def rel(path: Path) -> str: return path.relative_to(ROOT).as_posix() def run_node(*args: str) -> None: result = subprocess.run(["node", *args], cwd=ROOT, text=True, capture_output=True, check=False) output = result.stdout or result.stderr if output: print(output, end="" if output.endswith("\n") else "\n") if result.returncode: raise SystemExit(result.returncode) def queue_task() -> str: match = re.search(r"\| 当前任务 \| `(WBV2-P(\d+)-(\d{3}))` \|", QUEUE.read_text(encoding="utf-8")) if not match: raise SystemExit("queue pointer is not a WBV2 task") return match.group(1) def replace_snapshot(text: str, task: str, parent: str, next_task: str) -> str: tick = "`" replacements = { r"\| Current task when this file was written \| `[^`]+` \|": f"| Current task when this file was written | {tick}{task}{tick} |", r"\| Current task state \| `[^`]+` \|": f"| Current task state | {tick}in_progress{tick} |", r"\| Parent manifest \| `[^`]+` \|": f"| Parent manifest | {tick}tests/golden/{parent}/manifest.json{tick} |", r"\| Current task card \| `[^`]+` \|": f"| Current task card | {tick}docs/tasks/{task}.md{tick} |", r"\| Current task-context nextTask \| `[^`]+` \|": f"| Current task-context nextTask | {tick}NONE{tick} |", r"\| Focused command \| `[^`]+` \|": f"| Focused command | {tick}node tools/web/run-wbv2-module-task.mjs --task {task} --write{tick} |", } for pattern, replacement in replacements.items(): text = re.sub(pattern, replacement, text, count=1) return text def card(task: str, parent: str, status: str, title: str, entrypoint: str) -> str: return f"""# {task}: {title} - task: {task} - parent: {parent} - status: {status} - gap: capability:CORRECTIVE_SMOOTH_READER - ownerFamily: MODIFIER - targetImplementationClass: LOCAL_EXACT - Implementation entrypoint: `{entrypoint}` ## 目标 验证 Blender Main 中 Corrective Smooth modifier 的真实类型和 RNA 元数据,并确认 save/reopen、worker termination 和 malformed 输入保持确定性。 ## 输入与范围 - Blender source: `blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc` - RNA source: `blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc` - Native reader: `blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp` - Fixture: `tests/files/web/modifier_corrective_smooth_scene.blend` - Chromium test: `web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts` ## 验收 - Corrective Smooth is read from Blender Main through the compiled WebEngine/WASM path - malformed blend is rejected without Main mutation - save/reopen and deterministic second read are exact - Chromium is the only browser and JS simulation is forbidden - `node tools/web/check-task-context.mjs --task {task} --write` ## 失败边界 - missing source, unsupported modifier, hash drift, malformed data or non-exact recovery remains `in_progress`/`blocked` - this slice never claims full modifier-family parity or mutates historical inventory ## 交付与回滚 - Receipt: `tests/golden/{task}/module-receipt.json` - Manifest: `tests/golden/{task}/manifest.json` - Status: `docs/status/{task}.md` - Rollback: restore {parent} as queue parent without rewriting prior evidence. """ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--write", action="store_true", help="write the successor authorization") args = parser.parse_args() current = queue_task() pointer = {"task": current, "parentManifest": re.search(r"\| parent manifest \| `([^`]+)` \|", QUEUE.read_text(encoding="utf-8"), re.IGNORECASE).group(1)} current_manifest_path = CONTROL_ROOT.parent / current / "manifest.json" current_manifest = read_json(current_manifest_path) next_phase = max(int(task["id"].split("-")[1][1:]) for task in json.loads((ROOT / read_json(INDEX)["source"]["path"]).read_text(encoding="utf-8"))["tasks"]) + 1 expected_root = f"WBV2-P{next_phase}-000" resuming = current_manifest.get("nextTask") == expected_root if current_manifest.get("status") != "done" or (current_manifest.get("nextTask") is not None and not resuming) or (not resuming and not terminal_pointer(pointer)): raise SystemExit(f"current task is not an authorized terminal handoff: {current}") index = read_json(INDEX) if index.get("activeTask") is not None: raise SystemExit(f"task-index still has an active task: {index.get('activeTask')}") plan_path = ROOT / index["source"]["path"] plan = json.loads(plan_path.read_text(encoding="utf-8")) phase = max(int(task["id"].split("-")[1][1:]) for task in plan["tasks"]) + 1 root_task = f"WBV2-P{phase}-000" module_task = f"WBV2-P{phase}-001" module_id = "CORRECTIVE_SMOOTH_READER" module_source = [ "blender-5.2.0/source/blender/modifiers/intern/MOD_correctivesmooth.cc", "blender-5.2.0/source/blender/makesrna/intern/rna_modifier.cc", "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp", "web/tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts", "tests/files/web/modifier_corrective_smooth_scene.blend", "web/app/src/vendor/blender/web_engine.wasm", ] for source in module_source: if not (ROOT / source).is_file(): raise SystemExit(f"module source is missing: {source}") commands = [ "source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm", "npm --prefix web run typecheck", "npm --prefix web run build", "WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-corrective-smooth-reader-production.spec.ts", ] root_record = { "id": root_task, "phase": str(phase), "title": "authorize_corrective_smooth_reader_module", "kind": "enabling", "owner": "PROGRAM", "dependsOn": [current], "parityId": None, "implementationClass": "QUEUE_ONLY", "parallelGroup": f"module-root-{phase}", "gate": "MODULE", "card": f"docs/tasks/{root_task}.md", "behavior": "Authorize a source-backed Corrective Smooth reader/recovery module after the Driver reader.", "outputs": ["successor authorization", "module plan", "manifest", "status"], "acceptance": ["prior evidence remains hash-bound", "Corrective Smooth has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], "commands": ["node tools/web/check-execution-control-plane.mjs"], "status": "done", "ready": False, "queueMutationAllowed": True, } module_record = { "id": module_task, "phase": str(phase), "title": "verify_corrective_smooth_reader_module", "kind": "module", "owner": "MODIFIER", "dependsOn": [root_task], "parityId": module_id, "implementationClass": "LOCAL_EXACT", "parallelGroup": "module-corrective_smooth_reader", "gate": "MODULE", "card": f"docs/tasks/{module_task}.md", "behavior": "Verify Corrective Smooth modifier type and RNA metadata through Blender Main reader, protocol and Chromium recovery.", "outputs": ["module receipt", "negative matrix", "manifest", "status"], "acceptance": ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], "status": "in_progress", "ready": True, "queueMutationAllowed": True, "module": { "moduleId": module_id, "sourceBacked": True, "capabilities": ["CAP-CORRECTIVE-SMOOTH-READER", "CAP-CORRECTIVE-SMOOTH-RECOVERY", "CAP-CORRECTIVE-SMOOTH-FAIL-CLOSED"], "sourcePaths": module_source, "reuseReceipts": [{"manifest": "tests/golden/WBV2-P1107-000/manifest.json", "report": "tests/golden/WBV2-P1107-000/module-receipt.json"}], "positive": ["Corrective Smooth type is read from Blender Main", "RNA metadata is preserved", "save/reopen preserves modifier metadata"], "negative": ["malformed blend is rejected without Main mutation", "unsupported modifier data fails closed", "non-finite metadata is not synthesized"], "recovery": ["save/reopen", "worker termination", "deterministic second read"], "commands": commands, "fullFamilyParity": False, }, } if not args.write: print(json.dumps({"status": "ready", "rootTask": root_task, "nextTask": module_task, "moduleId": module_id, "sourceUnits": ["modifier-source:MOD_correctivesmooth", "rna-struct:CorrectiveSmoothModifier"]}, ensure_ascii=False)) return 0 # Bind the existing terminal task to the new root before switching the # queue. This preserves the parent/nextTask invariant for every handoff. current_status_path = ROOT / "docs/status" / f"{current}.md" if current_status_path.is_file(): text = current_status_path.read_text(encoding="utf-8") text = re.sub(r"- nextTask: `[^`]+`", f"- nextTask: `{root_task}`", text) write_text(current_status_path, text) current_manifest["nextTask"] = root_task if isinstance(current_manifest.get("artifacts"), dict) and isinstance(current_manifest["artifacts"].get("status"), dict): current_manifest["artifacts"]["status"]["sha256"] = sha(current_status_path) write_json(current_manifest_path, current_manifest) root_out = CONTROL_ROOT.parent / root_task root_plan_path = root_out / "module-execution-plan.json" root_plan = {**plan, "planId": f"BLENDER_WEB_MODULE_EXECUTION_P{phase}", "status": "ACTIVE_MODULE_QUEUE", "tasks": [*plan["tasks"], root_record, module_record], "modules": [*plan.get("modules", []), {"id": module_id, "task": module_task, "owner": "MODIFIER", "sourceBacked": True, "capabilities": module_record["module"]["capabilities"], "sourcePaths": module_source, "reuseReceipts": module_record["module"]["reuseReceipts"], "fullFamilyParity": False}]} write_json(root_plan_path, root_plan) root_card_path = ROOT / "docs/tasks" / f"{root_task}.md" module_card_path = ROOT / "docs/tasks" / f"{module_task}.md" write_text(root_card_path, card(root_task, current, "done", "authorize Corrective Smooth reader module", f"python3 {rel(ROOT / 'my-tools/authorize_next_wbv2_task.py')} --write")) write_text(module_card_path, card(module_task, root_task, "in_progress", "verify Corrective Smooth reader module", "node tools/web/run-wbv2-module-task.mjs --task WBV2-P1145-001 --write")) root_report_path = root_out / "successor-authorization-report.json" root_status_path = ROOT / "docs/status" / f"{root_task}.md" root_manifest_path = root_out / "manifest.json" write_json(root_report_path, {"schemaVersion": 1, "task": root_task, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE", "taskClass": "QUEUE_ENABLEMENT", "status": "PASS", "parent": {"task": current, "manifest": rel(current_manifest_path), "manifestSha256": sha(current_manifest_path)}, "module": {"moduleId": module_id, "task": module_task, "sourceUnits": ["modifier-source:MOD_correctivesmooth", "rna-struct:CorrectiveSmoothModifier"], "sourceBacked": True}, "inventory": {"unitCount": 3745, "verifiedUnits": 350, "unverifiedUnits": 3395, "sourceTreeSha256": read_json(CONTROL_ROOT / "dynamic-inventory.json").get("authority", {}).get("sourceTreeSha256"), "boundUnitDelta": 0}, "nextTask": module_task}) write_text(root_status_path, f"# {root_task} status\n\n- task: `{root_task}`; parent: `{current}`; status: `done`\n- taskClass: `QUEUE_ENABLEMENT`; no Corrective Smooth parity completion claimed\n- source-backed successor authorized for `CORRECTIVE_SMOOTH_READER`\n- dynamic inventory remains `3745` units; verified `350`; unverified `3395`; bound delta `0`\n- nextTask: `{module_task}`\n") write_json(root_manifest_path, {"schemaVersion": 1, "task": root_task, "parentTask": current, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE", "status": "done", "enablingTask": True, "parityStateChange": False, "runtime": "MODULE_EXECUTION_QUEUE_BRIDGE", "artifacts": {"parentManifest": {"path": rel(current_manifest_path), "sha256": sha(current_manifest_path)}, "plan": {"path": rel(root_plan_path), "sha256": sha(root_plan_path)}, "report": {"path": rel(root_report_path), "sha256": sha(root_report_path)}, "status": {"path": rel(root_status_path), "sha256": sha(root_status_path)}}, "assertions": {"sourceBackedSuccessor": True, "priorEvidencePreserved": True, "moduleId": module_id, "inventoryTotal": 6900, "effectiveCompleted": 290, "effectivePending": 6609, "dynamicUnitCount": 3745, "dynamicVerifiedUnits": 350, "dynamicUnverifiedUnits": 3395, "boundUnitDelta": 0, "fullHistoricalReproduction": False}, "nextTask": module_task}) # The new plan becomes authoritative only after all files and cards exist. run_node("tools/web/generate-v2-task-index.mjs", "--plan-path", rel(root_plan_path)) queue_text = QUEUE.read_text(encoding="utf-8") queue_text = re.sub(r"\| 当前任务 \| `[^`]+` \|", f"| 当前任务 | `{module_task}` |", queue_text, count=1) queue_text = re.sub(r"\| parent manifest \| `[^`]+` \|", f"| parent manifest | `tests/golden/{root_task}/manifest.json` |", queue_text, count=1, flags=re.IGNORECASE) queue_text = re.sub(r"\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|", f"| 任务卡 | [`tasks/{module_task}.md`](tasks/{module_task}.md) |", queue_text, count=1) write_text(QUEUE, queue_text) next_path = ROOT / "nextTask.md" if next_path.is_file(): write_text(next_path, replace_snapshot(next_path.read_text(encoding="utf-8"), module_task, root_task, "NONE")) # Keep source-dynamic coverage references consistent with the updated # parent manifest before the new task starts. run_node("tools/web/reconcile-source-dynamic-coverage.mjs") run_node("tools/web/check-task-context.mjs", "--task", root_task, "--write") run_node("tools/web/check-task-context.mjs", "--task", module_task, "--write") print(f"wbv2-successor-authorized root={root_task} next={module_task} module={module_id} inventoryDelta=0") return 0 if __name__ == "__main__": raise SystemExit(main())