perf(web): add idempotent batch verification fast paths
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-09-03 20:29:51 +08:00
parent b849eb657b
commit b28b365f3a
19 changed files with 652 additions and 71 deletions

View File

@@ -13,6 +13,8 @@ commit and push.
a missing scheduler and stops on a terminal or blocked checkpoint.
- `show_blender_web_scheduler.py`: delegates live RUNNING/IDLE/BLOCKED state to
`tools/web/print-wbv2-live-status.mjs`.
- `validate_incremental_context.py`: validates only the active batch metadata;
unchanged inputs return `incremental-context-ok ... context=REUSED`.
- `start_unattended.sh`: launches the scheduler and watchdog with `nohup` and
`setsid` for process-level continuity.
- `check_continuation_state.py` and `validate_web_context.py`: read-only WBV2
@@ -60,6 +62,12 @@ For machine-readable live state:
python3 my-tools/show_blender_web_scheduler.py --json
```
Implementation cycles use a bounded fast path: each module manifest carries a
stable `batchId` and selected record IDs, context metadata compares
`bytes`/`mtime_ns` before hashing, and the outer scheduler owns planner and
continuation rotation. Full API, format, Chromium, performance and release
gates remain an explicit final-audit operation.
`--commit --push` is enabled by the launcher and can be disabled for a local
dry run with `WBV2_AUTO_COMMIT=0 WBV2_AUTO_PUSH=0`.

View File

@@ -168,6 +168,17 @@ PREFERRED_BATCHES = [
"production": "tests/golden/{task}/line-style-reader-production.json",
"types": [],
},
{
"moduleId": "FCURVE_MODIFIER_SUBTYPE_READER_BATCH",
"title": "verify FCurve modifier subtype reader batch",
"owner": "ANIMATION",
"identifiers": [],
"rna": ["FModifierEnvelope", "FModifierGenerator", "FModifierLimits", "FModifierNoise", "FModifierSmooth", "FModifierStepped"],
"fixture": "tests/files/web/fcurve_modifier_subtype_batch_scene.blend",
"test": "web/tests/e2e/wbv2-fcurve-modifier-production.spec.ts",
"production": "tests/golden/{task}/fcurve-modifier-production.json",
"types": [],
},
{
"moduleId": "MODIFIER_SOURCE_DEPENDENCY_REUSE",
"title": "reuse verified modifier receipts for shared source utilities",
@@ -260,6 +271,12 @@ def rel(path: Path) -> str:
return path.relative_to(ROOT).as_posix()
def batch_contract(task: str, batch: dict) -> dict:
selected = [unit["id"] for unit in batch.get("units", [])]
digest = hashlib.sha256(json.dumps(selected, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest()[:16]
return {"batchId": batch.get("batchId") or f"{task}:{batch['moduleId']}:{digest}", "selectedRecordIds": selected}
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
@@ -298,7 +315,7 @@ def snapshot(text: str, task: str, parent: str, next_task: str) -> str:
return text
def make_card(task: str, parent: str, status: str, batch: dict, entrypoint: str) -> str:
def make_card(task: str, parent: str, status: str, batch: dict, entrypoint: str, contract: dict) -> str:
fixture_paths = batch.get("fixtures") or ([batch["fixture"]] if batch.get("fixture") else [])
fixture_text = ", ".join(f"`{fixture}`" for fixture in fixture_paths)
if not fixture_text:
@@ -312,6 +329,9 @@ def make_card(task: str, parent: str, status: str, batch: dict, entrypoint: str)
- gap: capability:{batch['moduleId']}
- ownerFamily: {batch['owner']}
- targetImplementationClass: QUEUE_ONLY
- batchId: {contract['batchId']}
- selectedRecordIds: {json.dumps(contract['selectedRecordIds'], ensure_ascii=False, separators=(',', ':'))}
- commandBudget: max=48; read/exploration max=16
- Implementation entrypoint: `{entrypoint}`
## 目标
@@ -345,6 +365,9 @@ def make_card(task: str, parent: str, status: str, batch: dict, entrypoint: str)
- gap: capability:{batch['moduleId']}
- ownerFamily: {batch['owner']}
- targetImplementationClass: LOCAL_EXACT
- batchId: {contract['batchId']}
- selectedRecordIds: {json.dumps(contract['selectedRecordIds'], ensure_ascii=False, separators=(',', ':'))}
- commandBudget: max=48; read/exploration max=16
- Implementation entrypoint: `{entrypoint}`
## 目标
@@ -388,6 +411,9 @@ def make_card(task: str, parent: str, status: str, batch: dict, entrypoint: str)
- gap: capability:{batch['moduleId']}
- ownerFamily: {batch['owner']}
- targetImplementationClass: LOCAL_EXACT
- batchId: {contract['batchId']}
- selectedRecordIds: {json.dumps(contract['selectedRecordIds'], ensure_ascii=False, separators=(',', ':'))}
- commandBudget: max=48; read/exploration max=16
- Implementation entrypoint: `{entrypoint}`
## 目标
@@ -435,7 +461,33 @@ def select_batch(inventory: dict) -> dict:
if all(selected):
unresolved = [unit for unit in selected if unit.get("coverageStatus") != "VERIFIED"]
if unresolved:
return {**candidate, "units": unresolved, "candidateUnits": selected}
key = f"{candidate['moduleId']}:LOCAL_EXACT:{candidate['owner']}"
history = []
for receipt_path in sorted(ROOT.glob("tests/golden/WBV2-P*-001/module-receipt.json"), key=lambda item: item.parent.name):
receipt = read_json(receipt_path)
if receipt.get("moduleId") != candidate["moduleId"]:
continue
duration = receipt.get("efficiency", {}).get("totalDurationMs")
if not isinstance(duration, (int, float)):
duration = sum(item.get("durationMs", 0) for item in receipt.get("commands", []) if isinstance(item, dict))
history.append({
"durationMs": duration,
"commands": len(receipt.get("commands", [])),
"timeout": any("timeout" in str(item.get("output", "")).lower() for item in receipt.get("commands", []) if isinstance(item, dict)),
"focusedGateFailure": receipt.get("status") != "PASS",
})
recent = history[-3:]
min_size = 1
max_size = 8 if len(unresolved) > 16 or len(candidate.get("fixtures", [])) > 1 else 16
current_size = min(len(unresolved), max_size)
if recent and any(item["timeout"] or item["durationMs"] > 900_000 or item["commands"] > 48 for item in recent):
next_size = max(min_size, current_size // 2)
elif recent and all(item["durationMs"] < 300_000 and item["commands"] < 24 and not item["focusedGateFailure"] for item in recent):
next_size = min(max_size, current_size + 4)
else:
next_size = current_size
chosen = unresolved[:max(min_size, min(max_size, len(unresolved), next_size))]
return {**candidate, "units": chosen, "candidateUnits": selected, "adaptiveKey": key, "adaptiveHistory": recent, "adaptiveCurrentSize": current_size, "adaptiveNextSize": len(chosen), "adaptiveMinSize": min_size, "adaptiveMaxSize": max_size}
raise SystemExit("no configured unresolved Blender source batch remains")
@@ -457,8 +509,10 @@ def main() -> int:
root_task, module_task = f"WBV2-P{phase}-000", f"WBV2-P{phase}-001"
inventory = read_json(CONTROL_ROOT / "dynamic-inventory.json")
batch = select_batch(inventory)
contract = batch_contract(module_task, batch)
batch["batchId"] = contract["batchId"]
if not args.write:
print(json.dumps({"status": "ready", "rootTask": root_task, "nextTask": module_task, "moduleId": batch["moduleId"], "batchSize": len(batch["units"]), "sourceUnits": [unit["id"] for unit in batch["units"]]}, ensure_ascii=False))
print(json.dumps({"status": "ready", "rootTask": root_task, "nextTask": module_task, "moduleId": batch["moduleId"], "batchId": contract["batchId"], "batchSize": len(batch["units"]), "selectedRecordIds": contract["selectedRecordIds"], "sourceUnits": [unit["id"] for unit in batch["units"]], "adaptive": {"key": batch.get("adaptiveKey"), "history": batch.get("adaptiveHistory", []), "nextSize": batch.get("adaptiveNextSize", len(batch["units"]))}}, ensure_ascii=False))
return 0
current_status = ROOT / "docs/status" / f"{current}.md"
@@ -497,19 +551,20 @@ def main() -> int:
f"WBV2_BATCH_TASK={module_task} {production_env} {fixture_env} WBV2_BATCH_TYPES='{batch_types}' WBV2_BATCH_UNITS='{batch_units}' WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/{Path(batch['test']).name}",
]
root_record = {"id": root_task, "phase": str(phase), "title": "authorize_modifier_reader_batch", "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 one shared source-backed modifier reader batch.", "outputs": ["successor authorization", "module plan", "manifest", "status"], "acceptance": ["prior evidence remains hash-bound", "batch source mapping is explicit", "inventory remains source-derived"], "commands": ["node tools/web/check-execution-control-plane.mjs"], "status": "done", "ready": False, "queueMutationAllowed": True}
module_record = {"id": module_task, "phase": str(phase), "title": batch["title"], "kind": "module", "owner": batch["owner"], "dependsOn": [root_task], "parityId": batch["moduleId"], "implementationClass": "LOCAL_EXACT", "parallelGroup": f"module-{batch['moduleId'].lower()}", "gate": "MODULE", "card": f"docs/tasks/{module_task}.md", "behavior": f"Verify the {batch['moduleId']} batch through Blender Main, WASM and Chromium recovery.", "outputs": ["aggregate module receipt", "coverage bindings", "manifest", "status"], "acceptance": ["source and protocol hashes are bound", "every selected Main reader type is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact"], "status": "in_progress", "ready": True, "queueMutationAllowed": True, "module": {"moduleId": batch["moduleId"], "sourceBacked": True, "capabilities": [f"CAP-{batch['moduleId']}-READER", f"CAP-{batch['moduleId']}-RECOVERY", f"CAP-{batch['moduleId']}-FAIL-CLOSED"], "sourcePaths": source_paths, "reuseReceipts": batch.get("reuseReceipts", [{"manifest": "tests/golden/WBV2-P1107-000/manifest.json", "report": "tests/golden/WBV2-P1107-000/module-receipt.json"}]), "dependencyReuse": batch.get("dependencyReuse", []), "positive": [f"{name} type is read from Blender Main" for name in batch["identifiers"]] + ["shared RNA metadata is preserved", "save/reopen preserves the batch metadata"], "negative": ["malformed blend is rejected without Main mutation", "unsupported batch data fails closed", "non-finite metadata is not synthesized"], "recovery": ["save/reopen", "worker termination", "deterministic second read"], "commands": commands, "fullFamilyParity": False, "evidencePaths": [batch["production"].format(task=module_task)], "coverageBindings": [unit["id"] for unit in batch["units"]]}}
module_record = {"id": module_task, "phase": str(phase), "title": batch["title"], "kind": "module", "owner": batch["owner"], "dependsOn": [root_task], "parityId": batch["moduleId"], "implementationClass": "LOCAL_EXACT", "parallelGroup": f"module-{batch['moduleId'].lower()}", "gate": "MODULE", "card": f"docs/tasks/{module_task}.md", "behavior": f"Verify the {batch['moduleId']} batch through Blender Main, WASM and Chromium recovery.", "outputs": ["aggregate module receipt", "coverage bindings", "manifest", "status"], "acceptance": ["source and protocol hashes are bound", "every selected Main reader type is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact"], "status": "in_progress", "ready": True, "queueMutationAllowed": True, "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "module": {"moduleId": batch["moduleId"], "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "sourceBacked": True, "capabilities": [f"CAP-{batch['moduleId']}-READER", f"CAP-{batch['moduleId']}-RECOVERY", f"CAP-{batch['moduleId']}-FAIL-CLOSED"], "sourcePaths": source_paths, "reuseReceipts": batch.get("reuseReceipts", [{"manifest": "tests/golden/WBV2-P1107-000/manifest.json", "report": "tests/golden/WBV2-P1107-000/module-receipt.json"}]), "dependencyReuse": batch.get("dependencyReuse", []), "positive": [f"{name} type is read from Blender Main" for name in batch["identifiers"]] + ["shared RNA metadata is preserved", "save/reopen preserves the batch metadata"], "negative": ["malformed blend is rejected without Main mutation", "unsupported batch data fails closed", "non-finite metadata is not synthesized"], "recovery": ["save/reopen", "worker termination", "deterministic second read"], "commands": commands, "fullFamilyParity": False, "evidencePaths": [batch["production"].format(task=module_task)], "coverageBindings": contract["selectedRecordIds"]}}
module_record["module"]["commandBudget"] = {"maxCommands": 48, "maxReadCommands": 16}
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": batch["moduleId"], "task": module_task, "owner": batch["owner"], "sourceBacked": True, "capabilities": module_record["module"]["capabilities"], "sourcePaths": source_paths, "reuseReceipts": module_record["module"]["reuseReceipts"], "fullFamilyParity": False}]}
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": batch["moduleId"], "task": module_task, "owner": batch["owner"], "sourceBacked": True, "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "capabilities": module_record["module"]["capabilities"], "sourcePaths": source_paths, "reuseReceipts": module_record["module"]["reuseReceipts"], "fullFamilyParity": False}]}
write_json(root_plan_path, root_plan)
root_card_path, module_card_path = ROOT / "docs/tasks" / f"{root_task}.md", ROOT / "docs/tasks" / f"{module_task}.md"
write_text(root_card_path, make_card(root_task, current, "done", batch, f"python3 my-tools/authorize_next_wbv2_task.py --write"))
write_text(module_card_path, make_card(module_task, root_task, "in_progress", batch, f"node tools/web/run-wbv2-module-task.mjs --task {module_task} --write"))
write_text(root_card_path, make_card(root_task, current, "done", batch, f"python3 my-tools/authorize_next_wbv2_task.py --write", contract))
write_text(module_card_path, make_card(module_task, root_task, "in_progress", batch, f"node tools/web/run-wbv2-module-task.mjs --task {module_task} --write", contract))
root_report_path, root_status_path, root_manifest_path = root_out / "successor-authorization-report.json", ROOT / "docs/status" / f"{root_task}.md", root_out / "manifest.json"
write_json(root_report_path, {"schemaVersion": 1, "task": root_task, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE_BATCH", "taskClass": "QUEUE_ENABLEMENT", "status": "PASS", "parent": {"task": current, "manifest": rel(current_manifest_path), "manifestSha256": sha(current_manifest_path)}, "module": {"moduleId": batch["moduleId"], "task": module_task, "batchSize": len(batch["units"]), "candidateBatchSize": len(batch.get("candidateUnits", batch["units"])), "sourceUnits": [unit["id"] for unit in batch["units"]], "sourceBacked": True}, "inventory": {"unitCount": inventory["summary"]["unitCount"], "verifiedUnits": inventory["coverage"]["verifiedUnits"], "unverifiedUnits": inventory["coverage"]["unverifiedUnits"], "sourceTreeSha256": inventory["authority"]["sourceTreeSha256"], "boundUnitDelta": 0}, "efficiency": {"existingBindingsReused": len(batch.get("candidateUnits", batch["units"])) - len(batch["units"]), "selectionPolicy": "all-candidate-units-then-unresolved-only"}, "nextTask": module_task})
write_json(root_report_path, {"schemaVersion": 1, "task": root_task, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE_BATCH", "taskClass": "QUEUE_ENABLEMENT", "status": "PASS", "parent": {"task": current, "manifest": rel(current_manifest_path), "manifestSha256": sha(current_manifest_path)}, "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "module": {"moduleId": batch["moduleId"], "task": module_task, "batchSize": len(batch["units"]), "candidateBatchSize": len(batch.get("candidateUnits", batch["units"])), "sourceUnits": contract["selectedRecordIds"], "sourceBacked": True}, "inventory": {"unitCount": inventory["summary"]["unitCount"], "verifiedUnits": inventory["coverage"]["verifiedUnits"], "unverifiedUnits": inventory["coverage"]["unverifiedUnits"], "sourceTreeSha256": inventory["authority"]["sourceTreeSha256"], "boundUnitDelta": 0}, "efficiency": {"existingBindingsReused": len(batch.get("candidateUnits", batch["units"])) - len(batch["units"]), "selectionPolicy": "all-candidate-units-then-unresolved-only", "adaptiveKey": batch.get("adaptiveKey"), "adaptiveHistory": batch.get("adaptiveHistory", []), "adaptiveNextSize": batch.get("adaptiveNextSize", len(batch["units"]))}, "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 modifier-family parity completion claimed\n- source-backed batch authorized: `{batch['moduleId']}` ({len(batch['units'])} units)\n- dynamic inventory: `{inventory['summary']['unitCount']}` units; verified `{inventory['coverage']['verifiedUnits']}`; unverified `{inventory['coverage']['unverifiedUnits']}`; 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_BATCH", "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, "moduleId": batch["moduleId"], "batchSize": len(batch["units"]), "inventoryTotal": 6900, "effectiveCompleted": 290, "effectivePending": 6609, "dynamicUnitCount": inventory["summary"]["unitCount"], "dynamicVerifiedUnits": inventory["coverage"]["verifiedUnits"], "dynamicUnverifiedUnits": inventory["coverage"]["unverifiedUnits"], "boundUnitDelta": 0, "fullHistoricalReproduction": False}, "nextTask": module_task})
write_json(root_manifest_path, {"schemaVersion": 1, "task": root_task, "parentTask": current, "operation": "AUTHORIZE_SOURCE_BACKED_MODULE_BATCH", "status": "done", "enablingTask": True, "parityStateChange": False, "runtime": "MODULE_EXECUTION_QUEUE_BRIDGE", "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "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, "moduleId": batch["moduleId"], "batchSize": len(batch["units"]), "inventoryTotal": 6900, "effectiveCompleted": 290, "effectivePending": 6609, "dynamicUnitCount": inventory["summary"]["unitCount"], "dynamicVerifiedUnits": inventory["coverage"]["verifiedUnits"], "dynamicUnverifiedUnits": inventory["coverage"]["unverifiedUnits"], "boundUnitDelta": 0, "fullHistoricalReproduction": False}, "nextTask": module_task})
run_node("tools/web/generate-v2-task-index.mjs", "--plan-path", rel(root_plan_path))
queue_text = QUEUE.read_text(encoding="utf-8")

View File

@@ -39,6 +39,7 @@ def main() -> int:
"schedulerWrapperStatus": read_json(CONTROL_ROOT / "my-tools-scheduler.json"),
}
status["progress"] = project_progress()
status["fastPath"] = fast_path_status(status)
if args.json:
print(json.dumps(status, ensure_ascii=False, separators=(",", ":")))
if not args.watch:
@@ -64,7 +65,7 @@ def main() -> int:
print(f" supervisor: pid={supervisor.get('pid') or 'none'} alive={supervisor.get('alive', False)} state={supervisor.get('state') or 'unknown'}")
print(f" latest handoff: {status.get('latestHandoff') or 'none'}")
print(f" local HEAD: {status.get('localHead') or 'unknown'}")
for line in format_progress(status["progress"]):
for line in format_progress(status["progress"], status["fastPath"]):
print(line)
print(f" message: {status.get('message', '')}")
if not args.watch:
@@ -180,12 +181,42 @@ def project_progress() -> dict:
}
def format_progress(progress: dict) -> list[str]:
def fast_path_status(status: dict) -> dict:
scheduler = status.get("scheduler", {}) if isinstance(status, dict) else {}
latest_batch = scheduler.get("latestBatch") if isinstance(scheduler.get("latestBatch"), dict) else {}
cache = _read_json(CONTROL_ROOT / "context-cache.json")
ledger = _read_json(CONTROL_ROOT / "failure-ledger.json")
return {
"batch": {
"batchId": latest_batch.get("batchId"),
"selectedRecordCount": len(latest_batch.get("selectedRecordIds", [])),
"completed": bool(latest_batch.get("completed")),
},
"contextCache": {
"status": cache.get("status", "MISSING"),
"task": cache.get("task"),
"schemaVersion": cache.get("schemaVersion"),
"inputCount": len(cache.get("inputs", {})) if isinstance(cache.get("inputs"), dict) else 0,
},
"failureLedger": {
"entryCount": len(ledger.get("entries", [])) if isinstance(ledger.get("entries"), list) else 0,
"hydratedLogs": len(ledger.get("hydratedLogs", [])) if isinstance(ledger.get("hydratedLogs"), list) else 0,
},
"commandBudget": {"maxCommands": 48, "maxReadCommands": 16},
"verificationPolicy": "incremental-context-only-until-final-audit",
}
def format_progress(progress: dict, fast_path: dict | None = None) -> list[str]:
estimate = progress.get("estimatedRemainingHours")
estimate_text = f"{estimate} h" if estimate is not None else "n/a"
total_estimate = progress.get("estimatedTotalHours")
total_text = f"{total_estimate} h" if total_estimate is not None else "n/a"
plan = progress.get("taskPlan", {})
fast_path = fast_path or {}
batch = fast_path.get("batch", {})
context_cache = fast_path.get("contextCache", {})
ledger = fast_path.get("failureLedger", {})
return [
"",
"Project migration progress",
@@ -197,6 +228,9 @@ def format_progress(progress: dict) -> list[str]:
f" stage medians: {progress.get('stageMedianDurationMs') or 'n/a'} ms (1=build, 2=typecheck, 3=web build, 4=Chromium)",
f" estimate: remaining={estimate_text}, total={total_text}",
f" estimate basis: {progress.get('estimateBasis', 'n/a')}",
f" fast path: batch={batch.get('batchId') or 'none'} selected={batch.get('selectedRecordCount', 0)} completed={batch.get('completed', False)}",
f" context cache: {context_cache.get('status', 'MISSING')} task={context_cache.get('task') or 'none'} inputs={context_cache.get('inputCount', 0)}; failure ledger entries={ledger.get('entryCount', 0)} hydratedLogs={ledger.get('hydratedLogs', 0)}",
f" command budget: max={fast_path.get('commandBudget', {}).get('maxCommands', 48)}, read/exploration max={fast_path.get('commandBudget', {}).get('maxReadCommands', 16)}; verification={fast_path.get('verificationPolicy', 'n/a')}",
]

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Validate only the active task's context metadata and focused pointer.
The normal path compares stat metadata first. The task-context checker is
invoked only when an input is missing, changed, or lacks the required hash.
"""
from __future__ import annotations
import argparse
import json
import subprocess
from pathlib import Path
from wbv2_runtime import ROOT, queue_pointer
def stat_metadata(path: Path) -> tuple[int, str] | None:
try:
value = path.stat()
except OSError:
return None
return value.st_size, str(value.st_mtime_ns)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--task", help="task id; defaults to the authoritative queue pointer")
args = parser.parse_args()
task = args.task or queue_pointer().get("task")
if not task:
parser.error("queue has no current task")
context_path = ROOT / "tests/golden" / task / "task-context.json"
try:
context = json.loads(context_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
context = {}
metadata = context.get("inputMetadata") if isinstance(context, dict) else None
unchanged = isinstance(metadata, dict) and bool(metadata)
if unchanged:
for relative, expected in metadata.items():
current = stat_metadata(ROOT / relative)
if (
not isinstance(expected, dict)
or current is None
or current[0] != expected.get("bytes")
or current[1] != expected.get("mtime_ns")
or not isinstance(expected.get("sha256"), str)
):
unchanged = False
break
if unchanged:
print(f"incremental-context-ok task={task} context=REUSED metadata={len(metadata)}")
return 0
command = ["node", "tools/web/check-task-context.mjs", "--task", task, "--write"]
result = subprocess.run(command, 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:
return result.returncode
print(f"incremental-context-ok task={task} context=REFRESHED")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -59,7 +59,7 @@
},
"moduleReceipt": {
"path": "tests/golden/WBV2-P1161-001/module-receipt.json",
"sha256": "48d1c06e1f03206fa68bec9db86ecd67fbfd858776b48a82b9505dc6ccd03459"
"sha256": "6b1131b452c509f600536baa2e95f0d9ad30a1fb5951ae9e36b3f540c36326bf"
},
"status": {
"path": "docs/status/WBV2-P1161-001.md",
@@ -75,7 +75,7 @@
},
"taskContext": {
"path": "tests/golden/WBV2-P1161-001/task-context.json",
"sha256": "cb7a0b446bfc6c27a2040f42107162c99290927f3115eab029c2a456442b8ad3"
"sha256": "f98e1ee00beed21da1ef51d60caee5629e52c11a4f2d9bda78bb7b0cf52d7d41"
}
},
"assertions": {
@@ -97,7 +97,33 @@
"historicalInventoryTotal": 6900,
"historicalEffectiveCompleted": 290,
"historicalEffectivePending": 6609,
"historicalReproduction": false
"historicalReproduction": false,
"batchId": "WBV2-P1161-001:FCURVE_MODIFIER_SUBTYPE_READER_BATCH:5eb990874b4e6015",
"selectedRecordIds": [
"rna-struct:FModifierEnvelope",
"rna-struct:FModifierGenerator",
"rna-struct:FModifierLimits",
"rna-struct:FModifierNoise",
"rna-struct:FModifierSmooth",
"rna-struct:FModifierStepped"
],
"commandBudget": {
"maxCommands": 48,
"maxReadCommands": 16,
"commandCount": 4,
"readCommandCount": 0,
"withinBudget": true
}
},
"nextTask": null
"nextTask": null,
"batchId": "WBV2-P1161-001:FCURVE_MODIFIER_SUBTYPE_READER_BATCH:5eb990874b4e6015",
"selectedRecordIds": [
"rna-struct:FModifierEnvelope",
"rna-struct:FModifierGenerator",
"rna-struct:FModifierLimits",
"rna-struct:FModifierNoise",
"rna-struct:FModifierSmooth",
"rna-struct:FModifierStepped"
],
"phase": "CONTINUE"
}

View File

@@ -123,5 +123,22 @@
"unverifiedUnits": 3222
}
},
"nextTask": null
"nextTask": null,
"phase": "CONTINUE",
"batchId": "WBV2-P1161-001:FCURVE_MODIFIER_SUBTYPE_READER_BATCH:5eb990874b4e6015",
"selectedRecordIds": [
"rna-struct:FModifierEnvelope",
"rna-struct:FModifierGenerator",
"rna-struct:FModifierLimits",
"rna-struct:FModifierNoise",
"rna-struct:FModifierSmooth",
"rna-struct:FModifierStepped"
],
"commandBudget": {
"maxCommands": 48,
"maxReadCommands": 16,
"commandCount": 4,
"readCommandCount": 0,
"withinBudget": true
}
}

View File

@@ -13,6 +13,36 @@
"node tools/web/check-task-context.mjs --task WBV2-P1161-001 --write"
],
"inputPaths": [],
"inputMetadata": {
"docs/EXECUTION_QUEUE.md": {
"path": "docs/EXECUTION_QUEUE.md",
"exists": true,
"bytes": 4030,
"mtime_ns": "1788433617614059639",
"sha256": "80fb937bc99b92fa59013e4ef8777a84139509d06420ab8306142f1d4c1b8c96"
},
"docs/tasks/WBV2-P1161-001.md": {
"path": "docs/tasks/WBV2-P1161-001.md",
"exists": true,
"bytes": 2201,
"mtime_ns": "1788433606139169929",
"sha256": "d2213ece2f0b5eeb99949ace5cd35ea2c18ba955b4b93d1a9bc5722f59d536fa"
},
"tests/golden/WBV2-P1161-000/manifest.json": {
"path": "tests/golden/WBV2-P1161-000/manifest.json",
"exists": true,
"bytes": 1391,
"mtime_ns": "1788433606139647344",
"sha256": "cf1b5e152c183463773621096cf34fafd30c9a6f556b1daa7a7258c40d3e0392"
},
"docs/status/WBV2-P1161-000.md": {
"path": "docs/status/WBV2-P1161-000.md",
"exists": true,
"bytes": 368,
"mtime_ns": "1788433606139326609",
"sha256": "85055f55c507388f8fba17b814bc7d23698bd6b901601699c663b63d867ed5f0"
}
},
"inputSelection": {
"schemaVersion": 1,
"limits": {
@@ -54,8 +84,8 @@
},
{
"path": "tests/golden/WBV2-P1161-001/module-receipt.json",
"bytes": null,
"reason": "MISSING_PATH"
"bytes": 5223,
"reason": "CONTEXT_REMAINING_SPACE"
},
{
"path": "tests/golden/WBV2-P1161-001/modifier-reader-batch-production.json",

View File

@@ -16,19 +16,44 @@ const task = process.argv.includes("--task")
if (!task) throw new Error("unable to resolve current WBV2 task");
const parentManifest = queue.match(/\| parent manifest \| `([^`]+)` \|/u)?.[1];
if (!parentManifest) throw new Error("queue parent manifest is missing");
const inputPaths = [
const baseInputPaths = [
"docs/EXECUTION_QUEUE.md",
`docs/tasks/${task}.md`,
parentManifest,
`tests/golden/${task}/task-context.json`,
];
const inputHashFor = () => digest(inputPaths.map((file) => `${file}\0${fs.existsSync(abs(file)) ? digest(read(file)) : "MISSING"}\n`).join(""));
const inputHash = inputHashFor();
const statInput = (file) => {
try {
const stat = fs.statSync(abs(file), { bigint: true });
return { bytes: Number(stat.size), mtime_ns: stat.mtimeNs.toString(), exists: true };
}
catch (error) {
return { bytes: null, mtime_ns: null, exists: false, error: error?.code ?? "STAT_FAILED" };
}
};
const metadataComparable = (value) => value && value.exists === true
&& Number.isSafeInteger(value.bytes) && typeof value.mtime_ns === "string";
let cached = null;
try { cached = JSON.parse(fs.readFileSync(cachePath, "utf8")); }
catch { /* First use or an interrupted write. */ }
if (!process.argv.includes("--force") && cached?.status === "PASS" && cached.task === task && cached.inputHash === inputHash) {
process.stdout.write(`wbv2-context-cache-hit task=${task} inputHash=${inputHash}\n`);
const inputPaths = [...new Set([
...baseInputPaths,
...Object.keys(cached?.inputs ?? {}),
])];
const inputMetadata = Object.fromEntries(inputPaths.map((file) => [file, statInput(file)]));
const metadataUnchanged = (cachedInputs) => inputPaths.every((file) => {
const current = inputMetadata[file];
const cached = cachedInputs?.[file];
return metadataComparable(current) && metadataComparable(cached)
&& current.bytes === cached.bytes && current.mtime_ns === cached.mtime_ns;
});
const digestPath = (file) => {
try { return digest(read(file)); }
catch { return "MISSING"; }
};
const inputHashFor = (paths = inputPaths) => digest(paths.map((file) => `${file}\0${digestPath(file)}\n`).join(""));
if (!process.argv.includes("--force") && cached?.status === "PASS" && cached.task === task && metadataUnchanged(cached.inputs)) {
process.stdout.write(`CONTEXT_PACKET_REUSED task=${task} inputHash=${cached.inputHash ?? "UNKNOWN"}\n`);
process.exit(0);
}
const result = spawnSync(process.execPath, [abs("tools/web/print-task-context.mjs"), "--task", task], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
@@ -38,8 +63,21 @@ if ((result.status ?? 1) !== 0 || (check.status ?? 1) !== 0) {
process.exit(Math.max(result.status ?? 1, check.status ?? 1));
}
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
const refreshedInputHash = inputHashFor();
const payload = { schemaVersion: 1, status: "PASS", task, inputHash: refreshedInputHash, refreshedAt: new Date().toISOString() };
let contextMetadata = {};
try {
const context = JSON.parse(read(`tests/golden/${task}/task-context.json`));
contextMetadata = context.inputMetadata && typeof context.inputMetadata === "object" ? context.inputMetadata : {};
} catch { /* The task-context checker already reported the precise failure. */ }
const refreshedPaths = [...new Set([...inputPaths, ...Object.keys(contextMetadata)])];
const refreshedInputHash = inputHashFor(refreshedPaths);
const payload = {
schemaVersion: 2,
status: "PASS",
task,
inputHash: refreshedInputHash,
inputs: Object.fromEntries(refreshedPaths.map((file) => [file, { ...statInput(file), sha256: contextMetadata[file]?.sha256 ?? digestPath(file) }])),
refreshedAt: new Date().toISOString(),
};
const temporary = `${cachePath}.tmp-${process.pid}`;
fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`);
fs.renameSync(temporary, cachePath);

View File

@@ -64,6 +64,25 @@ export function validateContextBundle(bundle, { current = true } = {}) {
if (context.commands.length > GOVERNANCE_LIMITS.maxTaskCommands) {
add(violations, "TASK_COMMANDS_TOO_WIDE", `count=${context.commands.length}`);
}
if (!context.inputMetadata || typeof context.inputMetadata !== "object") {
add(violations, "INPUT_METADATA_MISSING", context.task);
} else {
const requiredMetadata = [
relative(files.queuePath),
relative(files.taskPath),
relative(files.parentManifestPath),
...(fs.existsSync(files.parentStatusPath) ? [relative(files.parentStatusPath)] : []),
...context.inputPaths,
];
for (const file of [...new Set(requiredMetadata)]) {
const metadata = context.inputMetadata[file];
if (!metadata || typeof metadata !== "object" || metadata.exists !== true
|| !Number.isSafeInteger(metadata.bytes) || typeof metadata.mtime_ns !== "string"
|| !/^[a-f0-9]{64}$/u.test(metadata.sha256 ?? "")) {
add(violations, "INPUT_METADATA_INVALID", `${context.task}:${file}`);
}
}
}
const selection = context.inputSelection;
if (!selection || selection.schemaVersion !== 1 || !Array.isArray(selection.selected) || !Array.isArray(selection.excluded)) {

View File

@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
const controlRoot = "tests/golden/WBV2";
const schedulerLockPath = `${controlRoot}/continuous-scheduler.lock`;
const schedulerCheckpointPath = `${controlRoot}/continuous-scheduler.json`;
const guardianLockPath = `${controlRoot}/continuous-guardian.lock`;
const supervisorLockPath = `${controlRoot}/continuous-supervisor.lock`;
const guardianCheckpointPath = `${controlRoot}/continuous-guardian.json`;
@@ -80,15 +82,17 @@ const pushVerification = (task) => {
};
const collect = () => {
const queue = currentPointer();
const scheduler = readJson(schedulerCheckpointPath);
const guardian = readJson(guardianCheckpointPath);
const supervisor = readJson(supervisorCheckpointPath);
const schedulerLock = lockInfo(schedulerLockPath);
const guardianLock = lockInfo(guardianLockPath);
const supervisorLock = lockInfo(supervisorLockPath);
const handoff = latestHandoff();
const active = guardianLock.alive && supervisorLock.alive;
const blocked = guardian?.state === "blocked" || supervisor?.state === "blocked";
const active = schedulerLock.alive || (guardianLock.alive && supervisorLock.alive);
const blocked = scheduler?.state === "blocked" || guardian?.state === "blocked" || supervisor?.state === "blocked";
const state = blocked ? "BLOCKED" : active ? "RUNNING" : "IDLE";
const phase = blocked ? "blocked" : supervisor?.state === "running" ? "task-or-gate" : supervisor?.state ?? "unknown";
const phase = blocked ? "blocked" : scheduler?.state === "running" ? "scheduler-task" : supervisor?.state === "running" ? "task-or-gate" : scheduler?.state ?? supervisor?.state ?? "unknown";
const observed = new Date().toISOString();
return {
schemaVersion: 1,
@@ -97,14 +101,15 @@ const collect = () => {
phase,
message: state === "RUNNING"
? `RUNNING: ${queue.task ?? "unknown task"} is owned by the guardian/supervisor process; wait for its handoff.`
: state === "BLOCKED" ? "BLOCKED: inspect the checkpoint reason before any write or retry." : "IDLE: no live guardian/supervisor lock was found.",
: state === "BLOCKED" ? "BLOCKED: inspect the scheduler/guardian/supervisor checkpoint reason before any write or retry." : "IDLE: no live scheduler, guardian or supervisor lock was found.",
queue,
scheduler: { pid: scheduler?.pid ?? schedulerLock.pid, alive: schedulerLock.alive, state: scheduler?.state ?? null, updatedAt: scheduler?.updatedAt ?? null, iteration: scheduler?.iteration ?? null, before: scheduler?.before ?? null, after: scheduler?.after ?? null, latestBatch: scheduler?.latestBatch ?? null },
guardian: { pid: guardian?.pid ?? guardianLock.pid, alive: guardianLock.alive, state: guardian?.state ?? null, updatedAt: guardian?.updatedAt ?? null, heartbeatAt: guardian?.heartbeatAt ?? null, restartCount: guardian?.restartCount ?? null },
supervisor: { pid: supervisor?.pid ?? supervisorLock.pid, alive: supervisorLock.alive, state: supervisor?.state ?? null, updatedAt: supervisor?.updatedAt ?? null, iteration: supervisor?.iteration ?? null, before: supervisor?.before ?? null, after: supervisor?.after ?? null },
latestHandoff: handoff,
latestPushVerification: pushVerification(handoff?.before ?? supervisor?.before?.task),
localHead: gitHead(),
checkpoints: { guardian: guardianCheckpointPath, supervisor: supervisorCheckpointPath },
checkpoints: { scheduler: schedulerCheckpointPath, guardian: guardianCheckpointPath, supervisor: supervisorCheckpointPath },
};
};
const format = (status) => [
@@ -112,6 +117,7 @@ const format = (status) => [
`阶段: ${status.phase}`,
`当前任务: ${status.queue.task ?? "unknown"}`,
`parent manifest: ${status.queue.parentManifest ?? "unknown"}`,
`scheduler: pid=${status.scheduler.pid ?? "none"} alive=${status.scheduler.alive} state=${status.scheduler.state ?? "unknown"} iteration=${status.scheduler.iteration ?? "none"} batch=${status.scheduler.latestBatch?.batchId ?? "none"} completed=${status.scheduler.latestBatch?.completed ?? false}`,
`guardian: pid=${status.guardian.pid ?? "none"} alive=${status.guardian.alive} state=${status.guardian.state ?? "unknown"} heartbeat=${status.guardian.heartbeatAt ?? "none"}`,
`supervisor: pid=${status.supervisor.pid ?? "none"} alive=${status.supervisor.alive} state=${status.supervisor.state ?? "unknown"} iteration=${status.supervisor.iteration ?? "none"}`,
`最近交接: ${status.latestHandoff ? `${status.latestHandoff.before} -> ${status.latestHandoff.after} (#${status.latestHandoff.iteration})` : "none"}`,

View File

@@ -58,20 +58,6 @@ function taskCommand(task) {
return match[1];
}
function typecheckProven(task) {
try {
const manifest = JSON.parse(read(`tests/golden/${task}/manifest.json`));
const receiptPath = manifest.artifacts?.moduleReceipt?.path;
if (!receiptPath) return false;
const receipt = JSON.parse(read(receiptPath));
return receipt.status === "PASS" && receipt.source?.unchanged === true
&& receipt.commands?.some((entry) => entry.exitCode === 0 && entry.command.trim() === "npm --prefix web run typecheck");
}
catch {
return false;
}
}
let iterations = 0;
const summary = [];
while (maxIterations === 0 || iterations < maxIterations) {
@@ -91,10 +77,10 @@ while (maxIterations === 0 || iterations < maxIterations) {
const after = currentTask();
const terminal = after.task === before.task && terminalTask(before.task);
if (after.task === before.task && !terminal) throw new Error(`${before.task} focused command exited 0 without advancing queue; log=${logPath}`);
const typecheckCommand = typecheckProven(before.task)
? "printf 'typecheck reused from hash-bound module receipt\\n'"
: "npm --prefix web run typecheck";
const gates = command(`node tools/web/check-context-governance.mjs && node tools/web/check-execution-control-plane.mjs --allow-unrunnable && node tools/web/check-v2-task-index.mjs && node tools/web/check-v2-handoff.mjs && ${typecheckCommand} && npm --prefix web run lint && git diff --check`, `tests/golden/${before.task}/continuous-gates.log`);
// Implementation cycles keep the post-handoff gate incremental. Full API,
// format, Chromium, performance and release gates belong to the explicit
// final audit and must not be repeated for every batch.
const gates = command(`python3 my-tools/validate_incremental_context.py --task ${before.task}`, `tests/golden/${before.task}/continuous-gates.log`);
if (gates.code !== 0) throw new Error(`${before.task} post-handoff gate failed (exit ${gates.code}); log=tests/golden/${before.task}/continuous-gates.log`);
iterations += 1;
summary.push({ task: before.task, command: focused, nextTask: terminal ? null : after.task, terminal, log: logPath });

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { recordFailure } from "./wbv2-failure-ledger.mjs";
import { createFailureLedgerSession } from "./wbv2-failure-ledger.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
@@ -11,6 +11,7 @@ const logPath = `${controlRoot}/continuous-guardian.log`;
const checkpointPath = `${controlRoot}/continuous-guardian.json`;
const supervisorCheckpointPath = `${controlRoot}/continuous-supervisor.json`;
const lockPath = `${controlRoot}/continuous-guardian.lock`;
const failureLedger = createFailureLedgerSession(root, { logPaths: [`${controlRoot}/continuous-guardian.log`] });
const argv = process.argv.slice(2);
const numericArg = (name, fallback) => {
@@ -173,7 +174,7 @@ try {
}
const decision = safeRestart(checkpoint, after);
if (!decision.ok) {
recordFailure(root, { task: before.task, phase: "guardian", reason: decision.reason, childExit: result.code });
failureLedger.record({ task: before.task, phase: "guardian", reason: decision.reason, childExit: result.code });
writeCheckpoint({ state: "blocked", restartCount, before, after, reason: decision.reason, supervisorExit: result.code });
appendLog(`blocked reason=${decision.reason}; no automatic replay`);
guardianExit = 2;
@@ -181,7 +182,7 @@ try {
}
restartCount += 1;
if (maxRestarts > 0 && restartCount > maxRestarts) {
recordFailure(root, { task: before.task, phase: "guardian", reason: "max_safe_restarts_exceeded", childExit: result.code });
failureLedger.record({ task: before.task, phase: "guardian", reason: "max_safe_restarts_exceeded", childExit: result.code });
writeCheckpoint({ state: "blocked", restartCount, before, after, reason: "max_safe_restarts_exceeded", supervisorExit: result.code });
appendLog("blocked reason=max_safe_restarts_exceeded");
guardianExit = 3;
@@ -194,12 +195,13 @@ try {
}
catch (error) {
guardianExit = 4;
try { recordFailure(root, { task: currentTask()?.task ?? "UNKNOWN", phase: "guardian", reason: "guardian_error", detail: error?.message ?? String(error) }); }
try { failureLedger.record({ task: currentTask()?.task ?? "UNKNOWN", phase: "guardian", reason: "guardian_error", detail: error?.message ?? String(error) }); }
catch { /* Preserve the original guardian failure. */ }
writeCheckpoint({ state: "blocked", restartCount, reason: "guardian_error", error: error?.message ?? String(error) });
appendLog(`blocked reason=guardian_error error=${error?.message ?? String(error)}`);
}
finally {
failureLedger.flush();
signalChild("SIGTERM");
releaseLock();
}

View File

@@ -4,6 +4,7 @@ import fs from "node:fs";
import path from "node:path";
import { execFileSync, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { batchContract, commandBudget, completedBatch } from "./wbv2-batch-contract.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
@@ -11,6 +12,7 @@ const read = (file) => fs.readFileSync(abs(file), "utf8");
const json = (file) => JSON.parse(read(file));
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
const dynamicInventoryPath = "tests/golden/WBV2/dynamic-inventory.json";
const out = `tests/golden/${process.argv[process.argv.indexOf("--task") + 1]}`;
const writeJson = (file, value) => {
fs.mkdirSync(path.dirname(abs(file)), { recursive: true });
fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`);
@@ -35,8 +37,28 @@ const parentId = record.dependsOn?.at(-1);
const parentPath = `tests/golden/${parentId}/manifest.json`;
const parent = json(parentPath);
if (parent.status !== "done" || parent.nextTask !== task) throw new Error(`module parent is not a completed handoff: ${parentId}`);
if (record.status !== "in_progress" || record.ready !== true) throw new Error(`module task ${task} is not active`);
if (!record.module.sourceBacked) throw new Error(`module ${record.module.moduleId} has no source-backed implementation`);
const contract = batchContract(task, record.module);
if (!record.batchId) record.batchId = contract.batchId;
if (!record.selectedRecordIds) record.selectedRecordIds = contract.selectedRecordIds;
if (!record.module.batchId) record.module.batchId = contract.batchId;
if (!record.module.selectedRecordIds) record.module.selectedRecordIds = contract.selectedRecordIds;
const commandContract = commandBudget(record.module.commands ?? []);
if (!commandContract.withinBudget) throw new Error(`task ${task} exceeds command budget: ${JSON.stringify(commandContract)}`);
const existingManifestPath = `${out}/manifest.json`;
if (fs.existsSync(abs(existingManifestPath))) {
try {
const existingManifest = json(existingManifestPath);
const legacyCompleted = existingManifest.status === "done"
&& existingManifest.nextTask === null
&& JSON.stringify(existingManifest.selectedRecordIds ?? record.module.selectedRecordIds ?? []) === JSON.stringify(contract.selectedRecordIds);
if (completedBatch(existingManifest, task, record.module) || legacyCompleted) {
process.stdout.write(`module-batch-idempotent task=${task} batchId=${contract.batchId} plannerRequired=true\n`);
process.exit(0);
}
} catch { /* A partial manifest is not evidence and follows the normal path. */ }
}
if (record.status !== "in_progress" || record.ready !== true) throw new Error(`module task ${task} is not active`);
for (const source of record.module.sourcePaths ?? []) if (!fs.existsSync(abs(source))) throw new Error(`module source is missing: ${source}`);
const sourceAuditPaths = (record.module.sourcePaths ?? []).filter((source) =>
/^(blender-5\.2\.0\/source\/blender\/|web\/protocol\/|web\/app\/src\/(?!vendor\/))/u.test(source)
@@ -48,7 +70,6 @@ const reusedReceipts = (record.module.reuseReceipts ?? []).map((receipt) => ({
reportSha256: sha(receipt.report),
}));
const out = `tests/golden/${task}`;
const reportPath = `${out}/module-receipt.json`;
const statusPath = `docs/status/${task}.md`;
const logPath = `${out}/module-run.log`;
@@ -130,7 +151,11 @@ const report = {
schemaVersion: 1,
task,
operation: "MODULE_FUNCTION_EXECUTION",
phase: failed || !sourceUnchanged ? "BLOCKED" : "CONTINUE",
moduleId: record.module.moduleId,
batchId: contract.batchId,
selectedRecordIds: contract.selectedRecordIds,
commandBudget: commandContract,
status: failed || !sourceUnchanged ? "BLOCKED" : "PASS",
source: { paths: record.module.sourcePaths, auditedPaths: sourceAuditPaths, before: sourceBefore, after: sourceAfter, unchanged: sourceUnchanged },
reusedReceipts,
@@ -159,7 +184,7 @@ if (failed || !sourceUnchanged) {
const manifestPath = `${out}/manifest.json`;
const evidenceArtifacts = Object.fromEntries((record.module.evidencePaths ?? []).filter((file) => fs.existsSync(abs(file))).map((file, item) => [`evidence${item + 1}`, { path: file, sha256: sha(file) }]));
const artifacts = { parentManifest: { path: parentPath, sha256: sha(parentPath) }, ...Object.fromEntries(record.module.sourcePaths.map((source, item) => [`source${item + 1}`, { path: source, sha256: sha(source) }])), ...evidenceArtifacts, moduleReceipt: { path: reportPath, sha256: sha(reportPath) }, status: { path: statusPath, sha256: sha(statusPath) }, log: { path: logPath, sha256: sha(logPath) }, plan: { path: index.source.path, sha256: sha(index.source.path) }, taskContext: { path: `${out}/task-context.json`, sha256: sha(`${out}/task-context.json`) } };
writeJson(manifestPath, { schemaVersion: 1, task, parentTask: parentId, operation: "MODULE_FUNCTION_EXECUTION", taskClass: "MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "BLENDER_WEB_FUNCTION_MODULE", artifacts, assertions: { moduleId: record.module.moduleId, sourceBacked: true, positiveCases: record.module.positive.length, negativeCases: record.module.negative.length, recoveryCases: record.module.recovery.length, commandCount: results.length, reusedReceiptCount: reusedReceipts.length, sourceUnchanged, fullFamilyParity: false, dynamicInventoryPath, dynamicSourceTreeSha256: dynamicInventory.authority.sourceTreeSha256, dynamicUnitCount: dynamicInventory.summary.unitCount, dynamicVerifiedUnits: dynamicInventory.coverage.verifiedUnits, dynamicUnverifiedUnits: dynamicInventory.coverage.unverifiedUnits, planningEstimate: dynamicInventory.policy.planningEstimate, historicalInventoryTotal: 6900, historicalEffectiveCompleted: 290, historicalEffectivePending: 6609, historicalReproduction: false }, nextTask });
writeJson(manifestPath, { schemaVersion: 1, task, parentTask: parentId, operation: "MODULE_FUNCTION_EXECUTION", phase: "CONTINUE", taskClass: "MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "BLENDER_WEB_FUNCTION_MODULE", batchId: contract.batchId, selectedRecordIds: contract.selectedRecordIds, artifacts, assertions: { moduleId: record.module.moduleId, batchId: contract.batchId, selectedRecordIds: contract.selectedRecordIds, sourceBacked: true, positiveCases: record.module.positive.length, negativeCases: record.module.negative.length, recoveryCases: record.module.recovery.length, commandCount: results.length, commandBudget, reusedReceiptCount: reusedReceipts.length, sourceUnchanged, fullFamilyParity: false, dynamicInventoryPath, dynamicSourceTreeSha256: dynamicInventory.authority.sourceTreeSha256, dynamicUnitCount: dynamicInventory.summary.unitCount, dynamicVerifiedUnits: dynamicInventory.coverage.verifiedUnits, dynamicUnverifiedUnits: dynamicInventory.coverage.unverifiedUnits, planningEstimate: dynamicInventory.policy.planningEstimate, historicalInventoryTotal: 6900, historicalEffectiveCompleted: 290, historicalEffectivePending: 6609, historicalReproduction: false }, nextTask });
if (record.module.coverageBindings?.length) {
execFileSync(process.execPath, [abs("tools/web/bind-wbv2-module-coverage.mjs"), "--task", task], { cwd: root, stdio: "inherit" });
execFileSync(process.execPath, [abs("tools/web/finalize-wbv2-module-batch.mjs"), "--task", task], { cwd: root, stdio: "inherit" });

View File

@@ -3,7 +3,7 @@ import crypto from "node:crypto";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { recordFailure } from "./wbv2-failure-ledger.mjs";
import { createFailureLedgerSession } from "./wbv2-failure-ledger.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const controlRoot = "tests/golden/WBV2";
@@ -11,6 +11,10 @@ const checkpointPath = `${controlRoot}/continuous-scheduler.json`;
const lockPath = `${controlRoot}/continuous-scheduler.lock`;
const logPath = `${controlRoot}/continuous-scheduler.log`;
const abs = (file) => path.join(root, file);
const failureLedger = createFailureLedgerSession(root, {
logPaths: [`${controlRoot}/continuous-scheduler.log`, `${controlRoot}/continuous-supervisor.log`, `${controlRoot}/continuous-guardian.log`],
});
let latestBatch = null;
const argValue = (name, fallback) => {
const index = process.argv.indexOf(name);
@@ -60,6 +64,7 @@ function writeCheckpoint(payload) {
pid: process.pid,
updatedAt: new Date().toISOString(),
...payload,
latestBatch: payload.latestBatch ?? latestBatch,
};
const stable = (value) => { const { pid, updatedAt, ...rest } = value ?? {}; return rest; };
try {
@@ -72,6 +77,43 @@ function writeCheckpoint(payload) {
fs.renameSync(abs(temporaryPath), abs(checkpointPath));
}
function batchInfo(pointer) {
if (!pointer?.task) return null;
let manifest = {};
try { manifest = JSON.parse(read(`tests/golden/${pointer.task}/manifest.json`)); }
catch { /* The active batch may not have produced a manifest yet. */ }
let card = "";
try { card = read(`docs/tasks/${pointer.task}.md`); }
catch { /* Readiness reports the missing card separately. */ }
const batchId = manifest.batchId ?? card.match(/^- batchId: `?([^`\n]+)`?/mu)?.[1] ?? null;
let selectedRecordIds = Array.isArray(manifest.selectedRecordIds) ? manifest.selectedRecordIds : [];
if (selectedRecordIds.length === 0) {
try {
const selected = card.match(/^- selectedRecordIds:\s*(\[[^\n]+\])/mu)?.[1];
selectedRecordIds = selected ? JSON.parse(selected) : [];
} catch { selectedRecordIds = []; }
}
return {
task: pointer.task,
batchId,
selectedRecordIds,
completed: manifest.status === "done" && manifest.nextTask === null,
};
}
function planNextBatch(pointer) {
const result = spawnSync("python3", [abs("my-tools/authorize_next_wbv2_task.py"), "--write"], {
cwd: root,
encoding: "utf8",
maxBuffer: 8 * 1024 * 1024,
});
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
if (output) appendLog(`planner task=${pointer?.task ?? "NONE"} exit=${result.status ?? 1}\n${output}`);
if ((result.status ?? 1) !== 0 && /no configured unresolved Blender source batch remains/u.test(output)) return { terminal: true };
if ((result.status ?? 1) !== 0) throw new Error(`next batch planner failed: ${output}`);
return { terminal: false };
}
function appendLog(message) {
fs.appendFileSync(abs(logPath), `[${new Date().toISOString()}] ${message}\n`);
}
@@ -91,10 +133,10 @@ function refreshContinuation(task) {
if (!fs.existsSync(continuationPath)) return false;
const pointer = currentTask();
const index = JSON.parse(read("tests/golden/WBV2/task-index.json"));
const plan = JSON.parse(read(index.source.path));
const taskIndex = plan.tasks.findIndex((candidate) => candidate.id === task);
const terminal = terminalTask(task);
const nextTask = terminal ? null : plan.tasks[taskIndex + 1]?.id ?? null;
// The compact task index already contains the direct successor. Reading the
// full plan here adds no evidence and used to dominate short handoff cycles.
const nextTask = terminal ? null : index.entries?.[task]?.next ?? null;
const inputFiles = ["docs/EXECUTION_QUEUE.md", `docs/tasks/${task}.md`, pointer.parentManifest, `tests/golden/${task}/task-context.json`];
const fingerprint = crypto.createHash("sha256").update(inputFiles.map((file) => `${file}\0${crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex")}\n`).join("")).digest("hex");
try {
@@ -246,7 +288,9 @@ let iteration = 0;
let completed = false;
try {
appendLog(`started pid=${process.pid} maxIterations=${maxIterations} commit=${commitEnabled} push=${pushEnabled}`);
writeCheckpoint({ state: "started", iteration, current: currentTask(), maxIterations, commit: commitEnabled, push: pushEnabled });
const initial = currentTask();
latestBatch = batchInfo(initial);
writeCheckpoint({ state: "started", iteration, current: initial, maxIterations, commit: commitEnabled, push: pushEnabled });
while (maxIterations === 0 || iteration < maxIterations) {
if (stopRequested) {
writeCheckpoint({ state: "stopped", iteration, current: currentTask(), reason: stopSignal });
@@ -254,6 +298,7 @@ try {
break;
}
const before = currentTask();
latestBatch = batchInfo(before);
if (!before) {
writeCheckpoint({ state: "complete", iteration, current: null, reason: "queue_closed" });
appendLog(`complete queue_closed iteration=${iteration}`);
@@ -266,14 +311,24 @@ try {
try {
const index = JSON.parse(read("tests/golden/WBV2/task-index.json"));
if (index.activeTask === null && index.entries?.[before.task]) {
writeCheckpoint({ state: "complete", iteration, current: before, reason: "v2_plan_terminal" });
const completedBatchInfo = { ...batchInfo(before), completed: true };
latestBatch = completedBatchInfo;
const planned = preflightOnly ? { terminal: true } : planNextBatch(before);
if (!planned.terminal) {
const afterPlan = currentTask();
latestBatch = batchInfo(afterPlan);
writeCheckpoint({ state: "planned_next_batch", iteration, current: afterPlan, before, reason: "completed_batch_planned" });
appendLog(`planned next batch before=${before.task} after=${afterPlan.task}`);
continue;
}
writeCheckpoint({ state: "complete", iteration, current: before, reason: "v2_plan_terminal", latestBatch: completedBatchInfo });
appendLog(`complete v2_plan_terminal task=${before.task} iteration=${iteration}`);
completed = true;
break;
}
}
catch (error) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: "terminal_plan_check_failed", detail: error?.message ?? String(error) });
failureLedger.record({ task: before.task, phase: "scheduler", reason: "terminal_plan_check_failed", detail: error?.message ?? String(error) });
writeCheckpoint({ state: "blocked", iteration, before, reason: "terminal_plan_check_failed", error: error?.message ?? String(error) });
appendLog(`blocked task=${before.task} reason=terminal_plan_check_failed error=${error?.message ?? String(error)}`);
process.stderr.write(`scheduler-blocked task=${before.task} reason=terminal_plan_check_failed\n`);
@@ -290,7 +345,7 @@ try {
writeCheckpoint({ state: "preflight", iteration, before, readiness });
appendLog(`preflight task=${before.task} class=${readiness.taskClass ?? "UNKNOWN"} ok=${readiness.ok} reason=${readiness.reason ?? "ready"}`);
if (!readiness.ok) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: readiness.reason });
failureLedger.record({ task: before.task, phase: "scheduler", reason: readiness.reason });
writeCheckpoint({ state: "blocked", iteration, before, readiness });
appendLog(`blocked task=${before.task} reason=${readiness.reason}`);
process.stderr.write(`scheduler-blocked task=${before.task} reason=${readiness.reason}\n`);
@@ -306,7 +361,7 @@ try {
refreshTaskContext(before.task);
}
catch (error) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: "context_rotation_failed", detail: error?.message ?? String(error) });
failureLedger.record({ task: before.task, phase: "scheduler", reason: "context_rotation_failed", detail: error?.message ?? String(error) });
writeCheckpoint({ state: "blocked", iteration, before, reason: "context_rotation_failed", error: error?.message ?? String(error) });
process.stderr.write(`scheduler-blocked task=${before.task} reason=context_rotation_failed\n`);
exitCode = 2;
@@ -315,6 +370,7 @@ try {
writeCheckpoint({ state: "running", iteration: iteration + 1, before, readiness });
const child = runOne(before);
const after = currentTask();
latestBatch = batchInfo(after);
appendLog(`child task=${before.task} exit=${child.code} signal=${child.signal ?? "none"} after=${after?.task ?? "NONE"} log=${child.outputPath}`);
if (stopRequested) {
writeCheckpoint({ state: "stopped", iteration, before, after, reason: stopSignal, childExit: child.code });
@@ -323,7 +379,7 @@ try {
}
if (child.code !== 0) {
const reason = after?.task !== before.task ? "child_failed_after_queue_advance" : "child_failed_same_task";
recordFailure(root, { task: before.task, phase: "scheduler", reason, childExit: child.code });
failureLedger.record({ task: before.task, phase: "scheduler", reason, childExit: child.code });
writeCheckpoint({ state: "blocked", iteration, before, after, reason, childExit: child.code, childLog: child.outputPath });
appendLog(`blocked task=${before.task} reason=${reason}`);
process.stderr.write(`scheduler-blocked task=${before.task} reason=${reason} childExit=${child.code}\n`);
@@ -335,7 +391,7 @@ try {
if (refreshContinuation(after?.task ?? before.task) && commitEnabled) commitContinuation();
}
catch (error) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: "continuation_rotation_failed", detail: error?.message ?? String(error) });
failureLedger.record({ task: before.task, phase: "scheduler", reason: "continuation_rotation_failed", detail: error?.message ?? String(error) });
writeCheckpoint({ state: "blocked", iteration, before, after, reason: "continuation_rotation_failed", error: error?.message ?? String(error) });
process.stderr.write(`scheduler-blocked task=${before.task} reason=continuation_rotation_failed\n`);
exitCode = 5;
@@ -348,7 +404,7 @@ try {
break;
}
if (!after || after.task === before.task) {
recordFailure(root, { task: before.task, phase: "scheduler", reason: "child_exited_without_queue_advance", childExit: child.code });
failureLedger.record({ task: before.task, phase: "scheduler", reason: "child_exited_without_queue_advance", childExit: child.code });
writeCheckpoint({ state: "blocked", iteration, before, after, reason: "child_exited_without_queue_advance", childExit: child.code, childLog: child.outputPath });
appendLog(`blocked task=${before.task} reason=child_exited_without_queue_advance`);
process.stderr.write(`scheduler-blocked task=${before.task} reason=child_exited_without_queue_advance\n`);
@@ -372,12 +428,13 @@ try {
}
catch (error) {
exitCode = 5;
try { recordFailure(root, { task: currentTask()?.task ?? "UNKNOWN", phase: "scheduler", reason: "scheduler_error", detail: error?.message ?? String(error) }); }
try { failureLedger.record({ task: currentTask()?.task ?? "UNKNOWN", phase: "scheduler", reason: "scheduler_error", detail: error?.message ?? String(error) }); }
catch { /* Preserve the original scheduler failure. */ }
writeCheckpoint({ state: "blocked", iteration, reason: "scheduler_error", error: error?.message ?? String(error) });
appendLog(`blocked reason=scheduler_error error=${error?.message ?? String(error)}`);
}
finally {
failureLedger.flush();
releaseLock();
}
process.exitCode = exitCode;

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { recordFailure } from "./wbv2-failure-ledger.mjs";
import { createFailureLedgerSession } from "./wbv2-failure-ledger.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const abs = (file) => path.join(root, file);
@@ -10,6 +10,7 @@ const supervisorRoot = "tests/golden/WBV2";
const supervisorLog = `${supervisorRoot}/continuous-supervisor.log`;
const checkpointPath = `${supervisorRoot}/continuous-supervisor.json`;
const lockPath = `${supervisorRoot}/continuous-supervisor.lock`;
const failureLedger = createFailureLedgerSession(root, { logPaths: [`${supervisorRoot}/continuous-supervisor.log`] });
const valueArg = (name, fallback) => {
const index = process.argv.indexOf(name);
@@ -158,7 +159,7 @@ try {
}
if (child.code !== 0) {
const reason = after.task === before.task ? "child_failed_same_task" : "child_failed_after_queue_advance";
recordFailure(root, { task: before.task, phase: "supervisor", reason, childExit: child.code });
failureLedger.record({ task: before.task, phase: "supervisor", reason, childExit: child.code });
writeCheckpoint({ state: "blocked", iteration: iteration + 1, before, after, reason, childExit: child.code });
exitCode = 3;
break;
@@ -170,7 +171,7 @@ try {
break;
}
if (after.task === before.task) {
recordFailure(root, { task: before.task, phase: "supervisor", reason: "child_exited_without_queue_advance", childExit: child.code });
failureLedger.record({ task: before.task, phase: "supervisor", reason: "child_exited_without_queue_advance", childExit: child.code });
writeCheckpoint({ state: "blocked", iteration: iteration + 1, before, after, reason: "child_exited_without_queue_advance", childExit: child.code });
exitCode = 4;
break;
@@ -193,6 +194,7 @@ try {
}
}
finally {
failureLedger.flush();
releaseLock();
}
process.exitCode = exitCode;

View File

@@ -22,6 +22,19 @@ const json = (file) => JSON.parse(read(file));
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const tokenEstimate = (value) => Math.ceil(Buffer.byteLength(value, "utf8") / 4);
function inputMetadata(file, includeHash = true) {
const relativePath = relative(file);
let stat;
try {
stat = fs.statSync(file, { bigint: true });
} catch (error) {
return { path: relativePath, exists: false, bytes: null, mtime_ns: null, sha256: null, error: error?.code ?? "STAT_FAILED" };
}
const metadata = { path: relativePath, exists: true, bytes: Number(stat.size), mtime_ns: stat.mtimeNs.toString() };
if (includeHash) metadata.sha256 = sha256File(file);
return metadata;
}
const queuePath = path.join(root, "docs/EXECUTION_QUEUE.md");
const planPath = path.join(root, "tests/golden/M15-03A/next-task-plan.json");
const legacyTaskIndexPath = path.join(root, "tests/golden/M15-03A/task-index.json");
@@ -367,6 +380,13 @@ export function buildTaskContext(requestedTask) {
const ownOutputDir = path.join(root, "tests/golden", task);
const generatedPaths = [relative(ownOutputDir), relative(ownManifestPath), relative(ownStatusPath)];
const selectedInputs = selectInputPaths(candidatePaths, { sourceBytes, sourceTokens, reservedPaths, generatedPaths });
const metadataPaths = [...new Set([
relative(queuePath),
relative(taskPath),
relative(parentManifestPath),
...(fs.existsSync(parentStatusPath) ? [relative(parentStatusPath)] : []),
...selectedInputs.inputPaths,
])].map((candidate) => path.join(root, candidate));
const nextTask = ownManifest?.nextTask ?? nextFromPlan(task) ?? null;
const context = {
schemaVersion: 1,
@@ -381,6 +401,7 @@ export function buildTaskContext(requestedTask) {
},
commands,
inputPaths: selectedInputs.inputPaths,
inputMetadata: Object.fromEntries(metadataPaths.map((file) => [relative(file), inputMetadata(file)])),
inputSelection: selectedInputs.inputSelection,
exitCriteria: entry?.exitCriteria ?? ["focused command exits 0", "report and manifest are hash-bound", "failure path does not publish a late result"],
nextTask,

View File

@@ -0,0 +1,60 @@
import crypto from "node:crypto";
export const COMMAND_BUDGET = Object.freeze({
maxCommands: 48,
maxReadCommands: 16,
});
/** Return the explicit source/RNA record IDs selected for one module batch. */
export function selectedRecordIds(module = {}) {
const values = module.selectedRecordIds ?? module.coverageBindings ?? module.sourceUnits ?? [];
return [...new Set((Array.isArray(values) ? values : []).filter((value) => typeof value === "string" && value.length > 0))];
}
/** A batch identity is stable across retries and changes when selection changes. */
export function batchIdFor(task, module = {}) {
const ids = selectedRecordIds(module);
const moduleId = module.moduleId ?? "UNKNOWN_MODULE";
const digest = crypto.createHash("sha256").update(JSON.stringify(ids)).digest("hex").slice(0, 16);
return module.batchId ?? `${task}:${moduleId}:${digest}`;
}
export function batchContract(task, module = {}) {
const selected = selectedRecordIds(module);
return {
batchId: batchIdFor(task, module),
selectedRecordIds: selected,
selectedCount: selected.length,
};
}
export function completedBatch(manifest, task, module = {}) {
const expected = batchContract(task, module);
return manifest?.status === "done"
&& manifest.batchId === expected.batchId
&& JSON.stringify(manifest.selectedRecordIds ?? []) === JSON.stringify(expected.selectedRecordIds);
}
export function commandBudget(commands = []) {
const values = Array.isArray(commands) ? commands.filter((value) => typeof value === "string") : [];
const readCommands = values.filter((value) => /\b(?:cat|head|tail|sed|rg|find|ls|stat)\b/u.test(value));
return {
maxCommands: COMMAND_BUDGET.maxCommands,
maxReadCommands: COMMAND_BUDGET.maxReadCommands,
commandCount: values.length,
readCommandCount: readCommands.length,
withinBudget: values.length <= COMMAND_BUDGET.maxCommands && readCommands.length <= COMMAND_BUDGET.maxReadCommands,
};
}
export function adaptiveBatchSize({ currentSize, history = [], minSize = 1, maxSize = currentSize, remaining = Number.MAX_SAFE_INTEGER }) {
const size = Math.max(minSize, Math.min(maxSize, currentSize));
const recent = history.filter((item) => Number.isFinite(item.durationMs) && Number.isFinite(item.commands)).slice(-3);
let next = size;
if (recent.some((item) => item.timeout === true || item.durationMs > 900_000 || item.commands > COMMAND_BUDGET.maxCommands)) {
next = Math.floor(size / 2);
} else if (recent.length > 0 && recent.every((item) => item.durationMs < 300_000 && item.commands < 24 && item.focusedGateFailure !== true)) {
next = size + 4;
}
return Math.max(minSize, Math.min(maxSize, remaining, next));
}

View File

@@ -5,7 +5,87 @@ import path from "node:path";
const ledgerPath = (root) => path.join(root, "tests/golden/WBV2/failure-ledger.json");
const keyFor = (entry) => crypto.createHash("sha256").update(JSON.stringify({ task: entry.task, phase: entry.phase, reason: entry.reason, childExit: entry.childExit ?? null })).digest("hex");
const readLedger = (root) => {
try {
const value = JSON.parse(fs.readFileSync(ledgerPath(root), "utf8"));
return {
schemaVersion: 2,
entries: Array.isArray(value.entries) ? value.entries : [],
hydratedLogs: Array.isArray(value.hydratedLogs) ? value.hydratedLogs : [],
};
}
catch {
return { schemaVersion: 2, entries: [], hydratedLogs: [] };
}
};
export function createFailureLedgerSession(root, { logPaths = [] } = {}) {
const ledger = readLedger(root);
const existing = new Set(ledger.entries.map((entry) => entry.key).filter(Boolean));
const hydrated = new Set(ledger.hydratedLogs);
let dirty = false;
// Recovery is deliberately narrow: only explicit cycle.completed events can
// add entries, and each log is imported at most once.
for (const relative of logPaths) {
if (hydrated.has(relative)) continue;
const file = path.join(root, relative);
if (!fs.existsSync(file)) continue;
let source;
try { source = fs.readFileSync(file, "utf8"); }
catch { continue; }
if (!/cycle\.completed/u.test(source)) continue;
for (const line of source.split("\n")) {
if (!/cycle\.completed/u.test(line)) continue;
let event;
try {
const start = line.indexOf("{");
event = JSON.parse(start >= 0 ? line.slice(start) : line);
}
catch { event = null; }
const entry = event?.failure ?? event?.entry;
if (!entry || typeof entry !== "object" || !entry.task || !entry.phase || !entry.reason) continue;
const key = keyFor(entry);
if (existing.has(key)) continue;
ledger.entries.push({ ...entry, key, recordedAt: event.recordedAt ?? new Date().toISOString(), hydratedFrom: relative });
existing.add(key);
dirty = true;
}
hydrated.add(relative);
dirty = true;
}
const record = (entry) => {
const key = keyFor(entry);
if (existing.has(key)) return false;
ledger.entries.push({ ...entry, key, recordedAt: new Date().toISOString() });
ledger.entries = ledger.entries.slice(-200);
existing.add(key);
dirty = true;
return true;
};
const flush = () => {
if (!dirty) return false;
const file = ledgerPath(root);
fs.mkdirSync(path.dirname(file), { recursive: true });
const temporary = `${file}.tmp-${process.pid}`;
fs.writeFileSync(temporary, `${JSON.stringify({ ...ledger, hydratedLogs: [...hydrated] }, null, 2)}\n`);
fs.renameSync(temporary, file);
dirty = false;
return true;
};
return { record, flush, ledger };
}
export function recordFailure(root, entry) {
const session = createFailureLedgerSession(root);
const recorded = session.record(entry);
session.flush();
return recorded;
}
/* Legacy callers should migrate to createFailureLedgerSession so a cycle only writes once. */
export function _legacyRecordFailure(root, entry) {
const file = ledgerPath(root);
let ledger = { schemaVersion: 1, entries: [] };
try { ledger = JSON.parse(fs.readFileSync(file, "utf8")); }

View File

@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
adaptiveBatchSize,
batchContract,
commandBudget,
completedBatch,
} from "../../../tools/web/wbv2-batch-contract.mjs";
import { createFailureLedgerSession } from "../../../tools/web/wbv2-failure-ledger.mjs";
test("batch contract is stable and completion is idempotent", () => {
const module = { moduleId: "FCURVE", coverageBindings: ["rna:a", "rna:b"] };
const contract = batchContract("WBV2-P1-001", module);
assert.equal(contract.selectedCount, 2);
assert.match(contract.batchId, /^WBV2-P1-001:FCURVE:[a-f0-9]{16}$/u);
assert.equal(completedBatch({ status: "done", batchId: contract.batchId, selectedRecordIds: contract.selectedRecordIds }, "WBV2-P1-001", module), true);
});
test("adaptive batches follow the three-cycle fast and slow paths", () => {
assert.equal(adaptiveBatchSize({ currentSize: 24, maxSize: 32, history: [{ durationMs: 120_000, commands: 10, focusedGateFailure: false }] }), 28);
assert.equal(adaptiveBatchSize({ currentSize: 24, maxSize: 32, history: [{ durationMs: 901_000, commands: 10 }] }), 12);
assert.equal(adaptiveBatchSize({ currentSize: 24, maxSize: 32, remaining: 5, history: [] }), 5);
});
test("command budget separates exploration from execution", () => {
assert.equal(commandBudget(["cat file", "npm run build"]).withinBudget, true);
assert.equal(commandBudget(Array.from({ length: 49 }, () => "npm run focused")).withinBudget, false);
assert.equal(commandBudget(Array.from({ length: 17 }, () => "rg pattern")).withinBudget, false);
});
test("failure ledger hydrates completed cycles and flushes once", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "wbv2-fast-path-ledger-"));
const logPath = "tests/golden/WBV2/cycle.log";
fs.mkdirSync(path.dirname(path.join(root, logPath)), { recursive: true });
fs.writeFileSync(path.join(root, logPath), 'prefix {"event":"cycle.completed","failure":{"task":"T","phase":"gate","reason":"failed"}}\n');
const session = createFailureLedgerSession(root, { logPaths: [logPath] });
assert.equal(session.ledger.entries.length, 1);
session.record({ task: "T2", phase: "gate", reason: "failed" });
assert.equal(fs.existsSync(path.join(root, "tests/golden/WBV2/failure-ledger.json")), false);
session.flush();
const written = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/WBV2/failure-ledger.json"), "utf8"));
assert.equal(written.entries.length, 2);
assert.deepEqual(written.hydratedLogs, [logPath]);
});