Files
linuxcncAllWasm/scripts/context_packet.py

316 lines
12 KiB
Python

#!/usr/bin/env python3
"""Generate bounded, auditable task context packets.
The selector applies byte and count limits before writing a packet. It never
creates an over-budget packet for a later validator to reject.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
PROFILES = {
"doc": {"tokens": 2048, "bytes": 8192, "evidence_bytes": 2048, "files": 8},
"feature": {"tokens": 4096, "bytes": 16384, "evidence_bytes": 6144, "files": 16},
"contract": {"tokens": 6144, "bytes": 24576, "evidence_bytes": 10240, "files": 24},
"milestone": {"tokens": 8192, "bytes": 32768, "evidence_bytes": 14336, "files": 32},
}
SEGMENTS = ("header", "baseline", "scope", "contracts", "evidence", "risks", "next", "forbidden")
# Compact codes keep the audit trail inside the packet budget. The human
# explanation is stable and can be rendered by the reader without repeating
# large strings in every packet.
EXCLUSION_REASONS = {
"E1": "duplicate_path",
"E2": "evidence_item_bytes_exceed_budget",
"E3": "evidence_file_count_exceed_budget",
"E4": "evidence_bytes_exceed_budget",
"E5": "context_remaining_bytes_insufficient",
}
class PacketError(ValueError):
"""Raised when a packet cannot be generated without violating a budget."""
@dataclass(frozen=True)
class Candidate:
path: str
size: int
sha256: str | None
kind: str
priority: int
required: bool
def _json_bytes(value: Any) -> bytes:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _candidate(raw: dict[str, Any], base_dir: Path) -> Candidate:
path_value = str(raw.get("path", ""))
if not path_value:
raise PacketError("candidate is missing path")
path = Path(path_value)
if "size" in raw:
size = int(raw["size"])
else:
file_path = path if path.is_absolute() else base_dir / path
try:
size = file_path.stat().st_size
except OSError as exc:
raise PacketError(f"cannot stat evidence {path_value}: {exc}") from exc
if size < 0:
raise PacketError(f"evidence size must be non-negative: {path_value}")
sha256 = raw.get("sha256")
if sha256 is None and raw.get("hash_file", False):
file_path = path if path.is_absolute() else base_dir / path
sha256 = _sha256_file(file_path)
return Candidate(
path=path_value,
size=size,
sha256=str(sha256) if sha256 is not None else None,
kind=str(raw.get("kind", "artifact")),
priority=int(raw.get("priority", 0)),
required=bool(raw.get("required", False)),
)
def _evidence_item(candidate: Candidate) -> dict[str, Any]:
item: dict[str, Any] = {"path": candidate.path, "bytes": candidate.size, "kind": candidate.kind}
if candidate.sha256:
item["sha256"] = candidate.sha256
return item
def _packet_bytes(packet: dict[str, Any]) -> int:
return len(_json_bytes(packet))
def _context_bytes(packet: dict[str, Any], evidence_used: int) -> int:
"""Return the bytes consumed by the envelope and referenced evidence.
Evidence is referenced by path in the JSON envelope, but the referenced
payload is read into the task context by the consumer. Counting both
here prevents a packet that is small on disk from expanding past the
context limit when it is loaded.
"""
return _packet_bytes(packet) + evidence_used
def _finalize_packet_size(packet: dict[str, Any]) -> int:
"""Store the self-consistent serialized size in the budget metadata."""
packet["budget"]["packet_bytes"] = 0
for _ in range(4):
packet_bytes = _packet_bytes(packet)
if packet["budget"]["packet_bytes"] == packet_bytes:
return packet_bytes
packet["budget"]["packet_bytes"] = packet_bytes
raise PacketError("could not stabilize serialized packet size")
def _base_packet(input_data: dict[str, Any], context_id: str, packet_id: str) -> dict[str, Any]:
packet: dict[str, Any] = {
"context_id": context_id,
"packet_id": packet_id,
"task": str(input_data.get("task", "")),
"header": dict(input_data.get("header", {})),
"baseline": dict(input_data.get("baseline", {})),
"scope": dict(input_data.get("scope", {})),
"contracts": list(input_data.get("contracts", [])),
"evidence": {"selected": [], "excluded": []},
"risks": list(input_data.get("risks", [])),
"next": list(input_data.get("next", []))[:3],
"forbidden": list(input_data.get("forbidden", [])),
}
return packet
def _budget_metadata(limits: dict[str, int], selected: list[dict[str, Any]], excluded: list[dict[str, Any]], evidence_used: int) -> dict[str, Any]:
return {
"profile": limits["profile"],
"token_limit": limits["tokens"],
"byte_limit": limits["bytes"],
"evidence_byte_limit": limits["evidence_bytes"],
"evidence_file_limit": limits["files"],
"selected_evidence_bytes": evidence_used,
"selected_evidence_files": len(selected),
"excluded_evidence_files": len(excluded),
# Reserve the complete decimal width of the configured limit while
# selecting. This keeps the selection check conservative.
"packet_bytes": limits["bytes"],
}
def generate_packet(
input_data: dict[str, Any],
*,
context_id: str,
packet_id: str,
profile: str = "feature",
total_bytes: int | None = None,
evidence_bytes: int | None = None,
max_files: int | None = None,
base_dir: Path = Path("."),
) -> dict[str, Any]:
"""Select evidence under all budgets and return a packet within its limit."""
if profile not in PROFILES:
raise PacketError(f"unknown profile: {profile}")
limits = dict(PROFILES[profile])
limits["profile"] = profile
if total_bytes is not None:
limits["bytes"] = int(total_bytes)
if evidence_bytes is not None:
limits["evidence_bytes"] = int(evidence_bytes)
if max_files is not None:
limits["files"] = int(max_files)
if any(limits[key] < 0 for key in ("tokens", "bytes", "evidence_bytes", "files")):
raise PacketError("budgets must be non-negative")
raw_candidates = input_data.get("candidates", [])
if not isinstance(raw_candidates, list):
raise PacketError("candidates must be a list")
candidates = []
for raw in raw_candidates:
if not isinstance(raw, dict):
raise PacketError("each candidate must be an object")
candidates.append(_candidate(raw, base_dir))
candidates.sort(key=lambda item: (-int(item.required), -item.priority, item.path))
packet = _base_packet(input_data, context_id, packet_id)
skeleton_bytes = _packet_bytes(packet)
if skeleton_bytes > limits["bytes"]:
raise PacketError(
f"context skeleton exceeds byte budget ({skeleton_bytes}>{limits['bytes']}); "
"reduce task metadata or use a larger profile"
)
selected: list[dict[str, Any]] = []
excluded: list[dict[str, Any]] = []
evidence_used = 0
seen: set[str] = set()
for candidate in candidates:
if candidate.path in seen:
excluded.append({"path": candidate.path, "reason_code": "E1"})
continue
seen.add(candidate.path)
item = _evidence_item(candidate)
reason: str | None = None
if candidate.size > limits["evidence_bytes"]:
reason = "E2"
elif len(selected) >= limits["files"]:
reason = "E3"
elif evidence_used + candidate.size > limits["evidence_bytes"]:
reason = "E4"
else:
trial = dict(packet)
trial_selected = selected + [item]
trial["evidence"] = {"selected": trial_selected, "excluded": excluded}
trial["budget"] = _budget_metadata(
limits, trial_selected, excluded, evidence_used + candidate.size
)
# This check is intentionally performed before selection is committed.
trial_context_bytes = _context_bytes(trial, evidence_used + candidate.size)
if trial_context_bytes > limits["bytes"]:
reason = "E5"
if reason:
excluded.append({"path": candidate.path, "reason_code": reason})
if candidate.required:
raise PacketError(f"required evidence cannot fit ({reason}): {candidate.path}")
continue
selected.append(item)
evidence_used += candidate.size
# The exclusion audit trail is part of the packet, so it can consume the
# remaining context space. Remove optional selections until the complete
# packet (selected evidence, exclusions and budget metadata) fits.
while True:
packet["evidence"] = {"selected": selected, "excluded": excluded}
packet["budget"] = _budget_metadata(limits, selected, excluded, evidence_used)
packet_bytes = _finalize_packet_size(packet)
context_bytes = packet_bytes + evidence_used
if packet_bytes <= limits["bytes"] and context_bytes <= limits["bytes"]:
break
removable_index = next(
(index for index in range(len(selected) - 1, -1, -1)
if not any(item["path"] == selected[index]["path"] and candidate.required
for candidate in candidates)),
None,
)
if removable_index is None:
raise PacketError("context skeleton and required evidence exceed byte budget")
removed = selected.pop(removable_index)
evidence_used -= int(removed["bytes"])
excluded.append({
"path": removed["path"],
"reason_code": "E5",
})
return packet
def _load(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise PacketError("input must be a JSON object")
return value
def main(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", required=True, type=Path, help="candidate/context JSON")
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--context-id", required=True)
parser.add_argument("--packet-id", required=True)
parser.add_argument("--profile", choices=sorted(PROFILES), default="feature")
parser.add_argument("--base-dir", type=Path, default=Path("."))
parser.add_argument("--total-bytes", type=int)
parser.add_argument("--evidence-bytes", type=int)
parser.add_argument("--max-files", type=int)
args = parser.parse_args(list(argv) if argv is not None else None)
try:
packet = generate_packet(
_load(args.input),
context_id=args.context_id,
packet_id=args.packet_id,
profile=args.profile,
total_bytes=args.total_bytes,
evidence_bytes=args.evidence_bytes,
max_files=args.max_files,
base_dir=args.base_dir,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
payload = _json_bytes(packet)
# Do not expose a partially written packet to a concurrent validator.
temporary = args.output.with_name(f".{args.output.name}.tmp")
temporary.write_bytes(payload)
if len(payload) != packet["budget"]["packet_bytes"]:
raise PacketError("serialized packet size changed before write")
temporary.replace(args.output)
except (PacketError, OSError, json.JSONDecodeError) as exc:
print(f"context packet generation failed: {exc}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())