tewip-ai 0.20.0 → 0.21.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/dist/cli/help.js CHANGED
@@ -100,6 +100,11 @@ Usage:
100
100
  same way more than once, which rule decided it, and the failures
101
101
  it came from. Says "not yet" rather than inventing a pattern
102
102
  from two cases. No model, no cost.
103
+ tewip recall "<an error, or a question>" [--test "<id>"] [--limit 5]
104
+ What this project already concluded: earlier failures that look
105
+ like this one, what was decided, by which rule or which person,
106
+ and the run each came from. Searches the history Tewip keeps -
107
+ no model, no network, nothing to set up.
103
108
  tewip incident <sentry.json | otel.json | stack.txt> [--framework <id>] [--out incident.md]
104
109
  Turn a production incident into a test that proves it: reads
105
110
  Sentry, OpenTelemetry or a pasted stack trace, writes a test
package/dist/cli/tewip.js CHANGED
@@ -177,6 +177,14 @@ async function main() {
177
177
  runLearn({ root: process.cwd(), name: flag(args, 'name'), labels: flag(args, 'labels'), out: flag(args, 'out') });
178
178
  return;
179
179
  }
180
+ case 'recall': {
181
+ const question = args.filter((a, i) => !a.startsWith('--') && !['--test', '--limit', '--name', '--out'].includes(args[i - 1] ?? '')).join(' ').trim();
182
+ if (!question)
183
+ throw new Error('usage: tewip recall "<an error, or a question>" [--test "<test id>"] [--limit 5]');
184
+ const { runRecall } = await import('../engine/recall/recall-command.js');
185
+ runRecall({ root: process.cwd(), question, testId: flag(args, 'test'), limit: Number(flag(args, 'limit')) || undefined, name: flag(args, 'name'), out: flag(args, 'out') });
186
+ return;
187
+ }
180
188
  case 'incident': {
181
189
  const file = args.find((a, i) => !a.startsWith('--') && !['--framework', '--out'].includes(args[i - 1] ?? ''));
182
190
  if (!file)
@@ -0,0 +1,39 @@
1
+ // `tewip recall "<an error, or a question>"`: what this project already concluded.
2
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { datasetFiles, readTriageRecords } from '../../reporters/store.js';
5
+ import { loadCorrections } from '../corrections.js';
6
+ import { memories, recall, renderRecall } from './recall.js';
7
+ /** Everything kept for this project: its failures, and the times a person overruled Tewip. */
8
+ function kept(root, only) {
9
+ const dir = join(root, '.tewip', 'history');
10
+ if (!existsSync(dir))
11
+ return { records: [], corrections: [] };
12
+ const records = [];
13
+ const corrections = [];
14
+ for (const project of only ? [only] : readdirSync(dir)) {
15
+ const where = join(dir, project);
16
+ if (!existsSync(where))
17
+ continue;
18
+ records.push(...readTriageRecords(datasetFiles(where).triage));
19
+ corrections.push(...loadCorrections(where));
20
+ }
21
+ return { records, corrections };
22
+ }
23
+ export function runRecall(cmd) {
24
+ const { records, corrections } = kept(cmd.root, cmd.name);
25
+ if (records.length === 0) {
26
+ const empty = 'Nothing has been kept here yet, so there is no history to search. Run `tewip explain <report>` first.';
27
+ console.log(empty);
28
+ return empty;
29
+ }
30
+ const found = recall(cmd.question, memories(records, corrections), { testId: cmd.testId, limit: cmd.limit ?? 5 });
31
+ const report = renderRecall(cmd.question, found);
32
+ console.log(report);
33
+ console.error(`\nSearched ${records.length} kept failure(s) and ${corrections.length} correction(s). No model was called.`);
34
+ if (cmd.out) {
35
+ mkdirSync(dirname(resolve(cmd.out)), { recursive: true });
36
+ writeFileSync(resolve(cmd.out), `${report}\n`, 'utf8');
37
+ }
38
+ return report;
39
+ }
@@ -0,0 +1,109 @@
1
+ import { errorShape } from '../signature.js';
2
+ /** Words worth matching on: rare, specific, not the furniture of every stack trace. */
3
+ const COMMON = new Set([
4
+ 'error', 'failed', 'failure', 'test', 'expected', 'received', 'actual', 'value', 'object', 'null', 'undefined', 'true', 'false', 'none', 'with', 'from', 'this', 'that', 'have', 'been', 'were', 'when', 'then', 'there', 'which', 'while', 'after', 'before', 'timeout', 'waiting', 'call', 'log', 'line', 'file', 'at', 'in', 'on', 'of', 'to', 'for', 'and', 'the', 'a', 'an', 'is', 'it', 'was', 'not', 'no', 'yes',
5
+ ]);
6
+ const words = (text) => (errorShape(text).toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) ?? []).filter((w) => !COMMON.has(w));
7
+ /** How rare each word is across the history: a word in every failure tells you nothing. */
8
+ function rarity(all) {
9
+ const seen = new Map();
10
+ for (const doc of all)
11
+ for (const w of new Set(doc))
12
+ seen.set(w, (seen.get(w) ?? 0) + 1);
13
+ const total = Math.max(all.length, 1);
14
+ const out = new Map();
15
+ for (const [w, n] of seen)
16
+ out.set(w, Math.log(total / n) + 1);
17
+ return out;
18
+ }
19
+ /** Everything Tewip has kept about this project, as memories that can be cited. */
20
+ export function memories(records, corrections = []) {
21
+ const byShape = new Map();
22
+ for (const c of corrections)
23
+ byShape.set(`${c.testId}::${c.errorShape}`, c);
24
+ const out = [];
25
+ for (const record of records) {
26
+ const diagnosis = record.diagnosis;
27
+ if (!diagnosis?.category)
28
+ continue;
29
+ const correction = byShape.get(`${record.testId}::${errorShape(record.errorMessage)}`);
30
+ out.push({
31
+ testId: record.testId,
32
+ category: correction?.category ?? diagnosis.category,
33
+ decidedBy: correction ? `${correction.by} (a person)` : String(diagnosis.source ?? 'unknown'),
34
+ when: record.timestamp,
35
+ runId: record.runId,
36
+ error: (record.errorMessage ?? '').replace(/\s+/g, ' ').slice(0, 220),
37
+ full: (record.errorMessage ?? '').replace(/\s+/g, ' ').slice(0, 4000),
38
+ ...(correction ? { correctedBy: correction.by, correctionReason: correction.reason } : {}),
39
+ });
40
+ }
41
+ return out;
42
+ }
43
+ /**
44
+ * The memories that answer a question, most useful first.
45
+ *
46
+ * Three things decide the order, and each is stated in `why` so a person can disagree with it: the
47
+ * same error shape is the strongest signal there is, the same test is next, and shared rare words
48
+ * carry the rest. A memory a person corrected is lifted above one only Tewip decided, because what
49
+ * the team concluded outranks what the tool concluded.
50
+ */
51
+ export function recall(question, all, { testId, limit = 5 } = {}) {
52
+ if (all.length === 0)
53
+ return [];
54
+ const docs = all.map((m) => words(`${m.testId} ${m.full ?? m.error}`));
55
+ const weight = rarity(docs);
56
+ const asked = new Set(words(question));
57
+ const askedShape = errorShape(question);
58
+ const scored = all.map((memory, i) => {
59
+ const shared = docs[i].filter((w) => asked.has(w));
60
+ const mass = [...new Set(shared)].reduce((sum, w) => sum + (weight.get(w) ?? 1), 0);
61
+ const possible = [...asked].reduce((sum, w) => sum + (weight.get(w) ?? 1), 0) || 1;
62
+ let score = Math.min(mass / possible, 1);
63
+ const reasons = [];
64
+ if (askedShape && errorShape(memory.full ?? memory.error) === askedShape) {
65
+ score = Math.max(score, 0.95);
66
+ reasons.push('the same error, word for word once the changing parts are removed');
67
+ }
68
+ else if (shared.length > 0) {
69
+ reasons.push(`shares ${[...new Set(shared)].slice(0, 4).join(', ')}`);
70
+ }
71
+ if (testId && memory.testId === testId) {
72
+ score = Math.min(score + 0.25, 1);
73
+ reasons.push('the same test');
74
+ }
75
+ if (memory.correctedBy) {
76
+ score = Math.min(score + 0.1, 1);
77
+ reasons.push(`${memory.correctedBy} corrected this one`);
78
+ }
79
+ return { ...memory, score, sameTest: Boolean(testId && memory.testId === testId), why: reasons.join('; ') || 'nothing in common but the project' };
80
+ });
81
+ // Two memories can both reach the ceiling - an identical error recorded twice does - so the
82
+ // ordering needs a tie-break of its own. When the question is about one test, that test's own
83
+ // history comes first; otherwise the most recent, because the project changes underneath you.
84
+ return scored
85
+ .filter((m) => m.score > 0.15)
86
+ .sort((a, b) => b.score - a.score || Number(b.sameTest) - Number(a.sameTest) || (b.when > a.when ? 1 : -1))
87
+ .map(({ sameTest, ...rest }) => rest)
88
+ .slice(0, limit);
89
+ }
90
+ /** What a person reads. Every line names the run it came from, so nothing has to be believed. */
91
+ export function renderRecall(question, found) {
92
+ const lines = [`# What this project already knows`, '', `Asked: ${question.replace(/\s+/g, ' ').slice(0, 160)}`, ''];
93
+ if (found.length === 0) {
94
+ lines.push('Nothing in this project\'s history resembles it. That is an answer, not a failure: it means', 'this is new here, and there is no earlier decision to follow.');
95
+ return lines.join('\n');
96
+ }
97
+ const corrected = found.filter((f) => f.correctedBy);
98
+ lines.push(`${found.length} earlier failure(s) look like this one.${corrected.length > 0 ? ` ${corrected.length} of them a person judged themselves.` : ''}`, '');
99
+ for (const memory of found) {
100
+ lines.push(`- **${memory.category}** — ${memory.testId}`);
101
+ lines.push(` - Decided by ${memory.decidedBy}${memory.when ? ` on ${memory.when.slice(0, 10)}` : ''}, run \`${memory.runId}\``);
102
+ if (memory.correctionReason)
103
+ lines.push(` - They said: ${memory.correctionReason}`);
104
+ lines.push(` - Why it matched: ${memory.why}`);
105
+ lines.push(` - The error then: ${memory.error.slice(0, 140)}`);
106
+ }
107
+ lines.push('', 'No model was called to answer this, and nothing above is a guess: each line is a record kept from a real run of this project.');
108
+ return lines.join('\n');
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
5
5
  "keywords": [
6
6
  "test-failures",