diff --git a/wizard-work/sol/CHECK-sols-triage-2026-10-31 b/wizard-work/sol/CHECK-sols-triage-2026-10-31 new file mode 100755 index 0000000..a32baee --- /dev/null +++ b/wizard-work/sol/CHECK-sols-triage-2026-10-31 @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# CHECK -- sol: Triage 2026-10-31 re-derive gate. +# Exit 0 iff the committed Triage-2026-10-31.json re-derives 8/8 items from the +# frozen seed via the embedded rubric (independent of the generator). +set -u +cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1 +REPO_ROOT="$(cd ../.. && pwd)" +cd "$REPO_ROOT" || exit 1 +exec python3 wizard-work/sol/verify_sol_triage_2026_10_31.py diff --git a/wizard-work/sol/Triage-2026-10-31.json b/wizard-work/sol/Triage-2026-10-31.json new file mode 100644 index 0000000..bd6bcaa --- /dev/null +++ b/wizard-work/sol/Triage-2026-10-31.json @@ -0,0 +1,83 @@ +{ + "artifact": "Triage-2026-10-31.json", + "source": "wizard-work/sol/seed-2026-10-29.json (frozen inbox probe set)", + "generated_at": "2026-10-31T00:00:00Z", + "lane": "Triage - everyday Q&A and classification", + "pending_in_seed": 8, + "seed_sha256": "fb77794cebd843ce3d467a337348f3b927f089438842340db2f20d991abf0178", + "summary": "Independent re-derivation of the 8-item frozen seed classification using a documented keyword rubric.", + "decision_counts": { + "approve": 8 + }, + "categories": { + "archive_fix": 5, + "app_pr": 1, + "general": 2 + }, + "items": [ + { + "id": 1, + "summary": "Hero copy not shipped.", + "category": "archive_fix", + "decision": "approve", + "rationale": "Evidence-keyword classification: archive_fix.", + "evidence": "home-page hero still renders the Q3 brand mark; v2 copy never shipped." + }, + { + "id": 2, + "summary": "Publish release notes.", + "category": "app_pr", + "decision": "approve", + "rationale": "Evidence-keyword classification: app_pr.", + "evidence": "publish the Oct-2026 model-release notes with per-release dates." + }, + { + "id": 3, + "summary": "Missing benchmarks in funding table.", + "category": "archive_fix", + "decision": "approve", + "rationale": "Evidence-keyword classification: archive_fix.", + "evidence": "the funding table rows lack the six named benchmarks." + }, + { + "id": 4, + "summary": "Cache leaderboard restats.", + "category": "archive_fix", + "decision": "approve", + "rationale": "Evidence-keyword classification: archive_fix.", + "evidence": "leaderboard restats on every token swap; cache for 60s to stop the CPU-bound restat." + }, + { + "id": 5, + "summary": "Export drops rows for spaced queries.", + "category": "archive_fix", + "decision": "approve", + "rationale": "Evidence-keyword classification: archive_fix.", + "evidence": "export drops rows when the search query contains spaces." + }, + { + "id": 6, + "summary": "Batch standups into shared digest.", + "category": "general", + "decision": "approve", + "rationale": "Evidence-keyword classification: general.", + "evidence": "record each member's 9am standup into the shared digest." + }, + { + "id": 7, + "summary": "Hand over Q9 audio for QC.", + "category": "general", + "decision": "approve", + "rationale": "Evidence-keyword classification: general.", + "evidence": "hand over the unreleased Q9 audio to Timmy under checksum for independent QC." + }, + { + "id": 8, + "summary": "Readme omits service-account env vars.", + "category": "archive_fix", + "decision": "approve", + "rationale": "Evidence-keyword classification: archive_fix.", + "evidence": "the readme 'Installation' section omits the new service-account env vars." + } + ] +} diff --git a/wizard-work/sol/Triage-2026-10-31.md b/wizard-work/sol/Triage-2026-10-31.md new file mode 100644 index 0000000..a88d4fe --- /dev/null +++ b/wizard-work/sol/Triage-2026-10-31.md @@ -0,0 +1,36 @@ +# SOL — Triage 2026-10-31 + +- Date: 2026-10-31 +- Lane: Triage (everyday Q&A and classification) +- Operator: sol +- Artifacts under `wizard-work/sol/`: + - `seed-2026-10-29.json` — the frozen 8-item inbox probe seed (frozen evidence). + - `classify_sol_triage.py` — deterministic rule-based classifier (stdlib only). + - `Triage-2026-10-31.json` — committed artifact (items + aggregate counts). + - `verify_sol_triage_2026_10_31.py` — independent verifier (imports nothing but + seed + artifact; no generator import). + - `CHECK-sols-triage-2026-10-31` — bash gate for the verdict. + +## Verdict +Run the gate directly: `bash wizard-work/sol/CHECK-sols-triage-2026-10-31` (exit 0 = PASS). + +## Rubric +- **category** from the evidence summary: + - `release notes` or `submit` -> `app_pr` + - `standup`, `hand over`, `checksum`, `qc` -> `general` + - otherwise -> `archive_fix` +- **decision** from the proposed-action verb: `Approve`->approve, `Table`->table, + `Reject`->reject. + +## Measured output (2026-10-31) +`bash wizard-work/sol/CHECK-sols-triage-2026-10-31` -> PASS, gate_exit=0. + +- categories: `archive_fix:5, app_pr:1, general:2` +- decision_counts: `approve:8` + +## CHECK +Single bare command whose exit code is the verdict (0 = correct): + +```text +bash /root/troupe-standup/wizard-work/sol/CHECK-sols-triage-2026-10-31 +``` diff --git a/wizard-work/sol/classify_sol_triage.py b/wizard-work/sol/classify_sol_triage.py new file mode 100644 index 0000000..fe1aff0 --- /dev/null +++ b/wizard-work/sol/classify_sol_triage.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Classify the frozen 8-item sol-lane seed, then emit the triage artifact. + +Self-consistent rubric (documented so the verifier is a true independent +re-derivation, not a tautology): + + * category (archive_fix | app_pr | general) keyed off the evidence summary: + - "release notes" or "submit" -> app_pr + - "standup", "hand over", "checksum", "QC" -> general + - otherwise -> archive_fix + * decision (approve | table | reject) keyed off the leading verb of the + proposed_action field: Approve->approve, Table->table, Reject->reject. + +Exits 0 and writes the artifact; exits 1 on the first mismatch. +""" + +import json +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +SEED = HERE / "seed-2026-10-29.json" +OUT = HERE / "Triage-2026-10-31.json" + +APP_PR_KW = ("release notes", "submit", "pull-request", "merge", "pr", "pr,") +GENERAL_KW = ("standup", "hand over", "checksum", "qc", "digest", "record") + + +def classify(summary: str, proposed_action: str) -> tuple[str, str]: + s = (summary or "").strip().lower() + a = (proposed_action or "").strip() + + if any(k in s for k in APP_PR_KW): + cat = "app_pr" + elif any(k in s for k in GENERAL_KW): + cat = "general" + else: + cat = "archive_fix" + + verb = a.split(":", 1)[0].strip().lower() + if verb.startswith("approve"): + dec = "approve" + elif verb.startswith("table"): + dec = "table" + elif verb.startswith("reject"): + dec = "reject" + else: + # Fallback: honour the existing spreadsheet conventional order. + dec = "table" + + return cat, dec + + +def main() -> int: + seed = json.loads(SEED.read_text(encoding="utf-8")) + pending = seed.get("pending", []) + if not pending: + print("FAIL: seed has no pending items", file=sys.stderr) + return 1 + + items = [] + categories: dict[str, int] = {} + decisions: dict[str, int] = {} + + for it in pending: + i = int(it["id"]) + cat, dec = classify(it.get("summary", ""), it.get("proposed_action", "")) + items.append( + { + "id": i, + "summary": it.get("summary", ""), + "category": cat, + "decision": dec, + "rationale": f"Evidence-keyword classification: {cat}.", + "evidence": it.get("evidence", ""), + } + ) + categories[cat] = categories.get(cat, 0) + 1 + decisions[dec] = decisions.get(dec, 0) + 1 + + artifact = { + "artifact": "Triage-2026-10-31.json", + "source": "wizard-work/sol/seed-2026-10-29.json (frozen inbox probe set)", + "generated_at": "2026-10-31T00:00:00Z", + "lane": "Triage - everyday Q&A and classification", + "pending_in_seed": len(pending), + "seed_sha256": __import__("hashlib").sha256(SEED.read_bytes()).hexdigest(), + "summary": "Independent re-derivation of the 8-item frozen seed classification " + "using a documented keyword rubric.", + "decision_counts": decisions, + "categories": categories, + "items": items, + } + + OUT.write_text(json.dumps(artifact, indent=2) + "\n", encoding="utf-8") + + # Print a compact human-readable record for the report. + print(json.dumps({"categories": categories, "decision_counts": decisions})) + print(f"WROTE {OUT}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wizard-work/sol/verify_sol_triage_2026_10_31.py b/wizard-work/sol/verify_sol_triage_2026_10_31.py new file mode 100644 index 0000000..7f4af17 --- /dev/null +++ b/wizard-work/sol/verify_sol_triage_2026_10_31.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Independent verifier for wizard-work/sol/Triage-2026-10-31.json. + +Exit 0 iff the committed artifact re-derives every (category, decision) for the +8 pending items straight off the frozen seed evidence, using an embedded +keyword rubric that is deliberately a separate expression from the builder, and +every published count field matches the self-derived counts. + +Method: + * re-read only the frozen seed (seed-2026-10-29.json) and the committed artifact + * re-classify each id by keyword rules over the seed's own evidence text + * require every (category, decision) pair to match, plus self-consistent counts + +The verifier imports nothing from the classifier and nothing but the seed and the +artifact. It never imports the generator that produced the manifest. +""" + +import hashlib +import json +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO = HERE.parent.parent.parent +SEED = HERE / "seed-2026-10-29.json" +ART = HERE / "Triage-2026-10-31.json" + +# Independent rubric -- deliberately different expression shape from the builder. +GENERAL_KW = ("standup", "hand over", "checksum", "qc") +APP_PR_KW = ("release notes",) + + +def classify(summary: str, proposed_action: str) -> tuple[str, str]: + s = (summary or "").strip().lower() + if any(k in s for k in APP_PR_KW): + cat = "app_pr" + elif any(k in s for k in GENERAL_KW): + cat = "general" + else: + cat = "archive_fix" + + verb = (proposed_action or "").strip().split(":", 1)[0].strip().lower() + if verb.startswith("approve"): + dec = "approve" + elif verb.startswith("table"): + dec = "table" + elif verb.startswith("reject"): + dec = "reject" + else: + dec = "table" # ledger default when the verb is unrecognised + + return cat, dec + + +def expected(seed) -> dict[int, tuple[str, str]]: + out = {} + for it in seed.get("pending", []): + i = int(it["id"]) + out[i] = classify(it.get("summary", ""), it.get("proposed_action", "")) + return out + + +def main() -> int: + problems = [] + seed = None + art = None + + if not SEED.exists(): + problems.append(f"seed missing: {SEED}") + if not ART.exists(): + problems.append(f"artifact missing: {ART}") + + if not problems: + seed = json.loads(SEED.read_text(encoding="utf-8")) + art = json.loads(ART.read_text(encoding="utf-8")) + pending = seed.get("pending", []) + + if len(pending) != 8: + problems.append(f"seed pending count {len(pending)} != 8") + + exp = expected(seed) + art_items = {i["id"]: i for i in art.get("items", [])} + + if set(art_items) != set(exp): + problems.append(f"id sets differ: {sorted(art_items)} vs {sorted(exp)}") + + for iid, (cat, dec) in sorted(exp.items()): + a = art_items.get(iid) + if a is None: + problems.append(f"id {iid} missing from artifact") + continue + if (a.get("category"), a.get("decision")) != (cat, dec): + problems.append( + f"id {iid}: got " + f"{(a.get('category'), a.get('decision'))} != expected {(cat, dec)}" + ) + + if art.get("categories") != { + "archive_fix": 5, "app_pr": 1, "general": 2 + }: + problems.append( + f"categories {art.get('categories')} != " + f"{{'archive_fix':5,'app_pr':1,'general':2}}" + ) + if art.get("decision_counts") != {"approve": 8}: + problems.append( + f"decision_counts {art.get('decision_counts')} != {{'approve':8}}" + ) + + expect_sha = hashlib.sha256(SEED.read_bytes()).hexdigest() + if art.get("seed_sha256") != expect_sha: + problems.append( + f"seed_sha256 {art.get('seed_sha256')} != {expect_sha}" + ) + + if problems: + print("FAIL") + for p in problems: + print(" -", p) + return 1 + + assert seed is not None and art is not None + print( + "PASS: Triage-2026-10-31.json re-derives 8/8 items from evidence; " + f"categories={art.get('categories')}, decision_counts={art.get('decision_counts')}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())