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>
248 lines
11 KiB
Python
248 lines
11 KiB
Python
"""Build local workflows while preserving behavioral acceptance for explicit use."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import time
|
|
import uuid
|
|
from wbv2_execution_phase import implementation_only
|
|
|
|
|
|
def verification_timing(runner):
|
|
from wbv2_autopilot import read_json
|
|
|
|
value = read_json(runner.directory / "preferences.json", {}).get(
|
|
"verificationTiming", "automatic"
|
|
)
|
|
if value not in {"automatic", "on_user_use", "after_all_features"}:
|
|
raise ValueError("unsupported verificationTiming preference")
|
|
return value
|
|
|
|
|
|
def set_verification_timing(runner, value):
|
|
from wbv2_autopilot import read_json, write_json
|
|
|
|
if value not in {"automatic", "on_user_use", "after_all_features"}:
|
|
raise ValueError("unsupported verificationTiming preference")
|
|
path = runner.directory / "preferences.json"
|
|
preferences = read_json(path, {})
|
|
preferences.update(schemaVersion=1, verificationTiming=value)
|
|
write_json(path, preferences)
|
|
|
|
|
|
def deferred(runner):
|
|
return verification_timing(runner) in {"on_user_use", "after_all_features"} and not runner.pending_request
|
|
|
|
|
|
def _digest(runner, relative):
|
|
from wbv2_autopilot import contained, sha
|
|
|
|
path = contained(runner.root, relative)
|
|
stat = path.stat()
|
|
stamp = (stat.st_dev, stat.st_ino, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns)
|
|
cache = runner.__dict__.setdefault("_build_hash_cache", {})
|
|
prior = cache.get(relative)
|
|
if prior is None or prior[0] != stamp:
|
|
prior = (stamp, sha(path))
|
|
cache[relative] = prior
|
|
return prior[1]
|
|
|
|
|
|
def build_record_valid(runner, record):
|
|
from wbv2_autopilot import contained, read_json
|
|
|
|
if record.get("state") != "built_pending_verification":
|
|
return False
|
|
try:
|
|
path = record["buildReceipt"]
|
|
if _digest(runner, path) != record["buildReceiptSha256"]:
|
|
return False
|
|
receipt = read_json(contained(runner.root, path))
|
|
if receipt.get("status") != "built_pending_verification":
|
|
return False
|
|
inputs = receipt["inputs"]
|
|
artifacts = receipt["artifacts"]
|
|
if not inputs or not artifacts or not receipt.get("checks"):
|
|
return False
|
|
return all(_digest(runner, p) == value for p, value in inputs.items()) and all(
|
|
_digest(runner, item["path"]) == item["sha256"] for item in artifacts
|
|
)
|
|
except (OSError, ValueError, KeyError, TypeError):
|
|
return False
|
|
|
|
|
|
def work_ready(runner, record):
|
|
return runner.receipt_valid(record) or (deferred(runner) and build_record_valid(runner, record))
|
|
|
|
|
|
def dispatch_for(runner):
|
|
from wbv2_task_dispatch import TaskDispatch
|
|
|
|
if not deferred(runner):
|
|
return TaskDispatch(runner)
|
|
|
|
class BuildDependencies:
|
|
def __getattr__(self, name):
|
|
return getattr(runner, name)
|
|
|
|
def receipt_valid(self, record):
|
|
return work_ready(runner, record)
|
|
|
|
class BuildDispatch(TaskDispatch):
|
|
def dependency_issue(self, job, jobs):
|
|
return TaskDispatch(BuildDependencies()).dependency_issue(job, jobs)
|
|
|
|
return BuildDispatch(runner)
|
|
|
|
|
|
def work_signature(runner, context, jobs):
|
|
from wbv2_autopilot import contained
|
|
from wbv2_task_dispatch import input_signature
|
|
|
|
paths = {"tests/golden/WBV2/autopilot/preferences.json"}
|
|
for record in runner.state.get("jobs", {}).values():
|
|
if record.get("state") == "built_pending_verification":
|
|
paths.add(record.get("buildReceipt", ""))
|
|
paths.update(record.get("buildArtifacts", []))
|
|
stamps = []
|
|
for relative in sorted(paths - {""}):
|
|
try:
|
|
stat = contained(runner.root, relative).stat()
|
|
stamps.append((relative, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns))
|
|
except (OSError, ValueError):
|
|
stamps.append((relative, None))
|
|
return hashlib.sha256(
|
|
json.dumps([input_signature(runner, context, jobs), stamps], sort_keys=True).encode()
|
|
).hexdigest()
|
|
|
|
|
|
def _build_result(result, implementation_phase=False):
|
|
allowed = {"source_build", "typecheck"}
|
|
checks = [dict(check) for check in result["checks"] if set(check["kinds"]) & allowed]
|
|
if implementation_phase:
|
|
checks = [check for check in checks if "typecheck" not in check["kinds"]
|
|
or any(str(arg).endswith("tools/web/incremental-web-build.mjs")
|
|
for arg in check["argv"])]
|
|
for check in checks:
|
|
if "typecheck" in check["kinds"]:
|
|
check["kinds"] = ["source_build"]
|
|
if not checks:
|
|
raise ValueError("local workflow has no source or Web build check")
|
|
ids = {check["id"] for check in checks}
|
|
all_ids = {check["id"] for check in result["checks"]}
|
|
for check in checks:
|
|
if not set(check["kinds"]) <= allowed:
|
|
raise ValueError("build check mixes compilation with deferred behavioral verification")
|
|
dependencies = check.get("dependsOn", [])
|
|
if not set(dependencies) <= all_ids:
|
|
raise ValueError("build check has an unknown dependency")
|
|
check["deferredValidationDependencies"] = [name for name in dependencies if name not in ids]
|
|
check["dependsOn"] = [name for name in dependencies if name in ids]
|
|
return {**result, "checks": checks, "cases": []}
|
|
|
|
|
|
def build_only_attempt(runner, context, job):
|
|
from wbv2_autopilot import POLICY, contained, now, sha, write_json
|
|
from wbv2_local_execution import execute_checks
|
|
|
|
previous = runner.state["jobs"].get(job["id"], {})
|
|
run_dir = runner.directory / "runs" / f"{job['id']}-{uuid.uuid4().hex[:12]}"
|
|
run_dir.mkdir(parents=True)
|
|
record = {
|
|
"state": "implementing", "executionKind": "build_only",
|
|
"attempts": int(previous.get("attempts", 0)) + 1,
|
|
"providerFailures": 0, "runDirectory": str(run_dir.relative_to(runner.root)),
|
|
}
|
|
contract = runner.local_contract_path(job)
|
|
record["localContractSha256"] = sha(contained(runner.root, contract))
|
|
runner.state["jobs"][job["id"]] = record
|
|
protected = runner.protected_hashes(context)
|
|
owned = {p: sha(contained(runner.root, p)) for p in job.get("ownedPaths", [])}
|
|
record["localInputs"] = {
|
|
**owned,
|
|
contract: record["localContractSha256"],
|
|
context["parentManifest"]: sha(contained(runner.root, context["parentManifest"])),
|
|
"my-tools/wbv2_deferred_verification.py": sha(
|
|
runner.root / "my-tools/wbv2_deferred_verification.py"
|
|
),
|
|
"my-tools/wbv2_compilation_contract.py": sha(
|
|
runner.root / "my-tools/wbv2_compilation_contract.py"
|
|
),
|
|
}
|
|
write_json(run_dir / "before-worker.json", {"protected": protected, "owned": owned})
|
|
runner.checkpoint("implementing", activeJob=job["id"], queue=context,
|
|
reason="building; behavioral verification deferred until user use",
|
|
nextRetryAt=None, providerFailures=0)
|
|
started = time.monotonic()
|
|
try:
|
|
result = runner.local_result(context, job, run_dir)
|
|
build = _build_result(result, implementation_only(runner.root))
|
|
runner.validate_result(build, run_dir, compilation_only=True)
|
|
paths = sorted(set(result["sourceReferences"] + result["dependencies"] +
|
|
job.get("ownedPaths", []) + result["changedFiles"] +
|
|
[POLICY, context["card"], context["parentManifest"],
|
|
"my-tools/wbv2_deferred_verification.py",
|
|
"my-tools/wbv2_compilation_contract.py"]))
|
|
inputs = {p: sha(contained(runner.root, p)) for p in paths}
|
|
record["localInputs"] = inputs
|
|
if any(value is None for value in inputs.values()):
|
|
raise ValueError("build input is missing")
|
|
env = {
|
|
"WBV2_RECEIPT_DIR": str(run_dir / "cases"),
|
|
"WBV2_DEPLOYED_ORIGIN": runner.policy["origin"],
|
|
"WEB_BUILD_JOBS": str(min(4, runner.policy["maxBuildJobs"])),
|
|
"CMAKE_BUILD_PARALLEL_LEVEL": str(min(4, runner.policy["maxBuildJobs"])),
|
|
}
|
|
if implementation_only(runner.root):
|
|
env["WBV2_SKIP_TYPECHECK"] = "1"
|
|
checks = execute_checks(runner, build, run_dir, inputs, env)
|
|
if protected != runner.protected_hashes(context) or any(
|
|
sha(contained(runner.root, p)) != digest for p, digest in inputs.items()
|
|
):
|
|
raise InterruptedError("build authority or input changed during compilation")
|
|
artifacts = []
|
|
for p in sorted(set(result["artifacts"])):
|
|
digest = sha(contained(runner.root, p))
|
|
if digest is None:
|
|
raise ValueError(f"compiled artifact missing: {p}")
|
|
artifacts.append({"path": p, "sha256": digest})
|
|
if not artifacts:
|
|
raise ValueError("build has no compiled artifacts")
|
|
receipt_path = run_dir / "built-pending-verification.json"
|
|
write_json(receipt_path, {
|
|
"schemaVersion": 1, "queueTask": context["task"], "job": job["id"],
|
|
"parentSha256": context["parentSha256"], "workflow": result["workflow"],
|
|
"status": "built_pending_verification", "verificationTiming": verification_timing(runner),
|
|
"builtAt": now(), "durationSeconds": time.monotonic() - started,
|
|
"sourceUnits": result["sourceUnits"], "routeIds": result["routeIds"],
|
|
"sourceReferences": result["sourceReferences"], "inputs": inputs,
|
|
"checks": checks, "artifacts": artifacts,
|
|
"deferredChecks": [c["id"] for c in result["checks"] if c not in build["checks"]],
|
|
"coverageIncrementClaimed": 0,
|
|
})
|
|
record.update(state="built_pending_verification",
|
|
buildReceipt=str(receipt_path.relative_to(runner.root)),
|
|
buildReceiptSha256=sha(receipt_path),
|
|
buildArtifacts=[a["path"] for a in artifacts],
|
|
changedFiles=result["changedFiles"],
|
|
reason="compiled; behavioral verification deferred until user use")
|
|
runner.checkpoint("ready", activeJob=None)
|
|
runner.event("job.built_pending_verification", job=job["id"],
|
|
receipt=record["buildReceipt"], seconds=time.monotonic() - started)
|
|
return True
|
|
except InterruptedError as error:
|
|
record.update(state="needs_reconcile", reason=str(error))
|
|
runner.checkpoint("needs_reconcile", reason=str(error))
|
|
return False
|
|
except Exception as error:
|
|
changed = protected != runner.protected_hashes(context) or any(
|
|
sha(contained(runner.root, p)) != digest for p, digest in owned.items()
|
|
)
|
|
state = "needs_reconcile" if changed else "blocked"
|
|
record.update(state=state, reason=str(error))
|
|
runner.checkpoint(state, reason=str(error))
|
|
runner.event("job.build_failed", job=job["id"], errorType=type(error).__name__,
|
|
seconds=time.monotonic() - started)
|
|
return False
|