Continue FreeCAD Web UI parity repairs
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
CodeQL Advanced (c-cpp) / Analyze (c-cpp) (push) Has been cancelled
Push Crowdin Translations / update-crowdin (push) Has been cancelled
Fetch Crowdin Translations / update-crowdin (push) Has been cancelled
Build Release / upload_src (push) Has been cancelled
Build Release / build (10.13, macos-15-intel, osx-64) (push) Has been cancelled
Build Release / build (11.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (15.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (ubuntu-22.04, linux-64) (push) Has been cancelled
Build Release / build (ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Build Release / build (windows-latest, win-64) (push) Has been cancelled
Monthly issue metrics / issue metrics (push) Has been cancelled
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
CodeQL Advanced (c-cpp) / Analyze (c-cpp) (push) Has been cancelled
Push Crowdin Translations / update-crowdin (push) Has been cancelled
Fetch Crowdin Translations / update-crowdin (push) Has been cancelled
Build Release / upload_src (push) Has been cancelled
Build Release / build (10.13, macos-15-intel, osx-64) (push) Has been cancelled
Build Release / build (11.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (15.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (ubuntu-22.04, linux-64) (push) Has been cancelled
Build Release / build (ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Build Release / build (windows-latest, win-64) (push) Has been cancelled
Monthly issue metrics / issue metrics (push) Has been cancelled
This commit is contained in:
235
tools/read_implementation_source.py
Normal file
235
tools/read_implementation_source.py
Normal file
@@ -0,0 +1,235 @@
|
||||
#!/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 re
|
||||
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, [])}
|
||||
gap = packet.get("confirmedGap") or {}
|
||||
names.add(gap.get("source"))
|
||||
names.update(name for name in gap.get("sourceAnchors", []) if isinstance(name, str))
|
||||
for member in packet.get("confirmedGaps", []):
|
||||
names.add(member.get("source"))
|
||||
names.update(name for name in member.get("sourceAnchors", []) if isinstance(name, str))
|
||||
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 packet.get("confirmedGaps") and relative.parent == Path("freecad-source/src/WebAssembly/FreeCADCoreProbe"):
|
||||
# Functional packages legitimately cross module boundaries through
|
||||
# existing document owners. Keep exact-file/window budgets, but don't
|
||||
# turn missing packet metadata into a user-authorization blocker.
|
||||
return path.suffix.lower() in {".cpp", ".h", ".hpp", ".py"} or path.name == "CMakeLists.txt"
|
||||
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 frontend_dependency_path(path, packet):
|
||||
"""Allow one exact relative import from a confirmed package's named sources.
|
||||
|
||||
Workbench UI packages cross module boundaries through document facades and
|
||||
shared task components. Resolve only direct imports; this is not a recursive
|
||||
module crawl and it does not change the existing excerpt/turn budgets.
|
||||
"""
|
||||
if window_limit() != CONTINUOUS_MAX_WINDOWS or not (packet.get("confirmedGaps") or packet.get("confirmedGap")):
|
||||
return False
|
||||
frontend = (ROOT / "web/src").resolve()
|
||||
if not path.is_relative_to(frontend) or path.suffix not in {".ts", ".tsx", ".json"}:
|
||||
return False
|
||||
for anchor in sorted(packet_paths(packet)):
|
||||
if (not anchor.is_relative_to(frontend) or anchor.suffix not in {".ts", ".tsx"}
|
||||
or not anchor.is_file() or anchor.stat().st_size > 512 * 1024):
|
||||
continue
|
||||
source = anchor.read_text(encoding="utf-8")
|
||||
for specifier in re.findall(r'''(?:\bfrom\s*|\bimport\s*(?:\(\s*)?)["'](\.[^"'\n]+)["']''', source):
|
||||
dependency = (anchor.parent / specifier).resolve()
|
||||
candidates = {dependency}
|
||||
if dependency.suffix == ".js":
|
||||
candidates.update(dependency.with_suffix(suffix) for suffix in (".ts", ".tsx"))
|
||||
elif not dependency.suffix:
|
||||
candidates.update(Path(str(dependency) + suffix) for suffix in (".ts", ".tsx", ".json"))
|
||||
if path in candidates:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
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)
|
||||
and not frontend_dependency_path(path, packet)) or not path.is_file():
|
||||
raise ValueError("Use an exact packet dependency, its direct frontend import, 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())
|
||||
Reference in New Issue
Block a user