131 lines
6.8 KiB
Python
131 lines
6.8 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
|
|
MAX_WINDOW_BYTES = 12000
|
|
|
|
|
|
def packet_paths(packet):
|
|
names = {row.get("path") for key in ("supportFiles", "editableExcerpts") for row in packet.get(key, [])}
|
|
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 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."}
|
|
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 row in packet.get("supportFiles", []):
|
|
if not isinstance(row.get("path"), str):
|
|
continue
|
|
result["supportPaths"].append(row["path"])
|
|
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 not path.is_relative_to(ROOT) or (not metadata and path not in packet_paths(packet)) or not path.is_file():
|
|
raise ValueError("Use an exact source/support/native-match file named by the current packet; 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
|
|
# four-window 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
|
|
if len(state["windows"]) >= MAX_WINDOWS:
|
|
raise ValueError("Four supplemental windows used; implement from retained excerpts or record the exact remaining dependency")
|
|
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": MAX_WINDOWS - 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())
|