Files
FreeCADWasmDev/tools/read_implementation_source.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

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())