aboutsummaryrefslogtreecommitdiff
path: root/api/debug/ingest.php
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 16:16:39 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 16:16:39 +0200
commitdded2e40ece2d5f2148744b51636c75ba625bf43 (patch)
tree5a99f607a854628437e4fff2dbd08768200d0800 /api/debug/ingest.php
parentef7e4d4d64c294240542c74d8ad2f99b7ef641f2 (diff)
feat(newos4): fix ledger + norm clustering — the loop now knows what a fix was worth
Lifetime counts ranked an 8-day-old outage above today's live crash, and nothing asserted that a shipped fix actually held: a regression of a fixed bug looked exactly like a new bug. - api/debug/ingest.php: norm_key() stores `norm` (host/port/scheme/`?v=`/`//` stripped) — the client hash covers the raw URL, so one outage became two signatures (`/ollama//api/tags` 18 203x and `/ollama/api/tags` 16 540x). PHP + Python implementations verified byte-identical on 5 real messages. - scripts/newos4-ledger.py: data/maintenance/fixes.json maps norm -> {commit, fixed_at, note}, and verify() returns REGRESSED / WATCHING / CLOSED / UNSEEN. "Seen after the fix" alone is not a regression (a fix shipped 10d ago with one sighting 9d ago has been silent since) — regressed means seen after the fix AND recent; otherwise the silence wins. - scripts/newos4-triage.py: clusters (grouped by norm, recency-ordered), active<24h / stale>7d counts, FIX| verdict lines; digest gains clusters + fixes and active/stale/cluster/regressed counters. - api/debug/dashboard.php: Clusters / Active<24h / Stale>7d / Regressed columns and a red REGRESSED detail line, so a regression is visible without reading JSON. Seeded with the two fixes whose signatures exist: 2855c63 (pipe crash, WATCHING 69.3h silent) and 563a9be (emptyTrash, UNSEEN). Verdict logic proven on fixtures: recurrence 5m after fix -> REGRESSED, silent 9d -> CLOSED, last seen 2d -> REGRESSED, fixed 5m ago/last seen 3d ago -> WATCHING. Loop verified end to end: triage via newos4-maintenance.sh prints ACTIVE/CLUSTER/FIX lines, maintain OK, skill audit 7/7, dashboard ALL VIEWPORTS OK.
Diffstat (limited to 'api/debug/ingest.php')
-rw-r--r--api/debug/ingest.php16
1 files changed, 16 insertions, 0 deletions
diff --git a/api/debug/ingest.php b/api/debug/ingest.php
index 98354fe..dc425b8 100644
--- a/api/debug/ingest.php
+++ b/api/debug/ingest.php
@@ -94,6 +94,18 @@ function first_frame($stack) {
return null;
}
+// Cross-language contract: MUST match norm_key() in scripts/newos4-ledger.py.
+// One outage produced two signatures (`/ollama//api/tags` 18 203x, `/ollama/api/tags` 16 540x) because
+// the client hash covers the raw source URL. `norm` is what the triage and the fix ledger group on.
+function norm_key($message, $type = '') {
+ $s = mb_strtolower((string)$message);
+ $s = preg_replace('#https?://[^\s/]+#', '', $s);
+ $s = preg_replace('#/{2,}#', '/', $s);
+ $s = preg_replace('#\?v=[\w.]+#', '', $s);
+ $s = trim(preg_replace('#\s+#', ' ', $s));
+ return substr(mb_strtolower(trim((string)$type)) . '|' . $s, 0, 400);
+}
+
$processed = 0; // entries handled (NOT occurrences) — the client's truncation warning uses this
foreach ($errors as $e) {
$processed++;
@@ -119,6 +131,9 @@ foreach ($errors as $e) {
$allErrors[$idx]['stack'] = substr($e['stack'], 0, 2000);
$allErrors[$idx]['frame'] = first_frame($e['stack']) ?? ($allErrors[$idx]['frame'] ?? null);
}
+ if (empty($allErrors[$idx]['norm']) && !empty($e['message'])) {
+ $allErrors[$idx]['norm'] = norm_key($e['message'], $e['type'] ?? '');
+ }
$ingested += $delta;
}
continue;
@@ -134,6 +149,7 @@ foreach ($errors as $e) {
'module' => $e['module'] ?? null,
'stack' => isset($e['stack']) ? substr($e['stack'], 0, 2000) : null,
'frame' => first_frame($e['stack'] ?? null),
+ 'norm' => norm_key($e['message'] ?? '', $e['type'] ?? ''),
'session_id' => $sessionId,
'count' => $delta,
'first_seen' => norm_ts($e['firstSeen'] ?? null, $now),