Files
FreeCADWasmDev/tools/read_implementation_source.py

194 lines
10 KiB
Python

#!/usr/bin/env python3
"""Read a bounded missing patch anchor from the current implementation packet."""
import argparse
import fcntl
import hashlib
import itertools
import json
import os
import shlex
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[1]
PACKET = ROOT / "docs/current/module-implementation-packet.json"
MAX_WINDOWS = 4
CONTINUOUS_MAX_WINDOWS = 16
MAX_TOTAL_WINDOW_BYTES = 48000
MAX_WINDOW_BYTES = 12000
def window_limit():
try:
policy = json.loads((ROOT / "docs/current/execution-policy.json").read_text())
except (OSError, ValueError):
return MAX_WINDOWS
return CONTINUOUS_MAX_WINDOWS if policy.get("continuousDevelopment") is True else MAX_WINDOWS
def source_limits(packet):
limit = window_limit()
count = len({row.get("apiId") for row in packet.get("records", [])
if isinstance(row, dict) and isinstance(row.get("apiId"), str) and row["apiId"]})
scale = min(3, max(1, count)) if limit == CONTINUOUS_MAX_WINDOWS else 1
return limit * scale, MAX_TOTAL_WINDOW_BYTES * scale
def packet_paths(packet):
names = {row.get("path") for key in ("supportFiles", "editableExcerpts") for row in packet.get(key, [])}
contract_name = packet.get("acceptanceContract")
if isinstance(contract_name, str):
contract_path = (ROOT / contract_name).resolve()
if contract_path.is_relative_to(ROOT) and contract_path.is_relative_to((ROOT / "docs/business-acceptance").resolve()) and contract_path.is_file():
try:
contract = json.loads(contract_path.read_text(encoding="utf-8"))
if isinstance(contract, dict):
names.add(contract_name)
command = contract.get("command", [])
if isinstance(command, list) and len(command) > 1 and isinstance(command[1], str):
names.add(command[1])
for key in ("dependencies", "caseDependencies"):
paths = contract.get(key, [])
if isinstance(paths, list):
names.update(name for name in paths if isinstance(name, str))
except (OSError, ValueError):
pass
for row in packet.get("records", []):
source = row.get("source")
if source:
names.add(source if (ROOT / source).is_file() else str(Path("freecad-source") / source))
names.update(match.get("path") for match in row.get("nativeMatches", []))
for gap in row.get("implementationGaps", []):
names.update(match.get("path") for match in gap.get("sourceCandidates", []))
return {(ROOT / name).resolve() for name in names if isinstance(name, str) and name}
def module_source_path(path, packet):
"""Permit named in-module implementation anchors before case declaration."""
module = packet.get("module")
if window_limit() != CONTINUOUS_MAX_WINDOWS or not isinstance(module, str) or not module.isalnum():
return False
if not path.is_relative_to(ROOT):
return False
relative = path.relative_to(ROOT)
if relative.is_relative_to(Path("freecad-source/src/Mod") / module):
return path.suffix.lower() in {".cpp", ".cxx", ".h", ".hpp", ".py", ".ui", ".xml", ".txt"}
slug = module.lower()
if relative.parent == Path("tools"):
return path.name.startswith(slug + "-") and path.name.endswith("-business-cases.mjs")
return (relative.parent == Path("web/src") and path.name.startswith(slug + "-")
and path.suffix in {".ts", ".tsx", ".json"})
def packet_summary(packet):
# Task metadata is structured JSON, not a source file to slice by lines.
result = {"batchId": packet.get("batchId"), "records": [], "supportPaths": [],
"instruction": "This is task metadata, not source lines. Use sourceReadCommand below; changing --start on the packet does not read C++ or Python."}
retained = packet.get("retainedBusinessResults", [])
result["retainedBusinessResults"] = []
for row in retained:
result["retainedBusinessResults"].append(row)
if len(json.dumps(result).encode("utf-8")) > MAX_WINDOW_BYTES // 2:
result["retainedBusinessResults"].pop()
break
for row in packet.get("records", []):
entry = {key: row[key] for key in ("apiId", "name", "source", "excerptLine") if key in row}
if isinstance(row.get("source"), str) and row.get("excerptLine"):
source = row["source"]
if not (ROOT / source).is_file(): source = str(Path("freecad-source") / source)
entry["sourceReadCommand"] = shlex.join([
"python3", "tools/read_implementation_source.py", source,
"--start", str(max(1, int(row["excerptLine"]))), "--lines", "60",
"--reason", "Read selected command native source anchor",
])
entry["gaps"] = [{key: gap[key] for key in ("stage", "classification", "reason") if key in gap}
for gap in row.get("implementationGaps", [])]
result["records"].append(entry)
if len(json.dumps(result).encode("utf-8")) > MAX_WINDOW_BYTES - 1000:
result["records"].pop()
break
result["omittedRecords"] = len(packet.get("records", [])) - len(result["records"])
for path in sorted(packet_paths(packet)):
if not path.is_relative_to(ROOT) or not path.is_file():
continue
result["supportPaths"].append(str(path.relative_to(ROOT)))
if len(json.dumps(result).encode("utf-8")) > MAX_WINDOW_BYTES - 200:
result["supportPaths"].pop()
break
return json.dumps(result)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("path")
parser.add_argument("--start", required=True, type=int)
parser.add_argument("--lines", type=int, default=60)
parser.add_argument("--reason", required=True)
args = parser.parse_args()
try:
marker_value = os.environ.get("FREECAD_SOURCE_BUDGET_MARKER")
if not marker_value:
raise ValueError("This reader requires a scheduler-owned turn budget")
if not 1 <= args.lines <= 100 or args.start < 1:
raise ValueError("Use a positive --start and 1..100 --lines")
if not args.reason.strip() or len(args.reason) > 240:
raise ValueError("Explain the missing anchor in --reason (1..240 characters)")
packet = json.loads(PACKET.read_text(encoding="utf-8"))
path = (ROOT / args.path).resolve()
metadata = path == PACKET.resolve()
if path.is_relative_to(ROOT) and not path.is_file():
raise ValueError("Requested source file does not exist or is not a file; locate the exact filename with a scoped file search")
if not path.is_relative_to(ROOT) or (not metadata and path not in packet_paths(packet)
and not module_source_path(path, packet)) or not path.is_file():
raise ValueError("Use an exact packet dependency or current-module source/case path; no directory scans")
if path.stat().st_size > 16 * 1024 * 1024:
raise ValueError("This file exceeds the bounded source reader size limit")
if metadata:
excerpt = packet_summary(packet)
args.start = 1
else:
with path.open(encoding="utf-8") as source:
excerpt = "".join(itertools.islice(source, args.start - 1, args.start - 1 + args.lines))
if not excerpt:
raise ValueError("The requested anchor is beyond EOF; use the packet's named line")
size = len(excerpt.encode("utf-8"))
if size > MAX_WINDOW_BYTES:
raise ValueError("Excerpt exceeds 12000 bytes; reduce --lines, do not dump a minified file")
relative = str(path.relative_to(ROOT))
fingerprint = hashlib.sha256(f"{relative}:{args.start}:{excerpt}".encode("utf-8")).hexdigest()
# Per-turn file locking prevents parallel requests from exceeding the
# bounded allowance. Repeated unchanged windows add no context.
budget_path = Path(marker_value + ".windows.json")
with budget_path.open("a+", encoding="utf-8") as budget:
fcntl.flock(budget, fcntl.LOCK_EX)
budget.seek(0)
data = budget.read()
state = json.loads(data) if data else {"batchId": packet["batchId"], "windows": [], "bytes": 0}
if state["batchId"] != packet["batchId"]:
raise ValueError("Batch changed during this turn; retain the current implementation checkpoint")
if any(row["fingerprint"] == fingerprint for row in state["windows"]):
print(json.dumps({"status": "already_in_context", "path": relative, "start": args.start,
"instruction": "Reuse the unchanged excerpt already returned in this turn"}))
return 0
limit, byte_limit = source_limits(packet)
if len(state["windows"]) >= limit or state["bytes"] + size > byte_limit:
raise ValueError(f"Supplemental source budget reached ({limit} windows / {byte_limit} bytes); reuse retained excerpts and continue the identified repair")
state["windows"].append({"path": relative, "start": args.start, "lines": args.lines,
"fingerprint": fingerprint, "bytes": size, "reason": args.reason})
state["bytes"] += size
budget.seek(0)
budget.truncate()
budget.write(json.dumps(state, separators=(",", ":")))
budget.flush()
print(json.dumps({"status": "packet_summary" if metadata else "source_window", "batchId": packet["batchId"], "path": relative,
"start": args.start, "bytes": size, "remainingWindows": limit - len(state["windows"]),
"excerpt": excerpt}, ensure_ascii=False))
return 0
except (OSError, ValueError, KeyError) as error:
print(f"IMPLEMENTATION_SOURCE_WINDOW_REJECTED: {error}", file=sys.stderr)
return 64
if __name__ == "__main__":
raise SystemExit(main())