feat(pre-push-safety-net): measure frozen PMPro and Node replay feasibility

This commit is contained in:
Marius Ciepluch
2026-09-13 15:24:06 +02:00
parent 8fe3a7e51e
commit 78a7dfc8c4
12 changed files with 3669 additions and 41 deletions
File diff suppressed because it is too large Load Diff
+48
View File
@@ -89,3 +89,51 @@ results, per-capability metrics, coverage and cold/warm latency. Missing benign
have unknown interruption rate. VAmPI's readable regression may be measured while
independent admission prerequisites remain missing. Unsupported/unreviewed populations
stay visible. All results remain experimental pending the later joint admission gate.
## First full replay experiment (task 5.2)
`measure.py` runs the six prepared PMPro/NodeGoat security, fixed and benign cases
through the production `ousast pre-push` CLI. It does not supply a hand-selected
source slice or regions. The complete 11-case population remains in the scorer;
Ghost is not scanned, and the separate VAmPI/libpng populations are retained as
unexecuted regression/envelope evidence rather than being silently dropped.
Build the local image first, then run with a writable output parent and the existing
repository-object cache mounted read-only:
```sh
docker run --rm --network none --memory 3g --entrypoint python \
-v "$PWD:/context:ro" \
-v "$HOME/.cache/openultrasast/repos:/cache:ro" \
-v /path/to/output-parent:/results \
openultrasast:dev /context/benchmarks/push/measure.py \
--manifest /context/benchmarks/push/inputs.json --cache /cache --out /results/new-run
```
The output directory must not already exist. Input validation opens and verifies
pinned source/license blobs before any measurement. A private bare Git repository
reads the existing objects and creates reproducible authored commits using only the
specified edits. Every other tracked blob remains in the tree; the cache's files,
index and refs are unchanged. No project code, package installation or network is used.
`profile.json` is written before the first replay. PMPro runs first, followed by an
explicit `freeze-after-pmpro.json` checkpoint and Node with identical installed core,
facts, queries and policy. Both analysis and cancellation allowances stay 30 and 2
seconds. Whole CLI elapsed time, separate available stage costs, exact input pins,
source byte counts, selected/deferred identities, raw artifacts, process exits and
terminal output remain available. `rank` denotes an observed selected-question position;
an unlocated/unselected target retains null. A completed empty query cannot earn a
witnessed negative or fixed-side silence. No transitive path is fabricated from rank.
`scorecard.json` uses the existing frozen-population scorer. `decision.json` explicitly
returns NO-GO because this initial cold diagnostic does not qualify independent
capabilities or prove joint quality and warm latency. Exit **1** with a completed
`decision.json` means measured NO-GO; a traceback without that completion record is a
failed instrument. Zero CLI exit means advisory allow, never complete analysis.
Missing artifacts and unanswered questions remain errors/unresolved, not clean results.
Cold here means no persisted graph/query/comparison reuse; filesystem caches are not
flushed. Six one-shot runs are diagnostic samples, not representative warm p95 evidence.
Normal capabilities remain disabled, so zero alerts do not establish precision or useful
recall. A separately named longer lab run may obtain witnesses, but cannot replace the
30-second measurements or satisfy the hook's latency gate.
+313
View File
@@ -0,0 +1,313 @@
"""First frozen-core PMPro/Node diagnostic experiment, using local Git objects only."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import platform
import subprocess
import tempfile
import time
from dataclasses import replace
from pathlib import Path
from openultrasast.model.contracts import QuestionIdentity
from openultrasast.model.scan import ScanBudget
from openultrasast.push.runner import _provenance
from openultrasast.push_inputs import validate_manifest
from openultrasast.push_scoring import freeze_profile, score
from openultrasast.repos import checkout_path, load_repo_recipe
def git(root: Path, *args: str, data: bytes | None = None) -> bytes:
env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
env.update(
GIT_CONFIG_GLOBAL=os.devnull,
GIT_CONFIG_NOSYSTEM="1",
GIT_NO_REPLACE_OBJECTS="1",
GIT_NO_LAZY_FETCH="1",
GIT_ALLOW_PROTOCOL="",
GIT_GRAFT_FILE=os.devnull,
GIT_AUTHOR_NAME="Authored evaluation",
GIT_AUTHOR_EMAIL="[email protected]",
GIT_COMMITTER_NAME="Authored evaluation",
GIT_COMMITTER_EMAIL="[email protected]",
GIT_AUTHOR_DATE="2026-09-12T00:00:00+00:00",
GIT_COMMITTER_DATE="2026-09-12T00:00:00+00:00",
)
return subprocess.run(
["git", "-c", "core.hooksPath=" + os.devnull, "-C", str(root), *args],
input=data,
capture_output=True,
check=True,
timeout=30,
env=env,
).stdout
def pin_snapshot(bare: Path, commit: str, snapshot: dict) -> str:
"""Apply declared edits to a private Git index; retain every other tracked blob."""
git(bare, "read-tree", commit)
edited = False
for entry in snapshot["files"]:
content = git(bare, "cat-file", "blob", commit + ":" + entry["path"])
if not content:
raise ValueError("empty input bytes")
if edit := entry.get("edit"):
before, after = edit["before"].encode(), edit["after"].encode()
if not edit["provenance"].startswith("authored") or not before or content.count(before) != 1:
raise ValueError("authored edit must match exactly once")
if hashlib.sha256(content).hexdigest() != entry["original_sha256"]:
raise ValueError("original hash mismatch")
content = content.replace(before, after, 1)
edited = True
if len(content) != entry["size"] or hashlib.sha256(content).hexdigest() != entry["sha256"]:
raise ValueError("resulting input identity mismatch")
if entry.get("edit"):
blob = git(bare, "hash-object", "-w", "--stdin", data=content).decode().strip()
mode = git(bare, "ls-tree", commit, "--", entry["path"]).split(b" ", 1)[0].decode()
git(bare, "update-index", "--add", "--cacheinfo", mode, blob, entry["path"])
if not edited:
return commit
tree = git(bare, "write-tree").decode().strip()
return (
git(bare, "commit-tree", tree, "-p", commit, data=("Authored evaluation snapshot: " + snapshot["id"] + "\n").encode())
.decode()
.strip()
)
def execution_record(case_id: str, artifact: dict, *, elapsed: float, target: dict | None = None) -> dict:
"""Associate existing answers; empty rows cannot manufacture a witnessed negative."""
selected, deferred, queries = [], [], []
rank = None
modes = set()
for record in artifact["scans"]:
if record["side"] != "head":
continue
scan = record["scan"]
if scope := scan.get("scope"):
modes.add(scope["ranking_mode"])
if target is not None:
rank = next(
(
index
for index, q in enumerate(scope["selected"], 1)
if all(q["identity"][key] == target[key] for key in ("path", "function", "family"))
),
None,
)
selected.extend(QuestionIdentity.from_payload(q["identity"]).question_id for q in scope["selected"])
deferred.extend(QuestionIdentity.from_payload(q["identity"]).question_id for q in scope["deferred"])
for outcome in scan["question_outcomes"]:
identity = QuestionIdentity.from_payload(outcome["identity"])
query = {
"id": identity.question_id,
"status": "answered" if outcome["status"] == "completed" else "unanswered",
"outcome": "unknown",
}
# Keep the exact question/answer association. A raw finding reached from
# another function is not a proved transitive path for this scorer.
findings = [
f
for f in scan["findings"]
if f["family"] == identity.family
and f["site"].startswith(identity.path + ":")
and f["site"].rsplit(":", 1)[-1] == identity.function
]
if outcome["status"] == "completed" and findings:
target_sites = {f"{target['path']}:{line}:{target['function']}" for line in target["lines"]} if target else set()
finding = next((f for f in findings if f["site"] in target_sites), findings[0])
query.update(outcome="positive", finding={**finding, "site": finding["site"].rsplit(":", 1)[0]})
queries.append(query)
if modes and modes != {"evidence"}:
raise ValueError("runtime ranker differs from frozen evidence mode")
deltas = [d["candidate"]["delta"]["novelty"] for d in artifact["admission"]["dispositions"]]
novelty = next((d for d in ("new", "worsened", "unknown", "unchanged") if d in deltas), "unknown")
reasons = artifact["admission"]["coverage_reasons"]
coverage = (
"timeout"
if any("deadline" in reason for reason in reasons)
else ("complete" if artifact["result"]["coverage_status"] == "complete_within_scope" else "incomplete")
)
if artifact["admission"]["defects"]:
raise ValueError("unqualified experiment unexpectedly emitted an admitted alert")
return {
"case_id": case_id,
"ranking_mode": "evidence",
"rank": rank,
"rank_kind": "selected_question_position",
"selected": selected,
"deferred": deferred,
"queries": queries,
"alerts": [],
"delta": novelty,
"coverage": coverage,
"cache_state": "cold",
"timings": {
"total": elapsed,
"build": sum(r["scan"].get("build_seconds", 0) for r in artifact["scans"]),
"query": sum(r["scan"].get("query_seconds", 0) for r in artifact["scans"]),
},
}
def write(path: Path, data: dict) -> None:
path.write_text(json.dumps(data, indent=2, allow_nan=False) + "\n")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--cache", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=False)
manifest = json.loads(args.manifest.read_bytes())
validation = validate_manifest(args.manifest, cache_root=args.cache, fetch=False)
write(args.out / "input-validation.json", validation)
settings = ScanBudget(max_model_calls=0, max_regions=500, order_by_evidence=True)
initial = _provenance(args.manifest.parent, settings)
keys = ("ranking_mode", "engine", "facts", "queries", "policy", "core")
config = {key: initial[key] for key in keys}
config.update(
version="first-replay-v2-2026-09-13",
scope="full tracked tree; declared first-party graph/target exclusions; evidence ranker",
hardware=platform.platform(),
budget_seconds=30,
targets=json.loads((args.manifest.parent / "diagnostic-config-v1.json").read_text())["targets"],
)
profile = freeze_profile(args.manifest.read_bytes(), config)
write(args.out / "profile.json", profile) # Freeze on disk BEFORE any measurement.
run = {"profile_sha256": profile["profile_sha256"], "context": dict(config), "records": []}
evidence = {"cases": [], "core_changes": [], "adapter_changes": [], "freeze_after_pmpro": None}
ready = {c["id"]: c["status"] == "ready" for c in validation["cases"]}
snapshots = {s["id"]: s for s in manifest["snapshots"]}
# Fixed declared order; reserved, regression and unsupported populations stay
# in the joint scorer without scanning or tuning on the holdout.
cases = [c for prefix in ("pmpro-", "nodegoat-") for c in manifest["cases"] if c["id"].startswith(prefix)]
with tempfile.TemporaryDirectory(prefix="ousast-experiment-") as temporary:
bare = Path(temporary) / "objects.git"
bare.mkdir()
git(bare, "init", "--bare")
sources = {}
for case in cases:
for sid in (case["base"], case["tip"]):
if sid in sources or not ready[case["id"]]:
continue
snapshot = snapshots[sid]
recipe = load_repo_recipe(args.manifest.parent / snapshot["recipe"])
recipe = replace(recipe, commit=snapshot.get("commit", recipe.commit))
source = checkout_path(recipe, args.cache)
objects = git(source, "rev-parse", "--path-format=absolute", "--git-path", "objects").decode().strip()
sources[sid] = (recipe.commit, objects)
(bare / "objects/info/alternates").write_text("\n".join(sorted({value[1] for value in sources.values()})) + "\n")
pins = {sid: pin_snapshot(bare, commit, snapshots[sid]) for sid, (commit, _) in sources.items()}
write(
args.out / "replay-pins.json",
{"snapshots": pins, "provenance": "Full upstream trees; declared authored edits in a private bare Git repository."},
)
for case in cases:
if not ready[case["id"]]:
evidence["cases"].append({"case_id": case["id"], "status": "missing_input_prerequisite"})
continue
current = _provenance(args.manifest.parent, settings)
if any(current[key] != initial[key] for key in keys):
raise ValueError("core/facts/query/policy changed after freezing")
if case["id"].startswith("nodegoat-") and evidence["freeze_after_pmpro"] is None:
evidence["freeze_after_pmpro"] = {key: current[key] for key in keys}
write(args.out / "freeze-after-pmpro.json", evidence["freeze_after_pmpro"])
artifact_path = args.out / (case["id"] + ".json")
command = [
"ousast",
"pre-push",
str(bare),
"--base",
pins[case["base"]],
"--head",
pins[case["tip"]],
"--artifact",
str(artifact_path),
"--deadline",
"30",
"--cancellation-allowance",
"2",
"--max-regions",
"500",
]
start = time.monotonic()
process = subprocess.run(command, capture_output=True, text=True, timeout=62)
elapsed = time.monotonic() - start
(args.out / (case["id"] + ".stdout")).write_text(process.stdout)
(args.out / (case["id"] + ".stderr")).write_text(process.stderr)
entry = {
"case_id": case["id"],
"command": command,
"process_exit_code": process.returncode,
"elapsed_seconds": elapsed,
"artifact": artifact_path.name if artifact_path.exists() else None,
"terminal": process.stdout,
"core_unchanged": True,
"input_pins": {"base": pins[case["base"]], "head": pins[case["tip"]]},
}
if artifact_path.exists():
artifact = json.loads(artifact_path.read_text())
# Unmeasured provenance caused by preparation timeout is explicit;
# never replace it with a fictitious runtime measurement.
entry["runtime_provenance_available"] = all(artifact["provenance"].get(key) == initial[key] for key in keys)
record = execution_record(case["id"], artifact, elapsed=elapsed, target=config["targets"][case["id"]])
entry["coverage_reasons"] = artifact["admission"]["coverage_reasons"]
entry["stage_timings"] = artifact["timings"]
entry["snapshot_bytes_read"] = sum(s["bytes_read"] for s in artifact["snapshots"])
entry["classification"] = "scope/deadline" if record["coverage"] == "timeout" else "recorded context/capability limits"
else:
record = {
"case_id": case["id"],
"ranking_mode": "evidence",
"rank": None,
"selected": [],
"deferred": [],
"queries": [],
"alerts": [],
"delta": "unknown",
"coverage": "error",
"cache_state": "cold",
"timings": {"total": elapsed, "build": 0, "query": 0},
}
entry["classification"] = "instrument/artifact unavailable; zero stage costs mean unmeasured"
if process.returncode != 0:
entry["classification"] = "instrument/CLI failure"
record["coverage"] = "error"
run["records"].append(record)
evidence["cases"].append(entry)
write(args.out / "run.json", run)
write(args.out / "execution-evidence.json", evidence)
print(json.dumps({"case": case["id"], "coverage": record["coverage"], "elapsed_seconds": elapsed}), flush=True)
scored = score(profile, validation, run)
write(args.out / "scorecard.json", scored)
decision = {
"decision": "NO-GO",
"capabilities_enabled": False,
"scorecard": "scorecard.json",
"reason": "First cold diagnostic experiment; independent eligibility and joint quality/warm-latency gates remain unmet.",
"recorded_cases": len(run["records"]),
"declared_population": len(manifest["cases"]),
"core_unchanged": True,
"adapter_changes": [],
"limits": [
"No warm-cache measurements",
"Untouched Ghost population not scanned",
"PHP/Python admission populations missing",
"libpng retained as unsupported envelope; no C/C++ detection claim",
"Zero normal alerts from an empty registry cannot establish precision or useful recall",
],
}
write(args.out / "decision.json", decision)
print(json.dumps(decision), flush=True)
return 1 # Measurement completed; the feasibility/admission decision is explicitly NO-GO.
if __name__ == "__main__":
raise SystemExit(main())