tewip-ai 0.67.0 → 0.69.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/tewip.js
CHANGED
|
@@ -199,7 +199,7 @@ async function main() {
|
|
|
199
199
|
if (!question)
|
|
200
200
|
throw new Error('usage: tewip recall "<an error, or a question>" [--test "<test id>"] [--limit 5]');
|
|
201
201
|
const { runRecall } = await import('../engine/recall/recall-command.js');
|
|
202
|
-
runRecall({ root: process.cwd(), question, testId: flag(args, 'test'), limit: Number(flag(args, 'limit')) || undefined, name: flag(args, 'name'), out: flag(args, 'out') });
|
|
202
|
+
await runRecall({ root: process.cwd(), question, testId: flag(args, 'test'), limit: Number(flag(args, 'limit')) || undefined, name: flag(args, 'name'), out: flag(args, 'out') });
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
205
|
case 'incident': {
|
|
@@ -24,7 +24,13 @@ function gather(root, only) {
|
|
|
24
24
|
}
|
|
25
25
|
export function runLearn(cmd) {
|
|
26
26
|
const { corrections, records } = gather(cmd.root, cmd.name);
|
|
27
|
-
|
|
27
|
+
// The denominator is diagnoses, not kept failures. A store can hold thousands of failures that
|
|
28
|
+
// nothing has diagnosed yet - the harvest writes exactly that - and counting those as decisions
|
|
29
|
+
// divides the corrections by a number far larger than the decisions Tewip actually made, making
|
|
30
|
+
// it look more accurate than it is. Found next to the same mistake in `recall`, which called 1,305
|
|
31
|
+
// undiagnosed failures "searched".
|
|
32
|
+
const diagnosed = records.filter((r) => r.diagnosis?.category).length;
|
|
33
|
+
const learned = whatWeLearned(corrections, loadVerdicts(cmd.root), records, diagnosed);
|
|
28
34
|
const report = renderLearning(learned);
|
|
29
35
|
console.log(report);
|
|
30
36
|
if (cmd.labels) {
|
|
@@ -1,43 +1,82 @@
|
|
|
1
1
|
// `tewip recall "<an error, or a question>"`: what this project already concluded.
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { buildItems } from '../../outlets/items.js';
|
|
5
|
+
import { datasetFiles, readRunSummaries, readTriageRecords } from '../../reporters/store.js';
|
|
5
6
|
import { loadCorrections } from '../corrections.js';
|
|
7
|
+
import { HistoryIndex } from '../history.js';
|
|
6
8
|
import { memories, recall, renderRecall } from './recall.js';
|
|
7
9
|
/** Everything kept for this project: its failures, and the times a person overruled Tewip. */
|
|
8
10
|
function kept(root, only) {
|
|
9
11
|
const dir = join(root, '.tewip', 'history');
|
|
10
12
|
if (!existsSync(dir))
|
|
11
|
-
return { records: [], corrections: [] };
|
|
13
|
+
return { records: [], runs: [], corrections: [] };
|
|
12
14
|
const records = [];
|
|
15
|
+
const runs = [];
|
|
13
16
|
const corrections = [];
|
|
14
17
|
for (const project of only ? [only] : readdirSync(dir)) {
|
|
15
18
|
const where = join(dir, project);
|
|
16
19
|
if (!existsSync(where))
|
|
17
20
|
continue;
|
|
18
|
-
|
|
21
|
+
const files = datasetFiles(where);
|
|
22
|
+
records.push(...readTriageRecords(files.triage));
|
|
23
|
+
runs.push(...readRunSummaries(files.runs));
|
|
19
24
|
corrections.push(...loadCorrections(where));
|
|
20
25
|
}
|
|
21
|
-
return { records, corrections };
|
|
26
|
+
return { records, runs, corrections };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The kept failures with a category on each, diagnosing the ones that do not have one stored.
|
|
30
|
+
*
|
|
31
|
+
* This is the whole bug Phase 59 was looking for. `memories()` keeps only records that carry a
|
|
32
|
+
* `diagnosis`, and **nothing on the collection path writes one**: the reporter and the harvest both
|
|
33
|
+
* store the raw failure, and the category is worked out when a report is rendered. So on a real
|
|
34
|
+
* 1,305-failure history, `recall` had nothing to search - and said "Nothing in this project's
|
|
35
|
+
* history resembles it... this is new here", footed by "Searched 1305 kept failure(s)". Both halves
|
|
36
|
+
* false: nothing was searched, and 660 of those 1,305 failures were the timeout being asked about.
|
|
37
|
+
*
|
|
38
|
+
* `note`, `flaky` and the risk digest had all been diagnosing on read for months. Recall was the
|
|
39
|
+
* one retrieval layer that demanded a field nobody writes, which made it permanently empty on every
|
|
40
|
+
* project that had not been through `tewip explain` - and empty in a way that read like an answer.
|
|
41
|
+
*
|
|
42
|
+
* Rules only, never a model: `recall`'s promise is no model, no network, nothing to set up, and a
|
|
43
|
+
* question asked of local history must not start costing money because of how it is indexed.
|
|
44
|
+
*/
|
|
45
|
+
async function diagnosed(records, runs, corrections) {
|
|
46
|
+
if (records.length === 0)
|
|
47
|
+
return [];
|
|
48
|
+
const items = await buildItems(records, new HistoryIndex(records, runs), { dataset: 'recall', llm: null, reuseStoredDiagnoses: true, corrections });
|
|
49
|
+
const byCase = new Map(items.map((item) => [`${item.runId}::${item.testId}`, item.diagnosis]));
|
|
50
|
+
return records.map((record) => (record.diagnosis ? record : { ...record, diagnosis: byCase.get(`${record.runId}::${record.testId}`) }));
|
|
22
51
|
}
|
|
23
52
|
/**
|
|
24
53
|
* The report, without printing it. An MCP server speaks JSON-RPC over stdout, so anything one of
|
|
25
54
|
* its tools calls has to return its answer: a stray console.log corrupts every response the
|
|
26
55
|
* assistant receives, and the corruption looks like the tool being broken rather than noisy.
|
|
27
56
|
*/
|
|
28
|
-
export function recallReport(cmd) {
|
|
29
|
-
const { records, corrections } = kept(cmd.root, cmd.name);
|
|
57
|
+
export async function recallReport(cmd) {
|
|
58
|
+
const { records, runs, corrections } = kept(cmd.root, cmd.name);
|
|
30
59
|
if (records.length === 0) {
|
|
31
60
|
return { report: 'Nothing has been kept here yet, so there is no history to search. Run `tewip explain <report>` first.', searched: 0, corrections: 0 };
|
|
32
61
|
}
|
|
33
|
-
|
|
34
|
-
|
|
62
|
+
// The count reported is the count searched. It used to be `records.length` whatever `memories`
|
|
63
|
+
// actually indexed, which is how an empty index came to be described as 1,305 failures searched.
|
|
64
|
+
const mem = memories(await diagnosed(records, runs, corrections), corrections);
|
|
65
|
+
if (mem.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
report: `${records.length} failure(s) are kept here, but none of them could be given a category, so there is nothing to compare this against. Recall searches what Tewip concluded about a failure, and a failure it cannot read is not a conclusion.`,
|
|
68
|
+
searched: 0,
|
|
69
|
+
corrections: corrections.length,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const found = recall(cmd.question, mem, { testId: cmd.testId, limit: cmd.limit ?? 5 });
|
|
73
|
+
return { report: renderRecall(cmd.question, found, mem.length), searched: mem.length, corrections: corrections.length };
|
|
35
74
|
}
|
|
36
|
-
export function runRecall(cmd) {
|
|
37
|
-
const { report, searched, corrections } = recallReport(cmd);
|
|
75
|
+
export async function runRecall(cmd) {
|
|
76
|
+
const { report, searched, corrections } = await recallReport(cmd);
|
|
38
77
|
console.log(report);
|
|
39
78
|
if (searched > 0)
|
|
40
|
-
console.error(`\nSearched ${searched}
|
|
79
|
+
console.error(`\nSearched ${searched} diagnosed failure(s) and ${corrections} correction(s). No model was called.`);
|
|
41
80
|
if (cmd.out) {
|
|
42
81
|
mkdirSync(dirname(resolve(cmd.out)), { recursive: true });
|
|
43
82
|
writeFileSync(resolve(cmd.out), `${report}\n`, 'utf8');
|
|
@@ -88,10 +88,20 @@ export function recall(question, all, { testId, limit = 5 } = {}) {
|
|
|
88
88
|
.slice(0, limit);
|
|
89
89
|
}
|
|
90
90
|
/** What a person reads. Every line names the run it came from, so nothing has to be believed. */
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* What a person reads.
|
|
93
|
+
*
|
|
94
|
+
* `searched` is not decoration. "Nothing in this project's history resembles it" is only an answer
|
|
95
|
+
* if something was actually compared against, and on a real 1,305-failure history this said exactly
|
|
96
|
+
* that while the searchable set was **empty** - see `recallReport`. The number belongs in the report
|
|
97
|
+
* body rather than only on stderr, because an MCP client and a `> file` both keep the body and throw
|
|
98
|
+
* stderr away, which is precisely where a reader would have caught it.
|
|
99
|
+
*/
|
|
100
|
+
export function renderRecall(question, found, searched) {
|
|
92
101
|
const lines = [`# What this project already knows`, '', `Asked: ${question.replace(/\s+/g, ' ').slice(0, 160)}`, ''];
|
|
93
102
|
if (found.length === 0) {
|
|
94
|
-
|
|
103
|
+
const against = searched === undefined ? 'this project\'s history' : `the ${searched} diagnosed failure(s) kept here`;
|
|
104
|
+
lines.push(`Nothing in ${against} resembles it. That is an answer, not a failure: it means`, 'this is new here, and there is no earlier decision to follow.');
|
|
95
105
|
return lines.join('\n');
|
|
96
106
|
}
|
|
97
107
|
const corrected = found.filter((f) => f.correctedBy);
|
|
@@ -24,6 +24,33 @@ async function latestReports(repo, branch) {
|
|
|
24
24
|
}
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Which of the artifact's reports to show, and in what order.
|
|
29
|
+
*
|
|
30
|
+
* This used to be two hard-coded names, `pr-comment.md` and `infra.md`, which are what the Action's
|
|
31
|
+
* *triage* mode writes. Its **check** mode writes a different set - `checks.md`, `done.md`,
|
|
32
|
+
* `schema.md`, `standards.md`, `vouch.md`, `access.md` - under the same artifact name, and both
|
|
33
|
+
* modes were read by the same two lines. Found by running `tewip ci hmamut39/tewip` against Tewip's
|
|
34
|
+
* own repository, whose self-check workflow runs in check mode: the artifact held 9KB of findings
|
|
35
|
+
* across six files and the command printed **167 characters** - a title, a run link, and nothing
|
|
36
|
+
* underneath. The answer to "why did CI fail on my PR?" was a URL.
|
|
37
|
+
*
|
|
38
|
+
* So the artifact decides what is shown, not this file. `pr-comment.md` leads when it exists because
|
|
39
|
+
* it is the one written to be read by a person on a pull request; `infra.md` stays behind its ENV
|
|
40
|
+
* count because it is about the machines rather than the change; everything else is named and shown.
|
|
41
|
+
*/
|
|
42
|
+
const LEADS = ['pr-comment.md', 'checks.md'];
|
|
43
|
+
export function reportsIn(files, envCount) {
|
|
44
|
+
const text = (name) => (files.get(name)?.toString('utf8') ?? '').trim();
|
|
45
|
+
const names = [...files.keys()].filter((n) => n.endsWith('.md'));
|
|
46
|
+
const ordered = [...LEADS.filter((n) => names.includes(n)), ...names.filter((n) => !LEADS.includes(n)).sort()];
|
|
47
|
+
return ordered
|
|
48
|
+
.filter((name) => (name === 'infra.md' ? envCount > 0 : true))
|
|
49
|
+
.map((name) => ({ name, body: text(name) }))
|
|
50
|
+
.filter((r) => r.body.length > 0);
|
|
51
|
+
}
|
|
52
|
+
/** A report's own heading if it has one, else its filename turned into words. */
|
|
53
|
+
const titleOf = (name) => name.replace(/\.md$/, '').replace(/[-_]/g, ' ');
|
|
27
54
|
export async function githubCiReport(repo, pr, branch) {
|
|
28
55
|
const head = pr ? (await ghApi(`repos/${repo}/pulls/${pr}`)).head.ref : branch;
|
|
29
56
|
const target = pr ? `PR #${pr} (${head})` : head ? `branch ${head}` : 'any branch';
|
|
@@ -37,14 +64,17 @@ export async function githubCiReport(repo, pr, branch) {
|
|
|
37
64
|
const bundle = JSON.parse(text('triage.json') || '{}');
|
|
38
65
|
const envCount = (bundle.items ?? []).filter((i) => i.diagnosis.category === 'ENV').length;
|
|
39
66
|
const suite = artifact.name.replace(/^(?:tewip|triage-agent)-report-?/, '');
|
|
67
|
+
const reports = reportsIn(files, envCount);
|
|
68
|
+
// A header with nothing under it is the worst of the three possible answers here, because it
|
|
69
|
+
// reads as "CI is fine" to anyone who does not know what the artifact was supposed to contain.
|
|
70
|
+
const body = reports.length > 0
|
|
71
|
+
? reports.map((r, i) => (i === 0 && LEADS.includes(r.name) ? r.body : `## ${titleOf(r.name)}\n\n${r.body}`)).join('\n\n')
|
|
72
|
+
: `This run's \`${artifact.name}\` artifact has nothing Tewip can show. It holds ${files.size === 0 ? 'no files' : `${files.size} file(s): ${[...files.keys()].sort().join(', ')}`}, and none of them has any content. The run itself is at ${found.run.html_url}.`;
|
|
40
73
|
sections.push([
|
|
41
74
|
`# Tewip CI report: ${repo}, ${target}${suite ? ` - suite "${suite}"` : ''}`,
|
|
42
75
|
`Run: ${bundle.runUrl ?? found.run.html_url} · commit \`${(bundle.sha ?? found.run.head_sha).slice(0, 8)}\` · generated ${bundle.generatedAt ?? artifact.created_at}${bundle.fixPr ? ` · verified fix PR: ${bundle.fixPr}` : ''}`,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
]
|
|
46
|
-
.filter(Boolean)
|
|
47
|
-
.join('\n\n'));
|
|
76
|
+
body,
|
|
77
|
+
].join('\n\n'));
|
|
48
78
|
}
|
|
49
79
|
return cap(sections.join('\n\n---\n\n'));
|
|
50
80
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -111,7 +111,7 @@ server.registerTool('recall', {
|
|
|
111
111
|
if (!isAbsolute(root) || !existsSync(root))
|
|
112
112
|
throw new Error(`project must be an existing absolute path, got "${root}"`);
|
|
113
113
|
const { recallReport } = await import('../engine/recall/recall-command.js');
|
|
114
|
-
return recallReport({ root, question, testId: test }).report;
|
|
114
|
+
return (await recallReport({ root, question, testId: test })).report;
|
|
115
115
|
}));
|
|
116
116
|
server.registerTool('trace_requirements', {
|
|
117
117
|
title: 'Is this requirement covered, and is it passing?',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.0",
|
|
4
4
|
"mcpName": "io.github.hmamut39/tewip",
|
|
5
5
|
"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.",
|
|
6
6
|
"keywords": [
|