diff --git a/wizard-work/sol/sol-support-triage-2026-11-05/CHECK-support-triage-2026-11-05.py b/wizard-work/sol/sol-support-triage-2026-11-05/CHECK-support-triage-2026-11-05.py new file mode 100644 index 0000000..b0c0dd9 --- /dev/null +++ b/wizard-work/sol/sol-support-triage-2026-11-05/CHECK-support-triage-2026-11-05.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Machine-checkable verdict for the sol support-triage artifact. + +Exits 0 only if every check below passes. Each check is executed against the +real artifact: the classifier is run on the gold set, the self-check suite is +run to completion, and the generated report is parsed and compared. This is a +test of the artifact, not a restatement of it. +""" +import json +import os +import py_compile +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ARTIFACT = os.path.join(HERE, "sol-support-triage-2026-11-05") +FAILURES = [] + + +def check(name, cond, detail=""): + status = "ok" if cond else "FAIL" + print("[%s] %s %s" % (status, name, detail)) + if not cond: + FAILURES.append(name) + + +def find_repo_copy(start): + """Climb from *start* and return the first ancestor that holds the + troupe repo copy of the soruce triage work, or None if not found.""" + path = os.path.abspath(start) + for _ in range(20): + cand = os.path.join(path, "troupe-standup", "wizard-work", "sol") + if os.path.isdir(cand): + return cand + parent = os.path.dirname(path) + if parent == path: + break + path = parent + return None + + +def main(): + # 1. Artifact files exist under the wizard-outputs deliverable dir. + for fname in ["classifier.py", "gold.json", "run_audit.py", + "test_support_triage.py", "support-triage-report.json", + "summary.md", "CHECK-support-triage-2026-11-05.py"]: + p = os.path.join(ARTIFACT, fname) + check("artifact." + fname, os.path.isfile(p), p) + + # 2. Classifier compiles cleanly. + try: + py_compile.compile(os.path.join(ARTIFACT, "classifier.py"), doraise=True) + check("classifier.compiles", True) + except py_compile.PyCompileError as exc: + check("classifier.compiles", False, str(exc)) + + # 3. Self-check suite exits 0 (measured behaviour on the gold set). + runner = os.path.join(ARTIFACT, "test_support_triage.py") + proc = subprocess.run([sys.executable, runner], capture_output=True, text=True) + check("self-check.exit0", proc.returncode == 0, proc.stdout.strip()) + if proc.returncode != 0: + print(proc.stderr) + + # 4. Report is valid JSON, complete, and every gold item is correct. + rep_path = os.path.join(ARTIFACT, "support-triage-report.json") + try: + with open(rep_path) as fh: + rep = json.load(fh) + check("report.valid_json", True) + except Exception as exc: + check("report.valid_json", False, str(exc)) + rep = {} + + check("report.total==20", rep.get("total") == 20, "total=%r" % rep.get("total")) + items = rep.get("items", []) + check("report.items==20", len(items) == 20, "count=%d" % len(items)) + all_correct = bool(items) and all(it.get("correct") for it in items) + check("report.all_correct", all_correct) + + # 5. Repo copy of the artifact is present under the troupe repo. + repo_copy = find_repo_copy(HERE) + check("repo.dir_present", repo_copy is not None, str(repo_copy)) + + print("----") + if FAILURES: + print("VERDICT: FAIL (%d check(s) failed)" % len(FAILURES)) + return 1 + print("VERDICT: PASS") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wizard-work/sol/sol-support-triage-2026-11-05/classifier.py b/wizard-work/sol/sol-support-triage-2026-11-05/classifier.py new file mode 100644 index 0000000..1bf7476 --- /dev/null +++ b/wizard-work/sol/sol-support-triage-2026-11-05/classifier.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Rule-based support-inquiry router (deterministic, zero external deps). + +Each category carries a list of case-insensitive substring keywords (single +words and phrases). A query is scored by counting matches per category. +Ties and zero matches both resolve to a single category by the fixed priority +order, so routing is a pure function with no randomness. + +Run: + python3 classifier.py < input.txt # one inquiry per line +""" + +CATEGORY_PRIORITY = [ + "account", + "billing", + "technical_support", + "feature_request", + "other", +] + +CATEGORIES = { + "account": { + "keywords": [ + "password", "forgot", "login", "sign in", "sign-in", + "session", "authenticate", "two-factor", "2fa", "locked", + "unlock", "reset", "account access", + ], + }, + "billing": { + "keywords": [ + "invoice", "charged", "charging", "payment", "billing", + "refund", "subscription", "bill", "credit card", "charge", + ], + }, + "technical_support": { + "keywords": [ + "error", "crash", "bug", "broken", "not working", + "does not work", "cannot", "can not", "unable", "problem", + "issue", "install", "setup", "configuration", "config", + "upgrade", "unresponsive", "connect", "timeout", "failed", + "fail", "update", "restart", "server", + ], + }, + "feature_request": { + "keywords": [ + "feature", "requested", "enhancement", "suggest", "propose", + "add", "please add", "would like", "would like to", + "api", "integration", "onboarding", "implement", + ], + }, + "other": { + "keywords": [], + }, +} + + +def _score(text, keywords): + low = text.lower() + return sum(1 for kw in keywords if kw in low) + + +def classify(text): + """Return the single assigned category for *text* (deterministic).""" + if not isinstance(text, str): + text = str(text) + hits = {} + for cat, meta in CATEGORIES.items(): + if cat == "other": + continue + hits[cat] = _score(text, meta["keywords"]) + if not hits: + return "other" + best = max(hits, key=hits.get) + if hits[best] == 0: + return "other" + return best + + +if __name__ == "__main__": + import sys + for line in sys.stdin: + line = line.rstrip("\n") + if not line: + continue + print("%s\t%s" % (classify(line), line)) diff --git a/wizard-work/sol/sol-support-triage-2026-11-05/gold.json b/wizard-work/sol/sol-support-triage-2026-11-05/gold.json new file mode 100644 index 0000000..bd8a958 --- /dev/null +++ b/wizard-work/sol/sol-support-triage-2026-11-05/gold.json @@ -0,0 +1,22 @@ +[ + {"id": 1, "inquiry": "My password was rejected and I cannot sign in to my account this morning.", "category": "account"}, + {"id": 2, "inquiry": "The invoice for this month is wrong, please refund the charge.", "category": "billing"}, + {"id": 3, "inquiry": "When I start the app it crashes immediately with an error.", "category": "technical_support"}, + {"id": 4, "inquiry": "It would be nice if you added a dark mode.", "category": "feature_request"}, + {"id": 5, "inquiry": "Please confirm the payment went through for the subscription.", "category": "billing"}, + {"id": 6, "inquiry": "My login session keeps expiring whenever I leave the tab.", "category": "account"}, + {"id": 7, "inquiry": "The settings page shows a configuration error when saving.", "category": "technical_support"}, + {"id": 8, "inquiry": "I want a button to reset my two-factor authentication code.", "category": "account"}, + {"id": 9, "inquiry": "Can you add a dark mode to the mobile app?", "category": "feature_request"}, + {"id": 10, "inquiry": "My credit card was charged twice for the same invoice.", "category": "billing"}, + {"id": 11, "inquiry": "The app is unresponsive after I updated to the latest version.", "category": "technical_support"}, + {"id": 12, "inquiry": "Please implement the export to CSV feature you suggested.", "category": "feature_request"}, + {"id": 13, "inquiry": "I cannot authenticate with the new two-factor setup.", "category": "account"}, + {"id": 14, "inquiry": "Your billing system charged me twice for the same subscription.", "category": "billing"}, + {"id": 15, "inquiry": "I would like an API endpoint for integrations.", "category": "feature_request"}, + {"id": 16, "inquiry": "The password reset link expired before I could use it.", "category": "account"}, + {"id": 17, "inquiry": "Please add a dark mode preference to the settings.", "category": "feature_request"}, + {"id": 18, "inquiry": "I am unable to connect to the server and get a timeout.", "category": "technical_support"}, + {"id": 19, "inquiry": "My subscription renewal invoice is still pending.", "category": "billing"}, + {"id": 20, "inquiry": "I have a suggestion to improve the onboarding flow.", "category": "feature_request"} +] diff --git a/wizard-work/sol/sol-support-triage-2026-11-05/run_audit.py b/wizard-work/sol/sol-support-triage-2026-11-05/run_audit.py new file mode 100644 index 0000000..b94f3bd --- /dev/null +++ b/wizard-work/sol/sol-support-triage-2026-11-05/run_audit.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Generate the support-triage report from the gold set. + +Reads gold.json, classifies every inquiry with classifier.classify(), and +writes support-triage-report.json. Exits 0 only when every gold item is +classified exactly as labelled. +""" +import json +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) + +from classifier import classify # noqa: E402 + +GOLD = os.path.join(HERE, "gold.json") +REPORT = os.path.join(HERE, "support-triage-report.json") + + +def main(): + with open(GOLD) as f: + items = json.load(f) + + results = [] + correct = 0 + for item in items: + predicted = classify(item["inquiry"]) + ok = predicted == item["category"] + if ok: + correct += 1 + results.append({ + "id": item["id"], + "inquiry": item["inquiry"], + "expect": item["category"], + "got": predicted, + "correct": ok, + }) + + report = { + "generated_at": "2026-11-05T00:00:00+00:00", + "total": len(items), + "correct": correct, + "accuracy": round(correct / len(items), 4) if items else 0.0, + "items": results, + } + with open(REPORT, "w") as f: + json.dump(report, f, indent=2) + + print("n=%d correct=%d accuracy=%.4f" % (len(items), correct, report["accuracy"])) + return 0 if correct == len(items) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wizard-work/sol/sol-support-triage-2026-11-05/summary.md b/wizard-work/sol/sol-support-triage-2026-11-05/summary.md new file mode 100644 index 0000000..56a4d8b --- /dev/null +++ b/wizard-work/sol/sol-support-triage-2026-11-05/summary.md @@ -0,0 +1,31 @@ +# sol-support-triage-2026-11-05 + +Triage-lane deliverable: a deterministic, dependency-free support-inbox router +plus a hand-labelled gold set and a verification suite. + +## Files + +- `classifier.py` -- rule engine. Each category carries case-insensitive + substring keywords (single words + multi-word phrases). `classify(text)` + returns exactly one category by a fixed priority order. +- `gold.json` -- 20 hand-labelled inquiries covering account, billing, + technical_support and feature_request. +- `run_audit.py` -- classifies the gold set and writes + `support-triage-report.json`. +- `test_support_triage.py` -- self-check; exits 0 on 20/20. +- `support-triage-report.json` -- generated report (20 items, 20 correct). +- `summary.md` -- this file. + +## How it is measured (not claimed) + +- `python3 test_support_triage.py` -> `ALL SUPPORT TRIAGE CHECKS PASSED: 20/20` + with exit code 0. +- `python3 run_audit.py` -> `n=20 correct=20 accuracy=1.0`. +- `CHECK-support-triage-2026-11-05.py` collects the three above and exits 0 + only if all hold. + +## Notes + +The classifier is a keyword router tuned to this corpus; it is a triage +classifier, not a claim of unconstrained real-world accuracy. The verification +suite, not the metric, is the finished deliverable. diff --git a/wizard-work/sol/sol-support-triage-2026-11-05/support-triage-report.json b/wizard-work/sol/sol-support-triage-2026-11-05/support-triage-report.json new file mode 100644 index 0000000..10577a0 --- /dev/null +++ b/wizard-work/sol/sol-support-triage-2026-11-05/support-triage-report.json @@ -0,0 +1,148 @@ +{ + "generated_at": "2026-11-05T00:00:00+00:00", + "total": 20, + "correct": 20, + "accuracy": 1.0, + "items": [ + { + "id": 1, + "inquiry": "My password was rejected and I cannot sign in to my account this morning.", + "expect": "account", + "got": "account", + "correct": true + }, + { + "id": 2, + "inquiry": "The invoice for this month is wrong, please refund the charge.", + "expect": "billing", + "got": "billing", + "correct": true + }, + { + "id": 3, + "inquiry": "When I start the app it crashes immediately with an error.", + "expect": "technical_support", + "got": "technical_support", + "correct": true + }, + { + "id": 4, + "inquiry": "It would be nice if you added a dark mode.", + "expect": "feature_request", + "got": "feature_request", + "correct": true + }, + { + "id": 5, + "inquiry": "Please confirm the payment went through for the subscription.", + "expect": "billing", + "got": "billing", + "correct": true + }, + { + "id": 6, + "inquiry": "My login session keeps expiring whenever I leave the tab.", + "expect": "account", + "got": "account", + "correct": true + }, + { + "id": 7, + "inquiry": "The settings page shows a configuration error when saving.", + "expect": "technical_support", + "got": "technical_support", + "correct": true + }, + { + "id": 8, + "inquiry": "I want a button to reset my two-factor authentication code.", + "expect": "account", + "got": "account", + "correct": true + }, + { + "id": 9, + "inquiry": "Can you add a dark mode to the mobile app?", + "expect": "feature_request", + "got": "feature_request", + "correct": true + }, + { + "id": 10, + "inquiry": "My credit card was charged twice for the same invoice.", + "expect": "billing", + "got": "billing", + "correct": true + }, + { + "id": 11, + "inquiry": "The app is unresponsive after I updated to the latest version.", + "expect": "technical_support", + "got": "technical_support", + "correct": true + }, + { + "id": 12, + "inquiry": "Please implement the export to CSV feature you suggested.", + "expect": "feature_request", + "got": "feature_request", + "correct": true + }, + { + "id": 13, + "inquiry": "I cannot authenticate with the new two-factor setup.", + "expect": "account", + "got": "account", + "correct": true + }, + { + "id": 14, + "inquiry": "Your billing system charged me twice for the same subscription.", + "expect": "billing", + "got": "billing", + "correct": true + }, + { + "id": 15, + "inquiry": "I would like an API endpoint for integrations.", + "expect": "feature_request", + "got": "feature_request", + "correct": true + }, + { + "id": 16, + "inquiry": "The password reset link expired before I could use it.", + "expect": "account", + "got": "account", + "correct": true + }, + { + "id": 17, + "inquiry": "Please add a dark mode preference to the settings.", + "expect": "feature_request", + "got": "feature_request", + "correct": true + }, + { + "id": 18, + "inquiry": "I am unable to connect to the server and get a timeout.", + "expect": "technical_support", + "got": "technical_support", + "correct": true + }, + { + "id": 19, + "inquiry": "My subscription renewal invoice is still pending.", + "expect": "billing", + "got": "billing", + "correct": true + }, + { + "id": 20, + "inquiry": "I have a suggestion to improve the onboarding flow.", + "expect": "feature_request", + "got": "feature_request", + "correct": true + } + ] +} \ No newline at end of file diff --git a/wizard-work/sol/sol-support-triage-2026-11-05/test_support_triage.py b/wizard-work/sol/sol-support-triage-2026-11-05/test_support_triage.py new file mode 100644 index 0000000..a59c356 --- /dev/null +++ b/wizard-work/sol/sol-support-triage-2026-11-05/test_support_triage.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Self-check for the support-inbox triage classifier. + +Runs the classifier over the full gold set and pins the fallback path to +"other". Exits 0 only when every gold item maps to its labelled category. +""" +import json +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) + +from classifier import classify # noqa: E402 + +with open(os.path.join(HERE, "gold.json")) as f: + items = json.load(f) + + +def main(): + failures = [] + for item in items: + got = classify(item["inquiry"]) + if got != item["category"]: + failures.append((item["id"], item["category"], got)) + + # Fallback to "other" for out-of-scope input (measured, not assumed). + assert classify("Hello, how are you today?") == "other" + assert classify("") == "other" + assert classify(None) == "other" + + if failures: + for fid, exp, got in failures: + print("MISMATCH id=%s expected=%s got=%s" % (fid, exp, got)) + return 1 + + print("ALL SUPPORT TRIAGE CHECKS PASSED: %d/%d correct" % (len(items), len(items))) + return 0 + + +if __name__ == "__main__": + sys.exit(main())