swarph-triage 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. swarph_triage-0.1.0/LICENSE +21 -0
  2. swarph_triage-0.1.0/PKG-INFO +129 -0
  3. swarph_triage-0.1.0/README.md +102 -0
  4. swarph_triage-0.1.0/pyproject.toml +47 -0
  5. swarph_triage-0.1.0/setup.cfg +4 -0
  6. swarph_triage-0.1.0/swarph_triage/__init__.py +28 -0
  7. swarph_triage-0.1.0/swarph_triage/_version.py +1 -0
  8. swarph_triage-0.1.0/swarph_triage/cli.py +116 -0
  9. swarph_triage-0.1.0/swarph_triage/config.py +63 -0
  10. swarph_triage-0.1.0/swarph_triage/fastapi.py +56 -0
  11. swarph_triage-0.1.0/swarph_triage/priority.py +133 -0
  12. swarph_triage-0.1.0/swarph_triage/queue.py +536 -0
  13. swarph_triage-0.1.0/swarph_triage/regression.py +82 -0
  14. swarph_triage-0.1.0/swarph_triage/schema.py +94 -0
  15. swarph_triage-0.1.0/swarph_triage/state_machine.py +43 -0
  16. swarph_triage-0.1.0/swarph_triage.egg-info/PKG-INFO +129 -0
  17. swarph_triage-0.1.0/swarph_triage.egg-info/SOURCES.txt +28 -0
  18. swarph_triage-0.1.0/swarph_triage.egg-info/dependency_links.txt +1 -0
  19. swarph_triage-0.1.0/swarph_triage.egg-info/entry_points.txt +2 -0
  20. swarph_triage-0.1.0/swarph_triage.egg-info/requires.txt +9 -0
  21. swarph_triage-0.1.0/swarph_triage.egg-info/top_level.txt +1 -0
  22. swarph_triage-0.1.0/tests/test_cli.py +105 -0
  23. swarph_triage-0.1.0/tests/test_edges.py +80 -0
  24. swarph_triage-0.1.0/tests/test_fastapi.py +89 -0
  25. swarph_triage-0.1.0/tests/test_ingest.py +173 -0
  26. swarph_triage-0.1.0/tests/test_maintenance.py +90 -0
  27. swarph_triage-0.1.0/tests/test_priority.py +171 -0
  28. swarph_triage-0.1.0/tests/test_reads.py +123 -0
  29. swarph_triage-0.1.0/tests/test_smoke.py +99 -0
  30. swarph_triage-0.1.0/tests/test_transition.py +124 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pierre Samson and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: swarph-triage
3
+ Version: 0.1.0
4
+ Summary: Generalizable ranked-queue triage primitive (fingerprint + priority + state machine + regression detector). Backend-agnostic via SQLAlchemy Core (sqlite + postgres).
5
+ Author: Pierre Samson, Claude
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/darw007d/swarph-triage
8
+ Project-URL: Issues, https://github.com/darw007d/swarph-triage/issues
9
+ Keywords: triage,queue,priority,state-machine,swarph
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Topic :: Database
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: sqlalchemy>=2.0
20
+ Provides-Extra: fastapi
21
+ Requires-Dist: fastapi>=0.110; extra == "fastapi"
22
+ Requires-Dist: sse-starlette>=2.0; extra == "fastapi"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # swarph-triage
29
+
30
+ > Generalizable ranked-queue triage primitive — fingerprint, priority, state machine, regression detector. Backend-agnostic via SQLAlchemy Core (sqlite + postgres).
31
+
32
+ **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).
33
+
34
+ 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.
35
+
36
+ ## The pattern
37
+
38
+ A ranked queue where:
39
+
40
+ 1. **Many concrete observations collapse to one logical row** via a `fingerprint_fn`. (Canonical case — error triage: 47 near-identical error log lines → 1 fingerprint with `count=47`. The pattern generalizes to any stream where many raw events map to one actionable unit.)
41
+ 2. **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).
42
+ 3. **A small explicit state machine** (`new → triaged → approved → patched`, with branches to `wontfix` and `needs_review`) — every transition logged to `state_log`, no implicit "kinda done."
43
+ 4. **A regression detector** — if a fingerprint with `status='patched'` gets a new occurrence within `regression_grace_hours`, resurrect to `new` with `regression=1`. Accepted dispositions don't silently mask returning problems.
44
+ 5. **Cooldown semantics** — a `cooldown_until` timestamp on `let_cool` dispositions. The priority calc ramps back from zero as cooldown expires, so a deliberately-deferred item doesn't immediately re-surface.
45
+
46
+ ## Public API surface (planned)
47
+
48
+ ```python
49
+ from swarph_triage import open as open_triage
50
+
51
+ q = open_triage(
52
+ "postgresql://user:pass@host:5433/mydb", # or sqlite:///path.db
53
+ config={"decay_half_life_hours": 72.0},
54
+ proposer_fn=my_proposer, # optional, domain-specific
55
+ )
56
+
57
+ # ingest one observation
58
+ fp_id = q.ingest(
59
+ fingerprint="NullPointerError|auth.py|login",
60
+ severity="high",
61
+ actionability=0.7,
62
+ context={"module": "auth", "first_seen": "..."},
63
+ )
64
+
65
+ # disposition
66
+ q.transition(fp_id, to_status="approved", actor="oncall", note="fix queued")
67
+
68
+ # top-N for the UI
69
+ for row in q.list(limit=20):
70
+ print(row["fingerprint"], row["priority_score"], row["status"])
71
+ ```
72
+
73
+ CLI:
74
+ ```
75
+ swarph-triage list
76
+ swarph-triage show <id>
77
+ swarph-triage approve <id>
78
+ swarph-triage wontfix <id> "reason"
79
+ swarph-triage stats
80
+ swarph-triage backlog # writes markdown snapshot
81
+ swarph-triage history <id>
82
+ ```
83
+
84
+ FastAPI routes (optional install: `pip install swarph-triage[fastapi]`):
85
+ ```python
86
+ from fastapi import FastAPI
87
+ from swarph_triage.fastapi import build_router
88
+
89
+ app = FastAPI()
90
+ app.include_router(build_router(q), prefix="/triage")
91
+ # → /list, /stats, /show/{id}, /{id}/{approve|wontfix|escalate|reopen}, /events (SSE)
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ Everything tunable lives in `swarph_triage.config.DEFAULT_CONFIG` — override per-consumer at `open()`:
97
+
98
+ ```python
99
+ DEFAULT_CONFIG = {
100
+ "decay_half_life_hours": 6.0, # 6h for hourly, 72h for daily
101
+ "severity_weights": {"critical": 1.0, "high": 0.7, "medium": 0.5, "low": 0.3},
102
+ "freq_curve": "log", # "log" | "linear" | "sqrt"
103
+ "freq_log_base": 10,
104
+ "actionability_floor": 0.1,
105
+ "regression_grace_hours": 24,
106
+ "cooldown_default_days": 14,
107
+ "priority_min": 0.0,
108
+ "priority_max": 100.0,
109
+ }
110
+ ```
111
+
112
+ ## Layout
113
+
114
+ ```
115
+ swarph_triage/
116
+ __init__.py — public API surface
117
+ config.py — DEFAULT_CONFIG + load/merge helpers
118
+ schema.py — SQLAlchemy Core table definitions (3 tables, 6 indexes)
119
+ state_machine.py — Status enum + valid transition matrix + side effects
120
+ priority.py — score formula + decay + recompute_all
121
+ regression.py — patched-then-reappearance detector
122
+ queue.py — TriageQueue main class (ingest, transition, list, show)
123
+ cli.py — argparse-driven CLI
124
+ fastapi.py — APIRouter factory (optional extra)
125
+ ```
126
+
127
+ ## License
128
+
129
+ MIT. Pierre Samson + Claude, co-authored — matching the `phawkes` / `fisherrao` / `tailcor` lineage.
@@ -0,0 +1,102 @@
1
+ # swarph-triage
2
+
3
+ > Generalizable ranked-queue triage primitive — fingerprint, priority, state machine, regression detector. Backend-agnostic via SQLAlchemy Core (sqlite + postgres).
4
+
5
+ **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).
6
+
7
+ 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.
8
+
9
+ ## The pattern
10
+
11
+ A ranked queue where:
12
+
13
+ 1. **Many concrete observations collapse to one logical row** via a `fingerprint_fn`. (Canonical case — error triage: 47 near-identical error log lines → 1 fingerprint with `count=47`. The pattern generalizes to any stream where many raw events map to one actionable unit.)
14
+ 2. **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).
15
+ 3. **A small explicit state machine** (`new → triaged → approved → patched`, with branches to `wontfix` and `needs_review`) — every transition logged to `state_log`, no implicit "kinda done."
16
+ 4. **A regression detector** — if a fingerprint with `status='patched'` gets a new occurrence within `regression_grace_hours`, resurrect to `new` with `regression=1`. Accepted dispositions don't silently mask returning problems.
17
+ 5. **Cooldown semantics** — a `cooldown_until` timestamp on `let_cool` dispositions. The priority calc ramps back from zero as cooldown expires, so a deliberately-deferred item doesn't immediately re-surface.
18
+
19
+ ## Public API surface (planned)
20
+
21
+ ```python
22
+ from swarph_triage import open as open_triage
23
+
24
+ q = open_triage(
25
+ "postgresql://user:pass@host:5433/mydb", # or sqlite:///path.db
26
+ config={"decay_half_life_hours": 72.0},
27
+ proposer_fn=my_proposer, # optional, domain-specific
28
+ )
29
+
30
+ # ingest one observation
31
+ fp_id = q.ingest(
32
+ fingerprint="NullPointerError|auth.py|login",
33
+ severity="high",
34
+ actionability=0.7,
35
+ context={"module": "auth", "first_seen": "..."},
36
+ )
37
+
38
+ # disposition
39
+ q.transition(fp_id, to_status="approved", actor="oncall", note="fix queued")
40
+
41
+ # top-N for the UI
42
+ for row in q.list(limit=20):
43
+ print(row["fingerprint"], row["priority_score"], row["status"])
44
+ ```
45
+
46
+ CLI:
47
+ ```
48
+ swarph-triage list
49
+ swarph-triage show <id>
50
+ swarph-triage approve <id>
51
+ swarph-triage wontfix <id> "reason"
52
+ swarph-triage stats
53
+ swarph-triage backlog # writes markdown snapshot
54
+ swarph-triage history <id>
55
+ ```
56
+
57
+ FastAPI routes (optional install: `pip install swarph-triage[fastapi]`):
58
+ ```python
59
+ from fastapi import FastAPI
60
+ from swarph_triage.fastapi import build_router
61
+
62
+ app = FastAPI()
63
+ app.include_router(build_router(q), prefix="/triage")
64
+ # → /list, /stats, /show/{id}, /{id}/{approve|wontfix|escalate|reopen}, /events (SSE)
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ Everything tunable lives in `swarph_triage.config.DEFAULT_CONFIG` — override per-consumer at `open()`:
70
+
71
+ ```python
72
+ DEFAULT_CONFIG = {
73
+ "decay_half_life_hours": 6.0, # 6h for hourly, 72h for daily
74
+ "severity_weights": {"critical": 1.0, "high": 0.7, "medium": 0.5, "low": 0.3},
75
+ "freq_curve": "log", # "log" | "linear" | "sqrt"
76
+ "freq_log_base": 10,
77
+ "actionability_floor": 0.1,
78
+ "regression_grace_hours": 24,
79
+ "cooldown_default_days": 14,
80
+ "priority_min": 0.0,
81
+ "priority_max": 100.0,
82
+ }
83
+ ```
84
+
85
+ ## Layout
86
+
87
+ ```
88
+ swarph_triage/
89
+ __init__.py — public API surface
90
+ config.py — DEFAULT_CONFIG + load/merge helpers
91
+ schema.py — SQLAlchemy Core table definitions (3 tables, 6 indexes)
92
+ state_machine.py — Status enum + valid transition matrix + side effects
93
+ priority.py — score formula + decay + recompute_all
94
+ regression.py — patched-then-reappearance detector
95
+ queue.py — TriageQueue main class (ingest, transition, list, show)
96
+ cli.py — argparse-driven CLI
97
+ fastapi.py — APIRouter factory (optional extra)
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT. Pierre Samson + Claude, co-authored — matching the `phawkes` / `fisherrao` / `tailcor` lineage.
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "swarph-triage"
7
+ version = "0.1.0"
8
+ description = "Generalizable ranked-queue triage primitive (fingerprint + priority + state machine + regression detector). Backend-agnostic via SQLAlchemy Core (sqlite + postgres)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "Pierre Samson" },
13
+ { name = "Claude" },
14
+ ]
15
+ requires-python = ">=3.9"
16
+ keywords = ["triage", "queue", "priority", "state-machine", "swarph"]
17
+ classifiers = [
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: Database",
22
+ "Topic :: Software Development :: Libraries",
23
+ "Development Status :: 3 - Alpha",
24
+ ]
25
+ dependencies = [
26
+ "sqlalchemy>=2.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ fastapi = [
31
+ "fastapi>=0.110",
32
+ "sse-starlette>=2.0",
33
+ ]
34
+ dev = [
35
+ "pytest>=7.0",
36
+ "pytest-asyncio>=0.21",
37
+ ]
38
+
39
+ [project.scripts]
40
+ swarph-triage = "swarph_triage.cli:main"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/darw007d/swarph-triage"
44
+ Issues = "https://github.com/darw007d/swarph-triage/issues"
45
+
46
+ [tool.setuptools.packages.find]
47
+ include = ["swarph_triage*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ """swarph-triage — ranked-queue triage primitive.
2
+
3
+ Public API:
4
+
5
+ from swarph_triage import open
6
+ q = open(db_url, config=..., proposer_fn=...)
7
+ q.ingest(fingerprint=..., severity=..., actionability=..., context=...)
8
+ q.transition(fp_id, to_status=..., actor=..., note=...)
9
+ q.list(limit=20)
10
+ q.show(fp_id)
11
+
12
+ See README.md for the full surface.
13
+ """
14
+
15
+ from swarph_triage._version import __version__
16
+ from swarph_triage.queue import TriageQueue, open # noqa: F401
17
+ from swarph_triage.state_machine import Status, VALID_TRANSITIONS # noqa: F401
18
+ from swarph_triage.config import DEFAULT_CONFIG, load_config # noqa: F401
19
+
20
+ __all__ = [
21
+ "__version__",
22
+ "open",
23
+ "TriageQueue",
24
+ "Status",
25
+ "VALID_TRANSITIONS",
26
+ "DEFAULT_CONFIG",
27
+ "load_config",
28
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,116 @@
1
+ """CLI — pure SQL, no LLM, instant.
2
+
3
+ Reads ``SWARPH_TRIAGE_DB_URL`` from env. Implementation lands with queue port.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import os
10
+ import sys
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> int:
14
+ parser = argparse.ArgumentParser(prog="swarph-triage")
15
+ parser.add_argument(
16
+ "--db-url",
17
+ default=os.environ.get("SWARPH_TRIAGE_DB_URL"),
18
+ help="Database URL (defaults to $SWARPH_TRIAGE_DB_URL).",
19
+ )
20
+ sub = parser.add_subparsers(dest="cmd", required=True)
21
+
22
+ sub.add_parser("list", help="Show top of queue").add_argument(
23
+ "--status", help="Filter by status", default=None,
24
+ )
25
+ sub.add_parser("stats", help="Status + category breakdown")
26
+ p_show = sub.add_parser("show", help="Show one row + history")
27
+ p_show.add_argument("id", type=int)
28
+ for verb in ("approve", "wontfix", "escalate", "reopen"):
29
+ p = sub.add_parser(verb, help=f"{verb} a fingerprint")
30
+ p.add_argument("id", type=int)
31
+ p.add_argument("note", nargs="?", default="")
32
+ p_hist = sub.add_parser("history", help="Show state_log for one row")
33
+ p_hist.add_argument("id", type=int)
34
+ sub.add_parser("backlog", help="Print queue as markdown")
35
+
36
+ args = parser.parse_args(argv)
37
+ if not args.db_url:
38
+ print("ERROR: --db-url or $SWARPH_TRIAGE_DB_URL required", file=sys.stderr)
39
+ return 2
40
+
41
+ import json
42
+
43
+ from swarph_triage import open as open_triage
44
+
45
+ q = open_triage(args.db_url)
46
+
47
+ # Verbs that map to a state transition / reopen.
48
+ _TRANSITION_VERBS = {
49
+ "approve": "approved",
50
+ "wontfix": "wontfix",
51
+ "escalate": "needs_review",
52
+ }
53
+
54
+ if args.cmd == "list":
55
+ rows = q.list(status=args.status)
56
+ if not rows:
57
+ print("(queue empty)")
58
+ for r in rows:
59
+ print(f"{r['id']:>5} {r['priority_score']:>7.2f} "
60
+ f"{r['status']:<12} {r['severity']:<8} {r['fingerprint']}")
61
+ return 0
62
+
63
+ if args.cmd == "stats":
64
+ print(json.dumps(q.stats(), indent=2, default=str))
65
+ return 0
66
+
67
+ if args.cmd == "show":
68
+ row = q.show(args.id)
69
+ if not row:
70
+ print(f"ERROR: no fingerprint id={args.id}", file=sys.stderr)
71
+ return 1
72
+ print(json.dumps(row, indent=2, default=str))
73
+ return 0
74
+
75
+ if args.cmd in _TRANSITION_VERBS:
76
+ ok = q.transition(
77
+ args.id,
78
+ to_status=_TRANSITION_VERBS[args.cmd],
79
+ actor="cli",
80
+ note=args.note,
81
+ )
82
+ if not ok:
83
+ print(f"ERROR: {args.cmd} on id={args.id} rejected "
84
+ f"(invalid transition or missing row)", file=sys.stderr)
85
+ return 1
86
+ print(f"{args.cmd}: id={args.id} -> {_TRANSITION_VERBS[args.cmd]}")
87
+ return 0
88
+
89
+ if args.cmd == "reopen":
90
+ ok = q.reopen(args.id, actor="cli", note=args.note)
91
+ if not ok:
92
+ print(f"ERROR: reopen on id={args.id} rejected "
93
+ f"(not terminal or missing row)", file=sys.stderr)
94
+ return 1
95
+ print(f"reopen: id={args.id} -> new")
96
+ return 0
97
+
98
+ if args.cmd == "history":
99
+ hist = q.history(args.id)
100
+ if not hist:
101
+ print("(no history)")
102
+ for h in hist:
103
+ print(f"{h['transitioned_at']} {h['from_status']} -> "
104
+ f"{h['to_status']} [{h['actor']}] {h['note'] or ''}")
105
+ return 0
106
+
107
+ if args.cmd == "backlog":
108
+ print(q.backlog_md())
109
+ return 0
110
+
111
+ print(f"ERROR: unknown command {args.cmd}", file=sys.stderr)
112
+ return 2
113
+
114
+
115
+ if __name__ == "__main__":
116
+ sys.exit(main())
@@ -0,0 +1,63 @@
1
+ """Calibration table — every tunable lives here.
2
+
3
+ DSPy-style retunes / per-consumer overrides go in here, not in worker code.
4
+ Pass overrides via ``swarph_triage.open(..., config={...})``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Mapping
10
+
11
+ DEFAULT_CONFIG: dict[str, Any] = {
12
+ # ─── priority decay ───
13
+ # Used in priority.compute(): score *= exp(-hours_since_last_seen / half_life).
14
+ # Tune to ingest cadence: 6h for hourly cron, 48–72h for daily refresh.
15
+ "decay_half_life_hours": 6.0,
16
+
17
+ # ─── severity → weight ───
18
+ # Map your severity labels to multipliers. Unknown labels fall back to 0.5.
19
+ "severity_weights": {
20
+ "critical": 1.0,
21
+ "high": 0.7,
22
+ "medium": 0.5,
23
+ "low": 0.3,
24
+ },
25
+
26
+ # ─── frequency curve ───
27
+ # "log" → log_base(1 + freq). "linear" → freq. "sqrt" → sqrt(freq).
28
+ # log is the production-tested default; whales don't drown fresh items.
29
+ "freq_curve": "log",
30
+ "freq_log_base": 10,
31
+
32
+ # ─── actionability ───
33
+ # Floor on the multiplier so nothing is truly zero (small items still
34
+ # accumulate via the freq term and bubble up over time).
35
+ "actionability_floor": 0.1,
36
+
37
+ # ─── regression detector ───
38
+ # After a `patched_at` timestamp, a new occurrence within this window
39
+ # resurrects the row to `new` + sets regression=1. Default: 24h.
40
+ "regression_grace_hours": 24,
41
+
42
+ # ─── cooldown semantics ───
43
+ # When a row is sent to `let_cool`, cooldown_until = now + N days.
44
+ # Priority ramps DURING the cooldown window — 0 at cooldown-start, linearly
45
+ # back to full at expiry (not "after"); see priority.compute's cooldown ramp.
46
+ "cooldown_default_days": 14,
47
+
48
+ # ─── normalization ───
49
+ "priority_min": 0.0,
50
+ "priority_max": 100.0,
51
+ }
52
+
53
+
54
+ def load_config(overrides: Mapping[str, Any] | None = None) -> dict[str, Any]:
55
+ """Merge ``overrides`` shallow-on-top-of-DEFAULT_CONFIG.
56
+
57
+ Nested dicts (e.g. ``severity_weights``) are replaced wholesale, not merged.
58
+ If you need partial-merge for a nested dict, pass the full merged dict.
59
+ """
60
+ cfg: dict[str, Any] = {**DEFAULT_CONFIG}
61
+ if overrides:
62
+ cfg.update(overrides)
63
+ return cfg
@@ -0,0 +1,56 @@
1
+ """FastAPI APIRouter factory — JSON only, no HTML.
2
+
3
+ Templates / HTMX surface stays consumer-side; this module exposes the data
4
+ endpoints the consumer's UI binds against.
5
+
6
+ Optional install: ``pip install swarph-triage[fastapi]``
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Optional
12
+
13
+
14
+ def build_router(queue) -> Any:
15
+ """Return a FastAPI ``APIRouter`` mountable into the consumer's app.
16
+
17
+ Routes:
18
+
19
+ GET /list?status=...&limit=...&offset=...
20
+ GET /stats
21
+ GET /show/{fp_id}
22
+ POST /transition/{fp_id} body: {to_status, actor, note?}
23
+ """
24
+ from fastapi import APIRouter, Body
25
+
26
+ router = APIRouter()
27
+
28
+ @router.get("/list")
29
+ def list_rows(status: Optional[str] = None, limit: int = 50, offset: int = 0):
30
+ return queue.list(status=status, limit=limit, offset=offset)
31
+
32
+ @router.get("/stats")
33
+ def stats():
34
+ return queue.stats()
35
+
36
+ @router.get("/show/{fp_id}")
37
+ def show(fp_id: int):
38
+ return queue.show(fp_id)
39
+
40
+ @router.post("/transition/{fp_id}")
41
+ def transition(fp_id: int, body: dict = Body(...)):
42
+ from fastapi import HTTPException
43
+ # Auth is the consumer's responsibility (mount this router behind your
44
+ # own auth middleware). Validate the required field → 422, not a 500.
45
+ to_status = body.get("to_status")
46
+ if not to_status:
47
+ raise HTTPException(status_code=422, detail="to_status is required")
48
+ ok = queue.transition(
49
+ fp_id,
50
+ to_status=to_status,
51
+ actor=body.get("actor", "api"),
52
+ note=body.get("note", ""),
53
+ )
54
+ return {"ok": ok}
55
+
56
+ return router