Generalizable ranked-queue triage primitive (fingerprint + priority + state machine + regression detector). Backend-agnostic via SQLAlchemy Core (sqlite + postgres).
Project description
swarph-triage
Generalizable ranked-queue triage primitive — fingerprint, priority, state machine, regression detector. Backend-agnostic via SQLAlchemy Core (sqlite + postgres).
Status: ✅ 0.1.0 — functional. Public API, priority formula, state machine, regression detector, CLI, and FastAPI router are implemented and tested (sqlite + postgres via SQLAlchemy Core).
Extracted from a production error-triage system (scan logs → fingerprint → prioritize → work the backlog down) and generalized: the same ranked-dedup-queue pattern fits any high-volume stream of observations that should collapse into prioritized, dispositionable rows.
The pattern
A ranked queue where:
- Many concrete observations collapse to one logical row via a
fingerprint_fn. (Canonical case — error triage: 47 near-identical error log lines → 1 fingerprint withcount=47. The pattern generalizes to any stream where many raw events map to one actionable unit.) - Items rank by
severity × log(1+freq) × decay(age) × actionability— log on freq stops whales drowning fresh items, exp-decay on age makes hot items rise. All coefficients live in a calibration table (config-driven, not hardcoded). - A small explicit state machine (
new → triaged → approved → patched, with branches towontfixandneeds_review) — every transition logged tostate_log, no implicit "kinda done." - A regression detector — if a fingerprint with
status='patched'gets a new occurrence withinregression_grace_hours, resurrect tonewwithregression=1. Accepted dispositions don't silently mask returning problems. - Cooldown semantics — a
cooldown_untiltimestamp onlet_cooldispositions. The priority calc ramps back from zero as cooldown expires, so a deliberately-deferred item doesn't immediately re-surface.
Public API surface (planned)
from swarph_triage import open as open_triage
q = open_triage(
"postgresql://user:pass@host:5433/mydb", # or sqlite:///path.db
config={"decay_half_life_hours": 72.0},
proposer_fn=my_proposer, # optional, domain-specific
)
# ingest one observation
fp_id = q.ingest(
fingerprint="NullPointerError|auth.py|login",
severity="high",
actionability=0.7,
context={"module": "auth", "first_seen": "..."},
)
# disposition
q.transition(fp_id, to_status="approved", actor="oncall", note="fix queued")
# top-N for the UI
for row in q.list(limit=20):
print(row["fingerprint"], row["priority_score"], row["status"])
CLI:
swarph-triage list
swarph-triage show <id>
swarph-triage approve <id>
swarph-triage wontfix <id> "reason"
swarph-triage stats
swarph-triage backlog # writes markdown snapshot
swarph-triage history <id>
FastAPI routes (optional install: pip install swarph-triage[fastapi]):
from fastapi import FastAPI
from swarph_triage.fastapi import build_router
app = FastAPI()
app.include_router(build_router(q), prefix="/triage")
# → /list, /stats, /show/{id}, /{id}/{approve|wontfix|escalate|reopen}, /events (SSE)
Configuration
Everything tunable lives in swarph_triage.config.DEFAULT_CONFIG — override per-consumer at open():
DEFAULT_CONFIG = {
"decay_half_life_hours": 6.0, # 6h for hourly, 72h for daily
"severity_weights": {"critical": 1.0, "high": 0.7, "medium": 0.5, "low": 0.3},
"freq_curve": "log", # "log" | "linear" | "sqrt"
"freq_log_base": 10,
"actionability_floor": 0.1,
"regression_grace_hours": 24,
"cooldown_default_days": 14,
"priority_min": 0.0,
"priority_max": 100.0,
}
Layout
swarph_triage/
__init__.py — public API surface
config.py — DEFAULT_CONFIG + load/merge helpers
schema.py — SQLAlchemy Core table definitions (3 tables, 6 indexes)
state_machine.py — Status enum + valid transition matrix + side effects
priority.py — score formula + decay + recompute_all
regression.py — patched-then-reappearance detector
queue.py — TriageQueue main class (ingest, transition, list, show)
cli.py — argparse-driven CLI
fastapi.py — APIRouter factory (optional extra)
License
MIT. Pierre Samson + Claude, co-authored — matching the phawkes / fisherrao / tailcor lineage.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file swarph_triage-0.1.0.tar.gz.
File metadata
- Download URL: swarph_triage-0.1.0.tar.gz
- Upload date:
- Size: 25.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a0c8a500045c12fa8e01cf27179dd681281a987b57f9ab2fb448dcdfb8a1cb5
|
|
| MD5 |
9bee0a6a3312153350957d618c414c03
|
|
| BLAKE2b-256 |
0aa27d449e36c591e843d0287cdaaaa4a265f10db6d1b691d7656ce90f7f570c
|
File details
Details for the file swarph_triage-0.1.0-py3-none-any.whl.
File metadata
- Download URL: swarph_triage-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dba86d50b04570f6564d8a0577abc92b1a1ae40bd70d75965319e0a38c0a90c0
|
|
| MD5 |
93cdfa31a3c1f21a5f1035b4f008bb24
|
|
| BLAKE2b-256 |
b91c6716d9e2f1d27f90b304c8af9c6e6f0d4bfa66464d326dd4140b31fdb99a
|