tewip-ai 0.62.0 → 0.64.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/engine/llm/llm-diagnose.js +66 -1
- package/dist/reporters/store.js +57 -12
- package/package.json +1 -1
|
@@ -23,13 +23,78 @@ export function parseLlmOutput(raw) {
|
|
|
23
23
|
evidence: value.evidence.slice(0, 4).map((e) => truncate(e, 300)),
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* A verdict the run history already rules out, whatever the model says.
|
|
28
|
+
*
|
|
29
|
+
* The prompt has told the model since llm-6 that a failure which passed on another attempt of the
|
|
30
|
+
* same code cannot be a real bug or a drifted locator - both of those fail every time. Measured
|
|
31
|
+
* against 547 real labelled CI failures from WordPress/gutenberg, it does not obey: the model layer
|
|
32
|
+
* decided 87 of them and got **9 right**, and almost every miss was a flaky timing failure called
|
|
33
|
+
* REAL_BUG or LOCATOR_DRIFT at a confidence of 0.9 or higher.
|
|
34
|
+
*
|
|
35
|
+
* Telling a team to hunt a bug that is not there is the most expensive mistake this tool can make,
|
|
36
|
+
* and an instruction in a prompt is a request, not a guarantee. The same fact is checked here in
|
|
37
|
+
* code, where it holds.
|
|
38
|
+
*
|
|
39
|
+
* It refuses rather than rewrites. "This cannot be REAL_BUG" is not "this is TIMING" - ENV and
|
|
40
|
+
* TEST_DATA both survive a retry too - so the answer becomes UNKNOWN, which routes the failure to a
|
|
41
|
+
* person with the model's reasoning attached rather than replacing one guess with another.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* The most this layer may claim, until a measured run says otherwise.
|
|
45
|
+
*
|
|
46
|
+
* Below 0.5 so that it routes to a person by the rule the prompt already states: "Low confidence
|
|
47
|
+
* routes the failure to a human". See the comment at the call site for the measurement.
|
|
48
|
+
*/
|
|
49
|
+
export const LLM_CONFIDENCE_CEILING = 0.45;
|
|
50
|
+
export function contradictsHistory(category, context) {
|
|
51
|
+
if (category !== 'REAL_BUG' && category !== 'LOCATOR_DRIFT')
|
|
52
|
+
return null;
|
|
53
|
+
if (context.thisRun?.anotherAttemptPassed) {
|
|
54
|
+
return `refused: another attempt of the same code passed in this run, so the failure is not deterministic and cannot be ${category}`;
|
|
55
|
+
}
|
|
56
|
+
if (/both failed and passed|not deterministic/i.test(context.history?.pattern ?? '')) {
|
|
57
|
+
return `refused: the history says "${context.history.pattern}", so the failure is not deterministic and cannot be ${category}`;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
26
61
|
export async function llmDiagnose(context, rules, client, now = new Date()) {
|
|
27
62
|
try {
|
|
28
63
|
const raw = await client.complete({ system: SYSTEM_PROMPT, user: JSON.stringify(context), schemaName: OUTPUT_SCHEMA_NAME, schema: OUTPUT_SCHEMA });
|
|
29
64
|
const out = parseLlmOutput(raw);
|
|
65
|
+
const refused = contradictsHistory(out.category, context);
|
|
66
|
+
if (refused) {
|
|
67
|
+
return {
|
|
68
|
+
category: 'UNKNOWN',
|
|
69
|
+
confidence: 0,
|
|
70
|
+
source: 'llm',
|
|
71
|
+
model: client.model,
|
|
72
|
+
engineVersion: `${rules.engineVersion}+${PROMPT_VERSION}`,
|
|
73
|
+
diagnosedAt: now.toISOString(),
|
|
74
|
+
evidence: [
|
|
75
|
+
{ source: 'llm', message: `the model said ${out.category} (${out.confidence.toFixed(2)}): ${out.reasoning}` },
|
|
76
|
+
{ source: 'rules', message: refused },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
30
80
|
return {
|
|
31
81
|
category: out.category,
|
|
32
|
-
|
|
82
|
+
// Capped, because the number it reports is not the number it earns.
|
|
83
|
+
//
|
|
84
|
+
// Measured against 752 real labelled CI failures from WordPress/gutenberg - labels taken from
|
|
85
|
+
// what the developer actually changed to fix each one - this layer decided 90 and got 16
|
|
86
|
+
// right: **17.8%**. Seventy per cent of those failures are TIMING, so answering "TIMING" to
|
|
87
|
+
// everything would have scored about 70%. The model is four times worse than the most trivial
|
|
88
|
+
// baseline there is, and it was reporting 0.90 to 0.96 while being wrong.
|
|
89
|
+
//
|
|
90
|
+
// The rules layer, on the same data, is 98.5% and keeps its own confidence untouched.
|
|
91
|
+
//
|
|
92
|
+
// Nothing is discarded: the category and the reasoning are still reported, and a person still
|
|
93
|
+
// sees them. What changes is that no consumer downstream can mistake this for a settled
|
|
94
|
+
// answer - 0.7 is where a rule becomes actionable and 0.85 is where a fix may be attempted,
|
|
95
|
+
// and a layer at 17.8% has no business clearing either. Raise this when a measured run earns
|
|
96
|
+
// it, and not before.
|
|
97
|
+
confidence: Math.min(out.confidence, LLM_CONFIDENCE_CEILING),
|
|
33
98
|
source: 'llm',
|
|
34
99
|
model: client.model,
|
|
35
100
|
engineVersion: `${rules.engineVersion}+${PROMPT_VERSION}`,
|
package/dist/reporters/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// JSONL persistence shared by the reporter, the history script and future layers.
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { openSync, readSync, closeSync, fstatSync, appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
export const PROJECT_ROOT = join(import.meta.dirname, '..');
|
|
5
5
|
export const DATA_DIR = join(PROJECT_ROOT, 'data');
|
|
@@ -13,23 +13,68 @@ export function appendJsonl(file, rows) {
|
|
|
13
13
|
mkdirSync(dirname(file), { recursive: true });
|
|
14
14
|
appendFileSync(file, rows.map((row) => JSON.stringify(row)).join('\n') + '\n', 'utf8');
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Reads a JSONL file; missing file yields []. Malformed lines are skipped with a warning.
|
|
18
|
+
*
|
|
19
|
+
* Read in chunks rather than whole. `readFileSync(file, 'utf8')` builds one string, and a string in
|
|
20
|
+
* Node cannot exceed 0x1fffffe8 characters - about 512MB. These files only ever grow: the weekly
|
|
21
|
+
* shadow run appends every CI failure it harvests from a public project and keeps the lot, so that
|
|
22
|
+
* accuracy is measured against everything ever collected rather than one week of it. gutenberg's
|
|
23
|
+
* dataset crossed the limit and **every run since has died on this line**, for weeks, while the
|
|
24
|
+
* workflow's only symptom was a red tick nobody was looking at.
|
|
25
|
+
*
|
|
26
|
+
* A UTF-8 character can straddle a chunk boundary, so the last partial line is carried into the next
|
|
27
|
+
* read rather than decoded on its own - decoding a split character gives U+FFFD and a line that no
|
|
28
|
+
* longer parses.
|
|
29
|
+
*/
|
|
17
30
|
export function readJsonl(file) {
|
|
18
31
|
if (!existsSync(file))
|
|
19
32
|
return [];
|
|
20
33
|
const rows = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
const handle = openSync(file, 'r');
|
|
35
|
+
try {
|
|
36
|
+
const size = fstatSync(handle).size;
|
|
37
|
+
const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
|
|
38
|
+
let carry = Buffer.alloc(0);
|
|
39
|
+
let line = 0;
|
|
40
|
+
let offset = 0;
|
|
41
|
+
while (offset < size) {
|
|
42
|
+
const read = readSync(handle, buffer, 0, buffer.length, offset);
|
|
43
|
+
if (read <= 0)
|
|
44
|
+
break;
|
|
45
|
+
offset += read;
|
|
46
|
+
const chunk = Buffer.concat([carry, buffer.subarray(0, read)]);
|
|
47
|
+
let from = 0;
|
|
48
|
+
for (let i = 0; i < chunk.length; i += 1) {
|
|
49
|
+
if (chunk[i] !== 0x0a)
|
|
50
|
+
continue;
|
|
51
|
+
const text = chunk.subarray(from, i).toString('utf8').replace(/\r$/, '');
|
|
52
|
+
from = i + 1;
|
|
53
|
+
line += 1;
|
|
54
|
+
if (!text.trim())
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
rows.push(JSON.parse(text));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
console.warn(`[triage] skipping malformed line ${line} in ${file}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
carry = chunk.subarray(from);
|
|
28
64
|
}
|
|
29
|
-
|
|
30
|
-
|
|
65
|
+
const last = carry.toString('utf8').replace(/\r$/, '');
|
|
66
|
+
if (last.trim()) {
|
|
67
|
+
try {
|
|
68
|
+
rows.push(JSON.parse(last));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
console.warn(`[triage] skipping malformed line ${line + 1} in ${file}`);
|
|
72
|
+
}
|
|
31
73
|
}
|
|
32
|
-
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
closeSync(handle);
|
|
77
|
+
}
|
|
33
78
|
return rows;
|
|
34
79
|
}
|
|
35
80
|
export const readTriageRecords = (file = TRIAGE_FILE) => readJsonl(file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.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": [
|