@ia-qa/self-healing 1.6.16 → 1.7.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/README.md +120 -1
- package/TUTORIAL.md +31 -4
- package/dist/browser/resolve.d.ts +20 -0
- package/dist/browser/resolve.js +78 -0
- package/dist/browser/resolve.js.map +1 -0
- package/dist/cli/args.js +4 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/audit.d.ts +40 -0
- package/dist/cli/audit.js +77 -32
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/baseline.js +16 -2
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/check.d.ts +50 -1
- package/dist/cli/check.js +63 -25
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/diff.d.ts +21 -1
- package/dist/cli/diff.js +143 -25
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/discover.js +9 -0
- package/dist/cli/discover.js.map +1 -1
- package/dist/cli/fix.js +33 -0
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/index.js +53 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ingest.d.ts +7 -1
- package/dist/cli/ingest.js +17 -8
- package/dist/cli/ingest.js.map +1 -1
- package/dist/cli/login.d.ts +28 -0
- package/dist/cli/login.js +179 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/map.d.ts +19 -3
- package/dist/cli/map.js +191 -17
- package/dist/cli/map.js.map +1 -1
- package/dist/config.d.ts +68 -0
- package/dist/config.js +94 -1
- package/dist/config.js.map +1 -1
- package/dist/crawl.d.ts +2 -0
- package/dist/crawl.js +13 -4
- package/dist/crawl.js.map +1 -1
- package/dist/fixEngine.d.ts +23 -0
- package/dist/fixEngine.js +27 -0
- package/dist/fixEngine.js.map +1 -1
- package/dist/ingest.d.ts +35 -0
- package/dist/ingest.js +32 -0
- package/dist/ingest.js.map +1 -1
- package/dist/launcher.d.ts +7 -0
- package/dist/launcher.js +4 -4
- package/dist/launcher.js.map +1 -1
- package/dist/mcp/server.d.ts +370 -1
- package/dist/mcp/server.js +196 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/resolution.d.ts +96 -0
- package/dist/resolution.js +197 -0
- package/dist/resolution.js.map +1 -0
- package/dist/ui/page.js +10 -0
- package/dist/ui/page.js.map +1 -1
- package/dist/ui/server.js +66 -0
- package/dist/ui/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/ia-qa-heal/SKILL.md +25 -1
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,2 +1,371 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
declare const TOOLS: ({
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: string;
|
|
7
|
+
properties: {
|
|
8
|
+
url: {
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
page_name: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
out_dir: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
wait_for_selector: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
wait_until: {
|
|
25
|
+
type: string;
|
|
26
|
+
enum: string[];
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
browser_channel: {
|
|
30
|
+
type: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
browser_path: {
|
|
34
|
+
type: string;
|
|
35
|
+
description: string;
|
|
36
|
+
};
|
|
37
|
+
locale: {
|
|
38
|
+
type: string;
|
|
39
|
+
description: string;
|
|
40
|
+
};
|
|
41
|
+
before_path?: undefined;
|
|
42
|
+
after_path?: undefined;
|
|
43
|
+
before_json?: undefined;
|
|
44
|
+
after_json?: undefined;
|
|
45
|
+
test_paths?: undefined;
|
|
46
|
+
dry_run?: undefined;
|
|
47
|
+
use_locators?: undefined;
|
|
48
|
+
provider?: undefined;
|
|
49
|
+
model?: undefined;
|
|
50
|
+
api_key_env?: undefined;
|
|
51
|
+
min_confidence?: undefined;
|
|
52
|
+
write?: undefined;
|
|
53
|
+
offline?: undefined;
|
|
54
|
+
concurrency?: undefined;
|
|
55
|
+
limit?: undefined;
|
|
56
|
+
};
|
|
57
|
+
required: string[];
|
|
58
|
+
};
|
|
59
|
+
} | {
|
|
60
|
+
name: string;
|
|
61
|
+
description: string;
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: string;
|
|
64
|
+
properties: {
|
|
65
|
+
before_path: {
|
|
66
|
+
type: string;
|
|
67
|
+
description: string;
|
|
68
|
+
};
|
|
69
|
+
after_path: {
|
|
70
|
+
type: string;
|
|
71
|
+
description: string;
|
|
72
|
+
};
|
|
73
|
+
before_json: {
|
|
74
|
+
type: string;
|
|
75
|
+
description: string;
|
|
76
|
+
};
|
|
77
|
+
after_json: {
|
|
78
|
+
type: string;
|
|
79
|
+
description: string;
|
|
80
|
+
};
|
|
81
|
+
url?: undefined;
|
|
82
|
+
page_name?: undefined;
|
|
83
|
+
out_dir?: undefined;
|
|
84
|
+
wait_for_selector?: undefined;
|
|
85
|
+
wait_until?: undefined;
|
|
86
|
+
browser_channel?: undefined;
|
|
87
|
+
browser_path?: undefined;
|
|
88
|
+
locale?: undefined;
|
|
89
|
+
test_paths?: undefined;
|
|
90
|
+
dry_run?: undefined;
|
|
91
|
+
use_locators?: undefined;
|
|
92
|
+
provider?: undefined;
|
|
93
|
+
model?: undefined;
|
|
94
|
+
api_key_env?: undefined;
|
|
95
|
+
min_confidence?: undefined;
|
|
96
|
+
write?: undefined;
|
|
97
|
+
offline?: undefined;
|
|
98
|
+
concurrency?: undefined;
|
|
99
|
+
limit?: undefined;
|
|
100
|
+
};
|
|
101
|
+
required?: undefined;
|
|
102
|
+
};
|
|
103
|
+
} | {
|
|
104
|
+
name: string;
|
|
105
|
+
description: string;
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: string;
|
|
108
|
+
properties: {
|
|
109
|
+
before_path: {
|
|
110
|
+
type: string;
|
|
111
|
+
description: string;
|
|
112
|
+
};
|
|
113
|
+
after_path: {
|
|
114
|
+
type: string;
|
|
115
|
+
description: string;
|
|
116
|
+
};
|
|
117
|
+
before_json: {
|
|
118
|
+
type: string;
|
|
119
|
+
description: string;
|
|
120
|
+
};
|
|
121
|
+
after_json: {
|
|
122
|
+
type: string;
|
|
123
|
+
description: string;
|
|
124
|
+
};
|
|
125
|
+
test_paths: {
|
|
126
|
+
type: string;
|
|
127
|
+
items: {
|
|
128
|
+
type: string;
|
|
129
|
+
};
|
|
130
|
+
description: string;
|
|
131
|
+
};
|
|
132
|
+
dry_run: {
|
|
133
|
+
type: string;
|
|
134
|
+
description: string;
|
|
135
|
+
};
|
|
136
|
+
use_locators: {
|
|
137
|
+
type: string;
|
|
138
|
+
description: string;
|
|
139
|
+
};
|
|
140
|
+
url?: undefined;
|
|
141
|
+
page_name?: undefined;
|
|
142
|
+
out_dir?: undefined;
|
|
143
|
+
wait_for_selector?: undefined;
|
|
144
|
+
wait_until?: undefined;
|
|
145
|
+
browser_channel?: undefined;
|
|
146
|
+
browser_path?: undefined;
|
|
147
|
+
locale?: undefined;
|
|
148
|
+
provider?: undefined;
|
|
149
|
+
model?: undefined;
|
|
150
|
+
api_key_env?: undefined;
|
|
151
|
+
min_confidence?: undefined;
|
|
152
|
+
write?: undefined;
|
|
153
|
+
offline?: undefined;
|
|
154
|
+
concurrency?: undefined;
|
|
155
|
+
limit?: undefined;
|
|
156
|
+
};
|
|
157
|
+
required: string[];
|
|
158
|
+
};
|
|
159
|
+
} | {
|
|
160
|
+
name: string;
|
|
161
|
+
description: string;
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: string;
|
|
164
|
+
properties: {
|
|
165
|
+
before_path: {
|
|
166
|
+
type: string;
|
|
167
|
+
description: string;
|
|
168
|
+
};
|
|
169
|
+
after_path: {
|
|
170
|
+
type: string;
|
|
171
|
+
description: string;
|
|
172
|
+
};
|
|
173
|
+
before_json: {
|
|
174
|
+
type: string;
|
|
175
|
+
description: string;
|
|
176
|
+
};
|
|
177
|
+
after_json: {
|
|
178
|
+
type: string;
|
|
179
|
+
description: string;
|
|
180
|
+
};
|
|
181
|
+
provider: {
|
|
182
|
+
type: string;
|
|
183
|
+
enum: string[];
|
|
184
|
+
description: string;
|
|
185
|
+
};
|
|
186
|
+
model: {
|
|
187
|
+
type: string;
|
|
188
|
+
description: string;
|
|
189
|
+
};
|
|
190
|
+
api_key_env: {
|
|
191
|
+
type: string;
|
|
192
|
+
description: string;
|
|
193
|
+
};
|
|
194
|
+
min_confidence: {
|
|
195
|
+
type: string;
|
|
196
|
+
description: string;
|
|
197
|
+
};
|
|
198
|
+
url?: undefined;
|
|
199
|
+
page_name?: undefined;
|
|
200
|
+
out_dir?: undefined;
|
|
201
|
+
wait_for_selector?: undefined;
|
|
202
|
+
wait_until?: undefined;
|
|
203
|
+
browser_channel?: undefined;
|
|
204
|
+
browser_path?: undefined;
|
|
205
|
+
locale?: undefined;
|
|
206
|
+
test_paths?: undefined;
|
|
207
|
+
dry_run?: undefined;
|
|
208
|
+
use_locators?: undefined;
|
|
209
|
+
write?: undefined;
|
|
210
|
+
offline?: undefined;
|
|
211
|
+
concurrency?: undefined;
|
|
212
|
+
limit?: undefined;
|
|
213
|
+
};
|
|
214
|
+
required: string[];
|
|
215
|
+
};
|
|
216
|
+
} | {
|
|
217
|
+
name: string;
|
|
218
|
+
description: string;
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: string;
|
|
221
|
+
properties: {
|
|
222
|
+
test_paths: {
|
|
223
|
+
type: string;
|
|
224
|
+
items: {
|
|
225
|
+
type: string;
|
|
226
|
+
};
|
|
227
|
+
description: string;
|
|
228
|
+
};
|
|
229
|
+
write: {
|
|
230
|
+
type: string;
|
|
231
|
+
description: string;
|
|
232
|
+
};
|
|
233
|
+
url?: undefined;
|
|
234
|
+
page_name?: undefined;
|
|
235
|
+
out_dir?: undefined;
|
|
236
|
+
wait_for_selector?: undefined;
|
|
237
|
+
wait_until?: undefined;
|
|
238
|
+
browser_channel?: undefined;
|
|
239
|
+
browser_path?: undefined;
|
|
240
|
+
locale?: undefined;
|
|
241
|
+
before_path?: undefined;
|
|
242
|
+
after_path?: undefined;
|
|
243
|
+
before_json?: undefined;
|
|
244
|
+
after_json?: undefined;
|
|
245
|
+
dry_run?: undefined;
|
|
246
|
+
use_locators?: undefined;
|
|
247
|
+
provider?: undefined;
|
|
248
|
+
model?: undefined;
|
|
249
|
+
api_key_env?: undefined;
|
|
250
|
+
min_confidence?: undefined;
|
|
251
|
+
offline?: undefined;
|
|
252
|
+
concurrency?: undefined;
|
|
253
|
+
limit?: undefined;
|
|
254
|
+
};
|
|
255
|
+
required?: undefined;
|
|
256
|
+
};
|
|
257
|
+
} | {
|
|
258
|
+
name: string;
|
|
259
|
+
description: string;
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: string;
|
|
262
|
+
properties: {
|
|
263
|
+
test_paths: {
|
|
264
|
+
type: string;
|
|
265
|
+
items: {
|
|
266
|
+
type: string;
|
|
267
|
+
};
|
|
268
|
+
description: string;
|
|
269
|
+
};
|
|
270
|
+
url?: undefined;
|
|
271
|
+
page_name?: undefined;
|
|
272
|
+
out_dir?: undefined;
|
|
273
|
+
wait_for_selector?: undefined;
|
|
274
|
+
wait_until?: undefined;
|
|
275
|
+
browser_channel?: undefined;
|
|
276
|
+
browser_path?: undefined;
|
|
277
|
+
locale?: undefined;
|
|
278
|
+
before_path?: undefined;
|
|
279
|
+
after_path?: undefined;
|
|
280
|
+
before_json?: undefined;
|
|
281
|
+
after_json?: undefined;
|
|
282
|
+
dry_run?: undefined;
|
|
283
|
+
use_locators?: undefined;
|
|
284
|
+
provider?: undefined;
|
|
285
|
+
model?: undefined;
|
|
286
|
+
api_key_env?: undefined;
|
|
287
|
+
min_confidence?: undefined;
|
|
288
|
+
write?: undefined;
|
|
289
|
+
offline?: undefined;
|
|
290
|
+
concurrency?: undefined;
|
|
291
|
+
limit?: undefined;
|
|
292
|
+
};
|
|
293
|
+
required?: undefined;
|
|
294
|
+
};
|
|
295
|
+
} | {
|
|
296
|
+
name: string;
|
|
297
|
+
description: string;
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: string;
|
|
300
|
+
properties: {
|
|
301
|
+
offline: {
|
|
302
|
+
type: string;
|
|
303
|
+
description: string;
|
|
304
|
+
};
|
|
305
|
+
concurrency: {
|
|
306
|
+
type: string;
|
|
307
|
+
description: string;
|
|
308
|
+
};
|
|
309
|
+
url?: undefined;
|
|
310
|
+
page_name?: undefined;
|
|
311
|
+
out_dir?: undefined;
|
|
312
|
+
wait_for_selector?: undefined;
|
|
313
|
+
wait_until?: undefined;
|
|
314
|
+
browser_channel?: undefined;
|
|
315
|
+
browser_path?: undefined;
|
|
316
|
+
locale?: undefined;
|
|
317
|
+
before_path?: undefined;
|
|
318
|
+
after_path?: undefined;
|
|
319
|
+
before_json?: undefined;
|
|
320
|
+
after_json?: undefined;
|
|
321
|
+
test_paths?: undefined;
|
|
322
|
+
dry_run?: undefined;
|
|
323
|
+
use_locators?: undefined;
|
|
324
|
+
provider?: undefined;
|
|
325
|
+
model?: undefined;
|
|
326
|
+
api_key_env?: undefined;
|
|
327
|
+
min_confidence?: undefined;
|
|
328
|
+
write?: undefined;
|
|
329
|
+
limit?: undefined;
|
|
330
|
+
};
|
|
331
|
+
required?: undefined;
|
|
332
|
+
};
|
|
333
|
+
} | {
|
|
334
|
+
name: string;
|
|
335
|
+
description: string;
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: string;
|
|
338
|
+
properties: {
|
|
339
|
+
limit: {
|
|
340
|
+
type: string;
|
|
341
|
+
description: string;
|
|
342
|
+
};
|
|
343
|
+
url?: undefined;
|
|
344
|
+
page_name?: undefined;
|
|
345
|
+
out_dir?: undefined;
|
|
346
|
+
wait_for_selector?: undefined;
|
|
347
|
+
wait_until?: undefined;
|
|
348
|
+
browser_channel?: undefined;
|
|
349
|
+
browser_path?: undefined;
|
|
350
|
+
locale?: undefined;
|
|
351
|
+
before_path?: undefined;
|
|
352
|
+
after_path?: undefined;
|
|
353
|
+
before_json?: undefined;
|
|
354
|
+
after_json?: undefined;
|
|
355
|
+
test_paths?: undefined;
|
|
356
|
+
dry_run?: undefined;
|
|
357
|
+
use_locators?: undefined;
|
|
358
|
+
provider?: undefined;
|
|
359
|
+
model?: undefined;
|
|
360
|
+
api_key_env?: undefined;
|
|
361
|
+
min_confidence?: undefined;
|
|
362
|
+
write?: undefined;
|
|
363
|
+
offline?: undefined;
|
|
364
|
+
concurrency?: undefined;
|
|
365
|
+
};
|
|
366
|
+
required?: undefined;
|
|
367
|
+
};
|
|
368
|
+
})[];
|
|
369
|
+
export declare function callTool(name: string, args: any): Promise<string>;
|
|
370
|
+
declare function main(): void;
|
|
371
|
+
export { TOOLS, main };
|
package/dist/mcp/server.js
CHANGED
|
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.TOOLS = void 0;
|
|
38
|
+
exports.callTool = callTool;
|
|
39
|
+
exports.main = main;
|
|
37
40
|
const fs = __importStar(require("fs"));
|
|
38
41
|
const aom_1 = require("../aom");
|
|
39
42
|
const mapUrl_1 = require("../mapUrl");
|
|
@@ -46,17 +49,32 @@ const ingest_1 = require("../ingest");
|
|
|
46
49
|
const config_1 = require("../config");
|
|
47
50
|
const resolver_1 = require("../ai/resolver");
|
|
48
51
|
const history_1 = require("../history");
|
|
52
|
+
const audit_1 = require("../cli/audit");
|
|
53
|
+
const check_1 = require("../cli/check");
|
|
54
|
+
const checks_1 = require("../checks");
|
|
49
55
|
/**
|
|
50
56
|
* @ia-qa/self-healing — local stdio MCP server.
|
|
51
57
|
*
|
|
52
58
|
* A dependency-free JSON-RPC 2.0 server over stdio (newline-delimited messages,
|
|
53
59
|
* the MCP stdio transport). It runs on the END USER's machine, launches its own
|
|
54
60
|
* headless Chromium via Playwright, and exposes these tools to an agent:
|
|
61
|
+
* - ingest_tests — inventory the locators the suite uses → .ia-qa/usage.json.
|
|
62
|
+
* The ONLY tool here that writes outside a diff, and the one
|
|
63
|
+
* diff_mappings needs to see name drift at all
|
|
55
64
|
* - map_app — scan one URL → page contract (json + md)
|
|
56
65
|
* - diff_mappings — two contracts → PASS/FIX/BLOCK verdict + drift detail
|
|
57
66
|
* - fix_tests — apply the deterministic healable rewrites to test files
|
|
67
|
+
* - audit_suite — day one, NO baseline: does the suite still name things that
|
|
68
|
+
* exist? Read-only, advisory — never a gate
|
|
69
|
+
* - check_app — is the mapped app sound at all? dead links · unnamed elements ·
|
|
70
|
+
* role+name collisions · orphan pages. Read-only
|
|
58
71
|
* - suggest_heal — OPTIONAL BYOK AI: propose matches for the lost/ambiguous rows
|
|
59
72
|
* the deterministic engine gave up on (suggestion only, no writes)
|
|
73
|
+
* - heal_history — the drift trend no single diff can reconstruct
|
|
74
|
+
*
|
|
75
|
+
* `audit_suite` and `check_app` are the entry verbs: every other tool compares two
|
|
76
|
+
* moments, so its value arrives one drift *after* install — an agent meeting a
|
|
77
|
+
* project for the first time has no baseline and needs a verdict anyway.
|
|
60
78
|
*
|
|
61
79
|
* Because it runs locally, it sees localhost / staging / VPN apps and nothing
|
|
62
80
|
* leaves the machine. stdout carries the protocol; all logs go to stderr.
|
|
@@ -136,6 +154,55 @@ const TOOLS = [
|
|
|
136
154
|
required: ['provider', 'model'],
|
|
137
155
|
},
|
|
138
156
|
},
|
|
157
|
+
{
|
|
158
|
+
name: 'ingest_tests',
|
|
159
|
+
description: "Inventory the locators the test suite ACTUALLY uses — CSS selectors and accessible names — by scanning the test/POM files statically (no execution, no browser). WRITES .ia-qa/usage.json, the file diff_mappings reads to escalate a rename the tests locate by name, and fix_tests reads as its default file list: without it those two tools see only half the drift, and report a *greener* verdict than the truth. Run it once before diffing, and again whenever the suite changes. It overwrites the whole inventory, so scanning a SUBSET of the suite narrows it — the response says so explicitly in `narrowed`. Use write=false to preview without touching the file. Only .ia-qa/usage.json is written; test files are read, never modified.",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
test_paths: {
|
|
164
|
+
type: 'array',
|
|
165
|
+
items: { type: 'string' },
|
|
166
|
+
description: 'Test files or directories to scan. Omit to use "testPaths" from .ia-qa/config.json. Pass the WHOLE suite — a partial path produces a partial inventory, and every later verdict inherits it.',
|
|
167
|
+
},
|
|
168
|
+
write: {
|
|
169
|
+
type: 'boolean',
|
|
170
|
+
description: 'Write .ia-qa/usage.json. Default true. Set false to see what the scan finds without replacing the current inventory.',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'audit_suite',
|
|
177
|
+
description: "DAY ONE, NO BASELINE. Answers 'does my E2E suite still name things that exist?' by cross-referencing the locators the tests use against ONE capture of the app — so it works the first time it is run, unlike diff_mappings which needs two moments. Read-only: never opens a browser, never edits a file, never leaves the machine. ADVISORY BY DESIGN — do NOT gate a pipeline on this verdict: the capture covers the pages in config.pages, and a locator reported as matching nothing may simply live on an unmapped page, or in a state nothing captured (behind a tab, a modal, a mode toggle). Only role-anchored locators (getByRole, getByLabel, By.linkText) are judged; getByText and cy.contains name a string, not an element, and are counted only. Suggestions prove a resemblance, never an identity — a human confirms before any edit.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
test_paths: {
|
|
182
|
+
type: 'array',
|
|
183
|
+
items: { type: 'string' },
|
|
184
|
+
description: 'Test files or directories to scan in memory. Omit to use .ia-qa/usage.json (written by `ia-qa-heal ingest`), falling back to "testPaths" in .ia-qa/config.json.',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'check_app',
|
|
191
|
+
description: "Is the app you mapped SOUND? — the question a contract diff structurally cannot answer, since a green diff over a page whose every link 404s is still green. Four checks: dead links, interactive elements with no accessible name, role+name collisions (a locator coin flip: drift that has not happened yet), and mapped pages nothing links to. Read-only, edits nothing. NETWORK: unless offline=true, this GETs the links found in the contract — your own configured baseUrl only, same site only, and never a URL that acts (/logout, /delete…); the response states the host and how many were requested. Only a dead link FAILs (a fact the app itself answered); everything else WARNs and is advisory. A 429 is this tool's own request rate, not a defect — those land in unverifiedLinks, which is 'not checked', never 'ok'.",
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
offline: {
|
|
196
|
+
type: 'boolean',
|
|
197
|
+
description: 'Skip the link requests entirely — purely local run. The verdict then covers less, and says so in `skipped`. Default false.',
|
|
198
|
+
},
|
|
199
|
+
concurrency: {
|
|
200
|
+
type: 'number',
|
|
201
|
+
description: 'How many links to request at once (1-32, default 6). Lower it for a target with a strict rate limiter; an out-of-range value falls back to the default and says so.',
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
139
206
|
{
|
|
140
207
|
name: 'heal_history',
|
|
141
208
|
description: "Read-only. The drift TREND for this project, which no single diff can answer: how many runs ended PASS/FIX/BLOCK over time, mean page coverage, how many rewrites were applied, and which pages actually churn. Reads .ia-qa/history.jsonl, written by every diff/run — it never opens a browser, never touches the app, and cannot change a verdict. IMPORTANT when reporting this to a human: runs captured differently (a `map` baseline vs a live suite capture) are excluded from the trend and listed in `excluded_reason` — say so rather than presenting the series as complete, because the two read a page differently and mixing them invents a trend out of a capture artifact. An empty history means nothing has been recorded yet, never that nothing drifted.",
|
|
@@ -147,6 +214,7 @@ const TOOLS = [
|
|
|
147
214
|
},
|
|
148
215
|
},
|
|
149
216
|
];
|
|
217
|
+
exports.TOOLS = TOOLS;
|
|
150
218
|
function loadMappingArg(inlineJson, filePath, label) {
|
|
151
219
|
let mapping = inlineJson;
|
|
152
220
|
if (!mapping && filePath) {
|
|
@@ -175,6 +243,123 @@ function assertComparable(before, after) {
|
|
|
175
243
|
`then let the app change, then capture again.`);
|
|
176
244
|
}
|
|
177
245
|
async function callTool(name, args) {
|
|
246
|
+
if (name === 'ingest_tests') {
|
|
247
|
+
const given = Array.isArray(args?.test_paths) ? args.test_paths : [];
|
|
248
|
+
for (const p of given)
|
|
249
|
+
(0, sandbox_1.assertInsideProject)(p, 'test_paths');
|
|
250
|
+
const paths = given.length > 0 ? given : (0, config_1.configuredTestPaths)();
|
|
251
|
+
if (paths.length === 0) {
|
|
252
|
+
throw new Error('Nothing to scan: no test_paths given and no "testPaths" in .ia-qa/config.json. ' +
|
|
253
|
+
'Pass the whole suite — a partial path produces a partial inventory, and diff_mappings inherits it.');
|
|
254
|
+
}
|
|
255
|
+
const write = args?.write !== false;
|
|
256
|
+
const previous = (0, ingest_1.loadUsage)();
|
|
257
|
+
const { usage, skipped } = (0, ingest_1.ingestPaths)(paths);
|
|
258
|
+
const report = (0, ingest_1.buildInventoryReport)(usage);
|
|
259
|
+
// The one hazard this tool has, and it fails in the reassuring direction:
|
|
260
|
+
// the whole inventory is replaced, so scanning a subset leaves later diffs
|
|
261
|
+
// seeing less drift and reporting a GREENER verdict. Nothing about the file
|
|
262
|
+
// afterwards reveals that it got narrower, so it is said here — as a field,
|
|
263
|
+
// not buried in prose an agent may not relay.
|
|
264
|
+
const before = previous
|
|
265
|
+
? { selectors: Object.keys(previous.selectors).length, names: Object.keys(previous.names ?? {}).length }
|
|
266
|
+
: null;
|
|
267
|
+
const after = { selectors: report.selectors.distinct, names: report.names.distinct };
|
|
268
|
+
const narrowed = before !== null && (after.selectors < before.selectors || after.names < before.names)
|
|
269
|
+
? { before, after, why: 'The inventory now covers LESS than it did. If that was not intended, re-run over the whole suite: every later diff_mappings and fix_tests verdict reads this file, and a narrower inventory reports less drift, not more.' }
|
|
270
|
+
: null;
|
|
271
|
+
const file = write ? (0, ingest_1.saveUsage)(usage) : null;
|
|
272
|
+
return JSON.stringify({
|
|
273
|
+
written: file ? (0, mapUrl_1.rel)(file) : null,
|
|
274
|
+
scanned_paths: paths,
|
|
275
|
+
skipped_paths: skipped,
|
|
276
|
+
narrowed,
|
|
277
|
+
...report,
|
|
278
|
+
note: (write
|
|
279
|
+
? 'Inventory replaced. Test files were read, never modified. '
|
|
280
|
+
: 'PREVIEW — write=false, .ia-qa/usage.json untouched. ') +
|
|
281
|
+
report.note,
|
|
282
|
+
}, null, 2);
|
|
283
|
+
}
|
|
284
|
+
if (name === 'audit_suite') {
|
|
285
|
+
const testPaths = Array.isArray(args?.test_paths) ? args.test_paths : [];
|
|
286
|
+
for (const p of testPaths)
|
|
287
|
+
(0, sandbox_1.assertInsideProject)(p, 'test_paths');
|
|
288
|
+
// `strict` is deliberately not an argument. It only ever changes an exit
|
|
289
|
+
// code, and an MCP tool has none — offering it would let an agent believe it
|
|
290
|
+
// had turned an advisory answer into a gate.
|
|
291
|
+
const outcome = (0, audit_1.auditProject)({ testPaths });
|
|
292
|
+
// A refusal is an ERROR, not a payload. At a terminal `audit` refuses with
|
|
293
|
+
// exit 2, which is what stops "verdict: null" from reading as a pass; over
|
|
294
|
+
// MCP there is no exit code, so the refusal has to arrive as isError or the
|
|
295
|
+
// absence of a check gets reported upstream as the success of one.
|
|
296
|
+
if (!outcome.ok)
|
|
297
|
+
throw new Error(`${outcome.reason}: ${outcome.message}`);
|
|
298
|
+
const r = outcome.report;
|
|
299
|
+
return JSON.stringify({
|
|
300
|
+
verdict: r.verdict,
|
|
301
|
+
advisory: true,
|
|
302
|
+
pages_mapped: r.pagesMapped,
|
|
303
|
+
elements: outcome.elements,
|
|
304
|
+
locators_judged: r.locatorsJudged,
|
|
305
|
+
locators_not_judged: r.locatorsNotJudged,
|
|
306
|
+
dead_locators: r.deadLocators.map((d) => ({
|
|
307
|
+
name: d.name,
|
|
308
|
+
roles: d.roles,
|
|
309
|
+
test_sites: d.sites.map((s) => `${s.file}:${s.line}`),
|
|
310
|
+
closest_live_label: d.suggestion
|
|
311
|
+
? { role: d.suggestion.role, name: d.suggestion.name, score: Number(d.suggestion.score.toFixed(2)), kind: d.suggestion.kind, pages: d.suggestion.pages }
|
|
312
|
+
: null,
|
|
313
|
+
})),
|
|
314
|
+
unresolved_selectors: r.unresolvedSelectors,
|
|
315
|
+
note: 'ADVISORY — never gate a pipeline on this. Coverage decides what the verdict is worth: ' +
|
|
316
|
+
`${r.pagesMapped} page(s) mapped, and a "dead" locator may live on an unmapped page or in an ` +
|
|
317
|
+
'unmapped state (behind a tab, a modal, a mode toggle). Nothing was edited — `audit` never writes. ' +
|
|
318
|
+
'A closest_live_label is a resemblance read from ONE capture, not proof of identity: a human confirms.',
|
|
319
|
+
}, null, 2);
|
|
320
|
+
}
|
|
321
|
+
if (name === 'check_app') {
|
|
322
|
+
const offline = !!args?.offline;
|
|
323
|
+
let concurrency = check_1.CONCURRENCY_BOUNDS.default;
|
|
324
|
+
let concurrencyNote;
|
|
325
|
+
if (args?.concurrency !== undefined) {
|
|
326
|
+
const n = Number(args.concurrency);
|
|
327
|
+
if (Number.isInteger(n) && n >= check_1.CONCURRENCY_BOUNDS.min && n <= check_1.CONCURRENCY_BOUNDS.max) {
|
|
328
|
+
concurrency = n;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
concurrencyNote = `concurrency ${JSON.stringify(args.concurrency)} is out of range ${check_1.CONCURRENCY_BOUNDS.min}-${check_1.CONCURRENCY_BOUNDS.max} — used ${check_1.CONCURRENCY_BOUNDS.default}.`;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const outcome = await (0, check_1.checkProject)({ offline, concurrency });
|
|
335
|
+
if (!outcome.ok)
|
|
336
|
+
throw new Error(`${outcome.reason}: ${outcome.message}`);
|
|
337
|
+
const { report: r, verdict, network } = outcome;
|
|
338
|
+
return JSON.stringify({
|
|
339
|
+
verdict,
|
|
340
|
+
headline: (0, checks_1.checkHeadline)(r, verdict),
|
|
341
|
+
advisory: verdict === 'WARN',
|
|
342
|
+
pages_checked: r.pagesChecked,
|
|
343
|
+
elements: r.elements,
|
|
344
|
+
// Where the requests went, restated in the payload: over stdio there is
|
|
345
|
+
// no "before the first request" to announce in.
|
|
346
|
+
network: network
|
|
347
|
+
? { host: network.host, links_requested: network.linksRequested, hrefs_left_alone: network.hrefsLeftAlone, rate_limited: network.throttled, method: 'GET', scope: 'configured baseUrl only, action URLs skipped' }
|
|
348
|
+
: null,
|
|
349
|
+
dead_links: r.deadLinks.map((d) => ({ url: d.url, status: d.status, error: d.error ?? null, linked_from: d.from })),
|
|
350
|
+
unverified_links: r.unverifiedLinks.map((u) => ({ url: u.url, reason: u.reason, linked_from: u.from })),
|
|
351
|
+
ambiguous_names: r.ambiguous,
|
|
352
|
+
unnamed_elements: r.unnamed,
|
|
353
|
+
orphan_pages: r.orphans,
|
|
354
|
+
checks_ran: (0, checks_1.checkCoverage)(r),
|
|
355
|
+
skipped: r.skipped,
|
|
356
|
+
note: (concurrencyNote ? `${concurrencyNote} ` : '') +
|
|
357
|
+
'FAIL means a dead link — a fact the app answered, not a judgement. WARN is advisory: an unnamed ' +
|
|
358
|
+
'icon button or a deep-link-only page is real but arguable. unverified_links reached NO verdict ' +
|
|
359
|
+
'(rate-limited or unanswered) — never report them as ok, and never as dead. A PASS speaks only for ' +
|
|
360
|
+
'the checks in checks_ran; whatever a skipped check would have found is invisible here.',
|
|
361
|
+
}, null, 2);
|
|
362
|
+
}
|
|
178
363
|
if (name === 'heal_history') {
|
|
179
364
|
const limit = Number.isFinite(args?.limit) ? Math.max(1, Math.floor(args.limit)) : 100;
|
|
180
365
|
const entries = (0, history_1.readHistory)(limit);
|
|
@@ -453,5 +638,15 @@ function main() {
|
|
|
453
638
|
});
|
|
454
639
|
process.stdin.on('end', () => process.exit(0));
|
|
455
640
|
}
|
|
456
|
-
|
|
641
|
+
/**
|
|
642
|
+
* Only when this file IS the process.
|
|
643
|
+
*
|
|
644
|
+
* `main()` attaches a stdin listener, which keeps the event loop alive forever —
|
|
645
|
+
* so an unconditional call made this module impossible to import, and therefore
|
|
646
|
+
* impossible to test. The dispatch in `callTool` is exactly where a CLI/MCP
|
|
647
|
+
* divergence would hide, so it has to be reachable from a test; as the `bin`
|
|
648
|
+
* entry point this condition is true and the server behaves as before.
|
|
649
|
+
*/
|
|
650
|
+
if (require.main === module)
|
|
651
|
+
main();
|
|
457
652
|
//# sourceMappingURL=server.js.map
|