#!/usr/bin/env bash # bin/audit-snapshot memory-queries / memory-notes: the FR-9 mempalace bridge. # Everything deterministic is asserted here — wing/room resolution, the query # packing, which drawers exist for which run shape, the rendered content, and # the redaction allowlist. # # NOT tested here, and not fakeable in this harness: the MCP calls themselves. # `add_drawer`/`kg_add`/`search` run in the caller's session, not in a shell, # so no assertion below proves a drawer landed. That is deliberate — the whole # reason the payload is computed by a script is to make the untestable part one # verbatim tool call with no judgment in it. The round trip was verified once, # by hand, against the live palace (design/tasks.md 7.9.5). set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" BIN=../bin/audit-snapshot tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT fail=0 check() { local what=$1 got=$2 want=$3 if [ "$got" = "$want" ]; then echo " ok $what = $want" else echo " FAIL $what = $got (want $want)" >&2 fail=1 fi } py() { python3 -c "$@"; } # drawer drawer() { py " import json,sys d=json.load(open('$1')) x=[k for k in d['drawers'] if k['kind']=='$2'] print(x[0]['$3'] if x else 'ABSENT')" } # -------------------------------------------------------------------------- # Fixtures — one fake target 'synth' with three candidates: one filed, one # boundary question, one no-offload (whose reason prose reaches snapshot notes). # -------------------------------------------------------------------------- # The strings that must never reach a drawer. Each stands for a different # free-text path into the snapshot: an analyst reason, and a caller --note. LEAK_REASON="the user said deploy the staging key AKIAIOSFODNN7EXAMPLE to prod" LEAK_NOTE="filing: unavailable — grep -rn 'password=hunter2' /home/oleks/secrets" inventory() { py " import json json.dump({'plugin': {'name': 'synth', 'version': '1.2.0', 'source': 'oleks-local'}, 'skills': [{'name': 'synth:alpha'}, {'name': 'synth:beta'}], 'agents': [{'name': 'synth:gamma'}]}, open('$1','w'))" } scan() { py " import json json.dump({'window': {'since': '2026-07-01', 'until': '2026-07-29', 'days': 28}, 'plugin': 'synth', 'tool_versions': {'cc_tokens_path': '/synthetic/cc-tokens'}, 'coverage': {'attributed_turns': 400, 'candidate_turns': 20000, 'ratio': 0.02}, 'totals': {'weighted_tokens': 10000000, 'invocations': 30, 'mechanical_share': 0.41}, 'skills': [{'skill': 'synth:alpha', 'invocations': 20, 'offload_waste': 2000000, 'ngrams': [{'sig': ['list_issues', 'issue_read'], 'recurrences': 20, 'invocations': 20, 'waste': 2000000}]}, {'skill': 'synth:beta', 'invocations': 10, 'offload_waste': 400000, 'ngrams': [{'sig': ['glob', 'read'], 'recurrences': 10, 'invocations': 10, 'waste': 400000}]}], 'agents': []}, open('$1','w'))" } # classified [--only-filed] classified() { py " import json,sys only = '${2:-}' == '--only-filed' def cand(cid, skill, value, share, verdict, conf, overrule, t3=True): return {'candidate_id': cid, 'skill': skill, 'position': 'llm-over-script-digest', 'determinism_tests': {t: True for t in ('T1','T2','T4','T5')} | {'T3': t3}, 'falsifiability_triple': {'signature': True, 'examples': True, 'overrule_case': overrule}, 'boundary_confidence': conf, 'measurement_strength': 'measured', 'share_of_audited_spend': share, 'offload_value': value, 'escalation_path': 'escalate', 'digest_schema': 'typed rows', 'verdict': verdict, 'reasons': ['synthetic']} cands = [cand('synth/alpha/dispatch', 'synth:alpha', 2000000, 0.20, 'file', 'high', True)] if not only: cands.append(cand('synth/beta/triage', 'synth:beta', 400000, 0.04, 'boundary-question', 'medium', False)) nono = cand('synth/beta/prose', 'synth:beta', 0, 0.0, 'no-offload', 'low', False, t3=False) nono['reasons'] = ['''$LEAK_REASON'''] cands.append(nono) json.dump({'rubric_version': '1.0.0', 'target': 'synth', 'candidates': cands}, open('$1','w'))" } # stable — one stability finding, the FR-4.5 downgrade shape stable() { py " import json json.dump({'stability_findings': [{ 'gate': 'FR-4.5', 'candidate_id': 'synth/beta/triage', 'target': 'synth', 'skill': 'synth:beta', 'matched_by': 'slug', 'prior': {'run_id': '2026-07-01T00-00-00Z', 'window': {'since': '2026-06-01'}, 'measurement_strength': 'thin', 'share_of_audited_spend': 0.0}, 'current': {'window': {'since': '2026-07-01'}, 'measurement_strength': 'measured', 'share_of_audited_spend': 0.04}, 'flips': [{'field': 'measurement_strength', 'prior': 'thin', 'current': 'measured'}], 'self_report': {'repo': 'kotkan/claude-plugin-inference-arbitrage', 'marker': ''}}]}, open('$1','w'))" } inventory "$tmp/inv.json" scan "$tmp/scan.json" classified "$tmp/cls.json" stable "$tmp/stable.json" store=$tmp/store $BIN --store "$store" write --target synth --from "$tmp/inv.json" "$tmp/scan.json" \ --classified "$tmp/cls.json" --repo oleks/claude-plugin-synth \ --run-id 2026-07-29T12-00-00Z --note "$LEAK_NOTE" >/dev/null # -------------------------------------------------------------------------- echo "== (a) memory-queries: wing/room resolution and query packing ==" $BIN --store "$store" memory-queries --target synth --inventory "$tmp/inv.json" \ >"$tmp/q.json" check "target wing" "$(py "import json;print(json.load(open('$tmp/q.json'))['wings']['target'])")" \ "synth" check "cross-target wing" \ "$(py "import json;print(json.load(open('$tmp/q.json'))['wings']['cross_target'])")" \ "claude-plugins" check "target room" "$(py "import json;print(json.load(open('$tmp/q.json'))['rooms']['target'])")" \ "inference-arbitrage-audits" check "cross-target room" \ "$(py "import json;print(json.load(open('$tmp/q.json'))['rooms']['cross_target'])")" \ "inference-arbitrage-lessons" py " import json d=json.load(open('$tmp/q.json')) qs=d['queries'] assert all(len(q['query'])<=250 for q in qs), [len(q['query']) for q in qs] assert {q['wing'] for q in qs}=={'synth','claude-plugins'}, {q['wing'] for q in qs} joined=' '.join(q['query'] for q in qs) for n in ('synth:alpha','synth:beta','synth:gamma'): assert n in joined, n print(' ok every query fits 250 chars and names every skill and agent')" || fail=1 py " import json d=json.load(open('$tmp/q.json')) a=d['advisory'] assert 'never override' in a and 'FR-4.2' in a and 'FR-4.5' in a, a print(' ok advisory says results never override the mechanical gates')" || fail=1 # A cold store with no inventory still resolves wings — the read step must not # depend on a pass that has not run yet. $BIN --store "$tmp/cold" memory-queries --target other >"$tmp/q2.json" check "cold-store target wing" \ "$(py "import json;print(json.load(open('$tmp/q2.json'))['wings']['target'])")" "other" # -------------------------------------------------------------------------- echo "== (b) memory-notes: three drawers, correct wings and rooms ==" $BIN --store "$store" memory-notes --target synth --stable "$tmp/stable.json" >"$tmp/n.json" check "drawer count" "$(py "import json;print(len(json.load(open('$tmp/n.json'))['drawers']))")" "3" check "run-summary wing" "$(drawer "$tmp/n.json" run-summary wing)" "synth" check "run-summary room" "$(drawer "$tmp/n.json" run-summary room)" "inference-arbitrage-audits" check "judgment-calls wing" "$(drawer "$tmp/n.json" judgment-calls wing)" "synth" check "lessons wing" "$(drawer "$tmp/n.json" cross-target-lessons wing)" "claude-plugins" check "lessons room" "$(drawer "$tmp/n.json" cross-target-lessons room)" \ "inference-arbitrage-lessons" check "added_by" "$(drawer "$tmp/n.json" run-summary added_by)" "inference-arbitrage" check "source_file" "$(drawer "$tmp/n.json" run-summary source_file)" \ "inference-arbitrage/Audits/synth/2026-07-29.md" # -------------------------------------------------------------------------- echo "== (c) rendered content carries the numbers, framed as the plugin frames them ==" py " import json c=[d for d in json.load(open('$tmp/n.json'))['drawers'] if d['kind']=='run-summary'][0]['content'] assert 'synth/alpha/dispatch' in c assert 'oleks/claude-plugin-synth#' not in c # no issue filed in this fixture assert '20.00%' in c, 'share of spend missing' assert '41.0%' in c, 'mechanical share missing' assert 'dilution' in c and 'not completeness' in c, 'coverage must be framed as dilution' assert 'llm-over-script-digest' in c print(' ok run summary carries share, mechanical share, position, dilution framing')" || fail=1 py " import json c=[d for d in json.load(open('$tmp/n.json'))['drawers'] if d['kind']=='judgment-calls'][0]['content'] assert 'synth/beta/triage' in c assert 'no overrule case' in c, 'decline reason must come off the triple' assert 'T3 FAIL' not in c, 'triage passes T3; a wrong test line would be worse than none' assert 'Evidence-stability downgrades' in c assert '' in c assert 'kotkan/claude-plugin-inference-arbitrage' in c print(' ok judgment drawer names the decline reason and the stability downgrade')" || fail=1 py " import json c=[d for d in json.load(open('$tmp/n.json'))['drawers'] if d['kind']=='cross-target-lessons'][0]['content'] assert 'llm-over-script-digest 2' in c, 'positions must be counted across both tiers' assert 'measured 2' in c assert '1 downgrade(s)' in c assert 'not a failed scan' in c, '2% coverage must be explained, not reported bare' print(' ok lessons drawer counts positions across tiers and explains low coverage')" || fail=1 # -------------------------------------------------------------------------- echo "== (d) redaction: free-text snapshot fields never reach a drawer (FR-3.5) ==" # `notes` holds a caller --note and analyst reason prose. Both are in the # snapshot on disk; neither may cross into a wing shared with other projects. py " import json snap=[json.loads(l) for l in open('$store/Data/synth/snapshots.jsonl')][-1] joined=' '.join(snap['notes']) assert '''$LEAK_NOTE''' in joined, 'fixture is not exercising the leak path' assert 'AKIAIOSFODNN7EXAMPLE' in joined, 'fixture is not exercising the leak path' print(' ok the snapshot on disk really does contain the free text')" || fail=1 py " import json d=json.load(open('$tmp/n.json')) blob=json.dumps(d) for needle in ('AKIAIOSFODNN7EXAMPLE', 'hunter2', 'grep -rn', 'the user said', 'filing: unavailable'): assert needle not in blob, needle print(' ok no snapshot note, reason, or command argument appears in any drawer or fact')" || fail=1 py " import json d=json.load(open('$tmp/n.json')) blob=json.dumps(d) assert 'FR-3.5' in blob, 'each drawer states the constraint it was rendered under' print(' ok drawers carry the redaction note themselves')" || fail=1 # -------------------------------------------------------------------------- echo "== (e) knowledge-graph facts mirror the verdicts ==" py " import json facts=json.load(open('$tmp/n.json'))['facts'] t={(f['subject'],f['predicate'],f['object']) for f in facts} assert ('synth','audited_by','inference-arbitrage') in t assert ('synth/alpha/dispatch','classified_as','llm-over-script-digest') in t assert ('synth/beta/triage','declined_as','boundary-question') in t assert ('synth/beta/triage','downgraded_by','evidence-stability gate FR-4.5') in t assert all(f['valid_from']=='2026-07-29' for f in facts), facts assert not any(f['predicate']=='filed_as' for f in facts), 'nothing was filed in this fixture' print(' ok facts cover audited/classified/declined/downgraded, dated by run')" || fail=1 # -------------------------------------------------------------------------- echo "== (f) a clean run: no judgment drawer, but the lessons drawer still writes ==" # The cross-target record accumulates on every run — a 'nothing to offload' # result is exactly the calibration evidence worth keeping. classified "$tmp/cls2.json" --only-filed store2=$tmp/store2 $BIN --store "$store2" write --target synth --from "$tmp/inv.json" "$tmp/scan.json" \ --classified "$tmp/cls2.json" --run-id 2026-07-29T12-00-00Z >/dev/null $BIN --store "$store2" memory-notes --target synth >"$tmp/n2.json" check "drawer count" "$(py "import json;print(len(json.load(open('$tmp/n2.json'))['drawers']))")" "2" check "judgment drawer" "$(drawer "$tmp/n2.json" judgment-calls wing)" "ABSENT" check "lessons drawer still present" "$(drawer "$tmp/n2.json" cross-target-lessons wing)" \ "claude-plugins" # -------------------------------------------------------------------------- echo "== (g) memory-notes refuses a target with no history, rather than writing nothing ==" if $BIN --store "$tmp/empty" memory-notes --target synth >/dev/null 2>&1; then echo " FAIL memory-notes succeeded with no snapshots" >&2 fail=1 else echo " ok memory-notes exits non-zero with no snapshots recorded" fi # -------------------------------------------------------------------------- echo "== (h) regression #16: a stability-downgraded candidate gets its real reason ==" # The bug: decline_reason() only ever looked at a candidate's own grade, so a # candidate boundary-classify graded high/complete-triple (it cleared FR-4.2 # fine) but that stability-classify then downgraded fell through to the # generic "did not clear the filing threshold" line -- the opposite of what # happened, and a direct contradiction of the FR-4.5 section rendered a few # lines below it in the same drawer. py " import json cand = {'candidate_id': 'synth/gamma/flip', 'skill': 'synth:gamma', 'position': 'llm-over-script-digest', 'determinism_tests': {t: True for t in ('T1','T2','T3','T4','T5')}, 'falsifiability_triple': {'signature': True, 'examples': True, 'overrule_case': True}, 'boundary_confidence': 'high', 'measurement_strength': 'thin', 'share_of_audited_spend': 0.0, 'offload_value': 0, 'escalation_path': 'escalate', 'digest_schema': 'typed rows', # rewritten by stability-classify in real use -- 'file' cleared, then downgraded 'verdict': 'boundary-question', 'reasons': ['downgraded by stability-classify']} json.dump({'rubric_version': '1.0.0', 'target': 'synth', 'candidates': [cand]}, open('$tmp/cls3.json','w')) json.dump({'stability_findings': [{ 'gate': 'FR-4.5', 'candidate_id': 'synth/gamma/flip', 'target': 'synth', 'skill': 'synth:gamma', 'matched_by': 'slug', 'prior': {'run_id': '2026-07-01T00-00-00Z', 'window': {'since': '2026-06-01'}, 'measurement_strength': 'thin', 'share_of_audited_spend': 0.0}, 'current': {'window': {'since': '2026-07-01'}, 'measurement_strength': 'measured', 'share_of_audited_spend': 0.1185}, 'flips': [{'field': 'measurement_strength', 'prior': 'thin', 'current': 'measured'}], 'self_report': {'repo': 'kotkan/claude-plugin-inference-arbitrage', 'marker': ''}}]}, open('$tmp/stable3.json','w')) " store3=$tmp/store3 $BIN --store "$store3" write --target synth --from "$tmp/inv.json" "$tmp/scan.json" \ --classified "$tmp/cls3.json" --run-id 2026-07-29T12-00-00Z >/dev/null $BIN --store "$store3" memory-notes --target synth --stable "$tmp/stable3.json" >"$tmp/n3.json" py " import json c=[d for d in json.load(open('$tmp/n3.json'))['drawers'] if d['kind']=='judgment-calls'][0]['content'] assert 'did not clear the filing threshold' not in c, \ 'regression: a candidate stability downgraded was never actually rejected by the threshold' assert 'cleared boundary-classify' in c and 'FR-4.5' in c, \ 'the decline reason must point at the real cause' assert 'no overrule case' not in c, 'this candidate has a complete triple, that reason is wrong too' print(' ok stability-downgraded candidate reports the real reason, not the filing-threshold line')" || fail=1 exit "$fail"