tewip-ai 0.21.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,12 @@ 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.
103
109
  tewip recall "<an error, or a question>" [--test "<id>"] [--limit 5]
104
110
  What this project already concluded: earlier failures that look
105
111
  like this one, what was decided, by which rule or which person,
package/dist/cli/tewip.js CHANGED
@@ -177,6 +177,11 @@ 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
+ }
180
185
  case 'recall': {
181
186
  const question = args.filter((a, i) => !a.startsWith('--') && !['--test', '--limit', '--name', '--out'].includes(args[i - 1] ?? '')).join(' ').trim();
182
187
  if (!question)
@@ -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.21.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",