Files
workinf_Blender_Wasm/my-tools/wbv2_local_preparation.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

172 lines
7.1 KiB
Python

"""Discover explicit local recipes and admit their output without model calls."""
import ast
import hashlib
import json
import re
import time
from pathlib import Path
def recipe_paths(root):
root = Path(root).resolve()
directory = root / "my-tools"
if directory.is_symlink():
return []
return sorted(p for p in directory.glob("prepare_wbv2_*workflow.py")
if p.is_file() and not p.is_symlink())[:128]
def _declarations(path):
if path.stat().st_size > 128 * 1024:
return None
tree = ast.parse(path.read_text(encoding="utf-8"))
values = {}
for node in tree.body:
if isinstance(node, ast.Assign) and len(node.targets) == 1:
target = node.targets[0]
if isinstance(target, ast.Name) and target.id in {"JOB", "PREPARATION_INPUTS"}:
values[target.id] = ast.literal_eval(node.value)
job = values.get("JOB")
inputs = values.get("PREPARATION_INPUTS", [])
if not isinstance(job, str) or not re.fullmatch(r"[A-Z0-9][A-Z0-9_-]{0,127}", job):
return None
if not isinstance(inputs, list) or len(inputs) > 128 or not all(isinstance(p, str) for p in inputs):
return None
return job, inputs
def _local(root, relative):
path = Path(relative)
if path.is_absolute() or ".." in path.parts:
raise ValueError("preparation input escapes repository")
current = root
for part in path.parts:
current /= part
if current.is_symlink():
raise ValueError("preparation input is a symlink")
return current
def _fingerprint(root, context, paths):
files = {}
for relative in sorted(set(paths)):
path = _local(root, relative)
files[relative] = hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None
authority = {key: context[key] for key in ("task", "parentSha256")}
return hashlib.sha256(json.dumps([authority, files], sort_keys=True).encode()).hexdigest()
def recipe_input_paths(root, jobs):
wanted = {job["id"] for job in jobs}
paths = set()
for script in recipe_paths(root):
paths.add(str(script.relative_to(root)))
try:
declaration = _declarations(script)
if declaration and declaration[0] in wanted:
for relative in declaration[1]:
_local(root, relative)
paths.add(relative)
except (OSError, SyntaxError, ValueError, TypeError):
continue
return paths
def _failed_origin_owned(root, origin, prior, context, identifier, script):
owned = prior.get("generatedOrigin")
if (prior.get("state") != "blocked" or prior.get("job") != identifier
or prior.get("script") != script or prior.get("task") != context["task"]
or prior.get("parentSha256") != context["parentSha256"]
or not isinstance(owned, dict)):
return False
try:
relative = str(origin.relative_to(root))
return (owned.get("path") == relative
and _local(root, relative).is_file()
and hashlib.sha256(origin.read_bytes()).hexdigest() == owned.get("sha256"))
except (OSError, ValueError):
return False
def prepare_local_contracts(runner, context, jobs):
from wbv2_autopilot import queue_context, read_json, write_json
from wbv2_task_dispatch import prepared_contract
recipes = {}
for script in recipe_paths(runner.root):
try:
declaration = _declarations(script)
except (OSError, SyntaxError, ValueError, TypeError):
continue
if declaration:
job, inputs = declaration
recipes.setdefault(job, []).append((script, inputs))
prepared = []
for job in jobs:
identifier = job["id"]
choices = recipes.get(identifier, [])
if not re.fullmatch(r"[A-Z0-9][A-Z0-9_-]{0,127}", identifier) or len(choices) != 1:
continue
# An existing origin belongs to the developer, including one being edited.
# Admission errors must not cause its generator to overwrite those edits.
origin = _local(runner.root, f"my-tools/local-workflows/{identifier}.json")
if runner.local_contract_path(job):
continue
script, declared = choices[0]
relative = str(script.relative_to(runner.root))
inputs = [relative, context["parentManifest"], *job.get("ownedPaths", []), *declared]
if len(inputs) > 256:
continue
try:
fingerprint = _fingerprint(runner.root, context, inputs)
except (OSError, ValueError):
continue
record_path = runner.directory / "preparation-attempts" / f"{identifier}.json"
prior = read_json(record_path, {})
if prior.get("fingerprint") == fingerprint:
continue
if runner.stop_requested or (runner.directory / "pause.request").exists():
break
if queue_context(runner.root) != context:
raise InterruptedError("queue changed before local preparation")
if origin.exists():
if not _failed_origin_owned(runner.root, origin, prior, context, identifier, relative):
continue
origin.unlink()
started = time.monotonic()
record = {"schemaVersion": 1, "job": identifier, "script": relative,
"fingerprint": fingerprint, "state": "running"}
record.update(task=context["task"], parentSha256=context["parentSha256"])
write_json(record_path, record)
proc = runner.run_process(["python3", relative], runner.root,
runner.directory / "preparation" / f"{identifier}.log",
min(120, runner.policy["checkTimeoutSeconds"]),
phase="local_workflow_prepare")
record.update(seconds=time.monotonic() - started, exitCode=proc["exitCode"])
if queue_context(runner.root) != context:
record["state"] = "authority_changed"
write_json(record_path, record)
raise InterruptedError("queue changed during local preparation")
ok = proc["exitCode"] == 0 and not any(proc.get(key) for key in ("interrupted", "timedOut", "logLimit"))
contract, findings = prepared_contract(runner.root, context, job) if ok else (None, [])
if not contract:
try:
output = str(origin.relative_to(runner.root))
if _local(runner.root, output).is_file():
record["generatedOrigin"] = {
"path": output,
"sha256": hashlib.sha256(origin.read_bytes()).hexdigest(),
}
except (OSError, ValueError):
pass
record.update(state="prepared" if contract else "blocked", findings=findings)
write_json(record_path, record)
runner.event("local.prepared" if contract else "local.prepare_failed", job=identifier,
script=relative, seconds=record["seconds"], exitCode=proc["exitCode"])
if contract:
prepared.append(identifier)
if proc.get("interrupted"):
break
return prepared