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>
194 lines
9.4 KiB
Python
194 lines
9.4 KiB
Python
"""Prepare bounded source/test pointers for the next local business adapter."""
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
|
|
|
|
def candidate_test_paths(root, candidates):
|
|
"""Keep preparation watches bounded and inside the local test tree."""
|
|
if not isinstance(candidates, list):
|
|
return ()
|
|
root = Path(root).resolve()
|
|
accepted = set()
|
|
for candidate in candidates[:12]:
|
|
if not isinstance(candidate, str) or len(candidate) > 512 or "\x00" in candidate:
|
|
continue
|
|
relative = Path(candidate)
|
|
if relative.is_absolute() or ".." in relative.parts:
|
|
continue
|
|
if relative.parts[:3] not in (("web", "tests", "e2e"), ("web", "tests", "unit")):
|
|
continue
|
|
if len(relative.parts) < 4:
|
|
continue
|
|
try:
|
|
path = root
|
|
for component in relative.parts:
|
|
path /= component
|
|
if path.is_symlink():
|
|
break
|
|
else:
|
|
if not path.exists() or path.is_file():
|
|
accepted.add(relative.as_posix())
|
|
except (OSError, ValueError):
|
|
continue
|
|
return tuple(sorted(accepted))
|
|
|
|
|
|
def preparation_test_paths(root, job):
|
|
"""Read only the small existing discovery report during idle polling."""
|
|
import re
|
|
|
|
identifier = job.get("id")
|
|
if not isinstance(identifier, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", identifier):
|
|
return ()
|
|
root = Path(root).resolve()
|
|
relative = Path("tests/golden/WBV2/autopilot/preparation") / (identifier + ".json")
|
|
try:
|
|
path = root
|
|
for component in relative.parts:
|
|
path /= component
|
|
if path.is_symlink():
|
|
return ()
|
|
if path.stat().st_size > 131072:
|
|
return ()
|
|
document = json.loads(path.read_text())
|
|
if not isinstance(document, dict) or document.get("job") != identifier:
|
|
return ()
|
|
except (OSError, ValueError):
|
|
return ()
|
|
return candidate_test_paths(root, document.get("candidateTests"))
|
|
|
|
|
|
def _candidate_fingerprint(root, inputs, candidates):
|
|
from wbv2_autopilot import sha
|
|
|
|
values = {path: sha(Path(root) / path) for path in candidate_test_paths(root, candidates)}
|
|
payload = {**inputs, "version": 3, "candidateTests": values}
|
|
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
|
|
|
|
|
|
def _blocker_input_paths(root, candidates):
|
|
root = Path(root).resolve()
|
|
accepted = set()
|
|
for candidate in candidates[:64]:
|
|
if not isinstance(candidate, str) or len(candidate) > 512 or "\x00" in candidate:
|
|
continue
|
|
relative = Path(candidate)
|
|
if relative.is_absolute() or ".." in relative.parts or not relative.parts:
|
|
continue
|
|
if relative.parts[0] not in {"web", "blender-5.2.0", "my-tools", "tests"}:
|
|
continue
|
|
try:
|
|
path = root
|
|
for component in relative.parts:
|
|
path /= component
|
|
if path.is_symlink():
|
|
break
|
|
else:
|
|
if not path.exists() or path.is_file():
|
|
accepted.add(relative.as_posix())
|
|
except (OSError, ValueError):
|
|
continue
|
|
return sorted(accepted)
|
|
|
|
|
|
def _business_blocker(root, report, previous):
|
|
import re
|
|
from wbv2_autopilot import now
|
|
|
|
identifier = report["job"]
|
|
if not isinstance(identifier, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", identifier):
|
|
raise ValueError("invalid preparation job identifier")
|
|
commands = [value for value in report["commands"][:32]
|
|
if isinstance(value, str) and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,127}", value)]
|
|
units = [value for value in report["sourceUnits"][:16] if isinstance(value, dict)
|
|
and isinstance(value.get("id"), str)
|
|
and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9:_.-]{0,159}", value["id"])]
|
|
tests = list(candidate_test_paths(root, report["candidateTests"]))
|
|
if not units or report["missingSourceBindings"]:
|
|
category = "source_binding_missing"
|
|
elif not tests:
|
|
category = "behavioral_test_missing"
|
|
else:
|
|
category = "workflow_contract_missing"
|
|
search = None
|
|
if units and commands:
|
|
pattern = units[0]["id"].split(":")[-1]
|
|
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,127}", pattern):
|
|
pattern = commands[0]
|
|
search = {"argv": ["node", "tools/web/search-wbv2-batch-source.mjs", "--unit", units[0]["id"],
|
|
"--pattern", pattern, "--max-hits", "8"], "cwd": "."}
|
|
contract = "my-tools/local-workflows/" + identifier + ".json"
|
|
proposed_test = "web/tests/e2e/" + identifier.lower() + "-ui-production.spec.ts"
|
|
locations = [location.get("path") for unit in units if isinstance(unit.get("locations"), list)
|
|
for location in unit["locations"][:4] if isinstance(location, dict)]
|
|
owned = _blocker_input_paths(root, list(report["ownedInputs"]))
|
|
retry_on = _blocker_input_paths(root, [contract, "tests/golden/WBV2/dynamic-inventory.json",
|
|
*tests, *owned[:16], *locations[:24], proposed_test])
|
|
actions = []
|
|
if search:
|
|
actions.append({"kind": "inspect_source", "sourceUnit": units[0]["id"], **search})
|
|
if category == "source_binding_missing":
|
|
actions.append({"kind": "resolve_source_binding", "commands": commands,
|
|
"inventoryPath": "tests/golden/WBV2/dynamic-inventory.json"})
|
|
actions.append({"kind": "implement_business_adapter", "commands": commands, "ownedPaths": owned})
|
|
actions.append({"kind": "complete_behavioral_test", "paths": tests or [proposed_test],
|
|
"commands": commands, "requiredCases": ["positive", "negative", "undo_redo",
|
|
"save_reopen", "determinism"]})
|
|
actions.append({"kind": "write_workflow_contract", "path": contract, "candidateTests": tests})
|
|
timestamp = now()
|
|
existing = previous.get("businessBlocker", {}) if previous.get("job") == identifier else {}
|
|
first_seen = existing.get("firstSeenAt", timestamp) if isinstance(existing, dict) else timestamp
|
|
report["state"] = "needs_implementation"
|
|
report["nextActions"] = actions
|
|
report["businessBlocker"] = {"state": "needs_implementation", "category": category,
|
|
"firstSeenAt": first_seen, "inputChangedAt": timestamp,
|
|
"inputFingerprint": report["fingerprint"], "sourceSearch": search,
|
|
"candidateTests": tests, "contractPath": contract, "retryOn": retry_on}
|
|
|
|
|
|
def prepare_module(root, directory, job):
|
|
from wbv2_autopilot import contained, read_json, sha, write_json
|
|
|
|
root, directory = Path(root), Path(directory)
|
|
import re
|
|
if not isinstance(job.get("id"), str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", job["id"]):
|
|
raise ValueError("invalid preparation job identifier")
|
|
commands = [value for value in job.get("commands", []) if isinstance(value, str)][:32]
|
|
inputs = {path: sha(contained(root, path)) for path in job.get("ownedPaths", [])}
|
|
test_directories = [root / "web/tests/e2e", root / "web/tests/unit"]
|
|
test_listing = {str(path.relative_to(root)): path.stat().st_mtime_ns if path.is_dir() else None for path in test_directories}
|
|
destination = directory / "preparation" / (job["id"] + ".json")
|
|
previous = read_json(destination, {})
|
|
fingerprint_inputs = {"commands": commands, "inputs": inputs, "inventory": sha(root / "tests/golden/WBV2/dynamic-inventory.json"), "testDirectories": test_listing, "version": 1}
|
|
fingerprint = _candidate_fingerprint(root, fingerprint_inputs, previous.get("candidateTests", []))
|
|
if previous.get("fingerprint") == fingerprint:
|
|
return str(destination.relative_to(root))
|
|
inventory = read_json(root / "tests/golden/WBV2/dynamic-inventory.json", {})
|
|
wanted = {"web-command:" + command for command in commands}
|
|
units = [{"id": unit["id"], "locations": unit.get("locations", [])} for unit in inventory.get("units", []) if unit["id"] in wanted][:16]
|
|
tests = []
|
|
directories = [str(path.relative_to(root)) for path in test_directories if path.is_dir()]
|
|
if commands and directories:
|
|
arguments = ["rg", "--files-with-matches", "--fixed-strings", "--glob", "*.{ts,mjs}"]
|
|
for command in commands:
|
|
arguments.extend(["--regexp", command])
|
|
result = subprocess.run([*arguments, "--", *directories], cwd=root, capture_output=True, text=True, timeout=10)
|
|
if result.returncode not in {0, 1}:
|
|
raise RuntimeError("bounded local test discovery failed")
|
|
tests = sorted(set(result.stdout.splitlines()))[:12]
|
|
fingerprint = _candidate_fingerprint(root, fingerprint_inputs, tests)
|
|
report = {
|
|
"schemaVersion": 1, "job": job["id"], "module": job.get("module"),
|
|
"state": "needs_implementation", "fingerprint": fingerprint,
|
|
"workflow": job.get("title"), "commands": commands, "ownedInputs": inputs,
|
|
"sourceUnits": units, "candidateTests": tests,
|
|
"missingSourceBindings": [command for command in commands if "web-command:" + command not in {unit["id"] for unit in units}],
|
|
"nextActions": [],
|
|
"acceptance": "Source/test pointers only; candidates have not been executed or accepted by preparation",
|
|
}
|
|
_business_blocker(root, report, previous)
|
|
write_json(destination, report)
|
|
return str(destination.relative_to(root))
|