@ia-qa/self-healing 1.6.6 → 1.6.8
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/README.md +118 -2
- package/TUTORIAL.md +138 -3
- package/assets/logoKawaiiGreenTiny.png +0 -0
- package/dist/cli/args.js +4 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/diff.d.ts +6 -1
- package/dist/cli/diff.js +114 -4
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/emit.d.ts +71 -0
- package/dist/cli/emit.js +150 -0
- package/dist/cli/emit.js.map +1 -0
- package/dist/cli/fix.js +36 -0
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/history.d.ts +12 -0
- package/dist/cli/history.js +95 -0
- package/dist/cli/history.js.map +1 -0
- package/dist/cli/index.js +53 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +32 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/map.js +60 -0
- package/dist/cli/map.js.map +1 -1
- package/dist/cli/run.js +77 -7
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/ui.d.ts +16 -0
- package/dist/cli/ui.js +275 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/evidence.d.ts +130 -0
- package/dist/evidence.js +232 -0
- package/dist/evidence.js.map +1 -0
- package/dist/history.d.ts +145 -0
- package/dist/history.js +230 -0
- package/dist/history.js.map +1 -0
- package/dist/htmlReport.d.ts +47 -0
- package/dist/htmlReport.js +76 -1
- package/dist/htmlReport.js.map +1 -1
- package/dist/junit.d.ts +53 -0
- package/dist/junit.js +150 -0
- package/dist/junit.js.map +1 -0
- package/dist/mcp/server.js +41 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/ui/icon.d.ts +41 -0
- package/dist/ui/icon.js +115 -0
- package/dist/ui/icon.js.map +1 -0
- package/dist/ui/lock.d.ts +40 -0
- package/dist/ui/lock.js +138 -0
- package/dist/ui/lock.js.map +1 -0
- package/dist/ui/page.d.ts +1 -0
- package/dist/ui/page.js +584 -0
- package/dist/ui/page.js.map +1 -0
- package/dist/ui/registry.d.ts +62 -0
- package/dist/ui/registry.js +139 -0
- package/dist/ui/registry.js.map +1 -0
- package/dist/ui/server.d.ts +67 -0
- package/dist/ui/server.js +604 -0
- package/dist/ui/server.js.map +1 -0
- package/dist/ui/shortcut.d.ts +90 -0
- package/dist/ui/shortcut.js +176 -0
- package/dist/ui/shortcut.js.map +1 -0
- package/package.json +2 -1
- package/skills/ia-qa-heal/SKILL.md +27 -0
package/dist/evidence.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CROP_MAX_WIDTH = exports.CROP_PADDING = void 0;
|
|
37
|
+
exports.loadPageShot = loadPageShot;
|
|
38
|
+
exports.cropWindow = cropWindow;
|
|
39
|
+
exports.ambiguousCandidates = ambiguousCandidates;
|
|
40
|
+
exports.rowEvidence = rowEvidence;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const config_1 = require("./config");
|
|
44
|
+
const match_1 = require("./browser/match");
|
|
45
|
+
/**
|
|
46
|
+
* Visual evidence for the rows a human has to decide — the decision-grade half
|
|
47
|
+
* of SPEC-visual-evidence.md layer 1.
|
|
48
|
+
*
|
|
49
|
+
* The measured problem this exists for: `ambiguous` rows are 10 % of elements on
|
|
50
|
+
* a dogfooded corpus and 20 % in the wild, and all the report could say about
|
|
51
|
+
* one was `div:nth-of-type(4) > … > button`. The human is not missing a
|
|
52
|
+
* description of what they were looking for — the row already says
|
|
53
|
+
* `button "Copy"` — they are missing *which of the N identical candidates* is
|
|
54
|
+
* the one. That question has exactly one cheap answer: show them, highlighted.
|
|
55
|
+
*
|
|
56
|
+
* Three constraints shape everything below.
|
|
57
|
+
*
|
|
58
|
+
* 1. **No pixel is ever an input to a verdict.** This module is called only when
|
|
59
|
+
* rendering HTML. `diffMappings` never sees it, no counter moves, and a
|
|
60
|
+
* report generated with no screenshots on disk is byte-identical to one from
|
|
61
|
+
* before the feature (a test pins that).
|
|
62
|
+
* 2. **No image is decoded in Node.** There is no PNG/JPEG codec here and no
|
|
63
|
+
* dependency to add one. A crop is a *CSS window* onto the page screenshot:
|
|
64
|
+
* the JPEG is emitted once per page as a `background-image`, and every
|
|
65
|
+
* thumbnail is a box with a negative background offset. N crops of one page
|
|
66
|
+
* therefore cost the bytes of one picture.
|
|
67
|
+
* 3. **A crop with no marker on the target is worse than no crop** — study D,
|
|
68
|
+
* measured on real captures. Six identical upvote arrows tell a human
|
|
69
|
+
* nothing about which one drifted. So the highlight is not decoration and is
|
|
70
|
+
* never optional: if the rect is unknown, the crop is not rendered at all.
|
|
71
|
+
*/
|
|
72
|
+
/** Pixels of page context kept around the element. 120 is the value study D found legible. */
|
|
73
|
+
exports.CROP_PADDING = 120;
|
|
74
|
+
/** Widest a thumbnail may render; wider windows are scaled down, never cropped further. */
|
|
75
|
+
exports.CROP_MAX_WIDTH = 360;
|
|
76
|
+
/**
|
|
77
|
+
* Load the screenshot and the element positions `map --screenshots` wrote for a
|
|
78
|
+
* page. Either half missing — flag never used, page skipped, old capture — means
|
|
79
|
+
* no evidence for that page and nothing else: every caller degrades to the
|
|
80
|
+
* report as it was.
|
|
81
|
+
*/
|
|
82
|
+
function loadPageShot(pageName, dir = safeShotsDir()) {
|
|
83
|
+
if (!dir)
|
|
84
|
+
return null;
|
|
85
|
+
const safe = pageName.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
86
|
+
const jpg = path.join(dir, `${safe}.jpg`);
|
|
87
|
+
const rectsFile = path.join(dir, `${safe}.rects.json`);
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(jpg) || !fs.existsSync(rectsFile))
|
|
90
|
+
return null;
|
|
91
|
+
const parsed = JSON.parse(fs.readFileSync(rectsFile, 'utf8'));
|
|
92
|
+
if (!parsed.rects || !parsed.viewport)
|
|
93
|
+
return null;
|
|
94
|
+
const b64 = fs.readFileSync(jpg).toString('base64');
|
|
95
|
+
return {
|
|
96
|
+
dataUri: `data:image/jpeg;base64,${b64}`,
|
|
97
|
+
viewport: parsed.viewport,
|
|
98
|
+
rects: parsed.rects,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function safeShotsDir() {
|
|
106
|
+
try {
|
|
107
|
+
return (0, config_1.shotsDir)();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The CSS window onto the page image for one element: a box of `rect ± padding`,
|
|
115
|
+
* clamped to the picture's bounds, plus where the target sits inside it.
|
|
116
|
+
*
|
|
117
|
+
* Pure arithmetic — same inputs, same output, no I/O — which is what lets a test
|
|
118
|
+
* pin it without a browser (AC-13's spirit: the crop is a function, not a
|
|
119
|
+
* capture).
|
|
120
|
+
*/
|
|
121
|
+
function cropWindow(rect, viewport, padding = exports.CROP_PADDING, maxWidth = exports.CROP_MAX_WIDTH) {
|
|
122
|
+
const [x, y, w, h] = rect;
|
|
123
|
+
const winW = Math.min(viewport.w, w + padding * 2);
|
|
124
|
+
const winH = Math.min(viewport.h, h + padding * 2);
|
|
125
|
+
const offsetX = Math.max(0, Math.min(viewport.w - winW, Math.round(x - (winW - w) / 2)));
|
|
126
|
+
const offsetY = Math.max(0, Math.min(viewport.h - winH, Math.round(y - (winH - h) / 2)));
|
|
127
|
+
const scale = Math.min(1, maxWidth / winW);
|
|
128
|
+
return {
|
|
129
|
+
width: Math.round(winW * scale),
|
|
130
|
+
height: Math.round(winH * scale),
|
|
131
|
+
scale,
|
|
132
|
+
offsetX,
|
|
133
|
+
offsetY,
|
|
134
|
+
hi: { left: x - offsetX, top: y - offsetY, width: w, height: h },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The elements an `ambiguous` row could not be told apart from.
|
|
139
|
+
*
|
|
140
|
+
* `matchElement` returns only a *count* — it decides, or refuses, and the
|
|
141
|
+
* refusal carries no list. Rather than widen its return type, this re-derives
|
|
142
|
+
* the same set from the same primitives on the "after" side. That choice is not
|
|
143
|
+
* stylistic: `src/browser/match.js` ships twice (npm and the web tool on
|
|
144
|
+
* ia-qa.com), so every change to it owes two deploys and risks the two copies
|
|
145
|
+
* disagreeing about a verdict. Reporting is allowed to import *from* the
|
|
146
|
+
* matcher; it is not allowed to change it — the same rule the AI layer follows.
|
|
147
|
+
*
|
|
148
|
+
* The tiers mirror `matchElement` exactly: exact accessible-name matches first,
|
|
149
|
+
* then the fuzzy band within 0.05 of the top Dice score.
|
|
150
|
+
*
|
|
151
|
+
* One case it cannot reproduce, on purpose: a row demoted to `ambiguous` by the
|
|
152
|
+
* many-to-one guard (N baseline elements all healing onto one target) has a
|
|
153
|
+
* `candidateCount` counting *baseline rows*, not live elements. There, this
|
|
154
|
+
* returns the single contested target, which is the useful thing to look at.
|
|
155
|
+
*/
|
|
156
|
+
function ambiguousCandidates(row, after) {
|
|
157
|
+
const sameRole = after.filter((c) => c.role === row.role);
|
|
158
|
+
const wanted = (0, match_1.normalize)(row.name);
|
|
159
|
+
const wantedCtx = (0, match_1.normalize)(row.context);
|
|
160
|
+
const pick = (cands) => {
|
|
161
|
+
if (cands.length <= 1)
|
|
162
|
+
return cands;
|
|
163
|
+
if (wantedCtx) {
|
|
164
|
+
const byCtx = cands.filter((c) => (0, match_1.normalize)(c.context) === wantedCtx);
|
|
165
|
+
if (byCtx.length === 1)
|
|
166
|
+
return byCtx;
|
|
167
|
+
}
|
|
168
|
+
return cands;
|
|
169
|
+
};
|
|
170
|
+
const exact = sameRole.filter((c) => (0, match_1.normalize)(c.name) === wanted && wanted !== '');
|
|
171
|
+
if (exact.length > 0)
|
|
172
|
+
return pick(exact).map(toCandidate);
|
|
173
|
+
const scored = sameRole
|
|
174
|
+
.map((c) => ({ c, score: (0, match_1.diceSimilarity)((0, match_1.normalize)(c.name), wanted) }))
|
|
175
|
+
.filter((s) => s.score >= match_1.FUZZY_THRESHOLD)
|
|
176
|
+
.sort((a, b) => b.score - a.score);
|
|
177
|
+
if (scored.length === 0)
|
|
178
|
+
return [];
|
|
179
|
+
const top = scored[0].score;
|
|
180
|
+
const tied = scored.filter((s) => top - s.score < 0.05).map((s) => s.c);
|
|
181
|
+
return pick(tied).map(toCandidate);
|
|
182
|
+
}
|
|
183
|
+
function toCandidate(e) {
|
|
184
|
+
return { selector: e.selector, name: e.name, role: e.role, context: e.context };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Assemble the evidence for one drifted row.
|
|
188
|
+
*
|
|
189
|
+
* Only the statuses where a human is actually stuck get pictures — `ambiguous`,
|
|
190
|
+
* and anything `rebound`. `ok`, `renamed` and `healable` are deliberately left
|
|
191
|
+
* bare: there is no decision to take on them, so a thumbnail would be weight
|
|
192
|
+
* without a question (F1.8).
|
|
193
|
+
*/
|
|
194
|
+
function rowEvidence(row, after, shot) {
|
|
195
|
+
if (!shot || !after)
|
|
196
|
+
return null;
|
|
197
|
+
const wants = row.status === 'ambiguous' || row.rebound === true;
|
|
198
|
+
if (!wants)
|
|
199
|
+
return null;
|
|
200
|
+
const locate = (selector, label) => {
|
|
201
|
+
const rect = shot.rects[selector];
|
|
202
|
+
// No rect means the element was below the fold or absent when the picture
|
|
203
|
+
// was taken. Showing the crop anyway would highlight empty space — exactly
|
|
204
|
+
// the misleading thumbnail study D rules out.
|
|
205
|
+
if (!rect)
|
|
206
|
+
return null;
|
|
207
|
+
return { selector, label, crop: cropWindow(rect, shot.viewport) };
|
|
208
|
+
};
|
|
209
|
+
const out = { candidates: [] };
|
|
210
|
+
if (row.rebound) {
|
|
211
|
+
const hit = locate(row.selector, `${row.role} "${row.reboundTo ?? ''}"`);
|
|
212
|
+
if (hit)
|
|
213
|
+
out.nowAt = hit;
|
|
214
|
+
}
|
|
215
|
+
if (row.status === 'ambiguous') {
|
|
216
|
+
for (const c of ambiguousCandidates(row, after.elements)) {
|
|
217
|
+
const located = locate(c.selector, c.name ? `${c.role} "${c.name}"` : `${c.role} (no name)`);
|
|
218
|
+
if (located)
|
|
219
|
+
out.candidates.push(located);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!out.nowAt && out.candidates.length === 0) {
|
|
223
|
+
return {
|
|
224
|
+
candidates: [],
|
|
225
|
+
note: row.status === 'ambiguous'
|
|
226
|
+
? 'No picture for these candidates — they were below the fold when the page was captured.'
|
|
227
|
+
: 'No picture for this element — it was below the fold when the page was captured.',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=evidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evidence.js","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only run history — `.ia-qa/history.jsonl`.
|
|
3
|
+
*
|
|
4
|
+
* Every other verb in this package is *stateless*: `diff` compares two moments
|
|
5
|
+
* and forgets. That is the right shape for a gate, and it is also why the tool
|
|
6
|
+
* can never answer the question a QA lead actually asks — "is this getting
|
|
7
|
+
* better or worse?". A trend is not derivable from any single invocation, so it
|
|
8
|
+
* has to be recorded as it happens; unlike a verdict, it cannot be recomputed
|
|
9
|
+
* after the fact. Every run not written down is a data point that will never
|
|
10
|
+
* exist.
|
|
11
|
+
*
|
|
12
|
+
* Three rules keep this from becoming a dashboard that lies:
|
|
13
|
+
*
|
|
14
|
+
* 1. **It never influences a verdict.** Nothing reads history back into a diff.
|
|
15
|
+
* It is written after the exit code is decided, and a failure to write is
|
|
16
|
+
* swallowed — a full disk must not turn a PASS into a crash.
|
|
17
|
+
* 2. **Every entry carries what makes it comparable** — `source` (`map` vs a
|
|
18
|
+
* live `run` capture) and `coverage` (pages measured / pages mapped). Two
|
|
19
|
+
* entries that disagree on either are not two points on one curve, and
|
|
20
|
+
* `trendable()` below is what refuses to draw them as one.
|
|
21
|
+
* 3. **It is small and deterministic.** Verdicts and counters, no pixels, no
|
|
22
|
+
* element contents. Unlike `_shots/`, committing this file is defensible —
|
|
23
|
+
* it is a few kB of text that changes by one line per run.
|
|
24
|
+
*/
|
|
25
|
+
export declare const HISTORY_FILENAME = "history.jsonl";
|
|
26
|
+
/** Entries kept on disk. Older ones are dropped on write — a trend does not need a year. */
|
|
27
|
+
export declare const HISTORY_LIMIT = 500;
|
|
28
|
+
export interface HistoryCounts {
|
|
29
|
+
total: number;
|
|
30
|
+
ok: number;
|
|
31
|
+
renamed: number;
|
|
32
|
+
healable: number;
|
|
33
|
+
ambiguous: number;
|
|
34
|
+
lost: number;
|
|
35
|
+
rebound: number;
|
|
36
|
+
added: number;
|
|
37
|
+
}
|
|
38
|
+
export interface HistoryEntry {
|
|
39
|
+
/** ISO-8601, stamped at write time. */
|
|
40
|
+
ts: string;
|
|
41
|
+
verb: 'diff' | 'run' | 'fix' | 'audit' | 'check';
|
|
42
|
+
/** The verb's own verdict word — PASS/FIX/BLOCK for the drift verbs, OK/WARN/FAIL for `check`. */
|
|
43
|
+
verdict: string;
|
|
44
|
+
/** Element-level counters. Absent for verbs that do not diff (`audit`, `check`, bare `fix`). */
|
|
45
|
+
counts?: HistoryCounts;
|
|
46
|
+
/**
|
|
47
|
+
* How the *current* side was captured. `mixed` means the pages did not agree —
|
|
48
|
+
* the same source mismatch `run` warns about, recorded rather than inferred.
|
|
49
|
+
*/
|
|
50
|
+
source?: 'map' | 'run' | 'mixed';
|
|
51
|
+
/** Pages that produced a verdict / pages mapped. A green run over 1 of 40 pages is not a green app. */
|
|
52
|
+
coverage?: {
|
|
53
|
+
measured: number;
|
|
54
|
+
total: number;
|
|
55
|
+
};
|
|
56
|
+
nameDrift?: {
|
|
57
|
+
total: number;
|
|
58
|
+
manual: number;
|
|
59
|
+
};
|
|
60
|
+
/** Only pages that actually drifted, so a 200-page app still writes a short line. */
|
|
61
|
+
pages?: Array<{
|
|
62
|
+
name: string;
|
|
63
|
+
drift: number;
|
|
64
|
+
}>;
|
|
65
|
+
baseUrl?: string;
|
|
66
|
+
commit?: string;
|
|
67
|
+
branch?: string;
|
|
68
|
+
/** What `fix`/`run` wrote to the test files. */
|
|
69
|
+
applied?: {
|
|
70
|
+
replacements: number;
|
|
71
|
+
files: number;
|
|
72
|
+
};
|
|
73
|
+
/** The post-fix re-run of the user's suite, when `run` performed one. */
|
|
74
|
+
verify?: 'PASS' | 'FAIL' | 'SKIPPED';
|
|
75
|
+
/** Findings, for the verbs that produce findings rather than diff rows. */
|
|
76
|
+
findings?: number;
|
|
77
|
+
}
|
|
78
|
+
export declare function historyPath(cwd?: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Append one entry. Best-effort by design: history is an observation of the run,
|
|
81
|
+
* never part of it, so every failure mode here (read-only checkout, missing
|
|
82
|
+
* directory, full disk, concurrent writer) costs the data point and nothing else.
|
|
83
|
+
*/
|
|
84
|
+
export declare function appendHistory(entry: Omit<HistoryEntry, 'ts'> & {
|
|
85
|
+
ts?: string;
|
|
86
|
+
}): void;
|
|
87
|
+
/**
|
|
88
|
+
* Read the history, oldest first. A malformed line is skipped rather than fatal:
|
|
89
|
+
* this file is appended to by every run and may be truncated mid-write by a
|
|
90
|
+
* killed process, and one bad line must not cost the other 499.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readHistory(limit?: number, cwd?: string): HistoryEntry[];
|
|
93
|
+
/**
|
|
94
|
+
* Which entries may be drawn as one curve.
|
|
95
|
+
*
|
|
96
|
+
* This is the whole reason the store carries `source`. A `map` baseline and a
|
|
97
|
+
* live `run` capture read the same page differently — the CLI already warns
|
|
98
|
+
* about diffing one against the other — so plotting both on one axis manufactures
|
|
99
|
+
* a trend out of a capture artifact. The same applies to coverage: a run that
|
|
100
|
+
* measured 1 of 40 pages and one that measured 40 are not comparable magnitudes,
|
|
101
|
+
* so entries below `minCoverage` of the largest observed coverage are held out.
|
|
102
|
+
*
|
|
103
|
+
* The excluded entries are returned, not discarded, so the caller can *say* what
|
|
104
|
+
* it left out. A chart that silently drops half its data is the dashboard-that-
|
|
105
|
+
* lies failure mode this function exists to prevent.
|
|
106
|
+
*/
|
|
107
|
+
export interface Trend {
|
|
108
|
+
series: HistoryEntry[];
|
|
109
|
+
excluded: HistoryEntry[];
|
|
110
|
+
/** Present when anything was held out — render it next to the chart, never swallow it. */
|
|
111
|
+
reason?: string;
|
|
112
|
+
/** The capture source the series is drawn from. */
|
|
113
|
+
source?: 'map' | 'run' | 'mixed';
|
|
114
|
+
}
|
|
115
|
+
export declare function trendable(entries: HistoryEntry[], minCoverage?: number): Trend;
|
|
116
|
+
export interface HistorySummary {
|
|
117
|
+
runs: number;
|
|
118
|
+
first?: string;
|
|
119
|
+
last?: string;
|
|
120
|
+
lastVerdict?: string;
|
|
121
|
+
/** Share of comparable runs that were not PASS. */
|
|
122
|
+
driftRate: number;
|
|
123
|
+
/** Mean measured/total across the comparable series, or 1 when nothing recorded coverage. */
|
|
124
|
+
coverage: number;
|
|
125
|
+
/** Pages ranked by how many runs they drifted in — where the churn actually lives. */
|
|
126
|
+
hotspots: Array<{
|
|
127
|
+
name: string;
|
|
128
|
+
runs: number;
|
|
129
|
+
drift: number;
|
|
130
|
+
}>;
|
|
131
|
+
/** Rewrites the tool applied across the whole history. */
|
|
132
|
+
healed: number;
|
|
133
|
+
trend: Trend;
|
|
134
|
+
}
|
|
135
|
+
export declare function summarize(entries: HistoryEntry[]): HistorySummary;
|
|
136
|
+
/**
|
|
137
|
+
* Best-effort git stamp. A trend of verdicts is far more useful when a spike can
|
|
138
|
+
* be tied to a commit, and every alternative (asking the user to pass it, reading
|
|
139
|
+
* CI env vars per provider) is worse than shelling out once. Never throws, never
|
|
140
|
+
* blocks: no git, no repo, detached HEAD — the entry is written without it.
|
|
141
|
+
*/
|
|
142
|
+
export declare function gitStamp(): {
|
|
143
|
+
commit?: string;
|
|
144
|
+
branch?: string;
|
|
145
|
+
};
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HISTORY_LIMIT = exports.HISTORY_FILENAME = void 0;
|
|
37
|
+
exports.historyPath = historyPath;
|
|
38
|
+
exports.appendHistory = appendHistory;
|
|
39
|
+
exports.readHistory = readHistory;
|
|
40
|
+
exports.trendable = trendable;
|
|
41
|
+
exports.summarize = summarize;
|
|
42
|
+
exports.gitStamp = gitStamp;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const config_1 = require("./config");
|
|
46
|
+
/**
|
|
47
|
+
* Append-only run history — `.ia-qa/history.jsonl`.
|
|
48
|
+
*
|
|
49
|
+
* Every other verb in this package is *stateless*: `diff` compares two moments
|
|
50
|
+
* and forgets. That is the right shape for a gate, and it is also why the tool
|
|
51
|
+
* can never answer the question a QA lead actually asks — "is this getting
|
|
52
|
+
* better or worse?". A trend is not derivable from any single invocation, so it
|
|
53
|
+
* has to be recorded as it happens; unlike a verdict, it cannot be recomputed
|
|
54
|
+
* after the fact. Every run not written down is a data point that will never
|
|
55
|
+
* exist.
|
|
56
|
+
*
|
|
57
|
+
* Three rules keep this from becoming a dashboard that lies:
|
|
58
|
+
*
|
|
59
|
+
* 1. **It never influences a verdict.** Nothing reads history back into a diff.
|
|
60
|
+
* It is written after the exit code is decided, and a failure to write is
|
|
61
|
+
* swallowed — a full disk must not turn a PASS into a crash.
|
|
62
|
+
* 2. **Every entry carries what makes it comparable** — `source` (`map` vs a
|
|
63
|
+
* live `run` capture) and `coverage` (pages measured / pages mapped). Two
|
|
64
|
+
* entries that disagree on either are not two points on one curve, and
|
|
65
|
+
* `trendable()` below is what refuses to draw them as one.
|
|
66
|
+
* 3. **It is small and deterministic.** Verdicts and counters, no pixels, no
|
|
67
|
+
* element contents. Unlike `_shots/`, committing this file is defensible —
|
|
68
|
+
* it is a few kB of text that changes by one line per run.
|
|
69
|
+
*/
|
|
70
|
+
exports.HISTORY_FILENAME = 'history.jsonl';
|
|
71
|
+
/** Entries kept on disk. Older ones are dropped on write — a trend does not need a year. */
|
|
72
|
+
exports.HISTORY_LIMIT = 500;
|
|
73
|
+
function historyPath(cwd = (0, config_1.baseDir)()) {
|
|
74
|
+
return path.join(cwd, config_1.CONFIG_DIR, exports.HISTORY_FILENAME);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Append one entry. Best-effort by design: history is an observation of the run,
|
|
78
|
+
* never part of it, so every failure mode here (read-only checkout, missing
|
|
79
|
+
* directory, full disk, concurrent writer) costs the data point and nothing else.
|
|
80
|
+
*/
|
|
81
|
+
function appendHistory(entry) {
|
|
82
|
+
const file = historyPath();
|
|
83
|
+
const full = { ts: entry.ts ?? new Date().toISOString(), ...entry };
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
86
|
+
fs.appendFileSync(file, JSON.stringify(full) + '\n', 'utf8');
|
|
87
|
+
truncate(file);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* history is a courtesy, never a gate */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Keep the file bounded without a second config knob nobody would ever tune. */
|
|
94
|
+
function truncate(file) {
|
|
95
|
+
try {
|
|
96
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
97
|
+
if (lines.length <= exports.HISTORY_LIMIT + 100)
|
|
98
|
+
return;
|
|
99
|
+
fs.writeFileSync(file, lines.slice(-exports.HISTORY_LIMIT).join('\n') + '\n', 'utf8');
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* as above */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Read the history, oldest first. A malformed line is skipped rather than fatal:
|
|
107
|
+
* this file is appended to by every run and may be truncated mid-write by a
|
|
108
|
+
* killed process, and one bad line must not cost the other 499.
|
|
109
|
+
*/
|
|
110
|
+
function readHistory(limit = exports.HISTORY_LIMIT, cwd) {
|
|
111
|
+
const file = cwd ? path.join(cwd, config_1.CONFIG_DIR, exports.HISTORY_FILENAME) : historyPath();
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const line of raw.split('\n')) {
|
|
121
|
+
if (!line.trim())
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(line);
|
|
125
|
+
if (parsed && typeof parsed.ts === 'string' && typeof parsed.verb === 'string')
|
|
126
|
+
out.push(parsed);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* skip */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out.slice(-limit);
|
|
133
|
+
}
|
|
134
|
+
function trendable(entries, minCoverage = 0.5) {
|
|
135
|
+
const drift = entries.filter((e) => e.counts && (e.verb === 'diff' || e.verb === 'run'));
|
|
136
|
+
if (drift.length === 0)
|
|
137
|
+
return { series: [], excluded: [] };
|
|
138
|
+
// Dominant capture source wins the axis; the minority is held out rather than
|
|
139
|
+
// averaged in. Ties go to the most recent, which is what the user is looking at.
|
|
140
|
+
const bySource = new Map();
|
|
141
|
+
for (const e of drift)
|
|
142
|
+
bySource.set(e.source ?? 'map', (bySource.get(e.source ?? 'map') ?? 0) + 1);
|
|
143
|
+
let source = 'map';
|
|
144
|
+
let best = -1;
|
|
145
|
+
for (const [s, n] of bySource) {
|
|
146
|
+
if (n > best) {
|
|
147
|
+
best = n;
|
|
148
|
+
source = s;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const sourceKept = drift.filter((e) => (e.source ?? 'map') === source);
|
|
152
|
+
const sourceDropped = drift.filter((e) => (e.source ?? 'map') !== source);
|
|
153
|
+
const maxCov = Math.max(...sourceKept.map((e) => (e.coverage ? e.coverage.measured / Math.max(1, e.coverage.total) : 1)));
|
|
154
|
+
const series = sourceKept.filter((e) => {
|
|
155
|
+
if (!e.coverage)
|
|
156
|
+
return true;
|
|
157
|
+
return e.coverage.measured / Math.max(1, e.coverage.total) >= maxCov * minCoverage;
|
|
158
|
+
});
|
|
159
|
+
const covDropped = sourceKept.filter((e) => !series.includes(e));
|
|
160
|
+
const excluded = [...sourceDropped, ...covDropped];
|
|
161
|
+
const parts = [];
|
|
162
|
+
if (sourceDropped.length > 0) {
|
|
163
|
+
parts.push(`${sourceDropped.length} run${sourceDropped.length === 1 ? '' : 's'} captured a different way ` +
|
|
164
|
+
`(a \`map\` capture and a live suite capture read a page differently)`);
|
|
165
|
+
}
|
|
166
|
+
if (covDropped.length > 0) {
|
|
167
|
+
parts.push(`${covDropped.length} run${covDropped.length === 1 ? '' : 's'} covered too few pages to compare`);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
series,
|
|
171
|
+
excluded,
|
|
172
|
+
source,
|
|
173
|
+
reason: parts.length > 0 ? `Left out of the trend: ${parts.join('; ')}.` : undefined,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function summarize(entries) {
|
|
177
|
+
const trend = trendable(entries);
|
|
178
|
+
const s = trend.series;
|
|
179
|
+
const notPass = s.filter((e) => e.verdict !== 'PASS').length;
|
|
180
|
+
const withCov = s.filter((e) => e.coverage);
|
|
181
|
+
const coverage = withCov.length === 0
|
|
182
|
+
? 1
|
|
183
|
+
: withCov.reduce((n, e) => n + e.coverage.measured / Math.max(1, e.coverage.total), 0) / withCov.length;
|
|
184
|
+
const pageStats = new Map();
|
|
185
|
+
for (const e of s) {
|
|
186
|
+
for (const p of e.pages ?? []) {
|
|
187
|
+
const cur = pageStats.get(p.name) ?? { runs: 0, drift: 0 };
|
|
188
|
+
cur.runs += 1;
|
|
189
|
+
cur.drift += p.drift;
|
|
190
|
+
pageStats.set(p.name, cur);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const hotspots = [...pageStats.entries()]
|
|
194
|
+
.map(([name, v]) => ({ name, ...v }))
|
|
195
|
+
.sort((a, b) => b.drift - a.drift || b.runs - a.runs)
|
|
196
|
+
.slice(0, 5);
|
|
197
|
+
return {
|
|
198
|
+
runs: entries.length,
|
|
199
|
+
first: entries[0]?.ts,
|
|
200
|
+
last: entries[entries.length - 1]?.ts,
|
|
201
|
+
lastVerdict: entries[entries.length - 1]?.verdict,
|
|
202
|
+
driftRate: s.length === 0 ? 0 : notPass / s.length,
|
|
203
|
+
coverage,
|
|
204
|
+
hotspots,
|
|
205
|
+
healed: entries.reduce((n, e) => n + (e.applied?.replacements ?? 0), 0),
|
|
206
|
+
trend,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Best-effort git stamp. A trend of verdicts is far more useful when a spike can
|
|
211
|
+
* be tied to a commit, and every alternative (asking the user to pass it, reading
|
|
212
|
+
* CI env vars per provider) is worse than shelling out once. Never throws, never
|
|
213
|
+
* blocks: no git, no repo, detached HEAD — the entry is written without it.
|
|
214
|
+
*/
|
|
215
|
+
function gitStamp() {
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
217
|
+
const { execFileSync } = require('child_process');
|
|
218
|
+
const run = (args) => {
|
|
219
|
+
try {
|
|
220
|
+
const out = execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 });
|
|
221
|
+
const v = out.trim();
|
|
222
|
+
return v === '' ? undefined : v;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
return { commit: run(['rev-parse', '--short', 'HEAD']), branch: run(['rev-parse', '--abbrev-ref', 'HEAD']) };
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"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"}
|