QA and context compress
This commit is contained in:
@@ -1,45 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CASAN-native token-killer (Plan-08 Track 3).
|
||||
"""CASAN-native deterministic context compressor (Plan-08 Track 3).
|
||||
|
||||
A deterministic tool-output compressor written for CASAN — NOT a wrapper around
|
||||
RTK. It reduces token count of long command/tool output before it enters model
|
||||
context, while (a) always preserving must-keep lines, (b) never compressing on
|
||||
failure (raw passthrough for debugging, RTK-style tee), and (c) reporting the
|
||||
token savings for H6 telemetry.
|
||||
The compressor reduces long tool output before it enters model context while
|
||||
preserving operationally significant lines. It is deliberately not a tokenizer
|
||||
or a billing source: token figures are whitespace-based estimates and are
|
||||
labelled as such.
|
||||
|
||||
Modes:
|
||||
dedup collapse consecutive duplicate lines with an (xN) counter
|
||||
extractive keep only important lines (errors/failures/warnings) + must-keep
|
||||
structural dedup + keep summary/important/must-keep lines (for test/log output)
|
||||
Safety contract:
|
||||
* preserve severity-tagged errors/warnings, failure/skip signals, summaries,
|
||||
operational result lines, and project-supplied must-keep patterns;
|
||||
* verify preservation after compression, including match multiplicity;
|
||||
* on a preservation failure, return non-zero and emit raw input by default so
|
||||
a caller that ignores the exit code still cannot consume a lossy view;
|
||||
* support halt-with-no-output for callers whose policy forbids raw fallback;
|
||||
* optionally emit a hash-bound JSON evidence report;
|
||||
* never compress a failed command when ``--failed`` is supplied.
|
||||
|
||||
Governance note: this runs AFTER `H4 scan raw` + `H5 hash raw` and BEFORE
|
||||
`H4 scan compressed` in the Plan-08 pipeline; it is deterministic and needs no
|
||||
model, so it cannot be used as a path to evade H4.
|
||||
Governance ordering remains the caller's responsibility:
|
||||
H4 scan raw -> H5 hash raw -> compress -> H3 preservation/faithfulness
|
||||
-> H4 scan compressed -> H5 bind raw/compressed hashes -> model.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
def _casan_app_root():
|
||||
# Plan-01: walk UP for the `.specify` state marker (harness code lives in
|
||||
# packages/casan-harness/, so a fixed __file__ depth would mis-root).
|
||||
_d = os.path.abspath(os.path.dirname(__file__))
|
||||
_p = _d
|
||||
while _p != os.path.dirname(_p):
|
||||
if os.path.isdir(os.path.join(_p, ".specify")) or os.path.isdir(os.path.join(_p, "packages/casan-harness")):
|
||||
return _p
|
||||
_p = os.path.dirname(_p)
|
||||
return os.path.abspath(os.path.join(_d, "..", "..", ".."))
|
||||
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Dict, Iterable, List, Pattern, Sequence, Tuple
|
||||
|
||||
|
||||
DEFAULT_MAX_BYTES = 2 * 1024 * 1024
|
||||
PatternRule = Tuple[str, Pattern[str]]
|
||||
|
||||
|
||||
def _casan_app_root() -> str:
|
||||
"""Find the project/source root without relying on a fixed file depth."""
|
||||
directory = os.path.abspath(os.path.dirname(__file__))
|
||||
current = directory
|
||||
while current != os.path.dirname(current):
|
||||
if (
|
||||
os.path.isdir(os.path.join(current, ".specify"))
|
||||
or os.path.isdir(os.path.join(current, "packages/casan-harness"))
|
||||
):
|
||||
return current
|
||||
current = os.path.dirname(current)
|
||||
return os.path.abspath(os.path.join(directory, "..", "..", ".."))
|
||||
|
||||
|
||||
def compression_enabled() -> bool:
|
||||
"""Read the effective `compression.enabled` from the control-plane settings
|
||||
store (the harness-owned governed settings). Absent/invalid ⇒ enabled (default).
|
||||
This is how a Control Plane setting change actually governs the harness."""
|
||||
"""Return the governed ``compression.enabled`` setting.
|
||||
|
||||
An absent or invalid store retains the historical enabled-by-default
|
||||
behavior. A malformed project-supplied must-keep policy is handled
|
||||
separately and fails closed.
|
||||
"""
|
||||
store_file = os.environ.get(
|
||||
"CASAN_CP_STORE_FILE",
|
||||
os.path.join(
|
||||
@@ -50,145 +67,544 @@ def compression_enabled() -> bool:
|
||||
if not os.path.isfile(store_file):
|
||||
return True
|
||||
try:
|
||||
data = json.load(open(store_file, encoding="utf-8"))
|
||||
with open(store_file, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
setting = data.get("settings", {}).get("compression.enabled")
|
||||
return True if setting is None else bool(setting["value"])
|
||||
except (OSError, ValueError, KeyError, TypeError):
|
||||
return True
|
||||
|
||||
|
||||
IMPORTANT_RE = re.compile(
|
||||
r"\b(error|errors|fail|failed|failure|failing|exception|panic|denied|blocked|warn|warning)\b",
|
||||
# Serilog/log4net-style severity tags need explicit recognition. Word-boundary
|
||||
# matching alone does not classify abbreviations such as ``[WRN]``.
|
||||
SEVERITY_TAG_RE = re.compile(
|
||||
r"\[(ERR(?:OR)?|FTL|FATAL|CRIT(?:ICAL)?|WRN|WARN(?:ING)?)\]",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SUMMARY_RE = re.compile(r"\b(\d+)\s+(pass|passed|fail|failed|tests?|errors?|warnings?)\b", re.IGNORECASE)
|
||||
IMPORTANT_RE = re.compile(
|
||||
r"\b("
|
||||
r"error|errors|fail|failed|failure|failing|exception|panic|"
|
||||
r"denied|blocked|warn|warning|timeout|timed out|"
|
||||
r"abort|aborted|cancel|cancelled|skip|skipped|incomplete|"
|
||||
r"partially completed|partial (?:result|output|analysis|run|failure|success)"
|
||||
r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SUMMARY_RE = re.compile(
|
||||
r"\b(\d+)\s+(pass|passed|fail|failed|tests?|errors?|warnings?|files?|artifacts?)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
OPERATIONAL_RE = re.compile(
|
||||
r"\b("
|
||||
r"exit\s*code|return\s*code|"
|
||||
r"output\s*(?:path|directory)|"
|
||||
r"generated\s+\d+\s+(?:files?|artifacts?)"
|
||||
r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
DEDUP_SUFFIX_RE = re.compile(r"^(.*) \(x([1-9][0-9]*)\)$")
|
||||
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
return len(text.split())
|
||||
def configured_max_bytes() -> int:
|
||||
raw = os.environ.get("CASAN_MAX_INPUT_BYTES", str(DEFAULT_MAX_BYTES))
|
||||
try:
|
||||
value = int(raw)
|
||||
except ValueError:
|
||||
print(
|
||||
"COMPRESS_FAIL invalid_CASAN_MAX_INPUT_BYTES fail-closed",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
if value <= 0:
|
||||
print(
|
||||
"COMPRESS_FAIL non_positive_CASAN_MAX_INPUT_BYTES fail-closed",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
return value
|
||||
|
||||
|
||||
# SEC-09 (M-10): bound input size (DoS) and read fail-closed. Non-UTF8 degrades via
|
||||
# errors="replace" instead of crashing; oversize/unreadable input exits non-zero and
|
||||
# emits nothing (never a crash traceback, never silent truncation).
|
||||
MAX_BYTES = int(os.environ.get("CASAN_MAX_INPUT_BYTES", str(2 * 1024 * 1024)))
|
||||
|
||||
|
||||
def read_capped(src: str) -> str:
|
||||
def read_capped(src: str, max_bytes: int) -> Tuple[str, int]:
|
||||
"""Read at most ``max_bytes`` and return text plus replacement count."""
|
||||
try:
|
||||
if src == "-":
|
||||
data = sys.stdin.buffer.read(MAX_BYTES + 1)
|
||||
data = sys.stdin.buffer.read(max_bytes + 1)
|
||||
else:
|
||||
with open(src, "rb") as fh:
|
||||
data = fh.read(MAX_BYTES + 1)
|
||||
with open(src, "rb") as handle:
|
||||
data = handle.read(max_bytes + 1)
|
||||
except OSError as exc:
|
||||
print(f"COMPRESS_FAIL unreadable_input: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
if len(data) > MAX_BYTES:
|
||||
print(f"COMPRESS_FAIL input_exceeds_cap({MAX_BYTES}B) fail-closed", file=sys.stderr)
|
||||
if len(data) > max_bytes:
|
||||
print(
|
||||
f"COMPRESS_FAIL input_exceeds_cap({max_bytes}B) fail-closed",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
return data.decode("utf-8", errors="replace")
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
return text, text.count("\ufffd")
|
||||
|
||||
|
||||
def load_patterns(path: str):
|
||||
def load_patterns(path: str) -> List[PatternRule]:
|
||||
"""Load and compile one regex per line, ignoring blank/comment lines."""
|
||||
if not path:
|
||||
return []
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
return [line.strip() for line in fh if line.strip()]
|
||||
with open(path, encoding="utf-8", errors="replace") as handle:
|
||||
raw_rules = [
|
||||
(line_number, line.strip())
|
||||
for line_number, line in enumerate(handle, start=1)
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
]
|
||||
except OSError as exc:
|
||||
print(f"COMPRESS_FAIL must_keep_file_unreadable: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def is_must_keep(line: str, patterns) -> bool:
|
||||
return any(re.search(p, line) for p in patterns)
|
||||
rules: List[PatternRule] = []
|
||||
for line_number, expression in raw_rules:
|
||||
try:
|
||||
rules.append((expression, re.compile(expression)))
|
||||
except re.error as exc:
|
||||
print(
|
||||
"COMPRESS_FAIL invalid_must_keep_regex "
|
||||
f"file={path} line={line_number}: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
return rules
|
||||
|
||||
|
||||
def dedup(lines):
|
||||
out = []
|
||||
i = 0
|
||||
n = len(lines)
|
||||
while i < n:
|
||||
j = i
|
||||
while j + 1 < n and lines[j + 1] == lines[i]:
|
||||
j += 1
|
||||
count = j - i + 1
|
||||
out.append(lines[i] if count == 1 else f"{lines[i]} (x{count})")
|
||||
i = j + 1
|
||||
return out
|
||||
def is_must_keep(line: str, patterns: Sequence[PatternRule]) -> bool:
|
||||
return any(pattern.search(line) for _, pattern in patterns)
|
||||
|
||||
|
||||
def compress(text: str, mode: str, must):
|
||||
lines = text.split("\n")
|
||||
if mode == "dedup":
|
||||
def is_protected(line: str, patterns: Sequence[PatternRule]) -> bool:
|
||||
"""Return whether a line is forbidden from disappearing."""
|
||||
return bool(
|
||||
SEVERITY_TAG_RE.search(line)
|
||||
or IMPORTANT_RE.search(line)
|
||||
or SUMMARY_RE.search(line)
|
||||
or OPERATIONAL_RE.search(line)
|
||||
or is_must_keep(line, patterns)
|
||||
)
|
||||
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
"""Return a whitespace estimate, never provider/billed token usage."""
|
||||
return len(text.split())
|
||||
|
||||
|
||||
def dedup(lines: Sequence[str]) -> List[str]:
|
||||
"""Collapse consecutive duplicate lines while retaining multiplicity."""
|
||||
output: List[str] = []
|
||||
index = 0
|
||||
count_lines = len(lines)
|
||||
while index < count_lines:
|
||||
end = index
|
||||
while end + 1 < count_lines and lines[end + 1] == lines[index]:
|
||||
end += 1
|
||||
occurrences = end - index + 1
|
||||
output.append(
|
||||
lines[index]
|
||||
if occurrences == 1
|
||||
else f"{lines[index]} (x{occurrences})"
|
||||
)
|
||||
index = end + 1
|
||||
return output
|
||||
|
||||
|
||||
def compress(
|
||||
text: str,
|
||||
mode: str,
|
||||
must_keep: Sequence[PatternRule],
|
||||
) -> List[str]:
|
||||
"""Create a candidate compressed view.
|
||||
|
||||
Preservation is verified independently after this function returns.
|
||||
"""
|
||||
lines = compression_source_lines(text.split("\n"), mode, must_keep)
|
||||
if mode in {"dedup", "structural"}:
|
||||
return dedup(lines)
|
||||
if mode == "extractive":
|
||||
return [ln for ln in lines if IMPORTANT_RE.search(ln) or is_must_keep(ln, must)]
|
||||
if mode == "structural":
|
||||
kept = [
|
||||
ln
|
||||
for ln in lines
|
||||
if IMPORTANT_RE.search(ln) or SUMMARY_RE.search(ln) or is_must_keep(ln, must)
|
||||
]
|
||||
return dedup(kept)
|
||||
return lines
|
||||
raise ValueError(f"unknown mode: {mode}")
|
||||
|
||||
|
||||
def compression_source_lines(
|
||||
raw_lines: Sequence[str],
|
||||
mode: str,
|
||||
must_keep: Sequence[PatternRule],
|
||||
) -> List[str]:
|
||||
"""Return the exact raw lines from which a candidate may be built."""
|
||||
if mode == "dedup":
|
||||
return list(raw_lines)
|
||||
return [line for line in raw_lines if is_protected(line, must_keep)]
|
||||
|
||||
|
||||
def expanded_line_counts(
|
||||
lines: Iterable[str],
|
||||
dedup_encoded: bool,
|
||||
source_lines: Sequence[str],
|
||||
) -> Counter:
|
||||
"""Decode ``(xN)`` markers produced by ``dedup`` into weighted counts."""
|
||||
candidate_lines = list(lines)
|
||||
# This is the normal path and resolves the otherwise ambiguous case where
|
||||
# a real log line itself ends in ``(xN)``.
|
||||
if dedup_encoded and candidate_lines == dedup(source_lines):
|
||||
return Counter(source_lines)
|
||||
|
||||
counts: Counter = Counter()
|
||||
for line in candidate_lines:
|
||||
match = DEDUP_SUFFIX_RE.match(line) if dedup_encoded else None
|
||||
if match:
|
||||
counts[match.group(1)] += int(match.group(2))
|
||||
else:
|
||||
counts[line] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def protected_line_deficits(
|
||||
source_lines: Sequence[str],
|
||||
candidate_lines: Sequence[str],
|
||||
dedup_encoded: bool,
|
||||
) -> Dict[str, int]:
|
||||
"""Return exact protected-line multiplicity missing from the candidate."""
|
||||
expected = Counter(source_lines)
|
||||
actual = expanded_line_counts(
|
||||
candidate_lines,
|
||||
dedup_encoded=dedup_encoded,
|
||||
source_lines=source_lines,
|
||||
)
|
||||
return {
|
||||
line: expected_count - actual.get(line, 0)
|
||||
for line, expected_count in expected.items()
|
||||
if actual.get(line, 0) < expected_count
|
||||
}
|
||||
|
||||
|
||||
def required_pattern_deficits(
|
||||
raw_lines: Sequence[str],
|
||||
source_lines: Sequence[str],
|
||||
candidate_lines: Sequence[str],
|
||||
required: Sequence[PatternRule],
|
||||
dedup_encoded: bool,
|
||||
) -> List[Dict[str, object]]:
|
||||
"""Verify every required match present in raw remains in the candidate.
|
||||
|
||||
A pattern absent from raw is not a failure: the invariant is preservation,
|
||||
not fabrication. Match multiplicity prevents one surviving warning from
|
||||
hiding the loss of a second warning matched by the same rule.
|
||||
"""
|
||||
deficits: List[Dict[str, object]] = []
|
||||
actual_lines = expanded_line_counts(
|
||||
candidate_lines,
|
||||
dedup_encoded=dedup_encoded,
|
||||
source_lines=source_lines,
|
||||
)
|
||||
for expression, pattern in required:
|
||||
expected = sum(1 for line in raw_lines if pattern.search(line))
|
||||
actual = sum(
|
||||
count
|
||||
for line, count in actual_lines.items()
|
||||
if pattern.search(line)
|
||||
)
|
||||
if actual < expected:
|
||||
deficits.append(
|
||||
{
|
||||
"pattern": expression,
|
||||
"expected": expected,
|
||||
"actual": actual,
|
||||
"missing": expected - actual,
|
||||
}
|
||||
)
|
||||
return deficits
|
||||
|
||||
|
||||
def severity_counts(text: str) -> Dict[str, int]:
|
||||
counts = {"error": 0, "warning": 0, "critical": 0}
|
||||
for line in text.splitlines():
|
||||
match = SEVERITY_TAG_RE.search(line)
|
||||
if not match:
|
||||
continue
|
||||
level = match.group(1).upper()
|
||||
if level.startswith(("ERR",)):
|
||||
counts["error"] += 1
|
||||
elif level.startswith(("WRN", "WARN")):
|
||||
counts["warning"] += 1
|
||||
else:
|
||||
counts["critical"] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def sha256_text(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def deficit_fingerprints(deficits: Dict[str, int]) -> List[Dict[str, object]]:
|
||||
"""Report hashes, not potentially sensitive raw lines."""
|
||||
return [
|
||||
{"line_sha256": sha256_text(line), "missing": missing}
|
||||
for line, missing in sorted(deficits.items())
|
||||
]
|
||||
|
||||
|
||||
def write_json_report(path: str, report: Dict[str, object]) -> None:
|
||||
"""Atomically write a private evidence report."""
|
||||
if path == "-":
|
||||
print(
|
||||
"COMPRESS_FAIL report_json_stdout_conflicts_with_compressed_output",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
absolute = os.path.abspath(path)
|
||||
directory = os.path.dirname(absolute)
|
||||
try:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
descriptor, temporary_path = tempfile.mkstemp(
|
||||
prefix=".context-compress-",
|
||||
suffix=".json.tmp",
|
||||
dir=directory,
|
||||
)
|
||||
try:
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
||||
json.dump(
|
||||
report,
|
||||
handle,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
)
|
||||
handle.write("\n")
|
||||
os.chmod(temporary_path, 0o600)
|
||||
os.replace(temporary_path, absolute)
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(temporary_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
except OSError as exc:
|
||||
print(f"COMPRESS_FAIL report_write_failed: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--mode", choices=["dedup", "extractive", "structural"], default="structural")
|
||||
ap.add_argument("--input", default="-", help="input file or - for stdin")
|
||||
ap.add_argument("--must-keep-file", default="", help="file with one must-keep regex per line")
|
||||
ap.add_argument("--failed", action="store_true", help="raw passthrough (tee) when the command failed")
|
||||
ap.add_argument(
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["dedup", "extractive", "structural"],
|
||||
default="structural",
|
||||
)
|
||||
parser.add_argument("--input", default="-", help="input file or - for stdin")
|
||||
parser.add_argument(
|
||||
"--must-keep-file",
|
||||
default="",
|
||||
help="file with one must-keep regex per line",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--failed",
|
||||
action="store_true",
|
||||
help="raw passthrough when the producing command failed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--respect-policy",
|
||||
action="store_true",
|
||||
help="honor control-plane `compression.enabled`; if disabled, pass raw through",
|
||||
help="honor control-plane compression.enabled",
|
||||
)
|
||||
ap.add_argument(
|
||||
parser.add_argument(
|
||||
"--require-must-keep-file",
|
||||
default="",
|
||||
help="verify every pattern in this file still appears; exit 1 (gate) if any is missing",
|
||||
help=(
|
||||
"verify every match present in raw remains in the candidate; "
|
||||
"match multiplicity is enforced"
|
||||
),
|
||||
)
|
||||
args = ap.parse_args()
|
||||
parser.add_argument(
|
||||
"--on-preservation-failure",
|
||||
choices=["raw", "halt"],
|
||||
default="raw",
|
||||
help=(
|
||||
"raw: emit raw input and return 1; "
|
||||
"halt: emit nothing and return 1"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report-json",
|
||||
default="",
|
||||
help="optional atomic JSON evidence report path",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
raw = read_capped(args.input)
|
||||
must = load_patterns(args.must_keep_file)
|
||||
raw, decode_replacements = read_capped(
|
||||
args.input,
|
||||
configured_max_bytes(),
|
||||
)
|
||||
must_keep = load_patterns(args.must_keep_file)
|
||||
required = load_patterns(args.require_must_keep_file)
|
||||
raw_lines = raw.split("\n")
|
||||
|
||||
exit_code = 0
|
||||
preservation_status = "not_applicable"
|
||||
pattern_deficits: List[Dict[str, object]] = []
|
||||
line_deficits: Dict[str, int] = {}
|
||||
|
||||
if args.failed:
|
||||
# RTK-style tee: never compress failing output; keep raw for debugging.
|
||||
out_text = raw
|
||||
candidate_text = raw
|
||||
output_text = raw
|
||||
mode_used = "passthrough"
|
||||
decision = "failed-command-raw-passthrough"
|
||||
saving_status = "not_compressed"
|
||||
elif args.respect_policy and not compression_enabled():
|
||||
# Control-plane setting governs the harness: compression disabled ⇒ raw.
|
||||
out_text = raw
|
||||
candidate_text = raw
|
||||
output_text = raw
|
||||
mode_used = "policy-disabled"
|
||||
decision = "policy-disabled-raw-passthrough"
|
||||
saving_status = "not_compressed"
|
||||
else:
|
||||
out_lines = compress(raw, args.mode, must)
|
||||
out_text = "\n".join(out_lines)
|
||||
mode_used = args.mode
|
||||
source_lines = compression_source_lines(
|
||||
raw_lines,
|
||||
args.mode,
|
||||
must_keep,
|
||||
)
|
||||
candidate_lines = compress(raw, args.mode, must_keep)
|
||||
candidate_text = "\n".join(candidate_lines)
|
||||
line_deficits = protected_line_deficits(
|
||||
source_lines,
|
||||
candidate_lines,
|
||||
dedup_encoded=args.mode in {"dedup", "structural"},
|
||||
)
|
||||
pattern_deficits = required_pattern_deficits(
|
||||
raw_lines,
|
||||
source_lines,
|
||||
candidate_lines,
|
||||
required,
|
||||
dedup_encoded=args.mode in {"dedup", "structural"},
|
||||
)
|
||||
empty_loss = bool(raw.strip()) and not candidate_text.strip()
|
||||
preservation_failed = bool(
|
||||
line_deficits or pattern_deficits or empty_loss
|
||||
)
|
||||
|
||||
in_tokens = estimate_tokens(raw)
|
||||
out_tokens = estimate_tokens(out_text)
|
||||
saved = in_tokens - out_tokens
|
||||
ratio = round(out_tokens / in_tokens, 4) if in_tokens else 1.0
|
||||
if preservation_failed:
|
||||
preservation_status = "failed"
|
||||
exit_code = 1
|
||||
mode_used = (
|
||||
"fallback-raw"
|
||||
if args.on_preservation_failure == "raw"
|
||||
else "halt"
|
||||
)
|
||||
decision = "compression-rejected"
|
||||
saving_status = "rejected"
|
||||
output_text = (
|
||||
raw if args.on_preservation_failure == "raw" else ""
|
||||
)
|
||||
else:
|
||||
preservation_status = "passed"
|
||||
mode_used = args.mode
|
||||
decision = "compressed"
|
||||
saving_status = "estimate_only_quality_gate_required"
|
||||
output_text = candidate_text
|
||||
|
||||
verify_patterns = load_patterns(args.require_must_keep_file)
|
||||
missing = [p for p in verify_patterns if not re.search(p, out_text)]
|
||||
input_estimate = estimate_tokens(raw)
|
||||
candidate_estimate = estimate_tokens(candidate_text)
|
||||
output_estimate = estimate_tokens(output_text)
|
||||
if saving_status == "rejected":
|
||||
# A rejected candidate has no claimable saving even when halt policy
|
||||
# intentionally emits zero bytes.
|
||||
estimated_saved = 0
|
||||
estimated_ratio = 1.0
|
||||
else:
|
||||
estimated_saved = input_estimate - output_estimate
|
||||
estimated_ratio = (
|
||||
round(output_estimate / input_estimate, 4)
|
||||
if input_estimate
|
||||
else 1.0
|
||||
)
|
||||
emitted_text = output_text
|
||||
if emitted_text and not emitted_text.endswith("\n"):
|
||||
emitted_text += "\n"
|
||||
|
||||
sys.stdout.write(out_text)
|
||||
if not out_text.endswith("\n"):
|
||||
sys.stdout.write("\n")
|
||||
report: Dict[str, object] = {
|
||||
"schema_version": 1,
|
||||
"measurement_source": "whitespace_estimate",
|
||||
"provider_telemetry": None,
|
||||
"mode_requested": args.mode,
|
||||
"mode_used": mode_used,
|
||||
"decision": decision,
|
||||
"exit_code": exit_code,
|
||||
"saving_status": saving_status,
|
||||
"preservation": {
|
||||
"status": preservation_status,
|
||||
"protected_line_missing": sum(line_deficits.values()),
|
||||
"protected_line_deficits": deficit_fingerprints(line_deficits),
|
||||
"required_pattern_deficits": pattern_deficits,
|
||||
},
|
||||
"estimated_tokens": {
|
||||
"input": input_estimate,
|
||||
"candidate": candidate_estimate,
|
||||
"output": output_estimate,
|
||||
"saved": estimated_saved,
|
||||
"ratio": estimated_ratio,
|
||||
},
|
||||
"severity": {
|
||||
"raw": severity_counts(raw),
|
||||
"candidate": severity_counts(candidate_text),
|
||||
"output": severity_counts(output_text),
|
||||
},
|
||||
"hashes": {
|
||||
"raw_sha256": sha256_text(raw),
|
||||
"candidate_sha256": sha256_text(candidate_text),
|
||||
"output_sha256": sha256_text(emitted_text),
|
||||
},
|
||||
"bytes": {
|
||||
"raw": len(raw.encode("utf-8")),
|
||||
"candidate": len(candidate_text.encode("utf-8")),
|
||||
"output": len(emitted_text.encode("utf-8")),
|
||||
},
|
||||
"decode_replacement_count": decode_replacements,
|
||||
"requires_downstream_h3_quality_gate": decision == "compressed",
|
||||
"requires_downstream_h4_compressed_scan": decision == "compressed",
|
||||
"requires_downstream_h5_audit_binding": decision == "compressed",
|
||||
}
|
||||
|
||||
if args.report_json:
|
||||
write_json_report(args.report_json, report)
|
||||
|
||||
sys.stdout.write(emitted_text)
|
||||
print(
|
||||
f"COMPRESS mode={mode_used} in_tokens={in_tokens} out_tokens={out_tokens} "
|
||||
f"saved={saved} ratio={ratio} must_keep_missing={len(missing)}",
|
||||
"COMPRESS "
|
||||
f"mode={mode_used} "
|
||||
f"measurement_source=whitespace_estimate "
|
||||
f"in_tokens={input_estimate} "
|
||||
f"candidate_tokens={candidate_estimate} "
|
||||
f"out_tokens={output_estimate} "
|
||||
f"saved={estimated_saved} "
|
||||
f"ratio={estimated_ratio} "
|
||||
f"preservation={preservation_status} "
|
||||
f"saving_status={saving_status} "
|
||||
f"must_keep_missing={sum(int(item['missing']) for item in pattern_deficits)} "
|
||||
f"protected_missing={sum(line_deficits.values())}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if missing:
|
||||
print(f"COMPRESS_MUST_KEEP_DROPPED {','.join(missing)}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
if pattern_deficits:
|
||||
details = ",".join(
|
||||
f"{item['pattern']}({item['actual']}/{item['expected']})"
|
||||
for item in pattern_deficits
|
||||
)
|
||||
print(f"COMPRESS_MUST_KEEP_DROPPED {details}", file=sys.stderr)
|
||||
if line_deficits:
|
||||
print(
|
||||
"COMPRESS_PROTECTED_CONTENT_DROPPED "
|
||||
f"occurrences={sum(line_deficits.values())}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if preservation_status == "failed":
|
||||
print(
|
||||
"COMPRESS_REJECTED "
|
||||
f"fallback={args.on_preservation_failure}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
# NEHOPS analyzer invariants. These expressions are project/domain policy,
|
||||
# while generic severity-tag preservation belongs to the Core compressor.
|
||||
VBSource root not found
|
||||
common code tracing skipped
|
||||
Error Messages
|
||||
Output Directory
|
||||
Generated [0-9]+ files
|
||||
exit code
|
||||
@@ -10,6 +10,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
||||
PROJECT_ROOT="$CASAN_APP_ROOT"
|
||||
CC="$CASAN_HARNESS_ROOT/scripts/bash/context-compress.py"
|
||||
FIXTURES="$SCRIPT_DIR/fixtures/context-compression/nehops"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
@@ -89,6 +90,178 @@ set -e 2>/dev/null || true
|
||||
[[ "$RC" -eq 0 ]] && pass "protecting the pattern keeps the line and passes the gate" \
|
||||
|| fail "protected must-keep still failed the gate (rc=$RC)"
|
||||
|
||||
# 8) Realistic NEHOPS regression: abbreviated [WRN] tags and domain invariants
|
||||
# survive structural compression, with hash-bound evidence that labels token
|
||||
# figures as whitespace estimates.
|
||||
set +e
|
||||
python3 "$CC" --mode structural \
|
||||
--input "$FIXTURES/analyzer-sanitized.log" \
|
||||
--must-keep-file "$FIXTURES/must-keep.patterns" \
|
||||
--require-must-keep-file "$FIXTURES/must-keep.patterns" \
|
||||
--report-json "$WORK/nehops-report.json" \
|
||||
>"$WORK/nehops-compressed.log" 2>"$WORK/nehops.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
RAW_WARNINGS="$(grep -c '\[WRN\]' "$FIXTURES/analyzer-sanitized.log")"
|
||||
OUT_WARNINGS="$(grep -c '\[WRN\]' "$WORK/nehops-compressed.log" || true)"
|
||||
if [[ "$RC" -eq 0 && "$RAW_WARNINGS" -eq 2 && "$OUT_WARNINGS" -eq "$RAW_WARNINGS" ]] \
|
||||
&& grep -q "FRR04700_CL.*common code tracing skipped" "$WORK/nehops-compressed.log" \
|
||||
&& grep -q "FRR04700_SV.*common code tracing skipped" "$WORK/nehops-compressed.log" \
|
||||
&& grep -q "Error Messages: 0" "$WORK/nehops-compressed.log"; then
|
||||
pass "NEHOPS [WRN] + CL/SV/domain invariants survive compression"
|
||||
else
|
||||
fail "NEHOPS warning preservation failed (rc=$RC raw=$RAW_WARNINGS out=$OUT_WARNINGS)"
|
||||
fi
|
||||
|
||||
set +e
|
||||
python3 - "$WORK/nehops-report.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
report = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
assert report["decision"] == "compressed"
|
||||
assert report["preservation"]["status"] == "passed"
|
||||
assert report["preservation"]["protected_line_missing"] == 0
|
||||
assert report["preservation"]["required_pattern_deficits"] == []
|
||||
assert report["measurement_source"] == "whitespace_estimate"
|
||||
assert report["provider_telemetry"] is None
|
||||
assert report["saving_status"] == "estimate_only_quality_gate_required"
|
||||
assert report["severity"]["raw"]["warning"] == 2
|
||||
assert report["severity"]["candidate"]["warning"] == 2
|
||||
assert len(report["hashes"]["raw_sha256"]) == 64
|
||||
assert len(report["hashes"]["candidate_sha256"]) == 64
|
||||
PY
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
[[ "$RC" -eq 0 ]] \
|
||||
&& pass "JSON evidence binds hashes and labels estimate/provider status" \
|
||||
|| fail "JSON evidence contract invalid"
|
||||
|
||||
# 9) Required-pattern multiplicity: retaining one of two matching lines is not
|
||||
# sufficient. Rejection defaults to raw fallback and reports zero savings.
|
||||
printf 'TRACE-MUST plain line\nERROR TRACE-MUST protected line\n' > "$WORK/multi.txt"
|
||||
printf 'TRACE-MUST\n' > "$WORK/multi.patterns"
|
||||
set +e
|
||||
python3 "$CC" --mode extractive --input "$WORK/multi.txt" \
|
||||
--require-must-keep-file "$WORK/multi.patterns" \
|
||||
>"$WORK/multi.out" 2>"$WORK/multi.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
if [[ "$RC" -eq 1 ]] \
|
||||
&& cmp -s "$WORK/multi.txt" "$WORK/multi.out" \
|
||||
&& grep -q "TRACE-MUST(1/2)" "$WORK/multi.err" \
|
||||
&& grep -q "saving_status=rejected" "$WORK/multi.err" \
|
||||
&& grep -q "saved=0" "$WORK/multi.err"; then
|
||||
pass "partial must-keep loss is rejected with raw fallback and no saving claim"
|
||||
else
|
||||
fail "multiplicity/fallback contract failed (rc=$RC)"
|
||||
fi
|
||||
|
||||
# 10) A caller may choose halt instead of raw fallback. Halt returns non-zero
|
||||
# and emits no candidate bytes.
|
||||
set +e
|
||||
python3 "$CC" --mode extractive --input "$WORK/multi.txt" \
|
||||
--require-must-keep-file "$WORK/multi.patterns" \
|
||||
--on-preservation-failure halt \
|
||||
>"$WORK/halt.out" 2>"$WORK/halt.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
if [[ "$RC" -eq 1 && ! -s "$WORK/halt.out" ]] \
|
||||
&& grep -q "COMPRESS_REJECTED fallback=halt" "$WORK/halt.err" \
|
||||
&& grep -q "saving_status=rejected" "$WORK/halt.err" \
|
||||
&& grep -q "saved=0" "$WORK/halt.err"; then
|
||||
pass "halt policy emits no lossy candidate on preservation failure"
|
||||
else
|
||||
fail "halt policy leaked output or returned success (rc=$RC)"
|
||||
fi
|
||||
|
||||
# 11) Required patterns absent from raw are not fabricated and do not create a
|
||||
# false failure; preservation checks only claims that raw evidence contained.
|
||||
printf 'ERROR real failure line\n' > "$WORK/absent.txt"
|
||||
printf 'NEVER_PRESENT_IN_RAW\n' > "$WORK/absent.patterns"
|
||||
set +e
|
||||
python3 "$CC" --mode extractive --input "$WORK/absent.txt" \
|
||||
--require-must-keep-file "$WORK/absent.patterns" \
|
||||
>"$WORK/absent.out" 2>"$WORK/absent.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
[[ "$RC" -eq 0 ]] \
|
||||
&& pass "pattern absent from raw does not create a false preservation failure" \
|
||||
|| fail "absent raw pattern incorrectly failed preservation (rc=$RC)"
|
||||
|
||||
# 12) Invalid project regex is a configuration defect and fails closed without
|
||||
# emitting a candidate.
|
||||
printf '[unterminated\n' > "$WORK/invalid.patterns"
|
||||
set +e
|
||||
python3 "$CC" --mode structural --input "$WORK/absent.txt" \
|
||||
--must-keep-file "$WORK/invalid.patterns" \
|
||||
>"$WORK/invalid.out" 2>"$WORK/invalid.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
if [[ "$RC" -eq 1 && ! -s "$WORK/invalid.out" ]] \
|
||||
&& grep -q "COMPRESS_FAIL invalid_must_keep_regex" "$WORK/invalid.err"; then
|
||||
pass "invalid must-keep regex fails closed"
|
||||
else
|
||||
fail "invalid regex did not fail closed (rc=$RC)"
|
||||
fi
|
||||
|
||||
# 13) Non-empty raw input may not silently compress to an empty view.
|
||||
printf 'ordinary informational chatter\n' > "$WORK/empty-loss.txt"
|
||||
set +e
|
||||
python3 "$CC" --mode structural --input "$WORK/empty-loss.txt" \
|
||||
>"$WORK/empty-loss.out" 2>"$WORK/empty-loss.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
if [[ "$RC" -eq 1 ]] \
|
||||
&& cmp -s "$WORK/empty-loss.txt" "$WORK/empty-loss.out" \
|
||||
&& grep -q "COMPRESS_REJECTED fallback=raw" "$WORK/empty-loss.err"; then
|
||||
pass "empty compressed view is rejected and raw is preserved"
|
||||
else
|
||||
fail "non-empty raw input was allowed to disappear (rc=$RC)"
|
||||
fi
|
||||
|
||||
# 14) Dedup may collapse repeated warnings only when the multiplicity marker
|
||||
# proves how many occurrences were present.
|
||||
printf '[WRN] repeated warning\n[WRN] repeated warning\n[WRN] repeated warning\n' > "$WORK/repeated-warning.txt"
|
||||
set +e
|
||||
python3 "$CC" --mode structural --input "$WORK/repeated-warning.txt" \
|
||||
>"$WORK/repeated-warning.out" 2>"$WORK/repeated-warning.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
if [[ "$RC" -eq 0 ]] \
|
||||
&& grep -q '\[WRN\] repeated warning (x3)' "$WORK/repeated-warning.out"; then
|
||||
pass "dedup preserves repeated-warning multiplicity"
|
||||
else
|
||||
fail "repeated-warning multiplicity was lost (rc=$RC)"
|
||||
fi
|
||||
|
||||
# 15) A literal line ending in "(xN)" is content, not necessarily a dedup
|
||||
# marker. It must survive in both non-dedup and dedup modes.
|
||||
printf 'ERROR literal marker (x3)\n' > "$WORK/literal-marker.txt"
|
||||
set +e
|
||||
python3 "$CC" --mode extractive --input "$WORK/literal-marker.txt" \
|
||||
>"$WORK/literal-marker.out" 2>"$WORK/literal-marker.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
if [[ "$RC" -eq 0 ]] \
|
||||
&& cmp -s "$WORK/literal-marker.txt" "$WORK/literal-marker.out"; then
|
||||
pass "extractive mode does not misread a literal (xN) suffix"
|
||||
else
|
||||
fail "literal (xN) content triggered a false preservation failure (rc=$RC)"
|
||||
fi
|
||||
|
||||
set +e
|
||||
python3 "$CC" --mode structural --input "$WORK/literal-marker.txt" \
|
||||
>"$WORK/literal-marker-structural.out" 2>"$WORK/literal-marker-structural.err"
|
||||
RC=$?
|
||||
set -e 2>/dev/null || true
|
||||
if [[ "$RC" -eq 0 ]] \
|
||||
&& cmp -s "$WORK/literal-marker.txt" "$WORK/literal-marker-structural.out"; then
|
||||
pass "structural mode disambiguates a literal (xN) suffix"
|
||||
else
|
||||
fail "structural mode misread literal (xN) content (rc=$RC)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "===== Plan-08 ⟷ Control Plane: settings govern harness ====="
|
||||
CPS="$CASAN_HARNESS_ROOT/scripts/bash/control-plane-settings.py"
|
||||
|
||||
Reference in New Issue
Block a user