"""Bounded, structured context and source identity for unattended turns.""" import hashlib import json from pathlib import Path import time MARKER = "[CONTEXT_COMPACTED by unattended scheduler: whole fields/excerpts only; full packet remains on disk.]" SOURCE_WINDOW_POLICY = ( "When source budget blocks a necessary missing patch anchor, use " "python3 tools/read_implementation_source.py --start --lines 60 --reason ''. " "This is allowed after edits for changed anchors: at most four supplemental windows per turn, " "100 lines/12000 bytes each, current packet files only; unchanged duplicate windows reuse context. " "Reading module-implementation-packet.json through this tool returns a bounded JSON metadata summary, not source lines. " "Do not bypass limits with awk/cat or report a budget block as absent business source. " "Keep the command/time budgets; no tests or validation are authorized by this reader." ) REPAIR_LAYER_POLICY = ( "Repair the reported failing layer, not all missing IDs as native registration. " "native: inspect real behavior before registering; transport/worker: trace the actual envelope, facade, " "worker, loaded WASM and dispatcher together, without adding native markers; artifact/build: build the " "changed real target; UI: connect the mounted host. A fabricated no-op, fixture or GUI-only fallback is not implementation. " "If desktop source itself has no operation, preserve and report that exact source behavior and activation prerequisites. " "Registration uses tools/generate_feature_capability_registry.py and actual JSON catalogs; editing shadow lists does not route a command. " "Endpoint, payload schema and loaded runtime must match. Never synthesize an unconditional route to clear a gap." ) def implementation_blockers(packet, readiness=None): rows = (readiness or {}).get("blockingGaps") or [ {"id": row.get("apiId"), "gaps": row.get("implementationGaps", [])} for row in packet.get("records", [])] return [{"id": row.get("id"), "gaps": [ {key: gap[key] for key in ("stage", "classification", "reason", "repair") if key in gap} for gap in row.get("gaps", [])]} for row in rows if row.get("gaps")] def tracked_edit_paths(root, packet): paths = source_snapshot(root, packet) return [Path(root) / name for name in paths if Path(name).suffix in {".cpp", ".h", ".hpp", ".ts", ".tsx", ".py", ".mjs", ".js"} or Path(name).name == "CMakeLists.txt" or name.startswith("docs/business-acceptance/") and name.endswith(".json") or name.startswith("web/src/") and name.endswith(".json")] class SourceChangeTracker: """Detect script-written edits after a tool exits; ignore timestamp-only changes.""" def __init__(self, paths): self.stamps = {} self.hashes = {} self.seconds = 0.0 for path in dict.fromkeys(Path(value) for value in paths): self.stamps[path] = None self.hashes[path] = None self.poll() def poll(self): started = time.monotonic() changed = [] for path in self.stamps: try: info = path.stat() stamp = (info.st_size, info.st_mtime_ns, info.st_ctime_ns, info.st_ino) except OSError: stamp = None if stamp == self.stamps[path]: continue try: with path.open("rb") as stream: digest = hashlib.file_digest(stream, "sha256").hexdigest() except OSError: digest = None if digest != self.hashes[path]: changed.append(str(path)) self.hashes[path], self.stamps[path] = digest, stamp self.seconds += time.monotonic() - started return changed def encoded_size(value): return len(value.encode("utf-8")) def compact_packet(batch, packet, pipeline, limit, packet_path): from implementation_first import user_validation_deferred implementation_only = user_validation_deferred() header = ( MARKER + "\nUse the supplied local desktop FreeCAD sources and existing Web work. " "Implement only this batch, never replace native behavior with fixed results or markers. " "Respect repository instructions. Read omitted material only at named file/line anchors; " "do not rescan the repository or historical logs. " + ("User instruction: connect all functionality first. Do not write or run tests, business acceptance, " "browser validation, benchmarks or historical reconciliation. Users validate during use. " "Outer pipeline owns incremental build/sync/route receipts only; " if implementation_only else "Outer pipeline owns build/sync/acceptance; ") + "finish edits and return. Receipts are not business acceptance.\n" ) payload = { "batchId": batch.get("batchId"), "selected": batch.get("selected"), "businessWorkflow": batch.get("businessWorkflow", packet.get("businessWorkflow")), "recordIds": [row[0] for row in batch.get("records", []) if row], "packetPath": str(packet_path), "sourceWindowPolicy": SOURCE_WINDOW_POLICY, "nextAction": "Fix the current first error or implement missing selected business behavior; persist remaining boundaries.", "omittedFields": [], "editableExcerpts": [], "sourceRecords": [], } def render(): return header + json.dumps(payload, ensure_ascii=False, separators=(",", ":")) # Reserve space for omission metadata before considering optional context. optional_limit = max(0, limit - 1800) def include(key, value): payload[key] = value if encoded_size(render()) > optional_limit: del payload[key] payload["omittedFields"].append(key) if implementation_only: payload["repairLayerPolicy"] = REPAIR_LAYER_POLICY readiness = pipeline.get("staticReadiness") if pipeline.get("batchId") == batch.get("batchId") else None include("implementationBlockers", implementation_blockers(packet, readiness)) try: progress = json.loads((Path(__file__).resolve().parents[1] / "docs/current/scheduler-progress.json").read_text(encoding="utf-8")) except (OSError, ValueError): progress = {} history = progress.get("history", []) selected_ids = {row[0] for row in batch.get("records", []) if row} last_cycles = {progress.get("records", {}).get(record_id, {}).get("lastCycle") for record_id in selected_ids} previous = [row for row in history if (row.get("batchId") == batch.get("batchId") or row.get("cycle") in last_cycles) and row.get("outcome") in {"no_progress", "source_edits_pending_outcome"}] if previous: include("ineffectiveAttempts", [{"cycle": row.get("cycle"), "seconds": row.get("seconds"), "fileChanges": row.get("fileChanges"), "reason": str(row.get("reason", ""))[:320]} for row in previous[-3:]]) for key in (("repositoryContext", "build", "integrationRepair", "moduleRecovery") if implementation_only else ("repositoryContext", "acceptanceSetup", "build", "acceptanceContract", "integrationRepair", "moduleRecovery")): if packet.get(key) is not None: include(key, packet[key]) if pipeline.get("batchId") == batch.get("batchId"): if implementation_only and isinstance(pipeline.get("staticReadiness"), dict): readiness = pipeline["staticReadiness"] include("registrationBlock", { "missingIds": readiness.get("missingIds", []), "rejectedMarkers": readiness.get("rejectedMarkers", []), "action": REPAIR_LAYER_POLICY, }) failed = next((row for row in pipeline.get("steps", []) if row.get("exitCode") not in (None, 0)), None) if failed: include("firstError", {"stage": failed.get("name"), "exitCode": failed.get("exitCode"), "diagnostic": str(failed.get("stderr") or failed.get("stdout") or "")[:1500]}) include("sourceAnchors", [{"id": row.get("apiId"), "path": row.get("source"), "line": row.get("excerptLine")} for row in packet.get("records", [])]) include("retainedSourceDecisions", [{"id": row.get("apiId"), **row["semanticDecision"]} for row in packet.get("records", []) if row.get("semanticDecision")]) include("supportFingerprints", [{"path": row.get("path"), "sha256": row.get("sha256")} for row in packet.get("supportFiles", [])]) for key, source in (("editableExcerpts", "editableExcerpts"), ("sourceRecords", "records")): omitted = 0 for row in packet.get(source, []): payload[key].append(row) if encoded_size(render()) > optional_limit: payload[key].pop() omitted += 1 payload["omitted" + key[0].upper() + key[1:]] = omitted if encoded_size(render()) > limit: # Pathological batch metadata is referenced as a whole, never sliced JSON. payload = {"batchId": batch.get("batchId"), "packetPath": str(packet_path), "recordCount": len(batch.get("records", [])), "nextAction": "Read batchId, records and acceptanceSetup from the packet with a JSON parser, then only named anchors."} if encoded_size(render()) > limit: raise ValueError("context identity exceeds prompt budget") return render() def source_snapshot(root, packet): root = Path(root).resolve() paths = {row.get("path") for row in packet.get("supportFiles", [])} paths.update(row.get("path") for row in packet.get("editableExcerpts", [])) for row in packet.get("records", []): name = row.get("source") if isinstance(name, str) and name: paths.add(name if (root / name).is_file() else str(Path("freecad-source") / name)) result = {} for name in sorted(value for value in paths if isinstance(value, str) and value): path = (root / name).resolve() if not path.is_relative_to(root): continue try: with path.open("rb") as stream: digest = hashlib.file_digest(stream, "sha256").hexdigest() except OSError: digest = None result[str(path.relative_to(root))] = digest return result def write_checkpoint(path, payload): path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_suffix(".tmp") temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") temporary.replace(path)