"""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