@bli-cockpit/log-digest 0.0.0-stage → 0.1.0
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.
- package/README.md +30 -2
- package/dist/classify.d.ts +48 -0
- package/dist/classify.js +81 -0
- package/dist/digest-source.d.ts +99 -0
- package/dist/digest-source.js +130 -0
- package/dist/digest.d.ts +81 -0
- package/dist/digest.js +259 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/judge.d.ts +33 -0
- package/dist/judge.js +198 -0
- package/dist/mask.d.ts +12 -0
- package/dist/mask.js +106 -0
- package/dist/merge.d.ts +12 -0
- package/dist/merge.js +65 -0
- package/dist/render.d.ts +51 -0
- package/dist/render.js +214 -0
- package/dist/sources/collector.d.ts +15 -0
- package/dist/sources/collector.js +166 -0
- package/dist/sources/process.d.ts +2 -0
- package/dist/sources/process.js +78 -0
- package/dist/sources/railway.d.ts +6 -0
- package/dist/sources/railway.js +156 -0
- package/dist/sources/stdin.d.ts +9 -0
- package/dist/sources/stdin.js +56 -0
- package/dist/sources/types.d.ts +67 -0
- package/dist/sources/types.js +13 -0
- package/dist/sources/vercel.d.ts +10 -0
- package/dist/sources/vercel.js +185 -0
- package/dist/tagged.d.ts +37 -0
- package/dist/tagged.js +114 -0
- package/dist/tower-classes.d.ts +28 -0
- package/dist/tower-classes.js +301 -0
- package/dist/window.d.ts +21 -0
- package/dist/window.js +39 -0
- package/package.json +40 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower's own class table: which masked log templates are bugs, which are
|
|
3
|
+
* hiccups, which are harmless. `docs/runbooks/tower-logs.md` ("Teach it a new
|
|
4
|
+
* pattern") is how to add one.
|
|
5
|
+
*
|
|
6
|
+
* Seeded on 2026-10-07 (BLI-12833) from real windows of every wired source: the
|
|
7
|
+
* dashboard (Vercel, 1 h of every level and 12 h of error/warning/5xx), Railway
|
|
8
|
+
* (every service, 1 d: crons keep only their latest deployment's logs) and this
|
|
9
|
+
* machine's collector logs (3 d, including the rotated archives). Each pattern
|
|
10
|
+
* names the evidence it came from. A template NO pattern matches is `unknown`
|
|
11
|
+
* and alerts: a new kind of problem must reach a person, so this table only ever
|
|
12
|
+
* grows by explaining something, never by silencing.
|
|
13
|
+
*
|
|
14
|
+
* Labels follow what the raw lines showed, not what a name suggests:
|
|
15
|
+
* - benign / dead_source are counted on one line and never alerted;
|
|
16
|
+
* - hiccup / rate_pushback are things the code retries or degrades on its own,
|
|
17
|
+
* each with the line that proves the retry;
|
|
18
|
+
* - a real defect is labelled our_bug / config_drift / capacity explicitly, so
|
|
19
|
+
* it stays an ERROR with a note saying why, instead of sitting in `unknown`.
|
|
20
|
+
* First match wins, so specific patterns sit above generic ones.
|
|
21
|
+
*/
|
|
22
|
+
import { ClassTable, type ClassInfo, type ClassTableData, type TablePattern } from "./classify.js";
|
|
23
|
+
export declare const TOWER_CLASSES: Record<string, ClassInfo>;
|
|
24
|
+
/** Patterns, most specific first. */
|
|
25
|
+
export declare const TOWER_PATTERNS: readonly TablePattern[];
|
|
26
|
+
export declare const TOWER_TABLE: ClassTableData;
|
|
27
|
+
/** The compiled Tower table (built once per process). */
|
|
28
|
+
export declare function towerClassTable(): ClassTable;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower's own class table: which masked log templates are bugs, which are
|
|
3
|
+
* hiccups, which are harmless. `docs/runbooks/tower-logs.md` ("Teach it a new
|
|
4
|
+
* pattern") is how to add one.
|
|
5
|
+
*
|
|
6
|
+
* Seeded on 2026-10-07 (BLI-12833) from real windows of every wired source: the
|
|
7
|
+
* dashboard (Vercel, 1 h of every level and 12 h of error/warning/5xx), Railway
|
|
8
|
+
* (every service, 1 d: crons keep only their latest deployment's logs) and this
|
|
9
|
+
* machine's collector logs (3 d, including the rotated archives). Each pattern
|
|
10
|
+
* names the evidence it came from. A template NO pattern matches is `unknown`
|
|
11
|
+
* and alerts: a new kind of problem must reach a person, so this table only ever
|
|
12
|
+
* grows by explaining something, never by silencing.
|
|
13
|
+
*
|
|
14
|
+
* Labels follow what the raw lines showed, not what a name suggests:
|
|
15
|
+
* - benign / dead_source are counted on one line and never alerted;
|
|
16
|
+
* - hiccup / rate_pushback are things the code retries or degrades on its own,
|
|
17
|
+
* each with the line that proves the retry;
|
|
18
|
+
* - a real defect is labelled our_bug / config_drift / capacity explicitly, so
|
|
19
|
+
* it stays an ERROR with a note saying why, instead of sitting in `unknown`.
|
|
20
|
+
* First match wins, so specific patterns sit above generic ones.
|
|
21
|
+
*/
|
|
22
|
+
import { ClassTable } from "./classify.js";
|
|
23
|
+
export const TOWER_CLASSES = {
|
|
24
|
+
our_bug: { severity: "ERROR", about: "our code is wrong: exceptions, schema errors, crashes, a 5xx we caused" },
|
|
25
|
+
unknown: { severity: "ERROR", about: "no pattern matched: a new kind of problem, so it reaches a person" },
|
|
26
|
+
capacity: { severity: "ERROR", about: "memory, disk, CPU or quota exhausted" },
|
|
27
|
+
config_drift: { severity: "ERROR", about: "a key, env var, schedule, PATH or file not as documented" },
|
|
28
|
+
hiccup: { severity: "hiccup", about: "timeouts and 5xx from a dependency: retried or degraded by the code itself, a WARNING only when retries run out" },
|
|
29
|
+
rate_pushback: { severity: "hiccup", about: "429 or a provider slowing us down: lower the rate" },
|
|
30
|
+
dead_source: { severity: "WARNING", about: "the thing asked for is gone or was never shared (404, revoked, repo deleted): count only" },
|
|
31
|
+
benign: { severity: "WARNING", about: "looks like trouble but is known harmless: count only" },
|
|
32
|
+
};
|
|
33
|
+
const raw = String.raw;
|
|
34
|
+
/** Patterns, most specific first. */
|
|
35
|
+
export const TOWER_PATTERNS = [
|
|
36
|
+
// ------------------------------------------------------------------ collector (this machine's sync.log / sync.err.log)
|
|
37
|
+
{
|
|
38
|
+
id: "collector-git-diff-no-index",
|
|
39
|
+
class: "our_bug",
|
|
40
|
+
source: "^collector$",
|
|
41
|
+
match: raw `^\[raw-evidence\] git diff failed `,
|
|
42
|
+
note: "git printed its `diff --no-index` usage, which is what git does when the directory is not a git repository (error_detail: unknown option `cached`, pathspec limiting). The collector should skip such a root the way it skips repo_root_missing. A lead on the cause, the usage text itself is the evidence.",
|
|
43
|
+
basis: "collector logs, 3 d: 1,218 lines, both diff_target values, every tick",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "collector-git-diff-root-missing",
|
|
47
|
+
class: "dead_source",
|
|
48
|
+
source: "^collector$",
|
|
49
|
+
match: raw `^\[raw-evidence\] git diff skipped, repository root is no longer on disk`,
|
|
50
|
+
note: "a repository that was deleted or moved: nothing to diff, the collector says so and moves on",
|
|
51
|
+
basis: "collector logs, 3 d: 26 lines",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "collector-evidence-file-vanished",
|
|
55
|
+
class: "dead_source",
|
|
56
|
+
source: "^collector$",
|
|
57
|
+
match: raw `^\[raw-evidence\] evidence file could not be stat'd reason=file_read_failed .*error_code=ENOENT`,
|
|
58
|
+
note: "a session file that was listed and then deleted before it was read: nothing to collect",
|
|
59
|
+
basis: "collector logs, 3 d: 1 line (error_code ENOENT, syscall stat)",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "collector-redelivery-nothing-plannable",
|
|
63
|
+
class: "our_bug",
|
|
64
|
+
source: "^collector$",
|
|
65
|
+
match: raw `^\[evidence-redelivery\] staged evidence could not be delivered reason=nothing_plannable`,
|
|
66
|
+
note: "staged evidence packs whose manifest is unreadable (unplannable.manifest_unreadable) are terminal and never delivered; the line repeats every tick. Staged bytes that never leave the machine.",
|
|
67
|
+
basis: "collector logs, 3 d: 201 lines, terminal=6 packs, 25 MB",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "collector-stubs-exhausted",
|
|
71
|
+
class: "benign",
|
|
72
|
+
source: "^collector$",
|
|
73
|
+
match: raw `^\[collector stubs\] redaction-crash stubs asked reason=stubs_exhausted`,
|
|
74
|
+
note: "a count line: the stub list was read to its end (exhausted is the pagination word for complete)",
|
|
75
|
+
basis: "collector logs, 3 d: 197 lines, each with pages, stub_count and session_count",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "collector-work-context-missing",
|
|
79
|
+
class: "benign",
|
|
80
|
+
source: "^collector$",
|
|
81
|
+
match: raw `^\[upload-envelope\] work context present but unreadable, reporting it as missing`,
|
|
82
|
+
note: "a repo with no `cockpit start` binding: the envelope goes up with no ticket label, by session-first design (a label, never a gate)",
|
|
83
|
+
basis: "collector logs, 3 d: 24 lines, error_detail `Active work context missing for <repo>`",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "collector-heartbeat-reports-failure",
|
|
87
|
+
class: "benign",
|
|
88
|
+
source: "^collector$",
|
|
89
|
+
match: raw `^\[heartbeat\] checked in reason=heartbeat_recorded http_status=202 sync_reason=(sync_failed|network_failed)`,
|
|
90
|
+
note: "the heartbeat itself succeeded (202); it only carries the last sync's reason. The sync failure shows as its own template.",
|
|
91
|
+
basis: "collector logs, 3 d: 20 lines",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "collector-transport-hiccups",
|
|
95
|
+
class: "hiccup",
|
|
96
|
+
source: "^collector$",
|
|
97
|
+
match: raw `^\[(evidence-upload|session-reports|cockpit-sync|install-receipts)\] .*(before a status came back|spooled for retry|entry kept for retry)`,
|
|
98
|
+
note: "the network dropped a request (TypeError fetch failed); the entry is kept or spooled and retried on the next tick",
|
|
99
|
+
basis: "collector logs, 3 d: 20 lines, every one says retry, spooled or kept",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "collector-commit-409-backoff",
|
|
103
|
+
class: "hiccup",
|
|
104
|
+
source: "^collector$",
|
|
105
|
+
match: raw `^(\[cockpit-sync\] raw evidence delivery failed reason=commit_failed_http_409|\[session-sync\] raw evidence held by delivery backoff)`,
|
|
106
|
+
note: "the server answered 409 to a commit: the pack is held by delivery backoff and tried again",
|
|
107
|
+
basis: "collector logs, 3 d: 9 lines, `held_backoff` names the same 409",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "collector-restage-missing-chunks",
|
|
111
|
+
class: "hiccup",
|
|
112
|
+
source: "^collector$",
|
|
113
|
+
match: raw `^\[evidence-upload\] restaging missing chunks reason=staging_missing_between_passes`,
|
|
114
|
+
note: "the server's staging disagreed between two passes; the client restages the missing chunks (the dashboard logs the other half as evidence-commit upload failed closed)",
|
|
115
|
+
basis: "collector logs 3 d and dashboard 12 h: the same reason on both sides",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "collector-agent-artifacts-pointer-not-ingested",
|
|
119
|
+
class: "hiccup",
|
|
120
|
+
source: "^collector$",
|
|
121
|
+
match: raw `^\[agent-artifacts\] report refused reason=report_failed http_status=404 server_reason=BLI-E004: Artifact raw evidence pointer has not been ingested`,
|
|
122
|
+
note: "the artifact report named a raw-evidence pointer the server had not ingested yet; the dashboard logs it as WARNING and the report is sent again. A count that never falls means the pointer never ingests.",
|
|
123
|
+
basis: "collector logs, 3 d: 49 lines; dashboard 1 h: 5 lines at level WARNING",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "collector-secret-guard-rejected",
|
|
127
|
+
class: "our_bug",
|
|
128
|
+
source: "^collector$",
|
|
129
|
+
match: raw `^\[cockpit-sync\] raw evidence (delivery failed reason=secret_guard_rejected|permanently rejected reason=raw_evidence_permanently_rejected:secret_guard_rejected)`,
|
|
130
|
+
note: "session-first collection says a secret guard masks the match and uploads the rest, never drops a file; a rejection is a finding to trace (which file, which rule), not noise",
|
|
131
|
+
basis: "collector logs, 3 d: 9 lines",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "collector-spawn-cockpit-enoent",
|
|
135
|
+
class: "config_drift",
|
|
136
|
+
source: "^collector$",
|
|
137
|
+
match: raw `^\[process-runner\] spawn failed reason=spawn_failed error_code=ENOENT error_syscall=spawn cockpit`,
|
|
138
|
+
note: "the scheduled tick could not find `cockpit` on its PATH; `cockpit doctor` repairs the scheduler",
|
|
139
|
+
basis: "collector logs, 3 d: 8 lines (error_code ENOENT, syscall `spawn cockpit`)",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "collector-prune-blocked-by-uncommitted",
|
|
143
|
+
class: "capacity",
|
|
144
|
+
source: "^collector$",
|
|
145
|
+
match: raw `^\[collector prune\] still over the cap and nothing committed is left to take`,
|
|
146
|
+
note: "the staging area is over its cap and everything left is uncommitted, so nothing can be freed",
|
|
147
|
+
basis: "collector logs, 3 d: 1 line",
|
|
148
|
+
},
|
|
149
|
+
// ------------------------------------------------------------------ dashboard (Vercel runtime logs)
|
|
150
|
+
{
|
|
151
|
+
id: "dashboard-http-429-403",
|
|
152
|
+
class: "rate_pushback",
|
|
153
|
+
source: "^dashboard$",
|
|
154
|
+
match: raw `^HTTP (429|403) `,
|
|
155
|
+
note: "a request answered 429 or 403: a caller is being slowed or refused (the 403 may be an auth refusal, read the route)",
|
|
156
|
+
basis: "synthetic line the dashboard source adds for a request that ended 429 or 403",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "dashboard-http-gateway",
|
|
160
|
+
class: "hiccup",
|
|
161
|
+
source: "^dashboard$",
|
|
162
|
+
match: raw `^HTTP (502|503|504) `,
|
|
163
|
+
note: "a gateway or timeout answer: clients retry; a burst on one route is worth a look",
|
|
164
|
+
basis: "synthetic line for a 502, 503 or 504",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "dashboard-http-500",
|
|
168
|
+
class: "our_bug",
|
|
169
|
+
source: "^dashboard$",
|
|
170
|
+
match: raw `^HTTP 5\d\d `,
|
|
171
|
+
note: "one of our routes answered with a server error",
|
|
172
|
+
basis: "synthetic line for a 5xx other than the gateway three; one HTTP 500 on evidence/upload/chunk in 12 h",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "dashboard-ops-reader-timeout",
|
|
176
|
+
class: "hiccup",
|
|
177
|
+
source: "^dashboard$",
|
|
178
|
+
match: raw `^\[ops status\] reader over budget reason=reader_timeout`,
|
|
179
|
+
note: "one ops reader ran past its time budget and the board shows the rest without it (declared WARNING by the code)",
|
|
180
|
+
basis: "dashboard 12 h: 15 lines",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "dashboard-memory-arm-deadline",
|
|
184
|
+
class: "hiccup",
|
|
185
|
+
match: raw `\barm_deadline_exceeded\b`,
|
|
186
|
+
note: "a BLI Memory arm hit its deadline and the answer was built from the other arms (docs/architecture/agent-memory.md: never fixed by moving a timeout)",
|
|
187
|
+
basis: "dashboard 12 h: 14 lines across eight arms, all declared WARNING",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "dashboard-evidence-staging-missing",
|
|
191
|
+
class: "hiccup",
|
|
192
|
+
source: "^dashboard$",
|
|
193
|
+
match: raw `^\[(evidence-assembly|evidence-commit)\] .*reason=staging_missing_between_passes`,
|
|
194
|
+
note: "the commit failed closed because staging changed between two passes; the collector restages and commits again",
|
|
195
|
+
basis: "dashboard 12 h: 6 lines; the collector logs the restage",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "dashboard-storage-bad-gateway",
|
|
199
|
+
class: "hiccup",
|
|
200
|
+
source: "^dashboard$",
|
|
201
|
+
match: raw `StorageApiError: Bad Gateway`,
|
|
202
|
+
note: "Supabase storage answered 502 on an offload write; the data was kept inline, nothing lost",
|
|
203
|
+
basis: "dashboard 12 h: 1 line `offload write failed, kept inline`",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "dashboard-agent-artifacts-pointer-404",
|
|
207
|
+
class: "hiccup",
|
|
208
|
+
source: "^dashboard$",
|
|
209
|
+
match: raw `^\[ambient agent-artifacts\] report refused reason=report_refused status=404`,
|
|
210
|
+
note: "the other half of collector-agent-artifacts-pointer-not-ingested: refused while the pointer is not ingested, sent again",
|
|
211
|
+
basis: "dashboard 1 h: 5 lines at level WARNING",
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "dashboard-work-ref-refused",
|
|
215
|
+
class: "benign",
|
|
216
|
+
source: "^dashboard$",
|
|
217
|
+
match: raw `^\[work (issues|issue-ref)\] refused reason=issue_not_found_or_unreadable`,
|
|
218
|
+
note: "a person or agent asked for a ticket that does not exist or that their RLS cannot read; the refusal is the design",
|
|
219
|
+
basis: "dashboard 1 h: 2 lines for a made-up ticket id",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "dashboard-work-completeness-capped",
|
|
223
|
+
class: "benign",
|
|
224
|
+
source: "^dashboard$",
|
|
225
|
+
match: raw `^\[work issues\] completeness reason=capped`,
|
|
226
|
+
note: "a list read stopped at its cap on purpose and says so (exhausted / capped / failed-partial); only failed-partial is trouble",
|
|
227
|
+
basis: "dashboard 1 h: 1 line at level WARNING",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: "dashboard-msg-audit-escape-read",
|
|
231
|
+
class: "benign",
|
|
232
|
+
source: "^dashboard$",
|
|
233
|
+
match: raw `^\[msg audit\] escape read`,
|
|
234
|
+
note: "the audit record of a message read outside the caller's channels; written at WARNING on purpose so it is counted, never an alert",
|
|
235
|
+
basis: "dashboard 12 h: 22 lines",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "dashboard-mcp-oversized",
|
|
239
|
+
class: "benign",
|
|
240
|
+
source: "^dashboard$",
|
|
241
|
+
match: raw `^\[bli-cockpit-mcp tool\] warn outcome=answered_oversized`,
|
|
242
|
+
note: "an MCP tool answered over its per-half byte budget; measured by scripts/smoke.ts, not a failure",
|
|
243
|
+
basis: "dashboard 1 h: 1 line",
|
|
244
|
+
},
|
|
245
|
+
// ------------------------------------------------------------------ railway cron services
|
|
246
|
+
{
|
|
247
|
+
id: "railway-cal-sync-token-expired",
|
|
248
|
+
class: "hiccup",
|
|
249
|
+
source: "^railway$",
|
|
250
|
+
unit: "^cal-sync$",
|
|
251
|
+
match: raw `(sync_token_expired|\[cal sync\] token expired|\[cal-sync\] stage # attempt # failed \(exit \d+\); retrying in #s$|^npm error (path|workspace|location|command|code|Lifecycle script))`,
|
|
252
|
+
note: "Google expired a calendar's sync token; attempt 1 exits 1 (the npm error lines are that exit), the stage retries after 60 s with a full sync and succeeds",
|
|
253
|
+
basis: "railway cal-sync 1 d: 04:01 attempt 1 failed, 04:02 attempt 2 ok, 98 events read in full mode. A stage that fails every attempt is NOT matched here and stays an alert.",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "railway-docs-import-notion-page-not-shared",
|
|
257
|
+
class: "dead_source",
|
|
258
|
+
source: "^railway$",
|
|
259
|
+
unit: "^docs-import-notion$",
|
|
260
|
+
match: raw `^error: Public API request failed \(# Not Found object_not_found\): Could not find page with ID`,
|
|
261
|
+
note: "the Notion integration cannot see a page that was never shared with it (notion_page_unreadable): the world, nothing to fix here",
|
|
262
|
+
basis: "railway docs-import-notion 1 d: 51 lines, `Make sure the relevant pages and databases are shared with your integration`",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: "railway-docs-import-notion-database-as-page",
|
|
266
|
+
class: "our_bug",
|
|
267
|
+
source: "^railway$",
|
|
268
|
+
unit: "^docs-import-notion$",
|
|
269
|
+
match: raw `^error: Public API request failed \(# Bad Request validation_error\): Provided ID <uuid> is a database, not a page`,
|
|
270
|
+
note: "the importer asked Notion for a PAGE with a DATABASE's id; it should use the retrieve-database API (the importer places each database as a folder)",
|
|
271
|
+
basis: "railway docs-import-notion 1 d: 10 lines, Notion's own message names the fix",
|
|
272
|
+
},
|
|
273
|
+
// ------------------------------------------------------------------ any source
|
|
274
|
+
{
|
|
275
|
+
id: "any-ai-sdk-warning",
|
|
276
|
+
class: "benign",
|
|
277
|
+
match: raw `^AI SDK Warning`,
|
|
278
|
+
note: "the AI SDK notes that a knob (temperature) is ignored by a reasoning model, once per call, plus its own switch-off hint; the provider ignores the knob. Dropping it at the call site (reasoning-policy) is the cleanup.",
|
|
279
|
+
basis: "dashboard 12 h: 46 lines on /api/memory/save; railway ladder 1 d: 6 lines",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: "any-npm-warn-production",
|
|
283
|
+
class: "benign",
|
|
284
|
+
source: "^railway$",
|
|
285
|
+
match: raw `^npm warn config production Use \`--omit=dev\` instead\.$`,
|
|
286
|
+
note: "every Railway service's npm command still passes the deprecated --production flag: harmless until npm removes it (a config_drift cleanup, not an incident)",
|
|
287
|
+
basis: "railway, every service, once per start: about 20 lines in 1 d",
|
|
288
|
+
},
|
|
289
|
+
];
|
|
290
|
+
export const TOWER_TABLE = {
|
|
291
|
+
version: 1,
|
|
292
|
+
classes: TOWER_CLASSES,
|
|
293
|
+
patterns: TOWER_PATTERNS,
|
|
294
|
+
noiseUnits: [],
|
|
295
|
+
};
|
|
296
|
+
let cached = null;
|
|
297
|
+
/** The compiled Tower table (built once per process). */
|
|
298
|
+
export function towerClassTable() {
|
|
299
|
+
cached ??= new ClassTable(TOWER_TABLE);
|
|
300
|
+
return cached;
|
|
301
|
+
}
|
package/dist/window.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The time window a digest covers: `90s`, `15m`, `2h`, `1d`, a bare number of
|
|
3
|
+
* minutes, or an ISO timestamp. Kaiben's `--since` took the same relative forms.
|
|
4
|
+
*/
|
|
5
|
+
export interface LogWindow {
|
|
6
|
+
/** Epoch seconds: lines older than this are outside the window. */
|
|
7
|
+
fromEpoch: number;
|
|
8
|
+
/** Epoch seconds the window was cut at (now). */
|
|
9
|
+
toEpoch: number;
|
|
10
|
+
/** How the person wrote it, for the header line. */
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
/** The only shapes `--since` may take: a count and one unit, or an ISO timestamp. Nothing else reaches a program. */
|
|
14
|
+
export declare const SINCE_ALLOWLIST: RegExp;
|
|
15
|
+
export declare class WindowError extends Error {
|
|
16
|
+
readonly reason = "invalid_since";
|
|
17
|
+
}
|
|
18
|
+
/** Seconds in a relative window like `15m`, or null when the text is not one. */
|
|
19
|
+
export declare function relativeSeconds(text: string): number | null;
|
|
20
|
+
export declare function parseWindow(text: string | undefined, nowMs?: number): LogWindow;
|
|
21
|
+
export declare function isoOf(epochSeconds: number): string;
|
package/dist/window.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The time window a digest covers: `90s`, `15m`, `2h`, `1d`, a bare number of
|
|
3
|
+
* minutes, or an ISO timestamp. Kaiben's `--since` took the same relative forms.
|
|
4
|
+
*/
|
|
5
|
+
const UNIT_SECONDS = { "": 60, s: 1, m: 60, h: 3600, d: 86400 };
|
|
6
|
+
/** The only shapes `--since` may take: a count and one unit, or an ISO timestamp. Nothing else reaches a program. */
|
|
7
|
+
export const SINCE_ALLOWLIST = /^(\d{1,4}[smhd]|\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})?)?)$/;
|
|
8
|
+
export class WindowError extends Error {
|
|
9
|
+
reason = "invalid_since";
|
|
10
|
+
}
|
|
11
|
+
/** Seconds in a relative window like `15m`, or null when the text is not one. */
|
|
12
|
+
export function relativeSeconds(text) {
|
|
13
|
+
const match = /^(\d+(?:\.\d+)?)\s*([smhd]?)$/.exec(text.trim());
|
|
14
|
+
if (!match)
|
|
15
|
+
return null;
|
|
16
|
+
return Number.parseFloat(match[1]) * UNIT_SECONDS[match[2]];
|
|
17
|
+
}
|
|
18
|
+
export function parseWindow(text, nowMs = Date.now()) {
|
|
19
|
+
const raw = (text ?? "15m").trim();
|
|
20
|
+
if (!SINCE_ALLOWLIST.test(raw))
|
|
21
|
+
throw new WindowError(`--since wants 15m, 1h, 2d or an ISO time, not ${JSON.stringify(raw.slice(0, 40))}`);
|
|
22
|
+
const toEpoch = nowMs / 1000;
|
|
23
|
+
const seconds = relativeSeconds(raw);
|
|
24
|
+
if (seconds !== null) {
|
|
25
|
+
if (seconds <= 0)
|
|
26
|
+
throw new WindowError(`--since wants 15m, 1h, 2d or an ISO time, not ${JSON.stringify(raw)}`);
|
|
27
|
+
return { fromEpoch: toEpoch - seconds, toEpoch, label: `last ${raw}` };
|
|
28
|
+
}
|
|
29
|
+
const parsed = Date.parse(raw);
|
|
30
|
+
if (!Number.isFinite(parsed) || !/^\d{4}-\d{2}-\d{2}/.test(raw)) {
|
|
31
|
+
throw new WindowError(`--since wants 15m, 1h, 2d or an ISO time, not ${JSON.stringify(raw)}`);
|
|
32
|
+
}
|
|
33
|
+
if (parsed > nowMs)
|
|
34
|
+
throw new WindowError("--since is in the future");
|
|
35
|
+
return { fromEpoch: parsed / 1000, toEpoch, label: `since ${new Date(parsed).toISOString().slice(0, 16)}Z` };
|
|
36
|
+
}
|
|
37
|
+
export function isoOf(epochSeconds) {
|
|
38
|
+
return new Date(epochSeconds * 1000).toISOString();
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/log-digest",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Token-cheap digest of log lines: variable parts masked into templates, equal templates counted, routine runs collapsed, every template classified. Node stdlib only.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"prepack": "rm -rf dist && tsc",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "20.19.39",
|
|
34
|
+
"typescript": "5.9.3",
|
|
35
|
+
"vitest": "3.2.4"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/BLI-INC/bli-cockpit.git",
|
|
40
|
+
"directory": "packages/log-digest"
|
|
41
|
+
}
|
|
42
|
+
}
|