Files
workinf_Blender_Wasm/my-tools/wbv2_task_dispatch.py
mes123456 f3e21c6b98
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
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

215 lines
11 KiB
Python

"""Local task admission and ownership records under the supervisor writer lease."""
import copy
import json
import os
import re
def candidate_paths(job):
candidates = []
for key in ("localContract", "localContractPath"):
if isinstance(job.get(key), str):
candidates.append(job[key])
candidates.append(f"my-tools/local-workflows/{job['id']}.json")
if isinstance(job.get("resultPath"), str):
candidates.append(job["resultPath"])
return list(dict.fromkeys(candidates))
def prepared_contract(root, context, job):
from wbv2_autopilot import contained, read_json, sha
from wbv2_compilation_contract import compilation_requested, validate_compilation_shape
missing = {"checks", "cases", "sourceUnits", "routeIds", "sourceReferences", "dependencies", "artifacts", "changedFiles", "workflow"}
findings = []
for relative in candidate_paths(job):
try:
path = contained(root, relative)
if not path.is_file():
continue
document = read_json(path)
if not isinstance(document, dict):
findings.append({"path": relative, "reason": "contract must be an object"})
continue
wrapped = isinstance(document.get("result"), dict)
result = document["result"] if wrapped else document
compilation_only = compilation_requested(root, result)
if wrapped and (document.get("task") != context["task"] or document.get("job") != job["id"] or document.get("parentSha256") != context["parentSha256"]):
findings.append({"path": relative, "reason": "task, job or parent mismatch"})
continue
if not wrapped and document.get("task") not in {None, context["task"], job["id"]}:
findings.append({"path": relative, "reason": "result belongs to another task"})
continue
absent = sorted((missing - ({"cases"} if compilation_only else set())) - result.keys())
if result.get("status") != "ready" or absent:
findings.append({"path": relative, "reason": "implementation metadata is not an executable contract", "missingFields": absent})
continue
if not result["checks"] or (not compilation_only and not result.get("cases")):
findings.append({"path": relative, "reason": "checks and behavioral cases must be nonempty"})
continue
if compilation_only:
validate_compilation_shape(result)
contract = {
"schemaVersion": 1,
"task": context["task"],
"job": job["id"],
"parentSha256": context["parentSha256"],
"generator": "local-contract-admission-v1",
"origin": {"path": relative, "sha256": sha(path)},
"acceptance": "pending independent local validation",
"result": copy.deepcopy(result),
}
if compilation_only:
contract["result"]["contractKind"] = "compilation_only"
contract["result"].setdefault("cases", [])
contract["acceptance"] = "pending compilation; behavioral verification deferred until user use"
contract["result"]["dependencies"] = sorted(set(contract["result"]["dependencies"] + [relative]))
return contract, findings
except (OSError, ValueError, KeyError, TypeError):
findings.append({"path": relative, "reason": "invalid local contract"})
return None, findings
def input_signature(runner, context, jobs):
"""Cheap wakeup signal; SHA-256 admission still decides whether to execute."""
from wbv2_autopilot import contained
from wbv2_preparation import preparation_test_paths
paths = {"docs/EXECUTION_QUEUE.md", context["parentManifest"], "tests/golden/WBV2/web-product-backlog.json", "tests/golden/WBV2/dynamic-inventory.json", "my-tools/wbv2_autopilot_policy.json", "web/tests/e2e", "web/tests/unit"}
for job in jobs:
paths.update(job.get("ownedPaths", []))
paths.update(candidate_paths(job))
paths.add(f"tests/golden/WBV2/autopilot/local-plans/{job['id']}.json")
paths.update(runner.state.get("jobs", {}).get(job["id"], {}).get("localInputs", {}))
paths.update(preparation_test_paths(runner.root, job))
from wbv2_local_preparation import recipe_input_paths
paths.add("my-tools")
paths.update(recipe_input_paths(runner.root, jobs))
result = []
for relative in sorted(paths):
try:
stat = contained(runner.root, relative).stat()
result.append((relative, stat.st_ino, stat.st_size, stat.st_mtime_ns))
except (OSError, ValueError):
result.append((relative, None))
return tuple(result)
class TaskDispatch:
def __init__(self, runner):
self.runner = runner
def dependency_issue(self, job, jobs):
dependencies = job.get("dependsOn", [])
if not isinstance(dependencies, list) or not all(isinstance(value, str) for value in dependencies):
return "invalid task dependency list"
known = {item["id"]: item for item in jobs}
def visit(identifier, stack):
if identifier in stack:
return "task dependency cycle"
if identifier not in known:
return "unknown task dependency: " + identifier
predecessors = known[identifier].get("dependsOn", [])
if not isinstance(predecessors, list) or not all(isinstance(value, str) for value in predecessors):
return "invalid task dependency list"
for previous in predecessors:
problem = visit(previous, stack | {identifier})
if problem:
return problem
return None
problem = visit(job["id"], set())
if problem:
return problem
pending = [identifier for identifier in dependencies if not self.runner.receipt_valid(self.runner.state.get("jobs", {}).get(identifier, {}))]
return "waiting for verified dependencies: " + ", ".join(pending) if pending else None
def claim(self, context, job, jobs):
from wbv2_autopilot import contained, now, queue_context, read_json, sha, write_json
if queue_context(self.runner.root) != context:
raise InterruptedError("queue changed before task claim")
problem = self.dependency_issue(job, jobs)
if problem:
raise ValueError(problem)
relative = self.runner.local_contract_path(job)
if not relative:
raise ValueError("local workflow contract missing")
contract = read_json(contained(self.runner.root, relative))
if (contract.get("task"), contract.get("job"), contract.get("parentSha256")) != (context["task"], job["id"], context["parentSha256"]):
raise ValueError("task claim contract binding mismatch")
identifier = job["id"]
if not re.fullmatch(r"[A-Z0-9_-]+", identifier):
raise ValueError("unsafe task ID")
record = {"schemaVersion": 1, "task": context["task"], "job": identifier, "state": "claimed", "claimedAt": now(), "ownerPid": self.runner.state.get("pid"), "parentSha256": context["parentSha256"], "contract": relative, "contractSha256": sha(contained(self.runner.root, relative))}
write_json(self.runner.directory / "claims" / (identifier + ".json"), record)
self.runner.event("task.claimed", job=identifier, task=context["task"], contractSha256=record["contractSha256"])
return record
def finish(self, claim):
from wbv2_autopilot import now, write_json
result = self.runner.state["jobs"].get(claim["job"], {})
claim = {**claim, "state": result.get("state", "needs_reconcile"), "finishedAt": now(), "runDirectory": result.get("runDirectory"), "receipt": result.get("receipt"), "reason": result.get("reason")}
write_json(self.runner.directory / "claims" / (claim["job"] + ".json"), claim)
self.runner.event("task.released", job=claim["job"], state=claim["state"])
def recover(self):
from wbv2_autopilot import alive, contained, identity, queue_context, read_json, sha
runner = self.runner
if runner.policy.get("modelExecution", "disabled") != "disabled" or runner.state.get("executionMode") != "local_only":
return False
if runner.state.get("state") not in {"implementing", "validating", "needs_reconcile"} or (runner.directory / "pause.request").exists():
return False
for prefix in ("", "child"):
pid_key, identity_key = ("pid", "identity") if not prefix else ("childPid", "childIdentity")
pid = runner.state.get(pid_key)
if pid and pid != os.getpid() and alive(pid) and identity(pid) == runner.state.get(identity_key):
return False
child = runner.state.get("childPid")
if child:
try:
os.killpg(child, 0)
return False
except ProcessLookupError:
pass
except PermissionError:
return False
try:
context = queue_context(runner.root)
identifier = runner.state["activeJob"]
if not re.fullmatch(r"[A-Z0-9_-]+", identifier):
return False
job = next(job for job in runner.jobs(context) if job["id"] == identifier)
record = runner.state["jobs"][identifier]
claim = read_json(runner.directory / "claims" / (identifier + ".json"))
if claim.get("state") != "claimed" or claim.get("job") != identifier or claim.get("ownerPid") != runner.state.get("pid"):
return False
if claim.get("task") != context["task"] or claim.get("parentSha256") != context["parentSha256"]:
return False
if claim.get("contract") != runner.local_contract_path(job) or sha(contained(runner.root, claim["contract"])) != claim.get("contractSha256"):
return False
run = contained(runner.root, record["runDirectory"])
if not run.is_relative_to((runner.directory / "runs").resolve()):
return False
snapshot = read_json(run / "before-worker.json")
if not snapshot.get("protected") or not isinstance(snapshot.get("owned"), dict):
return False
if runner.protected_hashes(context) != snapshot["protected"]:
return False
if set(snapshot["owned"]) != set(job.get("ownedPaths", [])):
return False
for hashes in (snapshot["protected"], snapshot["owned"], record.get("localInputs", {})):
if any(sha(contained(runner.root, path)) != digest for path, digest in hashes.items()):
return False
record.update(state="retry_pending", reason="recovered local claim; unchanged inputs and no surviving owner")
runner.state.pop("childPid", None)
runner.state.pop("childIdentity", None)
runner.checkpoint("idle", reason=record["reason"], currentProcess={"phase": "local_recovery", "status": "finished"})
runner.event("task.recovered", job=identifier, previousRun=record["runDirectory"])
return True
except (OSError, ValueError, KeyError, TypeError, StopIteration):
return False