107 lines
7.1 KiB
Python
107 lines
7.1 KiB
Python
"""Deterministic local task admission; never invokes a model or invents receipts."""
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
from wbv2_task_dispatch import prepared_contract
|
|
from wbv2_preparation import prepare_module
|
|
|
|
|
|
class LocalPlanner:
|
|
def __init__(self, root, directory):
|
|
self.root = Path(root).resolve()
|
|
self.directory = Path(directory)
|
|
|
|
def _path(self, relative):
|
|
path = (self.root / relative).resolve()
|
|
if not path.is_relative_to(self.root):
|
|
raise ValueError("planner path escapes repository")
|
|
return path
|
|
|
|
def _write(self, path, value):
|
|
from wbv2_autopilot import read_json, write_json
|
|
if path.is_file() and read_json(path) == value:
|
|
return False
|
|
write_json(path, value)
|
|
return True
|
|
|
|
def refresh(self, context, jobs, verified_jobs):
|
|
from wbv2_autopilot import read_json, sha
|
|
verified_jobs = set(verified_jobs)
|
|
backlog = read_json(self.root / "tests/golden/WBV2/web-product-backlog.json", {})
|
|
backlog_status = {item.get("id"): item.get("status") for item in backlog.get("tasks", []) if isinstance(item, dict)}
|
|
assignments = []
|
|
created = []
|
|
for job in jobs:
|
|
identifier = job["id"]
|
|
destination = self.directory / "local-plans" / (identifier + ".json")
|
|
candidate, admission_findings = prepared_contract(self.root, context, job)
|
|
existing = read_json(destination) if destination.is_file() else None
|
|
# A pending backlog item is a business gap even when an old local
|
|
# plan or build receipt exists. Do not let compilation history
|
|
# promote it to runnable work.
|
|
if backlog_status.get(identifier) == "pending":
|
|
preparation = prepare_module(self.root, self.directory, job)
|
|
assignments.append({"job": identifier, "module": job.get("module"),
|
|
"state": "needs_implementation",
|
|
"reason": "backlog task remains pending",
|
|
"ownedPaths": job.get("ownedPaths", []),
|
|
"admissionFindings": admission_findings,
|
|
"preparation": preparation})
|
|
continue
|
|
if candidate and (existing is None or existing.get("generator") == "local-contract-admission-v1"):
|
|
if self._write(destination, candidate):
|
|
created.append(identifier)
|
|
verified_jobs.discard(identifier)
|
|
if identifier in verified_jobs:
|
|
state, reason = "verified", None
|
|
elif destination.is_file():
|
|
state, reason = "ready", None
|
|
elif identifier == "SERVER-OPERATOR-SCHEMA-PALETTE":
|
|
prerequisite = "SERVER-OPERATOR-HTTP-PROVIDER"
|
|
test_path = "web/tests/e2e/server-operator-schema-palette.spec.ts"
|
|
base = self.directory / "local-plans" / (prerequisite + ".json")
|
|
if prerequisite not in verified_jobs or not base.is_file():
|
|
state, reason = "waiting_dependency", prerequisite
|
|
elif not self._path(test_path).is_file():
|
|
state, reason = "needs_test_entrypoint", test_path
|
|
else:
|
|
contract = read_json(base)
|
|
if contract["task"] != context["task"] or contract["parentSha256"] != context["parentSha256"]:
|
|
raise ValueError("local recipe parent mismatch")
|
|
result = copy.deepcopy(contract["result"])
|
|
result["workflow"] = "F3 selects a native operator, loads RNA properties, executes Add Object, and preserves native Main history"
|
|
result["summary"] = "Local UI/schema contract prepared; completion requires independent Chromium and native Main evidence"
|
|
for check in result["checks"]:
|
|
if check["id"] == "chromium-main":
|
|
check["id"] = "chromium-schema-palette"
|
|
check["argv"].append("tests/e2e/server-operator-schema-palette.spec.ts")
|
|
check["timeoutSeconds"] = 180
|
|
check["cacheInputs"] = sorted(set(check.get("cacheInputs", result["dependencies"]) + [test_path]))
|
|
for case in result["cases"]:
|
|
if case["checkId"] == "chromium-main":
|
|
case["checkId"] = "chromium-schema-palette"
|
|
result["dependencies"] = sorted(set(result["dependencies"] + [test_path, "my-tools/wbv2_local_planner.py"]))
|
|
result["changedFiles"] = [test_path]
|
|
candidate = {"schemaVersion": 1, "task": context["task"], "job": identifier, "parentSha256": context["parentSha256"], "generator": "schema-palette-v1", "sourceContract": {"path": str(base.relative_to(self.root)), "sha256": sha(base)}, "acceptance": "pending local behavior verification", "result": result}
|
|
if self._write(destination, candidate):
|
|
created.append(identifier)
|
|
state, reason = "ready", None
|
|
else:
|
|
state, reason = "needs_local_recipe", "executable checks and source-bound behavior cases are not defined"
|
|
if state == "ready":
|
|
dependencies = job.get("dependsOn", [])
|
|
if not isinstance(dependencies, list) or not all(isinstance(value, str) for value in dependencies):
|
|
state, reason = "invalid_dependencies", "task dependencies must be a list of job IDs"
|
|
elif any(value not in verified_jobs for value in dependencies):
|
|
state, reason = "waiting_dependency", ", ".join(value for value in dependencies if value not in verified_jobs)
|
|
preparation = prepare_module(self.root, self.directory, job) if state == "needs_local_recipe" else None
|
|
assignments.append({"job": identifier, "module": job.get("module"), "state": state, "reason": reason, "ownedPaths": job.get("ownedPaths", []), "admissionFindings": admission_findings, "preparation": preparation})
|
|
if (candidate or existing or {}).get("result", {}).get("contractKind") == "compilation_only":
|
|
assignments[-1]["contractKind"] = "compilation_only"
|
|
assignments[-1]["verificationDeferred"] = True
|
|
report = {"schemaVersion": 1, "task": context["task"], "parentSha256": context["parentSha256"], "executionMode": "local_only", "assignments": assignments, "readyCount": sum(item["state"] == "ready" for item in assignments), "verifiedCount": sum(item["state"] == "verified" for item in assignments), "blockedCount": sum(item["state"] not in {"ready", "verified"} for item in assignments)}
|
|
report["compilationReadyCount"] = sum(item["state"] == "ready" and item.get("contractKind") == "compilation_only" for item in assignments)
|
|
changed = self._write(self.directory / "local-task-assignments.json", report)
|
|
return {"created": created, "changed": changed, "readyCount": report["readyCount"], "blockedCount": report["blockedCount"]}
|