@ia-qa/self-healing 1.6.7 → 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 +94 -0
- package/TUTORIAL.md +112 -2
- 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/cli/ui.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
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.runUi = runUi;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const server_1 = require("../ui/server");
|
|
40
|
+
const registry_1 = require("../ui/registry");
|
|
41
|
+
const config_1 = require("../config");
|
|
42
|
+
const lock_1 = require("../ui/lock");
|
|
43
|
+
const shortcut_1 = require("../ui/shortcut");
|
|
44
|
+
const icon_1 = require("../ui/icon");
|
|
45
|
+
const config_2 = require("../config");
|
|
46
|
+
const diff_1 = require("./diff");
|
|
47
|
+
const summary_1 = require("./summary");
|
|
48
|
+
/**
|
|
49
|
+
* `ia-qa-heal ui [--port N] [--no-open] [--shortcut]` — open the local console.
|
|
50
|
+
*
|
|
51
|
+
* Runs until interrupted, which is the one verb here that does. Everything it
|
|
52
|
+
* shows is computed from this machine's `.ia-qa/` directory and everything it
|
|
53
|
+
* can do is spawn this package's own CLI — see `ui/server.ts` for why that
|
|
54
|
+
* boundary is the security model rather than a convenience.
|
|
55
|
+
*
|
|
56
|
+
* It opens the browser itself, on purpose: the URL carries a session token, and
|
|
57
|
+
* a token a person has to copy is a token they will paste somewhere. `--no-open`
|
|
58
|
+
* exists for scripted use (and for taking screenshots of it).
|
|
59
|
+
*
|
|
60
|
+
* Not a CI verb, and it says so: a pipeline wants `diff --json`, `--junit` or
|
|
61
|
+
* the exit code. A console nobody is sitting in front of is a hung build.
|
|
62
|
+
*/
|
|
63
|
+
async function runUi(args = []) {
|
|
64
|
+
if (args.includes('--shortcut')) {
|
|
65
|
+
installShortcut();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (args.includes('--forget')) {
|
|
69
|
+
forgetProjects(args);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const portIdx = args.indexOf('--port');
|
|
73
|
+
const port = portIdx !== -1 ? Number(args[portIdx + 1]) : undefined;
|
|
74
|
+
if (portIdx !== -1 && (!Number.isInteger(port) || port < 1 || port > 65535)) {
|
|
75
|
+
console.error(`❌ --port needs a number between 1 and 65535.`);
|
|
76
|
+
process.exitCode = 2;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const open = !args.includes('--no-open');
|
|
80
|
+
const keepAlive = args.includes('--keep-alive');
|
|
81
|
+
if (process.env.CI) {
|
|
82
|
+
console.error(`❌ Refusing to start the console under CI.\n\n` +
|
|
83
|
+
` It is an interactive surface: it waits for a human, and a pipeline that\n` +
|
|
84
|
+
` opens it just hangs. For a gate, use the machine-readable outputs instead:\n` +
|
|
85
|
+
` ia-qa-heal diff --json the verdict as a document\n` +
|
|
86
|
+
` ia-qa-heal diff --junit a file your CI already knows how to chart\n` +
|
|
87
|
+
` ia-qa-heal diff --report x.html an artifact to attach to the build`);
|
|
88
|
+
process.exitCode = 2;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Someone already opened this project's console — clicking the icon twice is
|
|
92
|
+
// the normal case, not a mistake. Reuse it rather than starting a rival server
|
|
93
|
+
// with a different token.
|
|
94
|
+
const running = await (0, lock_1.findRunningConsole)();
|
|
95
|
+
if (running) {
|
|
96
|
+
console.log(`\n🖥 The console for this project is already open.`);
|
|
97
|
+
console.log(` ${running}\n`);
|
|
98
|
+
if (open)
|
|
99
|
+
(0, diff_1.openInBrowser)(running);
|
|
100
|
+
(0, summary_1.summaryLine)('ui', 'info', 'console already running · reused');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let started;
|
|
104
|
+
try {
|
|
105
|
+
started = await (0, server_1.startUi)({ port, keepAlive });
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
109
|
+
console.error(/EADDRINUSE/.test(msg)
|
|
110
|
+
? `❌ Port ${port} is already in use — by something that is not this console.\n` +
|
|
111
|
+
` Omit --port to let the OS pick a free one.`
|
|
112
|
+
: `❌ Could not start the console: ${msg}`);
|
|
113
|
+
process.exitCode = 2;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Opening a console here is the act that makes this project reachable from
|
|
117
|
+
// the switcher — see ui/registry.ts. Nothing else grants it, and `--forget`
|
|
118
|
+
// takes it back.
|
|
119
|
+
const here = (0, config_1.baseDir)();
|
|
120
|
+
(0, server_1.setActiveProject)(here);
|
|
121
|
+
if ((0, registry_1.isProjectDir)(here))
|
|
122
|
+
(0, registry_1.rememberProject)(here, (0, config_1.configuredBaseUrl)());
|
|
123
|
+
const known = (0, registry_1.listProjects)();
|
|
124
|
+
const url = new URL(started.url);
|
|
125
|
+
(0, lock_1.writeLock)({
|
|
126
|
+
port: Number(url.port),
|
|
127
|
+
token: url.searchParams.get('t') ?? '',
|
|
128
|
+
pid: process.pid,
|
|
129
|
+
startedAt: new Date().toISOString(),
|
|
130
|
+
});
|
|
131
|
+
console.log(`\n🖥 ia-qa-heal console`);
|
|
132
|
+
console.log(` ${started.url}`);
|
|
133
|
+
console.log(`\n Loopback only (127.0.0.1) and gated by the one-time token in that URL —\n` +
|
|
134
|
+
` without it, any page open in your browser could read this. Nothing leaves\n` +
|
|
135
|
+
` this machine.\n` +
|
|
136
|
+
(keepAlive
|
|
137
|
+
? ` Staying up until you stop it (--keep-alive). Ctrl-C to stop.\n`
|
|
138
|
+
: ` Closing the browser tab stops it too — no process left running behind\n` +
|
|
139
|
+
` your back. Ctrl-C works as well; --keep-alive opts out.\n`) +
|
|
140
|
+
(known.length > 1
|
|
141
|
+
? `\n Showing ${path.basename(here)}. ${known.length - 1} other project${known.length === 2 ? '' : 's'} you have\n` +
|
|
142
|
+
` opened a console in can be picked from the dropdown — \`ia-qa-heal ui --forget\`\n` +
|
|
143
|
+
` clears that list.\n`
|
|
144
|
+
: '') +
|
|
145
|
+
`\n Tip: \`ia-qa-heal ui --shortcut\` puts an icon on your desktop that opens\n` +
|
|
146
|
+
` this in one click, so nobody has to touch a terminal or a URL again.\n`);
|
|
147
|
+
if (open)
|
|
148
|
+
(0, diff_1.openInBrowser)(started.url);
|
|
149
|
+
(0, summary_1.summaryLine)('ui', 'info', keepAlive ? 'console running · Ctrl-C to stop' : 'console running · closes with the browser tab');
|
|
150
|
+
await new Promise((resolve) => {
|
|
151
|
+
const stop = (why) => () => {
|
|
152
|
+
(0, lock_1.clearLock)();
|
|
153
|
+
started.close();
|
|
154
|
+
console.log(`\n ${why}`);
|
|
155
|
+
resolve();
|
|
156
|
+
};
|
|
157
|
+
process.on('SIGINT', stop('Console stopped.'));
|
|
158
|
+
process.on('SIGTERM', stop('Console stopped.'));
|
|
159
|
+
started.onIdle(stop('Browser closed — console stopped, nothing left running.'));
|
|
160
|
+
// A crash must not leave a lock claiming a console that is gone; the probe
|
|
161
|
+
// would clear it eventually, but only on the next launch.
|
|
162
|
+
process.on('exit', lock_1.clearLock);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* `ia-qa-heal ui --forget [dir]` — withdraw a project from the switcher.
|
|
167
|
+
*
|
|
168
|
+
* The registry is a consent list, so there has to be a way to revoke. With no
|
|
169
|
+
* argument it clears the lot: the safe reading of "forget", and the one someone
|
|
170
|
+
* reaches for after demoing a client's app on a shared machine.
|
|
171
|
+
*/
|
|
172
|
+
function forgetProjects(args) {
|
|
173
|
+
const i = args.indexOf('--forget');
|
|
174
|
+
const next = args[i + 1];
|
|
175
|
+
const target = next !== undefined && !next.startsWith('-') ? path.resolve(next) : null;
|
|
176
|
+
const before = (0, registry_1.listProjects)().length;
|
|
177
|
+
const removed = (0, registry_1.forgetProject)(target);
|
|
178
|
+
console.log(`\n🗂 Project list · ${(0, registry_1.registryPath)()}`);
|
|
179
|
+
if (removed === 0) {
|
|
180
|
+
console.log(target
|
|
181
|
+
? ` ${target} was not in the list — nothing to forget.`
|
|
182
|
+
: ` The list was already empty.`);
|
|
183
|
+
}
|
|
184
|
+
else if (target) {
|
|
185
|
+
console.log(` Forgot ${target}. ${before - removed} left.`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
console.log(` Forgot all ${removed} project${removed === 1 ? '' : 's'}.`);
|
|
189
|
+
}
|
|
190
|
+
console.log(`\n Forgetting only removes a folder from the console's switcher. It deletes\n` +
|
|
191
|
+
` nothing in the project itself, and opening a console there again re-adds it.\n`);
|
|
192
|
+
(0, summary_1.summaryLine)('ui', 'info', `${removed} project${removed === 1 ? '' : 's'} forgotten`);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Write the desktop launcher. Idempotent, and announces every absolute path
|
|
196
|
+
* before writing to it — the same restraint `skill --install` and
|
|
197
|
+
* `discover --apply` follow.
|
|
198
|
+
*/
|
|
199
|
+
function installShortcut() {
|
|
200
|
+
const projectDir = process.cwd();
|
|
201
|
+
const configDir = path.join(projectDir, config_2.CONFIG_DIR);
|
|
202
|
+
const plan = (0, shortcut_1.planShortcut)({
|
|
203
|
+
platform: process.platform,
|
|
204
|
+
desktop: (0, shortcut_1.desktopDir)(),
|
|
205
|
+
projectDir,
|
|
206
|
+
configDir,
|
|
207
|
+
nodePath: process.execPath,
|
|
208
|
+
cliEntry: path.join(__dirname, 'index.js'),
|
|
209
|
+
logoPath: (0, icon_1.brandLogoPath)(),
|
|
210
|
+
});
|
|
211
|
+
console.log(`\n🖥 Desktop shortcut for the console`);
|
|
212
|
+
console.log(` project → ${projectDir}`);
|
|
213
|
+
try {
|
|
214
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
215
|
+
for (const f of plan.files) {
|
|
216
|
+
console.log(` writing → ${f.file}`);
|
|
217
|
+
fs.writeFileSync(f.file, f.contents, 'utf8');
|
|
218
|
+
if (f.mode !== undefined)
|
|
219
|
+
fs.chmodSync(f.file, f.mode);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
console.error(`\n❌ Could not write the shortcut: ${e instanceof Error ? e.message : String(e)}`);
|
|
224
|
+
process.exitCode = 2;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// The icon. A PNG cannot be a Windows shortcut icon, so it is wrapped in an
|
|
228
|
+
// ICO container — no re-encoding, see ui/icon.ts.
|
|
229
|
+
let iconOk = false;
|
|
230
|
+
if (plan.icoPath) {
|
|
231
|
+
const png = (0, icon_1.readBrandLogo)();
|
|
232
|
+
const ico = png ? (0, icon_1.pngToIco)(png) : null;
|
|
233
|
+
if (ico) {
|
|
234
|
+
try {
|
|
235
|
+
fs.writeFileSync(plan.icoPath, ico);
|
|
236
|
+
iconOk = true;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
/* the shortcut still works, it just wears the default icon */
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
let finalFile = plan.files[0].file;
|
|
244
|
+
if (plan.lnk) {
|
|
245
|
+
console.log(` writing → ${plan.lnk.file}`);
|
|
246
|
+
if ((0, shortcut_1.createWindowsLnk)(plan.lnk)) {
|
|
247
|
+
finalFile = plan.lnk.file;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// PowerShell refused (locked down policy, missing COM). A .cmd on the
|
|
251
|
+
// desktop still launches the console; only the icon is lost. Say which.
|
|
252
|
+
const fallback = path.join((0, shortcut_1.desktopDir)(), `${shortcut_1.SHORTCUT_NAME}.cmd`);
|
|
253
|
+
fs.copyFileSync(plan.files[0].file, fallback);
|
|
254
|
+
finalFile = fallback;
|
|
255
|
+
console.log(`\n ⚠️ Could not create a .lnk (PowerShell refused), so the launcher went\n` +
|
|
256
|
+
` on your desktop as a .cmd instead. It works the same — it just wears\n` +
|
|
257
|
+
` the default icon rather than the ia-qa one.`);
|
|
258
|
+
iconOk = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
console.log(`\n Double-click it and the console opens in your browser. It starts the\n` +
|
|
262
|
+
` server in this project folder, so the icon is tied to THIS project —\n` +
|
|
263
|
+
` run the command again from another one to get its own icon.\n` +
|
|
264
|
+
` The window that stays open behind the browser is the server; closing it\n` +
|
|
265
|
+
` stops the console.` +
|
|
266
|
+
(plan.defaultIconOnly
|
|
267
|
+
? `\n\n Note: macOS keeps its default icon here. Giving a .command file a custom\n` +
|
|
268
|
+
` one needs Finder scripting, which is not worth failing silently over.`
|
|
269
|
+
: ''));
|
|
270
|
+
(0, summary_1.summaryLine)('ui', 'ok', 'desktop shortcut installed', [
|
|
271
|
+
path.basename(finalFile),
|
|
272
|
+
iconOk ? 'ia-qa icon' : undefined,
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=ui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/cli/ui.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { MappedElement, PageMapping } from './aom';
|
|
2
|
+
import type { DiffRow } from './cli/diff';
|
|
3
|
+
/**
|
|
4
|
+
* Visual evidence for the rows a human has to decide — the decision-grade half
|
|
5
|
+
* of SPEC-visual-evidence.md layer 1.
|
|
6
|
+
*
|
|
7
|
+
* The measured problem this exists for: `ambiguous` rows are 10 % of elements on
|
|
8
|
+
* a dogfooded corpus and 20 % in the wild, and all the report could say about
|
|
9
|
+
* one was `div:nth-of-type(4) > … > button`. The human is not missing a
|
|
10
|
+
* description of what they were looking for — the row already says
|
|
11
|
+
* `button "Copy"` — they are missing *which of the N identical candidates* is
|
|
12
|
+
* the one. That question has exactly one cheap answer: show them, highlighted.
|
|
13
|
+
*
|
|
14
|
+
* Three constraints shape everything below.
|
|
15
|
+
*
|
|
16
|
+
* 1. **No pixel is ever an input to a verdict.** This module is called only when
|
|
17
|
+
* rendering HTML. `diffMappings` never sees it, no counter moves, and a
|
|
18
|
+
* report generated with no screenshots on disk is byte-identical to one from
|
|
19
|
+
* before the feature (a test pins that).
|
|
20
|
+
* 2. **No image is decoded in Node.** There is no PNG/JPEG codec here and no
|
|
21
|
+
* dependency to add one. A crop is a *CSS window* onto the page screenshot:
|
|
22
|
+
* the JPEG is emitted once per page as a `background-image`, and every
|
|
23
|
+
* thumbnail is a box with a negative background offset. N crops of one page
|
|
24
|
+
* therefore cost the bytes of one picture.
|
|
25
|
+
* 3. **A crop with no marker on the target is worse than no crop** — study D,
|
|
26
|
+
* measured on real captures. Six identical upvote arrows tell a human
|
|
27
|
+
* nothing about which one drifted. So the highlight is not decoration and is
|
|
28
|
+
* never optional: if the rect is unknown, the crop is not rendered at all.
|
|
29
|
+
*/
|
|
30
|
+
/** Pixels of page context kept around the element. 120 is the value study D found legible. */
|
|
31
|
+
export declare const CROP_PADDING = 120;
|
|
32
|
+
/** Widest a thumbnail may render; wider windows are scaled down, never cropped further. */
|
|
33
|
+
export declare const CROP_MAX_WIDTH = 360;
|
|
34
|
+
export interface PageShot {
|
|
35
|
+
/** `data:image/jpeg;base64,…` of the whole viewport. */
|
|
36
|
+
dataUri: string;
|
|
37
|
+
viewport: {
|
|
38
|
+
w: number;
|
|
39
|
+
h: number;
|
|
40
|
+
};
|
|
41
|
+
/** Selector → [x, y, w, h] in viewport coordinates at capture time. */
|
|
42
|
+
rects: Record<string, [number, number, number, number]>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load the screenshot and the element positions `map --screenshots` wrote for a
|
|
46
|
+
* page. Either half missing — flag never used, page skipped, old capture — means
|
|
47
|
+
* no evidence for that page and nothing else: every caller degrades to the
|
|
48
|
+
* report as it was.
|
|
49
|
+
*/
|
|
50
|
+
export declare function loadPageShot(pageName: string, dir?: string): PageShot | null;
|
|
51
|
+
export interface CropWindow {
|
|
52
|
+
/** Rendered size of the thumbnail, after scaling. */
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
/** Scale applied to the underlying page image. */
|
|
56
|
+
scale: number;
|
|
57
|
+
/** Background offset, in unscaled page pixels. */
|
|
58
|
+
offsetX: number;
|
|
59
|
+
offsetY: number;
|
|
60
|
+
/** Highlight box, in unscaled page pixels, relative to the window. */
|
|
61
|
+
hi: {
|
|
62
|
+
left: number;
|
|
63
|
+
top: number;
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The CSS window onto the page image for one element: a box of `rect ± padding`,
|
|
70
|
+
* clamped to the picture's bounds, plus where the target sits inside it.
|
|
71
|
+
*
|
|
72
|
+
* Pure arithmetic — same inputs, same output, no I/O — which is what lets a test
|
|
73
|
+
* pin it without a browser (AC-13's spirit: the crop is a function, not a
|
|
74
|
+
* capture).
|
|
75
|
+
*/
|
|
76
|
+
export declare function cropWindow(rect: [number, number, number, number], viewport: {
|
|
77
|
+
w: number;
|
|
78
|
+
h: number;
|
|
79
|
+
}, padding?: number, maxWidth?: number): CropWindow;
|
|
80
|
+
export interface Candidate {
|
|
81
|
+
selector: string;
|
|
82
|
+
name: string;
|
|
83
|
+
role: string;
|
|
84
|
+
context?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The elements an `ambiguous` row could not be told apart from.
|
|
88
|
+
*
|
|
89
|
+
* `matchElement` returns only a *count* — it decides, or refuses, and the
|
|
90
|
+
* refusal carries no list. Rather than widen its return type, this re-derives
|
|
91
|
+
* the same set from the same primitives on the "after" side. That choice is not
|
|
92
|
+
* stylistic: `src/browser/match.js` ships twice (npm and the web tool on
|
|
93
|
+
* ia-qa.com), so every change to it owes two deploys and risks the two copies
|
|
94
|
+
* disagreeing about a verdict. Reporting is allowed to import *from* the
|
|
95
|
+
* matcher; it is not allowed to change it — the same rule the AI layer follows.
|
|
96
|
+
*
|
|
97
|
+
* The tiers mirror `matchElement` exactly: exact accessible-name matches first,
|
|
98
|
+
* then the fuzzy band within 0.05 of the top Dice score.
|
|
99
|
+
*
|
|
100
|
+
* One case it cannot reproduce, on purpose: a row demoted to `ambiguous` by the
|
|
101
|
+
* many-to-one guard (N baseline elements all healing onto one target) has a
|
|
102
|
+
* `candidateCount` counting *baseline rows*, not live elements. There, this
|
|
103
|
+
* returns the single contested target, which is the useful thing to look at.
|
|
104
|
+
*/
|
|
105
|
+
export declare function ambiguousCandidates(row: DiffRow, after: MappedElement[]): Candidate[];
|
|
106
|
+
export interface RowEvidence {
|
|
107
|
+
/** What the test's old selector resolves to *today* — only when the row rebound. */
|
|
108
|
+
nowAt?: {
|
|
109
|
+
selector: string;
|
|
110
|
+
label: string;
|
|
111
|
+
crop: CropWindow;
|
|
112
|
+
};
|
|
113
|
+
/** The elements a human must choose between, each located in the picture. */
|
|
114
|
+
candidates: Array<{
|
|
115
|
+
selector: string;
|
|
116
|
+
label: string;
|
|
117
|
+
crop: CropWindow;
|
|
118
|
+
}>;
|
|
119
|
+
/** Why there is nothing to show, when there is nothing to show. */
|
|
120
|
+
note?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Assemble the evidence for one drifted row.
|
|
124
|
+
*
|
|
125
|
+
* Only the statuses where a human is actually stuck get pictures — `ambiguous`,
|
|
126
|
+
* and anything `rebound`. `ok`, `renamed` and `healable` are deliberately left
|
|
127
|
+
* bare: there is no decision to take on them, so a thumbnail would be weight
|
|
128
|
+
* without a question (F1.8).
|
|
129
|
+
*/
|
|
130
|
+
export declare function rowEvidence(row: DiffRow, after: PageMapping | null, shot: PageShot | null): RowEvidence | null;
|
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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"}
|