tewip-ai 0.20.0 → 0.22.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,17 @@ 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 trace [--only "<ticket or title>"] [--out trace.md]
|
|
104
|
+
Is this requirement covered, and is it passing right now? Joins
|
|
105
|
+
the plans Tewip wrote to the tests that cover them and the runs
|
|
106
|
+
that attempted them. A scenario counts as covered only when a
|
|
107
|
+
real test can be named, and passing only when a real run
|
|
108
|
+
attempted it. No model, no cost.
|
|
109
|
+
tewip recall "<an error, or a question>" [--test "<id>"] [--limit 5]
|
|
110
|
+
What this project already concluded: earlier failures that look
|
|
111
|
+
like this one, what was decided, by which rule or which person,
|
|
112
|
+
and the run each came from. Searches the history Tewip keeps -
|
|
113
|
+
no model, no network, nothing to set up.
|
|
103
114
|
tewip incident <sentry.json | otel.json | stack.txt> [--framework <id>] [--out incident.md]
|
|
104
115
|
Turn a production incident into a test that proves it: reads
|
|
105
116
|
Sentry, OpenTelemetry or a pasted stack trace, writes a test
|
package/dist/cli/tewip.js
CHANGED
|
@@ -177,6 +177,19 @@ 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 'trace': {
|
|
181
|
+
const { runTrace } = await import('../engine/trace/trace-command.js');
|
|
182
|
+
runTrace({ root: process.cwd(), only: flag(args, 'only'), name: flag(args, 'name'), out: flag(args, 'out') });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
case 'recall': {
|
|
186
|
+
const question = args.filter((a, i) => !a.startsWith('--') && !['--test', '--limit', '--name', '--out'].includes(args[i - 1] ?? '')).join(' ').trim();
|
|
187
|
+
if (!question)
|
|
188
|
+
throw new Error('usage: tewip recall "<an error, or a question>" [--test "<test id>"] [--limit 5]');
|
|
189
|
+
const { runRecall } = await import('../engine/recall/recall-command.js');
|
|
190
|
+
runRecall({ root: process.cwd(), question, testId: flag(args, 'test'), limit: Number(flag(args, 'limit')) || undefined, name: flag(args, 'name'), out: flag(args, 'out') });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
180
193
|
case 'incident': {
|
|
181
194
|
const file = args.find((a, i) => !a.startsWith('--') && !['--framework', '--out'].includes(args[i - 1] ?? ''));
|
|
182
195
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// `tewip trace`: is this requirement covered, and is it passing right now?
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { datasetFiles, readJsonl, readTriageRecords } from '../../reporters/store.js';
|
|
5
|
+
import { renderTrace, tracePlan } from './trace.js';
|
|
6
|
+
/** The plans this project has kept, as `tewip plan` saved them. */
|
|
7
|
+
function plans(root, only) {
|
|
8
|
+
const dir = join(root, '.tewip', 'plans');
|
|
9
|
+
if (!existsSync(dir))
|
|
10
|
+
return [];
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith('.json'))) {
|
|
13
|
+
try {
|
|
14
|
+
// `tewip plan` saves { plan, feature, urls, model }: the scenarios live under `plan`, and the
|
|
15
|
+
// feature - the words someone actually asked for - sits beside it. Reading the top level
|
|
16
|
+
// alone found no plans at all in a project that had one.
|
|
17
|
+
const saved = JSON.parse(readFileSync(join(dir, file), 'utf8'));
|
|
18
|
+
const inner = saved.plan ?? saved;
|
|
19
|
+
if (!Array.isArray(inner?.scenarios))
|
|
20
|
+
continue;
|
|
21
|
+
const parsed = { ...inner, feature: saved.feature ?? inner.feature, ticket: saved.ticket ?? inner.ticket };
|
|
22
|
+
const haystack = `${parsed.title ?? ''} ${parsed.feature ?? ''} ${parsed.ticket ?? ''} ${file}`.toLowerCase();
|
|
23
|
+
if (only && !haystack.includes(only.toLowerCase()))
|
|
24
|
+
continue;
|
|
25
|
+
out.push(parsed);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// A plan that is not readable JSON is not a plan; the Markdown beside it is still there.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function history(root, only) {
|
|
34
|
+
const dir = join(root, '.tewip', 'history');
|
|
35
|
+
if (!existsSync(dir))
|
|
36
|
+
return { runs: [], records: [] };
|
|
37
|
+
const runs = [];
|
|
38
|
+
const records = [];
|
|
39
|
+
for (const project of only ? [only] : readdirSync(dir)) {
|
|
40
|
+
const where = join(dir, project);
|
|
41
|
+
if (!existsSync(where))
|
|
42
|
+
continue;
|
|
43
|
+
runs.push(...readJsonl(datasetFiles(where).runs));
|
|
44
|
+
records.push(...readTriageRecords(datasetFiles(where).triage));
|
|
45
|
+
}
|
|
46
|
+
runs.sort((a, b) => (a.finishedAt > b.finishedAt ? 1 : -1));
|
|
47
|
+
return { runs, records };
|
|
48
|
+
}
|
|
49
|
+
export function runTrace(cmd) {
|
|
50
|
+
const found = plans(cmd.root, cmd.only);
|
|
51
|
+
const { runs, records } = history(cmd.root, cmd.name);
|
|
52
|
+
const traced = found.map((plan) => tracePlan(plan, runs, records));
|
|
53
|
+
const report = renderTrace(traced);
|
|
54
|
+
console.log(report);
|
|
55
|
+
if (found.length > 0) {
|
|
56
|
+
console.error(`\nTraced ${found.length} plan(s) against ${runs.length} kept run(s). No model was called.`);
|
|
57
|
+
}
|
|
58
|
+
if (cmd.out) {
|
|
59
|
+
mkdirSync(dirname(resolve(cmd.out)), { recursive: true });
|
|
60
|
+
writeFileSync(resolve(cmd.out), `${report}\n`, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
return traced;
|
|
63
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const clean = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
2
|
+
/**
|
|
3
|
+
* The test that covers a scenario, named from evidence rather than hope.
|
|
4
|
+
*
|
|
5
|
+
* Two ways a link is real: the plan says which existing test covers it - and that claim was already
|
|
6
|
+
* checked against the project's real test names when the plan was written - or a test file carries
|
|
7
|
+
* the scenario's own id, which is how `tewip write` names what it creates.
|
|
8
|
+
*/
|
|
9
|
+
export function testFor(scenario, attempted) {
|
|
10
|
+
const claim = (scenario.covered_by ?? '').trim();
|
|
11
|
+
if (claim) {
|
|
12
|
+
const exact = attempted.find((t) => t === claim);
|
|
13
|
+
if (exact)
|
|
14
|
+
return exact;
|
|
15
|
+
// A plan names a test as a person writes it; a report names it as the runner prints it.
|
|
16
|
+
const loose = attempted.find((t) => clean(t).includes(clean(claim.split(':')[0])) && clean(claim.split(':')[0]).length > 6);
|
|
17
|
+
if (loose)
|
|
18
|
+
return loose;
|
|
19
|
+
return claim; // named, but this project has not attempted it in any kept run
|
|
20
|
+
}
|
|
21
|
+
const id = clean(scenario.id);
|
|
22
|
+
return id.length >= 4 ? (attempted.find((t) => clean(t).includes(id)) ?? '') : '';
|
|
23
|
+
}
|
|
24
|
+
/** Joins one plan to what the runs actually say. Nothing here is inferred from anything else. */
|
|
25
|
+
export function tracePlan(plan, runs, records) {
|
|
26
|
+
const newest = runs.at(-1);
|
|
27
|
+
const attempted = newest ? Object.keys(newest.attempts ?? {}) : [];
|
|
28
|
+
const everAttempted = new Set(runs.flatMap((r) => Object.keys(r.attempts ?? {})));
|
|
29
|
+
const failures = new Map();
|
|
30
|
+
for (const record of records)
|
|
31
|
+
if (record.runId === newest?.runId)
|
|
32
|
+
failures.set(record.testId, record);
|
|
33
|
+
const scenarios = plan.scenarios.map((scenario) => {
|
|
34
|
+
const test = testFor(scenario, attempted);
|
|
35
|
+
const base = { id: scenario.id, title: scenario.title, priority: scenario.priority ?? '', test, runId: newest?.runId ?? '', when: newest?.finishedAt ?? '' };
|
|
36
|
+
if (!test) {
|
|
37
|
+
return { ...base, standing: 'not-covered', diagnosis: '', why: 'no test in this project can be named for it, so nothing is checking it' };
|
|
38
|
+
}
|
|
39
|
+
if (!everAttempted.has(test)) {
|
|
40
|
+
return { ...base, standing: 'never-run', runId: '', when: '', diagnosis: '', why: `${test} is named as the cover, but no run kept here has attempted it` };
|
|
41
|
+
}
|
|
42
|
+
const failed = failures.get(test);
|
|
43
|
+
if (failed) {
|
|
44
|
+
const d = failed.diagnosis;
|
|
45
|
+
const diagnosis = d?.category ? `${d.category} at ${Number(d.confidence ?? 0).toFixed(2)} (${d.source ?? 'unknown'})` : 'failed, undiagnosed';
|
|
46
|
+
return { ...base, standing: 'failing', diagnosis, why: `${test} failed in the last run, and Tewip calls it ${d?.category ?? 'undiagnosed'}` };
|
|
47
|
+
}
|
|
48
|
+
if (!attempted.includes(test)) {
|
|
49
|
+
return { ...base, standing: 'never-run', diagnosis: '', why: `${test} ran before but not in the last run, so its standing now is unknown` };
|
|
50
|
+
}
|
|
51
|
+
return { ...base, standing: 'passing', diagnosis: '', why: `${test} was attempted in the last run and recorded no failure` };
|
|
52
|
+
});
|
|
53
|
+
return { requirement: plan.title || plan.feature || 'an unnamed requirement', ticket: plan.ticket ?? '', scenarios };
|
|
54
|
+
}
|
|
55
|
+
const MARK = { passing: 'passing', failing: 'FAILING', 'never-run': 'never run', 'not-covered': 'NOT COVERED' };
|
|
56
|
+
/** The answer to "is this covered, and is it passing", written so it can be disagreed with. */
|
|
57
|
+
export function renderTrace(traced) {
|
|
58
|
+
const all = traced.flatMap((t) => t.scenarios);
|
|
59
|
+
const count = (s) => all.filter((x) => x.standing === s).length;
|
|
60
|
+
const lines = ['# From what was asked for, to what is passing', ''];
|
|
61
|
+
if (all.length === 0) {
|
|
62
|
+
return [
|
|
63
|
+
'# From what was asked for, to what is passing',
|
|
64
|
+
'',
|
|
65
|
+
'No plan has been kept here, so there is nothing to trace. `tewip plan "<what it should do>"`',
|
|
66
|
+
'writes one, and `tewip write` turns its scenarios into tests.',
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
lines.push(`${all.length} scenario(s) across ${traced.length} requirement(s): ${count('passing')} passing, ${count('failing')} failing, ${count('never-run')} never run, ${count('not-covered')} not covered.`, '', 'A scenario counts as covered only when a real test can be named, and as passing only when a real', 'run attempted it. Everything else says what is missing instead.', '');
|
|
70
|
+
for (const requirement of traced) {
|
|
71
|
+
lines.push(`## ${requirement.requirement}${requirement.ticket ? ` (${requirement.ticket})` : ''}`, '');
|
|
72
|
+
for (const s of requirement.scenarios) {
|
|
73
|
+
lines.push(`- **${MARK[s.standing]}** — ${s.id}${s.priority ? ` (${s.priority})` : ''}: ${s.title}`);
|
|
74
|
+
lines.push(` - ${s.why}`);
|
|
75
|
+
if (s.diagnosis)
|
|
76
|
+
lines.push(` - Diagnosis: ${s.diagnosis}`);
|
|
77
|
+
// A run is only worth quoting when it says something about this scenario. Printing the last
|
|
78
|
+
// run beside "no test can be named for it" invited the reader to think one had checked it.
|
|
79
|
+
if (s.runId && s.test)
|
|
80
|
+
lines.push(` - From run \`${s.runId}\`${s.when ? ` on ${s.when.slice(0, 10)}` : ''}`);
|
|
81
|
+
}
|
|
82
|
+
lines.push('');
|
|
83
|
+
}
|
|
84
|
+
const worst = all.filter((s) => s.standing === 'failing' || s.standing === 'not-covered');
|
|
85
|
+
if (worst.length > 0) {
|
|
86
|
+
lines.push('## What to do first', '');
|
|
87
|
+
for (const s of worst.slice(0, 5))
|
|
88
|
+
lines.push(`- ${s.standing === 'failing' ? `fix or triage ${s.test}` : `write a test for ${s.id}: ${s.title}`}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
}
|
|
91
|
+
lines.push('Every line above comes from a plan Tewip wrote and a run it recorded. Nothing is inferred.');
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|