context-mode 1.0.157 → 1.0.159

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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * error-classifier — PRD-context-as-a-service §5 ABI parity
3
+ *
4
+ * Pure classifiers that derive per-event metadata from PostToolUse hook
5
+ * stdin, mirroring the seed shape produced by context-mode-platform's
6
+ * `seed.ts` so the bridge can ship full seed-shape parity.
7
+ *
8
+ * The dashboard reads three derived columns that are populated by the
9
+ * platform seeder but, per the OSS handoff (ANOMALY #3), are NEVER READ
10
+ * by the engine when ingesting live events:
11
+ *
12
+ * • error_category — one of 10 fixed buckets
13
+ * • error_tool — tool name that produced the error
14
+ * • command_type — git / test / build / lint / install / format / run / other
15
+ * • command_tool — first token of the command (npm, git, pytest, …)
16
+ * • duration_bucket — fast / medium / slow / timeout
17
+ *
18
+ * We populate them anyway, with sane defaults derived from message text
19
+ * and tool name, so the dashboard renders symmetrically with seed.
20
+ *
21
+ * Hard constraints:
22
+ * 1. Pure functions — no I/O, no globals, no module state.
23
+ * 2. `error_category` MUST match seed's `pickErrorClassification`
24
+ * output exactly. The literal union below is the ABI.
25
+ * 3. No external dependencies; TypeScript stdlib only.
26
+ * 4. Robust to null / undefined / empty / malformed input.
27
+ */
28
+ export type ErrorCategory = "file_not_found" | "command_not_found" | "edit_match_failed" | "test_failed" | "syntax_error" | "runtime_error" | "permission_denied" | "git_conflict" | "timeout" | "unknown";
29
+ export type CommandType = "test" | "build" | "lint" | "git" | "install" | "format" | "run" | "other";
30
+ export type DurationBucket = "fast" | "medium" | "slow" | "timeout";
31
+ export interface ErrorClassification {
32
+ error_category: ErrorCategory;
33
+ error_tool: string;
34
+ }
35
+ export interface CommandClassification {
36
+ command_type: CommandType;
37
+ command_tool: string;
38
+ }
39
+ /**
40
+ * Classify an error message + tool name into one of seed's 10 categories.
41
+ *
42
+ * Patterns (ordered most-specific → least-specific so the first match
43
+ * wins). Rationale for each pattern is inline.
44
+ *
45
+ * • file_not_found — Node's ENOENT, Python's FileNotFoundError, and
46
+ * Claude's "Cannot find module" all signal a
47
+ * missing file/path.
48
+ * • command_not_found — POSIX shells emit "command not found" (exit 127)
49
+ * and "bash: <bin>: not found"; npx prints similar.
50
+ * • edit_match_failed — Claude's Edit tool prints "old_string not found"
51
+ * or "matches multiple locations" when the patch
52
+ * context is stale.
53
+ * • test_failed — vitest/jest/pytest format `FAIL ` prefix; npm
54
+ * prints "Test failed".
55
+ * • syntax_error — SyntaxError (JS/TS), tsc TS-codes, or Python's
56
+ * SyntaxError header.
57
+ * • runtime_error — TypeError/ReferenceError/RangeError (uncaught).
58
+ * • permission_denied — EACCES / "permission denied" from fs or sudo.
59
+ * • git_conflict — git's "CONFLICT" header on rebase/merge, plus
60
+ * "Merge conflict in".
61
+ * • timeout — explicit "timeout"/"timed out"/"ETIMEDOUT".
62
+ * • unknown — fallthrough; never throws.
63
+ */
64
+ export declare function classifyError(message: unknown, toolName: unknown): ErrorClassification;
65
+ /**
66
+ * Classify a Bash command into a workflow bucket. The bucket is derived
67
+ * from the (tool, sub-verb) pair: `npm test` → test, `npm run build` →
68
+ * build, `git commit` → git, `pip install` → install, etc.
69
+ *
70
+ * Heuristic ordering — most-specific verb checks first; falls through to
71
+ * `run` for generic execution and `other` for anything we can't classify.
72
+ */
73
+ export declare function classifyCommand(command: unknown): CommandClassification;
74
+ /**
75
+ * Bucketise a latency_ms reading into the four fixed buckets the
76
+ * dashboard renders. Bucket edges (in ms):
77
+ *
78
+ * fast : [0, 1_000)
79
+ * medium : [1_000, 10_000)
80
+ * slow : [10_000, 60_000)
81
+ * timeout : [60_000, ∞)
82
+ *
83
+ * Defensive: negative and non-numeric inputs collapse to `fast` (the
84
+ * neutral bucket) rather than throwing, because hooks have been seen
85
+ * to emit `null` for very short calls.
86
+ */
87
+ export declare function bucketizeDuration(latencyMs: unknown): DurationBucket;
@@ -0,0 +1,303 @@
1
+ /**
2
+ * error-classifier — PRD-context-as-a-service §5 ABI parity
3
+ *
4
+ * Pure classifiers that derive per-event metadata from PostToolUse hook
5
+ * stdin, mirroring the seed shape produced by context-mode-platform's
6
+ * `seed.ts` so the bridge can ship full seed-shape parity.
7
+ *
8
+ * The dashboard reads three derived columns that are populated by the
9
+ * platform seeder but, per the OSS handoff (ANOMALY #3), are NEVER READ
10
+ * by the engine when ingesting live events:
11
+ *
12
+ * • error_category — one of 10 fixed buckets
13
+ * • error_tool — tool name that produced the error
14
+ * • command_type — git / test / build / lint / install / format / run / other
15
+ * • command_tool — first token of the command (npm, git, pytest, …)
16
+ * • duration_bucket — fast / medium / slow / timeout
17
+ *
18
+ * We populate them anyway, with sane defaults derived from message text
19
+ * and tool name, so the dashboard renders symmetrically with seed.
20
+ *
21
+ * Hard constraints:
22
+ * 1. Pure functions — no I/O, no globals, no module state.
23
+ * 2. `error_category` MUST match seed's `pickErrorClassification`
24
+ * output exactly. The literal union below is the ABI.
25
+ * 3. No external dependencies; TypeScript stdlib only.
26
+ * 4. Robust to null / undefined / empty / malformed input.
27
+ */
28
+ // ── Internal helpers ────────────────────────────────────────────────────
29
+ /**
30
+ * Normalise message for case-insensitive substring matching. Coerces
31
+ * non-string input (null, undefined, numbers from errno objects) to
32
+ * empty string so downstream `.includes()` is always safe.
33
+ */
34
+ function normalise(s) {
35
+ if (typeof s !== "string")
36
+ return "";
37
+ return s.toLowerCase();
38
+ }
39
+ /**
40
+ * Best-effort `error_tool` derivation. Prefers the PostToolUse-supplied
41
+ * `toolName`; falls back to scanning the message for a canonical tool
42
+ * name (Read/Edit/Write/Bash/Grep/Glob) so isolated error strings still
43
+ * resolve to a useful value rather than the literal "unknown".
44
+ */
45
+ function deriveErrorTool(toolName, message) {
46
+ if (typeof toolName === "string" && toolName.trim().length > 0) {
47
+ return toolName.trim();
48
+ }
49
+ // Canonical Claude Code tool names that may appear in error strings.
50
+ const known = ["Edit", "Read", "Write", "Bash", "Grep", "Glob", "MultiEdit"];
51
+ for (const t of known) {
52
+ if (message.toLowerCase().includes(t.toLowerCase()))
53
+ return t;
54
+ }
55
+ return "unknown";
56
+ }
57
+ // ── Error classifier ────────────────────────────────────────────────────
58
+ /**
59
+ * Classify an error message + tool name into one of seed's 10 categories.
60
+ *
61
+ * Patterns (ordered most-specific → least-specific so the first match
62
+ * wins). Rationale for each pattern is inline.
63
+ *
64
+ * • file_not_found — Node's ENOENT, Python's FileNotFoundError, and
65
+ * Claude's "Cannot find module" all signal a
66
+ * missing file/path.
67
+ * • command_not_found — POSIX shells emit "command not found" (exit 127)
68
+ * and "bash: <bin>: not found"; npx prints similar.
69
+ * • edit_match_failed — Claude's Edit tool prints "old_string not found"
70
+ * or "matches multiple locations" when the patch
71
+ * context is stale.
72
+ * • test_failed — vitest/jest/pytest format `FAIL ` prefix; npm
73
+ * prints "Test failed".
74
+ * • syntax_error — SyntaxError (JS/TS), tsc TS-codes, or Python's
75
+ * SyntaxError header.
76
+ * • runtime_error — TypeError/ReferenceError/RangeError (uncaught).
77
+ * • permission_denied — EACCES / "permission denied" from fs or sudo.
78
+ * • git_conflict — git's "CONFLICT" header on rebase/merge, plus
79
+ * "Merge conflict in".
80
+ * • timeout — explicit "timeout"/"timed out"/"ETIMEDOUT".
81
+ * • unknown — fallthrough; never throws.
82
+ */
83
+ export function classifyError(message, toolName) {
84
+ const msg = normalise(message);
85
+ const tool = typeof toolName === "string" ? toolName : "";
86
+ const error_tool = deriveErrorTool(toolName, typeof message === "string" ? message : "");
87
+ // Empty / malformed input → unknown bucket but still return a valid tool.
88
+ if (msg.length === 0) {
89
+ return { error_category: "unknown", error_tool };
90
+ }
91
+ // ── file_not_found ────────────────────────────────────────────────
92
+ // Node's ENOENT carries "no such file or directory"; Claude's Read
93
+ // tool surfaces the raw errno line. "cannot find module" is the
94
+ // require-resolution variant.
95
+ if (msg.includes("enoent") ||
96
+ msg.includes("no such file") ||
97
+ msg.includes("cannot find module") ||
98
+ msg.includes("filenotfounderror")) {
99
+ return { error_category: "file_not_found", error_tool };
100
+ }
101
+ // ── command_not_found ────────────────────────────────────────────
102
+ // POSIX shells: "<shell>: <bin>: command not found" → exit 127.
103
+ // Some hooks surface the bare exit code without the message.
104
+ if (msg.includes("command not found") ||
105
+ msg.includes(": not found") ||
106
+ /\bexit(?:\s+code)?\s*[:=]?\s*127\b/.test(msg)) {
107
+ return { error_category: "command_not_found", error_tool };
108
+ }
109
+ // ── edit_match_failed ────────────────────────────────────────────
110
+ // Claude Code's Edit tool prints these exact phrases when the
111
+ // `old_string` parameter no longer matches the file contents.
112
+ // Gate on the Edit tool to avoid false positives when a Bash script
113
+ // happens to contain the literal text "old_string".
114
+ if ((tool === "Edit" || tool === "MultiEdit") &&
115
+ (msg.includes("old_string") ||
116
+ msg.includes("could not find string") ||
117
+ msg.includes("string to replace not found") ||
118
+ msg.includes("matches multiple"))) {
119
+ return { error_category: "edit_match_failed", error_tool };
120
+ }
121
+ // Edit-style failure can also leak without tool name — match the
122
+ // distinctive Claude phrasing alone.
123
+ if (msg.includes("old_string not found") || msg.includes("string to replace not found")) {
124
+ return { error_category: "edit_match_failed", error_tool };
125
+ }
126
+ // ── git_conflict ─────────────────────────────────────────────────
127
+ // Check BEFORE test_failed because "CONFLICT" can co-occur with the
128
+ // word "fail" in some merge outputs.
129
+ if (msg.includes("conflict") &&
130
+ (msg.includes("merge") || msg.includes("rebase") || msg.includes("git"))) {
131
+ return { error_category: "git_conflict", error_tool };
132
+ }
133
+ if (msg.startsWith("conflict") || msg.includes("merge conflict")) {
134
+ return { error_category: "git_conflict", error_tool };
135
+ }
136
+ // ── timeout ──────────────────────────────────────────────────────
137
+ // Check BEFORE test_failed; "test timed out" should bucket as timeout.
138
+ if (msg.includes("etimedout") ||
139
+ msg.includes("timed out") ||
140
+ msg.includes("timeout") ||
141
+ msg.includes("deadline exceeded")) {
142
+ return { error_category: "timeout", error_tool };
143
+ }
144
+ // ── permission_denied ────────────────────────────────────────────
145
+ if (msg.includes("eacces") ||
146
+ msg.includes("permission denied") ||
147
+ msg.includes("operation not permitted") ||
148
+ msg.includes("eperm")) {
149
+ return { error_category: "permission_denied", error_tool };
150
+ }
151
+ // ── syntax_error ─────────────────────────────────────────────────
152
+ // tsc errors look like "error TS2322"; JS SyntaxError header is exact.
153
+ if (msg.includes("syntaxerror") ||
154
+ /\berror\s+ts\d{3,5}\b/.test(msg) ||
155
+ msg.includes("unexpected token") ||
156
+ msg.includes("unexpected end of") ||
157
+ msg.includes("parse error")) {
158
+ return { error_category: "syntax_error", error_tool };
159
+ }
160
+ // ── test_failed ──────────────────────────────────────────────────
161
+ // vitest/jest print `FAIL `; npm test prints "Test failed".
162
+ // pytest prints "FAILED" capital. Bias to Bash tool but accept
163
+ // unscoped messages because hooks sometimes drop tool name.
164
+ if (msg.includes("test failed") ||
165
+ msg.includes("tests failed") ||
166
+ /\bfail(?:ed)?\b.*\btest\b/.test(msg) ||
167
+ /\b\d+\s+tests?\s+failed\b/.test(msg) ||
168
+ msg.includes("assertion") ||
169
+ /^fail\s/.test(msg) ||
170
+ /\nfail\s/.test(msg)) {
171
+ return { error_category: "test_failed", error_tool };
172
+ }
173
+ // ── runtime_error ────────────────────────────────────────────────
174
+ // Catch-all for uncaught JS exceptions and the common Python ones.
175
+ // Placed near the end so more specific buckets (syntax_error) win
176
+ // when both could match.
177
+ if (msg.includes("typeerror") ||
178
+ msg.includes("referenceerror") ||
179
+ msg.includes("rangeerror") ||
180
+ msg.includes("uncaught exception") ||
181
+ msg.includes("traceback (most recent call last)") ||
182
+ msg.includes("nullpointerexception")) {
183
+ return { error_category: "runtime_error", error_tool };
184
+ }
185
+ // Fallthrough — categorisation is best-effort; the dashboard renders
186
+ // "unknown" as a neutral bucket rather than dropping the event.
187
+ return { error_category: "unknown", error_tool };
188
+ }
189
+ // ── Command classifier ──────────────────────────────────────────────────
190
+ /**
191
+ * Tokenise a shell command, stripping leading env-var prefixes
192
+ * (`FOO=bar npm run build`) and `sudo`/`time` wrappers so the first
193
+ * meaningful token is returned.
194
+ */
195
+ function firstToken(command) {
196
+ const parts = command.trim().split(/\s+/);
197
+ let i = 0;
198
+ // Skip env-var assignments: KEY=value
199
+ while (i < parts.length && /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(parts[i]))
200
+ i++;
201
+ // Skip common wrappers that aren't the "real" tool.
202
+ while (i < parts.length && (parts[i] === "sudo" || parts[i] === "time" || parts[i] === "nice"))
203
+ i++;
204
+ return parts[i] ?? "";
205
+ }
206
+ /**
207
+ * Classify a Bash command into a workflow bucket. The bucket is derived
208
+ * from the (tool, sub-verb) pair: `npm test` → test, `npm run build` →
209
+ * build, `git commit` → git, `pip install` → install, etc.
210
+ *
211
+ * Heuristic ordering — most-specific verb checks first; falls through to
212
+ * `run` for generic execution and `other` for anything we can't classify.
213
+ */
214
+ export function classifyCommand(command) {
215
+ if (typeof command !== "string" || command.trim().length === 0) {
216
+ return { command_type: "other", command_tool: "" };
217
+ }
218
+ const raw = command.trim();
219
+ const tool = firstToken(raw).toLowerCase();
220
+ const lower = raw.toLowerCase();
221
+ // ── git ──────────────────────────────────────────────────────────
222
+ if (tool === "git") {
223
+ return { command_type: "git", command_tool: "git" };
224
+ }
225
+ // ── install ──────────────────────────────────────────────────────
226
+ // npm/pnpm/yarn/pip install variants come before generic test/build
227
+ // because `npm install --save-dev jest` would otherwise match test.
228
+ if (/\b(install|add|ci)\b/.test(lower) &&
229
+ (tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun" || tool === "pip" || tool === "pip3")) {
230
+ return { command_type: "install", command_tool: tool };
231
+ }
232
+ if (tool === "brew" && /\binstall\b/.test(lower)) {
233
+ return { command_type: "install", command_tool: "brew" };
234
+ }
235
+ // ── test ─────────────────────────────────────────────────────────
236
+ // Direct test runners, plus `npm test` / `pnpm test` / `yarn test`.
237
+ if (tool === "vitest" || tool === "jest" || tool === "pytest" ||
238
+ tool === "mocha" || tool === "playwright" || tool === "cypress") {
239
+ return { command_type: "test", command_tool: tool };
240
+ }
241
+ if ((tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun") &&
242
+ /\btest\b/.test(lower)) {
243
+ return { command_type: "test", command_tool: tool };
244
+ }
245
+ // ── lint ─────────────────────────────────────────────────────────
246
+ if (tool === "eslint" || tool === "tslint" || tool === "ruff" || tool === "flake8" || tool === "pylint") {
247
+ return { command_type: "lint", command_tool: tool };
248
+ }
249
+ if (/\blint\b/.test(lower) && (tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun")) {
250
+ return { command_type: "lint", command_tool: tool };
251
+ }
252
+ // ── format ───────────────────────────────────────────────────────
253
+ if (tool === "prettier" || tool === "black" || tool === "gofmt" || tool === "rustfmt") {
254
+ return { command_type: "format", command_tool: tool };
255
+ }
256
+ if (/\b(format|fmt)\b/.test(lower) && (tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun")) {
257
+ return { command_type: "format", command_tool: tool };
258
+ }
259
+ // ── build ────────────────────────────────────────────────────────
260
+ if (tool === "tsc" || tool === "webpack" || tool === "rollup" || tool === "vite" || tool === "esbuild" || tool === "make") {
261
+ return { command_type: "build", command_tool: tool };
262
+ }
263
+ if (/\bbuild\b/.test(lower) && (tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun" || tool === "cargo" || tool === "go")) {
264
+ return { command_type: "build", command_tool: tool };
265
+ }
266
+ // ── run ──────────────────────────────────────────────────────────
267
+ // Generic execution — `npm run …` (with no recognised sub-verb),
268
+ // `node script.js`, `python -m foo`, etc.
269
+ if ((tool === "npm" || tool === "pnpm" || tool === "yarn" || tool === "bun") &&
270
+ /\brun\b/.test(lower)) {
271
+ return { command_type: "run", command_tool: tool };
272
+ }
273
+ if (tool === "node" || tool === "python" || tool === "python3" || tool === "deno" || tool === "bun") {
274
+ return { command_type: "run", command_tool: tool };
275
+ }
276
+ // Fallthrough — preserves the tool name so the dashboard can still
277
+ // chart "other" commands by their underlying binary.
278
+ return { command_type: "other", command_tool: tool || "" };
279
+ }
280
+ // ── Duration bucketiser ────────────────────────────────────────────────
281
+ /**
282
+ * Bucketise a latency_ms reading into the four fixed buckets the
283
+ * dashboard renders. Bucket edges (in ms):
284
+ *
285
+ * fast : [0, 1_000)
286
+ * medium : [1_000, 10_000)
287
+ * slow : [10_000, 60_000)
288
+ * timeout : [60_000, ∞)
289
+ *
290
+ * Defensive: negative and non-numeric inputs collapse to `fast` (the
291
+ * neutral bucket) rather than throwing, because hooks have been seen
292
+ * to emit `null` for very short calls.
293
+ */
294
+ export function bucketizeDuration(latencyMs) {
295
+ const n = typeof latencyMs === "number" && Number.isFinite(latencyMs) ? latencyMs : 0;
296
+ if (n < 1_000)
297
+ return "fast";
298
+ if (n < 10_000)
299
+ return "medium";
300
+ if (n < 60_000)
301
+ return "slow";
302
+ return "timeout";
303
+ }