88fc58b2fd
ci/woodpecker/push/test Pipeline was successful
The stability gate treated every evidence flip as symmetric, and that produced a false downgrade on the candidate it was written for (kotkan/claude-plugin-inference-arbitrage#15). Re-measuring anxious/agent-wip/release-policy-derivation over four real windows with the same script gave 0 -> 2 -> 12 -> 15 invocations at 7/14/27/60 days: monotonic accumulation of a step that runs about once every two days, not a label flipping about. The 7-day window had not caught an unstable candidate, it had failed to observe a real one. So a thin/unmeasured reading now only overturns a `measured` one when its window is at least 90% as wide (observed since->until span). Below that the comparison takes a third verdict, `insufficient-window`: not filed, not downgraded to a boundary question, no FR-6.5 self-report, and carried forward with the wider snapshot left as the standing comparison point. The parity is relative rather than an absolute day count because the adequate width is a property of the candidate's invocation rate, which the auditor does not know in advance. Unchanged, and tested: the reverse direction (thin prior -> measured now) still downgrades, a flip between comparably wide windows still downgrades, and a threshold crossing with `measured` on both sides is never excused by narrowness. An unreadable window is not an exemption either — the flip stands and window_parity records that the check could not run. - bin/stability-classify: window_days/window_parity/deferrable, the insufficient-window verdict, stability_deferrals, window_parity on findings too, insufficient_window + window_parity_ratio in the gate block - bin/audit-snapshot: record deferrals in the snapshot notes so the row is visible on the wiki page instead of silently dropped - design/spec.md: FR-4.5.1 as a testable requirement; S4 restated - design/rubric.md: the third boundary in §5b - skills/offload-audit/SKILL.md: do not file and do not self-report a deferral - tests: real 60-day anxious fixture; cases (f)/(f2) on real data, (j)-(n) synthetic — 7 test files pass Verified against the live store (~/.cache/inference-arbitrage/wiki): the real 7.0d-vs-26.49d comparison now reports insufficient-window with zero findings, while the real 33.4d-vs-26.49d one reports stable at 13.87% and files normally.
1133 lines
50 KiB
Python
Executable File
1133 lines
50 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Persists one audit run as a snapshot record and diffs runs over time
|
|
(spec FR-5, plan §5-6).
|
|
|
|
It takes already-computed data — `plugin-inventory`, `offload-scan`,
|
|
`boundary-classify` — and does nothing but record, match, and subtract. Every
|
|
step here passes all five determinism tests, so none of it belongs in a model
|
|
(FR-8.1).
|
|
|
|
STORAGE AND THE PUSH BOUNDARY
|
|
-----------------------------
|
|
plan §5 says the store is this plugin's own Gitea wiki, "cloned to a local
|
|
cache and pushed via cluster:gitea-agent (this plugin never writes Gitea
|
|
directly)" — which is in tension with itself, because a git push to the wiki
|
|
repo IS a write to Gitea.
|
|
|
|
Resolved as: **this script is offline and filesystem-only.** It reads and
|
|
writes a local store directory laid out exactly like the wiki tree:
|
|
|
|
<store>/Data/<target>/snapshots.jsonl
|
|
<store>/Audits/<target>/<YYYY-MM-DD>.md
|
|
<store>/Audits/<target>/Latest.md
|
|
|
|
It never opens a socket — no git push, no Gitea API — which keeps plan §7's
|
|
"offline, only the two output paths touch the network" true, and keeps the
|
|
wiki credentials entirely outside this plugin. Publishing is a separate,
|
|
explicit action by the caller (the `offload-trend` / `offload-audit` skill, or
|
|
a human): `audit-snapshot pages` emits exactly which wiki pages to write with
|
|
their full content, and the caller performs the writes with the Gitea wiki
|
|
tools. `audit-snapshot import` is the other half of that bridge — it seeds the
|
|
local store from a `snapshots.jsonl` the caller fetched off the wiki, so a
|
|
machine with a cold cache still diffs against real history.
|
|
|
|
The append-only JSONL lives on the wiki as a page whose name is the file path;
|
|
Gitea wiki pages are files in a git repo, so the wiki IS the git-backed store
|
|
plan §5 asks for, and `git log` on it is the audit trail. Appending is
|
|
read-current-content, add one line, write back — which is why `pages` emits
|
|
the whole file, not a delta.
|
|
|
|
MEMORY (FR-9)
|
|
-------------
|
|
`memory-queries` and `memory-notes` are the same bridge pointed at mempalace
|
|
instead of the wiki: filesystem in, JSON out, no MCP call ever made here. The
|
|
caller runs `search` with the queries verbatim and `add_drawer` with the
|
|
payloads verbatim. No judgment lives in what text gets written — which is the
|
|
whole point, because a plugin that narrates its own bookkeeping at inference
|
|
time is the thing this plugin files issues about (FR-8.1).
|
|
|
|
The memory renderers are deliberately NOT `render_run`/`render_latest`. Those
|
|
emit `notes`, which is caller-supplied free text (`--note`), and
|
|
`falsifiability` prose is one field away. A wiki page lives in this plugin's
|
|
own repo; a drawer lands in a wing shared with unrelated projects. So the
|
|
memory renderers read from a fixed allowlist of structured snapshot fields —
|
|
ids, positions, booleans, counts, ratios — and no free-text field crosses into
|
|
a drawer at all (FR-3.5, FR-9.3). Allowlist, never blocklist: a blocklist is
|
|
one new snapshot field away from leaking.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
from ia_store import (DEFAULT_STORE, MIN_INVOCATIONS, MIN_SHARE_OF_SPEND,
|
|
audits_dir, build_index, candidate_signature, find_match,
|
|
load_snapshots, scan_rows, signature_hash, snapshots_path,
|
|
store_root, tiered)
|
|
|
|
# Relative change in the volume-normalized metrics below which a candidate is
|
|
# `persisting` rather than grown/shrunk. Sampling noise across two windows of
|
|
# different length is easily a few percent; 10% is the smallest band that does
|
|
# not turn every run into a trend.
|
|
NOISE_BAND = 0.10
|
|
|
|
STATUSES = ("new", "persisting", "grown", "shrunk", "resolved",
|
|
"signature-drifted", "stale", "claimed-fixed-unconfirmed",
|
|
# the step ran in one window and not the other: no continuous
|
|
# series, so no direction (FR-5.4)
|
|
"unmeasured")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# store
|
|
# --------------------------------------------------------------------------
|
|
|
|
def append_snapshot(store, target, snap):
|
|
path = snapshots_path(store, target)
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "a") as fh:
|
|
fh.write(json.dumps(snap, sort_keys=True) + "\n")
|
|
return path
|
|
|
|
|
|
def write_text(path, text):
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "w") as fh:
|
|
fh.write(text)
|
|
return path
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# write
|
|
# --------------------------------------------------------------------------
|
|
|
|
def tool_version():
|
|
manifest = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
".claude-plugin", "plugin.json")
|
|
try:
|
|
with open(manifest) as fh:
|
|
return json.load(fh).get("version")
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def measurements_by_signature(scan):
|
|
"""Every recurring signature the scan saw, keyed by hash — including ones
|
|
that no longer classify as candidates. FR-5.5 needs exactly this: a later
|
|
window's measured cost for a signature whose candidate is gone."""
|
|
total = scan.get("totals", {}).get("weighted_tokens") or 0
|
|
out = {}
|
|
for _name, row in scan_rows(scan):
|
|
for ng in row.get("ngrams", []):
|
|
h = signature_hash(ng["sig"])
|
|
invocations = ng.get("invocations", 0)
|
|
waste = ng.get("waste", 0)
|
|
out[h] = {
|
|
"invocations": invocations,
|
|
"offload_value": waste,
|
|
"share_of_plugin": round(waste / total, 4) if total else 0.0,
|
|
"cost_per_invocation": int(waste / invocations) if invocations else 0,
|
|
}
|
|
return out
|
|
|
|
|
|
def snapshot_candidate(cand, scan, explicit, issue_map):
|
|
sig, source = candidate_signature(cand, scan, explicit)
|
|
row = next((r for n, r in scan_rows(scan) if n == cand.get("skill")), {})
|
|
invocations = row.get("invocations", 0)
|
|
value = cand.get("offload_value", 0)
|
|
return {
|
|
"candidate_id": cand.get("candidate_id"),
|
|
"signature": sig,
|
|
"signature_source": source,
|
|
"signature_hash": signature_hash(sig),
|
|
"skill": cand.get("skill"),
|
|
"position": cand.get("position"),
|
|
"boundary_confidence": cand.get("boundary_confidence"),
|
|
"determinism_tests": cand.get("determinism_tests"),
|
|
"falsifiability": cand.get("falsifiability_triple"),
|
|
"measurement": {
|
|
"invocations": invocations,
|
|
"offload_waste": row.get("offload_waste", 0),
|
|
"offload_value": value,
|
|
"share_of_plugin": cand.get("share_of_audited_spend", 0.0),
|
|
# None, never 0, when the step did not run: a window in which a
|
|
# candidate was never invoked has no cost-per-invocation to report,
|
|
# and folding that to 0 makes "did not run" indistinguishable from
|
|
# "improved to zero" — the quiet-window artefact FR-5.4 exists to
|
|
# prevent, one level up from absolute tokens.
|
|
"cost_per_invocation": int(value / invocations) if invocations else None,
|
|
},
|
|
"measurement_strength": cand.get("measurement_strength"),
|
|
"digest_schema": cand.get("digest_schema"),
|
|
"escalation_path": cand.get("escalation_path"),
|
|
"issue": issue_map.get(cand.get("candidate_id")) or cand.get("issue"),
|
|
"verdict": cand.get("verdict"),
|
|
}
|
|
|
|
|
|
def build_snapshot(target, inventory, scan, classified, issue_map, explicit, run_id, repo,
|
|
extra_notes=()):
|
|
plugin = inventory.get("plugin", {})
|
|
scan_total = scan.get("totals", {})
|
|
|
|
rows = classified.get("candidates", [])
|
|
filed = [c for c in rows if c.get("verdict") == "file"]
|
|
questions = [c for c in rows if c.get("verdict") == "boundary-question"]
|
|
# Derived notes first, caller-supplied after. The caller's are outcomes of
|
|
# the filing pass — `filing: unavailable` and why (FR-6.4) — which this
|
|
# script cannot see and which have to reach the wiki page, not just the
|
|
# user's terminal.
|
|
# `insufficient-window` is in this list, not in `candidates` or
|
|
# `boundary_questions`: an FR-4.5 deferral is neither a filed candidate nor
|
|
# a question put to a human, and the wider snapshot it was deferred against
|
|
# remains the standing comparison point. Recording it as a note keeps the
|
|
# deferral visible on the wiki page instead of silently dropping the row.
|
|
notes = [
|
|
f"{c['candidate_id']}: {c['verdict']} — {c['reasons'][-1] if c.get('reasons') else ''}"
|
|
for c in rows if c.get("verdict") in ("no-offload", "drift-note", "insufficient-window")
|
|
] + list(extra_notes)
|
|
|
|
return {
|
|
"run_id": run_id,
|
|
"rubric_version": classified.get("rubric_version"),
|
|
"tool_versions": {
|
|
"inference-arbitrage": tool_version(),
|
|
"cc-tokens": scan.get("tool_versions", {}).get("cc_tokens_path"),
|
|
},
|
|
"target": {
|
|
"name": target,
|
|
"version": plugin.get("version"),
|
|
"source": plugin.get("source"),
|
|
"repo": repo,
|
|
# Not in plan §6, but FR-5.3's `stale` is uncomputable without it:
|
|
# "the skill no longer exists" needs the skill list of the run it
|
|
# no longer exists in.
|
|
"skills": sorted({s.get("name") for s in inventory.get("skills", []) if s.get("name")}
|
|
| {a.get("name") for a in inventory.get("agents", []) if a.get("name")}),
|
|
},
|
|
"window": scan.get("window", {}),
|
|
"coverage": {
|
|
"ratio": scan.get("coverage", {}).get("ratio"),
|
|
"attributed_turns": scan.get("coverage", {}).get("attributed_turns"),
|
|
},
|
|
"totals": {
|
|
"weighted_tokens": scan_total.get("weighted_tokens", 0),
|
|
"invocations": scan_total.get("invocations", 0),
|
|
"share_mechanical": scan_total.get("mechanical_share", 0.0),
|
|
"invocation_caveat": scan_total.get("invocation_caveat"),
|
|
},
|
|
"candidates": [snapshot_candidate(c, scan, explicit, issue_map) for c in filed],
|
|
"boundary_questions": [snapshot_candidate(c, scan, explicit, issue_map) for c in questions],
|
|
"measurements_by_signature": measurements_by_signature(scan),
|
|
"notes": notes,
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# diff (FR-5.3, FR-5.4, FR-5.5)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def rel(before, after):
|
|
"""None when the base is zero — a ratio against nothing is not a ratio.
|
|
Callers fall back to the sign of the absolute delta in that case."""
|
|
if not before:
|
|
return None
|
|
return round((after - before) / before, 4)
|
|
|
|
|
|
def metric_delta(prev_m, curr_m, key):
|
|
# A missing measurement dict means the candidate was absent from that
|
|
# snapshot entirely (new, or departed) and 0 is the right base. An explicit
|
|
# None means the candidate was present but not invoked — that is an
|
|
# absence of measurement and must not be folded to a number.
|
|
a = (prev_m or {}).get(key, 0)
|
|
b = (curr_m or {}).get(key, 0)
|
|
if a is None or b is None:
|
|
# At least one window has no measurement for this metric. There is no
|
|
# delta to take against an absence, and inventing one is how a window
|
|
# the step sat out becomes a reported improvement.
|
|
return {"from": a, "to": b, "delta": None, "rel": None, "unmeasured": True}
|
|
return {"from": a, "to": b, "delta": b - a, "rel": rel(a, b)}
|
|
|
|
|
|
def direction(primary, secondary):
|
|
"""Volume-normalized verdict (FR-5.4). cost-per-invocation leads: it is the
|
|
metric a quiet week cannot move. share-of-spend only breaks the tie, and
|
|
absolute tokens never vote."""
|
|
if primary.get("unmeasured"):
|
|
# The step was not invoked in one of the two windows, so the series is
|
|
# not continuous and no direction can be read from it.
|
|
return "unmeasured"
|
|
for m in (primary, secondary):
|
|
if m.get("unmeasured"):
|
|
continue
|
|
r = m["rel"]
|
|
if r is None:
|
|
# Base of zero: the ratio is undefined, so the sign of the move is
|
|
# all there is.
|
|
if m["delta"] > 0:
|
|
return "grown"
|
|
continue
|
|
if r > NOISE_BAND:
|
|
return "grown"
|
|
if r < -NOISE_BAND:
|
|
return "shrunk"
|
|
return "persisting"
|
|
|
|
|
|
def remeasured(prev_cand, curr_snap):
|
|
return (curr_snap.get("measurements_by_signature") or {}).get(
|
|
prev_cand.get("signature_hash"))
|
|
|
|
|
|
def departed_status(prev_cand, curr_snap, issue_state):
|
|
"""A candidate that is no longer on the list. `resolved` is the strict
|
|
FR-5.5 conjunction — issue closed AND remeasured below threshold with
|
|
enough invocations. Everything else that merely stopped being reported is
|
|
`claimed-fixed-unconfirmed`, with the missing leg named: committed is not
|
|
deployed, and closed is not fixed."""
|
|
skills = set((curr_snap.get("target") or {}).get("skills") or [])
|
|
if skills and prev_cand.get("skill") not in skills:
|
|
return "stale", (f"skill {prev_cand.get('skill')} is absent from "
|
|
f"{curr_snap['target'].get('name')} "
|
|
f"{curr_snap['target'].get('version')} — the candidate no longer exists")
|
|
|
|
issue = prev_cand.get("issue")
|
|
state = issue_state.get(issue) if issue else None
|
|
m = remeasured(prev_cand, curr_snap)
|
|
below = bool(m and m["invocations"] >= MIN_INVOCATIONS
|
|
and m["share_of_plugin"] < MIN_SHARE_OF_SPEND)
|
|
|
|
if state == "closed" and below:
|
|
return "resolved", (f"{issue} closed and remeasured at "
|
|
f"{m['share_of_plugin']:.2%} of spend over "
|
|
f"{m['invocations']} invocations")
|
|
if state == "closed":
|
|
if m:
|
|
return "claimed-fixed-unconfirmed", (
|
|
f"{issue} closed but remeasurement is {m['invocations']} invocation(s) at "
|
|
f"{m['share_of_plugin']:.2%} — needs >= {MIN_INVOCATIONS} invocations below "
|
|
f"{MIN_SHARE_OF_SPEND:.0%}")
|
|
return "claimed-fixed-unconfirmed", (
|
|
f"{issue} closed but the signature was not remeasured in this window")
|
|
if issue:
|
|
return "claimed-fixed-unconfirmed", (
|
|
f"dropped off the candidate list while {issue} is still {state or 'unknown'}")
|
|
return "claimed-fixed-unconfirmed", (
|
|
"dropped off the candidate list with no linked issue and no remeasurement below threshold")
|
|
|
|
|
|
def row_of(cand, tier, status, reason, matched_by, prev_cand=None):
|
|
prev_m = (prev_cand or {}).get("measurement")
|
|
curr_m = cand.get("measurement") if cand else None
|
|
return {
|
|
"candidate_id": (cand or prev_cand).get("candidate_id"),
|
|
"skill": (cand or prev_cand).get("skill"),
|
|
"tier": tier,
|
|
"status": status,
|
|
"matched_by": matched_by,
|
|
"issue": (cand or prev_cand).get("issue"),
|
|
"primary": {
|
|
"cost_per_invocation": metric_delta(prev_m, curr_m, "cost_per_invocation"),
|
|
"share_of_plugin": metric_delta(prev_m, curr_m, "share_of_plugin"),
|
|
},
|
|
"context_only": {
|
|
"offload_value": metric_delta(prev_m, curr_m, "offload_value"),
|
|
"invocations": metric_delta(prev_m, curr_m, "invocations"),
|
|
},
|
|
"reason": reason,
|
|
}
|
|
|
|
|
|
def diff_snapshots(prev, curr, issue_state):
|
|
prev_all = list(tiered(prev))
|
|
idx = build_index([c for c, _ in prev_all])
|
|
prev_tier = {c["candidate_id"]: t for c, t in prev_all}
|
|
seen = set()
|
|
rows = []
|
|
|
|
for cand, tier in tiered(curr):
|
|
match, by = find_match(cand, idx)
|
|
if not match:
|
|
rows.append(row_of(cand, tier, "new", "not present in the previous snapshot", None))
|
|
continue
|
|
seen.add(match["candidate_id"])
|
|
if match.get("signature_hash") != cand.get("signature_hash"):
|
|
reason = (f"tool-sequence signature changed "
|
|
f"{match.get('signature_hash')} -> {cand.get('signature_hash')} "
|
|
f"(matched by {by}); the step was refactored, so the cost series is not continuous")
|
|
rows.append(row_of(cand, tier, "signature-drifted", reason, by, match))
|
|
continue
|
|
row = row_of(cand, tier, "persisting", None, by, match)
|
|
row["status"] = direction(row["primary"]["cost_per_invocation"],
|
|
row["primary"]["share_of_plugin"])
|
|
row["reason"] = (f"cost/invocation {row['primary']['cost_per_invocation']['rel']}, "
|
|
f"share {row['primary']['share_of_plugin']['rel']} "
|
|
f"(band +/-{NOISE_BAND:.0%})")
|
|
rows.append(row)
|
|
|
|
for cand, tier in prev_all:
|
|
if cand["candidate_id"] in seen:
|
|
continue
|
|
status, reason = departed_status(cand, curr, issue_state)
|
|
rows.append(row_of(None, tier, status, reason, None, cand))
|
|
|
|
summary = {s: sum(1 for r in rows if r["status"] == s) for s in STATUSES}
|
|
unaddressed = [
|
|
r for r in rows
|
|
if r["tier"] == "candidate" and r["status"] in ("persisting", "grown", "signature-drifted")
|
|
]
|
|
promotions = [
|
|
r for r in rows
|
|
if r["tier"] == "boundary-question" and r["status"] == "grown"
|
|
and prev_tier.get(r["candidate_id"]) == "boundary-question"
|
|
]
|
|
|
|
prev_vol = prev.get("totals", {}).get("weighted_tokens", 0)
|
|
curr_vol = curr.get("totals", {}).get("weighted_tokens", 0)
|
|
|
|
return {
|
|
"target": curr.get("target", {}).get("name"),
|
|
"from": {"run_id": prev.get("run_id"), "window": prev.get("window"),
|
|
"rubric_version": prev.get("rubric_version"),
|
|
"target_version": prev.get("target", {}).get("version")},
|
|
"to": {"run_id": curr.get("run_id"), "window": curr.get("window"),
|
|
"rubric_version": curr.get("rubric_version"),
|
|
"target_version": curr.get("target", {}).get("version")},
|
|
"rubric_comparable": prev.get("rubric_version") == curr.get("rubric_version"),
|
|
"volume": {
|
|
"weighted_tokens": {"from": prev_vol, "to": curr_vol, "rel": rel(prev_vol, curr_vol)},
|
|
"note": ("absolute totals are context only — a quieter window lowers every absolute "
|
|
"number without anything improving. Per-candidate verdicts come from "
|
|
"cost-per-invocation and share-of-spend."),
|
|
},
|
|
"candidates": rows,
|
|
"summary": summary,
|
|
"unaddressed": [r["candidate_id"] for r in unaddressed],
|
|
"unaddressed_issues": sorted({r["issue"] for r in unaddressed if r["issue"]}),
|
|
"promotions": [r["candidate_id"] for r in promotions],
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# rendering
|
|
# --------------------------------------------------------------------------
|
|
|
|
def fmt_rel(r):
|
|
if r is None:
|
|
return "n/a"
|
|
return f"{r:+.0%}"
|
|
|
|
|
|
def fmt_cpi(v):
|
|
"""`not run`, never 0 — the step had no invocations in that window."""
|
|
return "not run" if v is None else f"{v:,}"
|
|
|
|
|
|
def candidate_table(rows):
|
|
if not rows:
|
|
return "— none —\n"
|
|
out = ["| Candidate | Status | Cost/invocation | Share of spend | Issue |",
|
|
"|---|---|---|---|---|"]
|
|
for r in rows:
|
|
cpi = r["primary"]["cost_per_invocation"]
|
|
share = r["primary"]["share_of_plugin"]
|
|
out.append(
|
|
f"| `{r['candidate_id']}` | {r['status']} | "
|
|
f"{fmt_cpi(cpi['from'])} → {fmt_cpi(cpi['to'])} ({fmt_rel(cpi['rel'])}) | "
|
|
f"{share['from']:.2%} → {share['to']:.2%} ({fmt_rel(share['rel'])}) | "
|
|
f"{r['issue'] or '—'} |")
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
def render_run(snap, diffdoc):
|
|
t = snap["target"]
|
|
w = snap.get("window", {})
|
|
lines = [
|
|
f"# {t['name']} — audit {snap['run_id']}",
|
|
"",
|
|
f"- Target: `{t['name']}` {t.get('version') or '?'} ({t.get('source') or '?'})"
|
|
+ (f", repo `{t['repo']}`" if t.get("repo") else ""),
|
|
f"- Window: {w.get('since')} → {w.get('until')}",
|
|
f"- Attribution coverage: {(snap['coverage'].get('ratio') or 0):.1%} "
|
|
f"({snap['coverage'].get('attributed_turns')} attributed turns) — "
|
|
"hook-driven and unattributed agent turns are excluded, so every figure below is a lower bound.",
|
|
f"- Mechanical share of audited spend: {(snap['totals'].get('share_mechanical') or 0):.1%}",
|
|
f"- Rubric v{snap.get('rubric_version')}, inference-arbitrage "
|
|
f"v{snap.get('tool_versions', {}).get('inference-arbitrage')}",
|
|
]
|
|
invocation_caveat = snap["totals"].get("invocation_caveat")
|
|
if invocation_caveat:
|
|
lines.append(f"- Note: {invocation_caveat}, so per-invocation figures below are "
|
|
"correspondingly inflated")
|
|
lines += [
|
|
"",
|
|
"## Candidates",
|
|
"",
|
|
]
|
|
for c in snap.get("candidates", []):
|
|
m = c["measurement"]
|
|
lines += [
|
|
f"### `{c['candidate_id']}`",
|
|
"",
|
|
f"- Skill: `{c['skill']}` — position {c['position']}, confidence {c['boundary_confidence']}",
|
|
f"- {m['offload_value']:,} weighted tokens of offload value over {m['invocations']} "
|
|
f"invocations ({fmt_cpi(m['cost_per_invocation'])}/invocation, "
|
|
f"{m['share_of_plugin']:.2%} of audited spend)",
|
|
f"- Evidence: {c.get('measurement_strength')}; signature `{c['signature_hash']}` "
|
|
f"({c.get('signature_source')})",
|
|
f"- Issue: {c.get('issue') or 'not yet filed'}",
|
|
"",
|
|
]
|
|
if not snap.get("candidates"):
|
|
lines += ["Nothing to offload here.", ""]
|
|
|
|
if snap.get("boundary_questions"):
|
|
lines += ["## Boundary questions", "",
|
|
"Reported, not filed — the rubric could not reach `high` confidence with an "
|
|
"overrule case.", ""]
|
|
for c in snap["boundary_questions"]:
|
|
lines.append(f"- `{c['candidate_id']}` (`{c['skill']}`) — {c['boundary_confidence']}, "
|
|
f"{c['measurement']['share_of_plugin']:.2%} of spend")
|
|
lines.append("")
|
|
|
|
if snap.get("notes"):
|
|
lines += ["## Notes", ""] + [f"- {n}" for n in snap["notes"]] + [""]
|
|
|
|
if diffdoc:
|
|
lines += ["## Change since " + str(diffdoc["from"]["run_id"]), "",
|
|
candidate_table(diffdoc["candidates"]), ""]
|
|
if not diffdoc["rubric_comparable"]:
|
|
lines.append(f"**Rubric version changed** "
|
|
f"({diffdoc['from']['rubric_version']} → {diffdoc['to']['rubric_version']}): "
|
|
"classifications either side of the change were not graded by the same "
|
|
"table and must not be compared naively.\n")
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def render_latest(snap, diffdoc, history):
|
|
t = snap["target"]
|
|
lines = [
|
|
f"# {t['name']} — latest audit",
|
|
"",
|
|
f"Regenerated from `Data/{t['name']}/snapshots.jsonl` — do not edit by hand.",
|
|
"",
|
|
f"- Latest run: `{snap['run_id']}` against {t['name']} {t.get('version') or '?'}",
|
|
f"- Runs recorded: {len(history)} (first `{history[0]['run_id']}`)",
|
|
f"- Open candidates: {len(snap.get('candidates', []))}; "
|
|
f"boundary questions: {len(snap.get('boundary_questions', []))}",
|
|
"",
|
|
]
|
|
if diffdoc:
|
|
s = diffdoc["summary"]
|
|
lines += [
|
|
"## Trend",
|
|
"",
|
|
f"Against `{diffdoc['from']['run_id']}`. Audited volume moved "
|
|
f"{fmt_rel(diffdoc['volume']['weighted_tokens']['rel'])} — context only; the verdicts "
|
|
"below are per-invocation and share-of-spend.",
|
|
"",
|
|
"| Status | Count |", "|---|---|",
|
|
] + [f"| {k} | {v} |" for k, v in s.items() if v] + [""]
|
|
if diffdoc["unaddressed"]:
|
|
refs = ", ".join(diffdoc["unaddressed_issues"]) or "none filed"
|
|
lines += [f"**{len(diffdoc['unaddressed'])} prior recommendation(s) still unaddressed** "
|
|
f"— {refs}", ""]
|
|
if diffdoc["promotions"]:
|
|
lines += ["**Boundary questions recurring with growing cost** (candidates for promotion): "
|
|
+ ", ".join(f"`{c}`" for c in diffdoc["promotions"]), ""]
|
|
lines += [candidate_table(diffdoc["candidates"]), ""]
|
|
|
|
lines += ["## Open candidates", "",
|
|
"| Candidate | Skill | Share of spend | Cost/invocation | Issue |",
|
|
"|---|---|---|---|---|"]
|
|
for c in snap.get("candidates", []):
|
|
m = c["measurement"]
|
|
lines.append(f"| `{c['candidate_id']}` | `{c['skill']}` | {m['share_of_plugin']:.2%} | "
|
|
f"{fmt_cpi(m['cost_per_invocation'])} | {c.get('issue') or '—'} |")
|
|
if not snap.get("candidates"):
|
|
lines.append("| — none — | | | | |")
|
|
lines += ["", "## Run history", "", "| Run | Target version | Coverage | Candidates |",
|
|
"|---|---|---|---|"]
|
|
for h in history[-10:]:
|
|
lines.append(f"| `{h['run_id']}` | {h['target'].get('version') or '?'} | "
|
|
f"{(h.get('coverage', {}).get('ratio') or 0):.1%} | "
|
|
f"{len(h.get('candidates', []))} |")
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def regenerate(store, target, history):
|
|
"""Dated page appended, Latest.md regenerated (plan §5)."""
|
|
snap = history[-1]
|
|
diffdoc = diff_snapshots(history[-2], snap, {}) if len(history) > 1 else None
|
|
day = str(snap["run_id"])[:10]
|
|
return [
|
|
write_text(os.path.join(audits_dir(store, target), f"{day}.md"), render_run(snap, diffdoc)),
|
|
write_text(os.path.join(audits_dir(store, target), "Latest.md"),
|
|
render_latest(snap, diffdoc, history)),
|
|
]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# memory (FR-9)
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Target-scoped: the audited plugin's own manifest name, mirroring FR-6.1's
|
|
# "the finding belongs on the target's surface, not this plugin's". The room is
|
|
# dedicated and namespaced by this plugin's name so that a wing collision — an
|
|
# audited plugin sharing a name with an unrelated wing, which really happens
|
|
# (`cluster`, `imagex`) — is a shelving overlap and never a content collision.
|
|
MEMORY_ROOM = "inference-arbitrage-audits"
|
|
|
|
# Cross-target: this plugin's own accumulated judgment, in the wing its own
|
|
# development history already lives in. Written every run regardless of target.
|
|
MEMORY_CROSS_WING = "claude-plugins"
|
|
MEMORY_CROSS_ROOM = "inference-arbitrage-lessons"
|
|
|
|
MEMORY_ADDED_BY = "inference-arbitrage"
|
|
|
|
# mempalace's search takes at most 250 characters of query.
|
|
QUERY_MAX = 250
|
|
|
|
REDACTION_NOTE = (
|
|
"Shapes and counts only — no transcript content, file contents, command "
|
|
"arguments, or user prose (FR-3.5). Rendered mechanically by "
|
|
"`audit-snapshot memory-notes`; nothing here was written at inference time."
|
|
)
|
|
|
|
ADVISORY_NOTE = (
|
|
"Results are advisory context for grading only. They never override "
|
|
"bin/boundary-classify (FR-4.2) or bin/stability-classify (FR-4.5), and a "
|
|
"prior drawer is never a reason to file, skip a marker search, or re-grade "
|
|
"a verdict (FR-9.2)."
|
|
)
|
|
|
|
|
|
def chunk_query(prefix, words):
|
|
"""Deterministic packing of names into queries that fit QUERY_MAX."""
|
|
out, cur = [], prefix
|
|
for w in words:
|
|
if len(cur) + 1 + len(w) > QUERY_MAX:
|
|
out.append(cur)
|
|
cur = prefix
|
|
cur = f"{cur} {w}" if cur else w
|
|
if cur != prefix:
|
|
out.append(cur)
|
|
return out
|
|
|
|
|
|
def surface_names(inventory, snap):
|
|
"""Skill and agent names, from the inventory if the caller has one (the
|
|
read step runs before any snapshot exists) and from the last snapshot
|
|
otherwise."""
|
|
if inventory:
|
|
names = {s.get("name") for s in inventory.get("skills", [])}
|
|
names |= {a.get("name") for a in inventory.get("agents", [])}
|
|
return sorted(n for n in names if n)
|
|
return sorted((snap or {}).get("target", {}).get("skills") or [])
|
|
|
|
|
|
def memory_queries(target, names):
|
|
q = [
|
|
{"wing": target, "room": MEMORY_ROOM,
|
|
"query": f"{target} offload audit declined candidates gotchas calibration"},
|
|
{"wing": MEMORY_CROSS_WING, "room": MEMORY_CROSS_ROOM,
|
|
"query": "offload audit calibration lessons false positives coverage dilution"},
|
|
{"wing": MEMORY_CROSS_WING, "room": MEMORY_CROSS_ROOM,
|
|
"query": f"{target} offload audit rubric verdict"},
|
|
]
|
|
for text in chunk_query(f"{target} offload candidates", names):
|
|
q.append({"wing": target, "room": MEMORY_ROOM, "query": text})
|
|
return q
|
|
|
|
|
|
def pct(x):
|
|
return f"{(x or 0):.1%}"
|
|
|
|
|
|
def share(x):
|
|
return f"{(x or 0):.2%}"
|
|
|
|
|
|
def tests_line(tests):
|
|
tests = tests or {}
|
|
return " ".join(f"{t} {'PASS' if tests.get(t) is True else 'FAIL'}"
|
|
for t in ("T1", "T2", "T3", "T4", "T5"))
|
|
|
|
|
|
def triple_line(f):
|
|
f = f or {}
|
|
return ", ".join(
|
|
f"{k.replace('_', ' ')} {'yes' if f.get(k) is True else 'no'}"
|
|
for k in ("signature", "examples", "overrule_case"))
|
|
|
|
|
|
def decline_reason(c, downgraded_ids=frozenset()):
|
|
"""Why the gate refused, read off the structured grade — never off the
|
|
graders' prose `reasons`, whose provenance is mixed.
|
|
|
|
Checks stay in FR-4.2's own order (overrule case, tests, confidence)
|
|
first — a candidate can be BOTH ungraded-for-filing AND coincidentally
|
|
named in a stability finding built from unrelated synthetic data, and
|
|
those real disqualifications must still win. Only the generic fallback at
|
|
the end — "did not clear the filing threshold" — is replaced when the
|
|
candidate is a genuine FR-4.5 downgrade, because a candidate that reaches
|
|
that fallback cleared every earlier check on its own grade: complete
|
|
triple, all determinism tests, `high` confidence. It cannot have failed
|
|
the filing threshold on its own merits and ALSO be one `stability-classify`
|
|
downgraded — real downgrades only ever touch candidates that already
|
|
cleared `boundary-classify`'s gate (verdict `file`), so reaching this line
|
|
while flagged in `downgraded_ids` means the fallback was always the wrong
|
|
reason for it and FR-4.5 is the real one. Fix for
|
|
kotkan/claude-plugin-inference-arbitrage#16, which contradicted the
|
|
correct FR-4.5 section rendered a few lines below it in the same drawer.
|
|
"""
|
|
f = c.get("falsifiability") or {}
|
|
tests = c.get("determinism_tests") or {}
|
|
if f.get("overrule_case") is not True:
|
|
return ("no overrule case — the falsifiability triple is incomplete, so the "
|
|
"hard FR-4.2 gate refused it")
|
|
failed = [t for t in ("T1", "T2", "T3", "T4", "T5") if tests.get(t) is not True]
|
|
if failed:
|
|
return (f"determinism test(s) {', '.join(failed)} did not hold — the cut is not "
|
|
"yet located")
|
|
if c.get("boundary_confidence") != "high":
|
|
return f"boundary confidence {c.get('boundary_confidence')} — reported, not filed"
|
|
if c.get("candidate_id") in downgraded_ids:
|
|
return ("cleared boundary-classify's gate on its own grade — downgraded "
|
|
"afterward by the evidence-stability gate (FR-4.5); see below")
|
|
return "did not clear the filing threshold in this window"
|
|
|
|
|
|
def cand_row(c):
|
|
m = c.get("measurement") or {}
|
|
return (f"| `{c.get('candidate_id')}` | `{c.get('skill')}` | {c.get('position')} | "
|
|
f"{c.get('boundary_confidence')} | {c.get('measurement_strength')} | "
|
|
f"{share(m.get('share_of_plugin'))} | {fmt_cpi(m.get('cost_per_invocation'))} | "
|
|
f"{c.get('issue') or '—'} |")
|
|
|
|
|
|
def render_memory_run(snap):
|
|
t = snap.get("target", {})
|
|
w = snap.get("window", {})
|
|
cov = snap.get("coverage", {})
|
|
tot = snap.get("totals", {})
|
|
lines = [
|
|
f"# inference-arbitrage audit — {t.get('name')} {t.get('version') or '?'} — run {snap.get('run_id')}",
|
|
"",
|
|
REDACTION_NOTE,
|
|
"",
|
|
f"- Target: `{t.get('name')}` {t.get('version') or '?'} ({t.get('source') or '?'})"
|
|
+ (f", repo `{t.get('repo')}`" if t.get("repo") else ""),
|
|
f"- Window: {w.get('since')} → {w.get('until')}",
|
|
f"- Attribution coverage: {pct(cov.get('ratio'))} of the turns in sessions where this "
|
|
f"plugin appeared ({cov.get('attributed_turns')} attributed turns) — a dilution "
|
|
"measure, not completeness.",
|
|
f"- Mechanical share of audited spend: {pct(tot.get('share_mechanical'))}",
|
|
f"- Audited volume: {tot.get('weighted_tokens', 0):,} weighted tokens over "
|
|
f"{tot.get('invocations', 0)} invocations",
|
|
f"- Rubric v{snap.get('rubric_version')}, inference-arbitrage "
|
|
f"v{snap.get('tool_versions', {}).get('inference-arbitrage')}",
|
|
"",
|
|
"## Filed candidates",
|
|
"",
|
|
]
|
|
header = ["| Candidate | Skill | Position | Confidence | Evidence | Share of spend "
|
|
"| Cost/invocation | Issue |", "|---|---|---|---|---|---|---|---|"]
|
|
filed = snap.get("candidates") or []
|
|
if filed:
|
|
lines += header + [cand_row(c) for c in filed]
|
|
else:
|
|
lines.append("— none — nothing cleared the filing gate in this window.")
|
|
lines += ["", "## Boundary questions — reported, never filed", ""]
|
|
questions = snap.get("boundary_questions") or []
|
|
if questions:
|
|
lines += header + [cand_row(c) for c in questions]
|
|
else:
|
|
lines.append("— none —")
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def render_memory_judgments(snap, findings):
|
|
t = snap.get("target", {})
|
|
questions = snap.get("boundary_questions") or []
|
|
if not questions and not findings:
|
|
return None
|
|
downgraded_ids = {f.get("candidate_id") for f in findings}
|
|
lines = [
|
|
f"# inference-arbitrage — judgment calls on {t.get('name')} (run {snap.get('run_id')})",
|
|
"",
|
|
"Why candidates were declined, so a later audit does not re-propose them without "
|
|
"new evidence. Every reason below is read off the structured grade, not off prose.",
|
|
"",
|
|
REDACTION_NOTE,
|
|
"",
|
|
]
|
|
if questions:
|
|
lines += ["## Declined — reported as boundary questions, never filed", ""]
|
|
for c in questions:
|
|
m = c.get("measurement") or {}
|
|
lines += [
|
|
f"- `{c.get('candidate_id')}` (`{c.get('skill')}`) — position "
|
|
f"{c.get('position')}, confidence {c.get('boundary_confidence')}, evidence "
|
|
f"{c.get('measurement_strength')} at {share(m.get('share_of_plugin'))} of "
|
|
f"audited spend over {m.get('invocations')} invocations.",
|
|
f" - Determinism tests: {tests_line(c.get('determinism_tests'))}",
|
|
f" - Falsifiability triple: {triple_line(c.get('falsifiability'))}",
|
|
f" - Declined because: {decline_reason(c, downgraded_ids)}",
|
|
]
|
|
lines.append("")
|
|
if findings:
|
|
lines += ["## Evidence-stability downgrades (FR-4.5)", "",
|
|
"The auditor described the same candidate two different ways depending on the "
|
|
"date range. That is a defect in this plugin, not a fact about the target.", ""]
|
|
for f in findings:
|
|
prior, curr = f.get("prior") or {}, f.get("current") or {}
|
|
lines += [
|
|
f"- `{f.get('candidate_id')}` (`{f.get('skill')}`), matched by "
|
|
f"{f.get('matched_by')}: {prior.get('measurement_strength')} at "
|
|
f"{share(prior.get('share_of_audited_spend'))} in run {prior.get('run_id')}, "
|
|
f"{curr.get('measurement_strength')} at "
|
|
f"{share(curr.get('share_of_audited_spend'))} in this window.",
|
|
f" - Self-reported on {(f.get('self_report') or {}).get('repo')}, marker "
|
|
f"`{(f.get('self_report') or {}).get('marker')}`",
|
|
]
|
|
lines.append("")
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def counted(items, key):
|
|
out = {}
|
|
for c in items:
|
|
out[c.get(key)] = out.get(c.get(key), 0) + 1
|
|
return ", ".join(f"{k} {v}" for k, v in sorted(out.items(), key=lambda kv: str(kv[0]))) or "none"
|
|
|
|
|
|
def render_memory_lessons(snap, findings):
|
|
t = snap.get("target", {})
|
|
cov = snap.get("coverage", {})
|
|
tot = snap.get("totals", {})
|
|
filed = snap.get("candidates") or []
|
|
questions = snap.get("boundary_questions") or []
|
|
every = filed + questions
|
|
|
|
lines = [
|
|
f"# inference-arbitrage — what the {t.get('name')} audit adds to the rubric's "
|
|
f"track record (run {snap.get('run_id')})",
|
|
"",
|
|
"Cross-target calibration record, written on every run regardless of outcome, so "
|
|
"this plugin's judgment accumulates independently of which plugin was audited.",
|
|
"",
|
|
REDACTION_NOTE,
|
|
"",
|
|
f"- Target: `{t.get('name')}` {t.get('version') or '?'} ({t.get('source') or '?'})",
|
|
f"- Rubric v{snap.get('rubric_version')}, inference-arbitrage "
|
|
f"v{snap.get('tool_versions', {}).get('inference-arbitrage')}",
|
|
f"- Coverage dilution {pct(cov.get('ratio'))} over {cov.get('attributed_turns')} "
|
|
f"attributed turns; mechanical share {pct(tot.get('share_mechanical'))}",
|
|
f"- Verdicts: {len(filed)} filed, {len(questions)} boundary questions",
|
|
f"- Positions seen: {counted(every, 'position')}",
|
|
f"- Evidence strengths: {counted(every, 'measurement_strength')}",
|
|
f"- Stability gate: {len(findings)} downgrade(s) this run",
|
|
"",
|
|
"## What this run says about the rubric",
|
|
"",
|
|
]
|
|
said = []
|
|
if not filed and (tot.get("share_mechanical") or 0) >= 0.2:
|
|
said.append(f"Nothing cleared the gate despite {pct(tot.get('share_mechanical'))} of "
|
|
"audited spend being mechanical — the conservative filing threshold held "
|
|
"against a target that looked like a candidate on the headline number.")
|
|
if not every:
|
|
said.append("No candidates at either tier: this target's boundary is already cut where "
|
|
"the rubric would put it.")
|
|
if findings:
|
|
said.append(f"{len(findings)} candidate(s) had window-dependent evidence, caught "
|
|
"mechanically by the stability gate rather than by human review.")
|
|
if len(questions) > len(filed):
|
|
said.append("More candidates were declined than filed at this rubric version — the "
|
|
"asymmetry (under-scripting costs money, over-scripting costs correctness) "
|
|
"is being applied, not just documented.")
|
|
if (cov.get("ratio") or 0) < 0.05:
|
|
said.append(f"Coverage dilution of {pct(cov.get('ratio'))} is normal for this "
|
|
"environment's long multi-topic sessions and is not a failed scan; the "
|
|
"honest completeness signal is the absolute count "
|
|
f"({cov.get('attributed_turns')} attributed turns).")
|
|
if not said:
|
|
said.append("Nothing anomalous: verdicts, evidence strengths and coverage all fell "
|
|
"inside the ranges the rubric was calibrated on.")
|
|
lines += [f"- {s}" for s in said]
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def memory_facts(snap, findings):
|
|
t = snap.get("target", {})
|
|
name = t.get("name")
|
|
day = str(snap.get("run_id"))[:10]
|
|
facts = [{"subject": name, "predicate": "audited_by", "object": "inference-arbitrage",
|
|
"valid_from": day}]
|
|
for c in snap.get("candidates") or []:
|
|
facts.append({"subject": c.get("candidate_id"), "predicate": "classified_as",
|
|
"object": c.get("position"), "valid_from": day})
|
|
if c.get("issue"):
|
|
facts.append({"subject": c.get("candidate_id"), "predicate": "filed_as",
|
|
"object": c.get("issue"), "valid_from": day})
|
|
for c in snap.get("boundary_questions") or []:
|
|
facts.append({"subject": c.get("candidate_id"), "predicate": "declined_as",
|
|
"object": "boundary-question", "valid_from": day})
|
|
for f in findings:
|
|
facts.append({"subject": f.get("candidate_id"), "predicate": "downgraded_by",
|
|
"object": "evidence-stability gate FR-4.5", "valid_from": day})
|
|
return facts
|
|
|
|
|
|
def memory_drawers(snap, findings):
|
|
t = snap.get("target", {})
|
|
name = t.get("name")
|
|
day = str(snap.get("run_id"))[:10]
|
|
src = f"inference-arbitrage/Audits/{name}/{day}.md"
|
|
out = [{
|
|
"kind": "run-summary",
|
|
"wing": name,
|
|
"room": MEMORY_ROOM,
|
|
"added_by": MEMORY_ADDED_BY,
|
|
"source_file": src,
|
|
"content": render_memory_run(snap),
|
|
}]
|
|
judgments = render_memory_judgments(snap, findings)
|
|
if judgments:
|
|
out.append({
|
|
"kind": "judgment-calls",
|
|
"wing": name,
|
|
"room": MEMORY_ROOM,
|
|
"added_by": MEMORY_ADDED_BY,
|
|
"source_file": src.replace(".md", ".judgments.md"),
|
|
"content": judgments,
|
|
})
|
|
out.append({
|
|
"kind": "cross-target-lessons",
|
|
"wing": MEMORY_CROSS_WING,
|
|
"room": MEMORY_CROSS_ROOM,
|
|
"added_by": MEMORY_ADDED_BY,
|
|
"source_file": src.replace(".md", ".lessons.md"),
|
|
"content": render_memory_lessons(snap, findings),
|
|
})
|
|
return out
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# commands
|
|
# --------------------------------------------------------------------------
|
|
|
|
def load_json(path):
|
|
with open(path) as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def cmd_write(args):
|
|
inventory = load_json(args.from_files[0])
|
|
scan = load_json(args.from_files[1])
|
|
classified = load_json(args.classified)
|
|
issue_map = load_json(args.issue_map) if args.issue_map else {}
|
|
explicit = load_json(args.signatures) if args.signatures else {}
|
|
|
|
run_id = args.run_id or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
history = load_snapshots(args.store, args.target)
|
|
if any(s.get("run_id") == run_id for s in history):
|
|
sys.exit(f"error: run_id {run_id} already recorded for {args.target}")
|
|
|
|
snap = build_snapshot(args.target, inventory, scan, classified,
|
|
issue_map, explicit, run_id, args.repo, args.note)
|
|
path = append_snapshot(args.store, args.target, snap)
|
|
history.append(snap)
|
|
pages = regenerate(args.store, args.target, history)
|
|
return {"run_id": run_id, "snapshots": path, "pages": pages,
|
|
"candidates": len(snap["candidates"]),
|
|
"boundary_questions": len(snap["boundary_questions"])}
|
|
|
|
|
|
def cmd_list(args):
|
|
history = load_snapshots(args.store, args.target)
|
|
return {
|
|
"target": args.target,
|
|
"runs": [{
|
|
"run_id": s.get("run_id"),
|
|
"target_version": s.get("target", {}).get("version"),
|
|
"rubric_version": s.get("rubric_version"),
|
|
"window": s.get("window", {}),
|
|
"coverage": s.get("coverage", {}).get("ratio"),
|
|
"weighted_tokens": s.get("totals", {}).get("weighted_tokens"),
|
|
"candidates": len(s.get("candidates", [])),
|
|
"boundary_questions": len(s.get("boundary_questions", [])),
|
|
} for s in history],
|
|
}
|
|
|
|
|
|
def cmd_diff(args):
|
|
history = load_snapshots(args.store, args.target)
|
|
if len(history) < 2:
|
|
sys.exit(f"error: {args.target} has {len(history)} snapshot(s); diff needs 2")
|
|
curr = history[-1]
|
|
if args.against:
|
|
prev = next((s for s in history if s.get("run_id") == args.against), None)
|
|
if prev is None:
|
|
sys.exit(f"error: no snapshot {args.against} for {args.target}")
|
|
else:
|
|
prev = history[-2]
|
|
issue_state = load_json(args.issue_state) if args.issue_state else {}
|
|
return diff_snapshots(prev, curr, issue_state)
|
|
|
|
|
|
def cmd_pages(args):
|
|
"""What to publish to the wiki, and with what content. The caller performs
|
|
the writes — see the module docstring on why this script never does."""
|
|
history = load_snapshots(args.store, args.target)
|
|
if not history:
|
|
sys.exit(f"error: no snapshots recorded for {args.target}")
|
|
snap = history[-1]
|
|
day = str(snap["run_id"])[:10]
|
|
root = store_root(args.store)
|
|
pages = [{
|
|
"page": f"Data/{args.target}/snapshots.jsonl",
|
|
"title": f"Data/{args.target}/snapshots.jsonl",
|
|
"append_only": True,
|
|
"local": snapshots_path(args.store, args.target),
|
|
}, {
|
|
"page": f"Audits/{args.target}/{day}",
|
|
"title": f"Audits/{args.target}/{day}",
|
|
"append_only": False,
|
|
"local": os.path.join(audits_dir(args.store, args.target), f"{day}.md"),
|
|
}, {
|
|
"page": f"Audits/{args.target}/Latest",
|
|
"title": f"Audits/{args.target}/Latest",
|
|
"append_only": False,
|
|
"local": os.path.join(audits_dir(args.store, args.target), "Latest.md"),
|
|
}]
|
|
for p in pages:
|
|
with open(p["local"]) as fh:
|
|
p["content"] = fh.read()
|
|
p["local"] = os.path.relpath(p["local"], root)
|
|
return {"target": args.target, "store": root, "run_id": snap["run_id"], "pages": pages}
|
|
|
|
|
|
def cmd_memory_queries(args):
|
|
"""What to search mempalace for before grading (FR-9.1). Runs before any
|
|
snapshot exists, so it takes the inventory rather than the store."""
|
|
inventory = load_json(args.inventory) if args.inventory else None
|
|
history = load_snapshots(args.store, args.target)
|
|
names = surface_names(inventory, history[-1] if history else None)
|
|
return {
|
|
"target": args.target,
|
|
"wings": {"target": args.target, "cross_target": MEMORY_CROSS_WING},
|
|
"rooms": {"target": MEMORY_ROOM, "cross_target": MEMORY_CROSS_ROOM},
|
|
"queries": memory_queries(args.target, names),
|
|
"advisory": ADVISORY_NOTE,
|
|
}
|
|
|
|
|
|
def cmd_memory_notes(args):
|
|
"""What to write to mempalace after a run (FR-9.1). Same contract as
|
|
`pages`: the caller makes the MCP calls with these payloads verbatim."""
|
|
history = load_snapshots(args.store, args.target)
|
|
if not history:
|
|
sys.exit(f"error: no snapshots recorded for {args.target}")
|
|
snap = history[-1]
|
|
findings = (load_json(args.stable).get("stability_findings", []) if args.stable else [])
|
|
return {
|
|
"target": args.target,
|
|
"run_id": snap["run_id"],
|
|
"drawers": memory_drawers(snap, findings),
|
|
"facts": memory_facts(snap, findings),
|
|
"note": "Call add_drawer once per drawer and kg_add once per fact, with these "
|
|
"fields verbatim. Do not summarize, reword, or add commentary — the "
|
|
"content is already the exact text to store.",
|
|
}
|
|
|
|
|
|
def cmd_import(args):
|
|
"""Seed the local store from a snapshots.jsonl fetched off the wiki, so a
|
|
cold cache still diffs against real history."""
|
|
with open(args.snapshots) as fh:
|
|
incoming = [json.loads(line) for line in fh if line.strip()]
|
|
# The only place a snapshot arrives from outside this script: a wiki page a
|
|
# human can edit. Validate here rather than defensively everywhere after.
|
|
for i, snap in enumerate(incoming, 1):
|
|
missing = [k for k in ("run_id", "target", "window", "coverage", "totals",
|
|
"candidates", "boundary_questions") if k not in snap]
|
|
if missing:
|
|
sys.exit(f"error: {args.snapshots}:{i}: not a snapshot record, missing {missing}")
|
|
existing = {s.get("run_id") for s in load_snapshots(args.store, args.target)}
|
|
added = [s for s in incoming if s.get("run_id") not in existing]
|
|
for snap in added:
|
|
append_snapshot(args.store, args.target, snap)
|
|
history = load_snapshots(args.store, args.target)
|
|
pages = regenerate(args.store, args.target, history) if history else []
|
|
return {"target": args.target, "imported": len(added), "skipped": len(incoming) - len(added),
|
|
"runs": len(history), "pages": pages}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(prog="audit-snapshot")
|
|
ap.add_argument("--store", default=DEFAULT_STORE,
|
|
help=f"local mirror of the wiki store (default {DEFAULT_STORE})")
|
|
ap.add_argument("--json", action="store_true", help="compact single-line JSON")
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
|
|
w = sub.add_parser("write", help="append one snapshot and regenerate the audit pages")
|
|
w.add_argument("--target", required=True)
|
|
w.add_argument("--from", dest="from_files", nargs=2, required=True,
|
|
metavar=("INVENTORY", "SCAN"))
|
|
w.add_argument("--classified", required=True, help="bin/boundary-classify output")
|
|
w.add_argument("--issue-map", help='JSON {candidate_id: "owner/repo#N"}')
|
|
w.add_argument("--signatures", help="JSON {candidate_id: [tool signature, ...]}")
|
|
w.add_argument("--repo", help="target's repo, owner/repo")
|
|
w.add_argument("--run-id", help="override the run id (tests; default is now, UTC)")
|
|
w.add_argument("--note", action="append", default=[], metavar="TEXT",
|
|
help="add a note to the snapshot and its summary page; repeatable. "
|
|
"Carries filing outcomes the audit knows and this script does not, "
|
|
"notably FR-6.4's `filing: unavailable` and its reason")
|
|
w.set_defaults(func=cmd_write)
|
|
|
|
ls = sub.add_parser("list", help="list recorded runs")
|
|
ls.add_argument("--target", required=True)
|
|
ls.set_defaults(func=cmd_list)
|
|
|
|
d = sub.add_parser("diff", help="diff the latest run against a prior one")
|
|
d.add_argument("--target", required=True)
|
|
d.add_argument("--against", help="run_id to compare against (default: previous)")
|
|
d.add_argument("--issue-state", help='JSON {"owner/repo#N": "open"|"closed"}')
|
|
d.set_defaults(func=cmd_diff)
|
|
|
|
p = sub.add_parser("pages", help="emit the wiki pages to publish, with content")
|
|
p.add_argument("--target", required=True)
|
|
p.set_defaults(func=cmd_pages)
|
|
|
|
mq = sub.add_parser("memory-queries",
|
|
help="emit the mempalace searches to run before grading (FR-9)")
|
|
mq.add_argument("--target", required=True)
|
|
mq.add_argument("--inventory", help="bin/plugin-inventory output, for skill/agent names")
|
|
mq.set_defaults(func=cmd_memory_queries)
|
|
|
|
mn = sub.add_parser("memory-notes",
|
|
help="emit the mempalace drawers and facts to write, with content (FR-9)")
|
|
mn.add_argument("--target", required=True)
|
|
mn.add_argument("--stable", help="bin/stability-classify output, for stability_findings")
|
|
mn.set_defaults(func=cmd_memory_notes)
|
|
|
|
i = sub.add_parser("import", help="seed the store from a wiki-fetched snapshots.jsonl")
|
|
i.add_argument("--target", required=True)
|
|
i.add_argument("--snapshots", required=True)
|
|
i.set_defaults(func=cmd_import)
|
|
|
|
args = ap.parse_args()
|
|
result = args.func(args)
|
|
json.dump(result, sys.stdout, indent=None if args.json else 2)
|
|
sys.stdout.write("\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|