@ia-qa/self-healing 1.7.18 → 1.7.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aom.js +19 -5
- package/dist/aom.js.map +1 -1
- package/dist/cli/diff.d.ts +75 -1
- package/dist/cli/diff.js +164 -13
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/emit.js +9 -0
- package/dist/cli/emit.js.map +1 -1
- package/dist/cli/exitCodes.js +1 -1
- package/dist/cli/exitCodes.js.map +1 -1
- package/dist/cli/history.js +6 -1
- package/dist/cli/history.js.map +1 -1
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.js +40 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ingest.js +20 -2
- package/dist/cli/ingest.js.map +1 -1
- package/dist/cli/init.d.ts +24 -0
- package/dist/cli/init.js +100 -8
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/map.js +28 -5
- package/dist/cli/map.js.map +1 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +29 -8
- package/dist/config.js.map +1 -1
- package/dist/explain.d.ts +19 -0
- package/dist/explain.js +38 -1
- package/dist/explain.js.map +1 -1
- package/dist/history.d.ts +12 -0
- package/dist/history.js.map +1 -1
- package/dist/resolution.d.ts +28 -0
- package/dist/resolution.js +35 -0
- package/dist/resolution.js.map +1 -1
- package/package.json +1 -1
package/dist/aom.js
CHANGED
|
@@ -88,12 +88,26 @@ function depthMismatch(baseline, current) {
|
|
|
88
88
|
}
|
|
89
89
|
/** The sentence every door prints or attaches when `depthMismatch` returns non-null. */
|
|
90
90
|
function depthMismatchMessage(m) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
91
|
+
const baselineDeeper = m.baseline > m.current;
|
|
92
|
+
const deeper = baselineDeeper ? 'baseline' : 'current capture';
|
|
93
|
+
const shallower = baselineDeeper ? 'current capture' : 'baseline';
|
|
94
|
+
const head = `Exploration depth mismatch — the ${deeper} was captured with \`map --deep\`, the ${shallower} ` +
|
|
94
95
|
`was not (depth ${m.baseline} vs ${m.current}). A deep contract holds the elements behind ` +
|
|
95
|
-
`menus, tabs and dialogs; a shallow one cannot
|
|
96
|
-
|
|
96
|
+
`menus, tabs and dialogs; a shallow one cannot. `;
|
|
97
|
+
// What follows must describe what the tool then DOES, and since 1.7.17 that changed:
|
|
98
|
+
// those elements are held out of the verdict rather than counted `lost`. The old wording
|
|
99
|
+
// survived the change and contradicted the line printed a few rows below it in the same
|
|
100
|
+
// run — the precise failure `depthDelta.ts` warns about, two readers of one run made to
|
|
101
|
+
// disagree. Branched, because only the deep-baseline direction holds anything out: the
|
|
102
|
+
// reverse produces `added` rows, which never moved a verdict.
|
|
103
|
+
return baselineDeeper
|
|
104
|
+
? head +
|
|
105
|
+
`Those elements are held OUT of this verdict rather than counted as \`lost\` — neither ` +
|
|
106
|
+
`present nor gone, simply not measured, and listed below. Capture both sides the same ` +
|
|
107
|
+
`way to gate them.`
|
|
108
|
+
: head +
|
|
109
|
+
`The extra elements the deep side found appear as \`added\`, which never moves a verdict, ` +
|
|
110
|
+
`so this comparison stands. Capture both sides the same way to gate them too.`;
|
|
97
111
|
}
|
|
98
112
|
async function extractInteractiveElements(page) {
|
|
99
113
|
return page.evaluate(extract_1.extractInPage);
|
package/dist/aom.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2GA,sCAKC;AAkBD,sCAOC;AAGD,
|
|
1
|
+
{"version":3,"file":"aom.js","sourceRoot":"","sources":["../src/aom.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/diff.d.ts
CHANGED
|
@@ -100,6 +100,58 @@ export declare function runDiff(args: string[]): Promise<void>;
|
|
|
100
100
|
* counts. A verdict that changed without a line saying why is the silent behaviour this
|
|
101
101
|
* package treats as a defect, and here the silence would be a green one.
|
|
102
102
|
*/
|
|
103
|
+
/**
|
|
104
|
+
* The CSS-Page-Object check did not run, and the verdict above does not cover it.
|
|
105
|
+
*
|
|
106
|
+
* Printed wherever a verdict is: this is the difference between "your selectors are fine"
|
|
107
|
+
* and "nobody looked at your selectors", and the second one wearing the first one's green
|
|
108
|
+
* is the failure mode this whole package exists to refuse.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* How to get a gate that DOES cover the suite's selectors — which depends on whether this
|
|
112
|
+
* project has pages to walk.
|
|
113
|
+
*
|
|
114
|
+
* `map && diff` was printed unconditionally, and on the zero-config path `init` now
|
|
115
|
+
* recommends it is a closed loop: `pages` is empty, so `map` exits 1 with "No pages
|
|
116
|
+
* configured" and points back at capture-during-run, which is where the reader already is.
|
|
117
|
+
* A remedy that returns you to your starting point is worse than none — it costs a run to
|
|
118
|
+
* find out, and it teaches that the advice is not to be trusted.
|
|
119
|
+
*/
|
|
120
|
+
/**
|
|
121
|
+
* The exit code when a run left the suite's own selectors unjudged.
|
|
122
|
+
*
|
|
123
|
+
* Transparency fixed the honesty of the message and left the harder question open: what
|
|
124
|
+
* should a CI *do* with it? For a team whose Page Objects are CSS, the check that did not
|
|
125
|
+
* run is the whole gate — so `diff` returned exit 0 while their suite was red, and no flag
|
|
126
|
+
* could say otherwise.
|
|
127
|
+
*
|
|
128
|
+
* **It is `2`, not `1`, and that is the whole design.** The contract in `exitCodes.ts` is
|
|
129
|
+
* explicit: 1 is "answered, and the answer is no"; 2 is "could not answer — NOT a failure
|
|
130
|
+
* of your app, and never a pass". Bindings that were never resolved are the second, exactly.
|
|
131
|
+
* Returning 1 would tell a pipeline the application drifted, which is the mistake this
|
|
132
|
+
* package documents at length — an agent once wrote `non-zero = BLOCK` into a generated
|
|
133
|
+
* workflow and turned "I do not know" into a permanently red build.
|
|
134
|
+
*
|
|
135
|
+
* **Behind `--strict`, and not on by default.** Every capture-path project would otherwise
|
|
136
|
+
* go non-zero on upgrade, over a limitation none of them introduced; a gate nobody can
|
|
137
|
+
* leave on is a gate nobody turns on. `--strict` already means "tighten what counts as
|
|
138
|
+
* failure", so it is the lever that exists rather than a new one to discover.
|
|
139
|
+
*
|
|
140
|
+
* **A real finding outranks an unanswered question.** BLOCK (and `--strict` FIX) still
|
|
141
|
+
* exit 1: something WAS answered and it is bad. Only a run that would otherwise be green
|
|
142
|
+
* reports 2 — a green that covers less than the reader thinks.
|
|
143
|
+
*/
|
|
144
|
+
export declare function bindingExitCode(failing: boolean, strict: boolean, coverage: BindingCoverage | undefined): 0 | 1 | 2;
|
|
145
|
+
export declare function bindingGateRemedy(hasDeclaredPages: boolean): string;
|
|
146
|
+
/**
|
|
147
|
+
* Why the process is leaving with 2 on a run whose verdict says PASS.
|
|
148
|
+
*
|
|
149
|
+
* An exit code with no sentence behind it is the thing this package refuses everywhere
|
|
150
|
+
* else: the reader sees a green verdict and a non-zero exit and has to guess which is
|
|
151
|
+
* lying. Neither is — they answer different questions.
|
|
152
|
+
*/
|
|
153
|
+
export declare function printBindingStrictExit(c: BindingCoverage | undefined, json: boolean): void;
|
|
154
|
+
export declare function printBindingCoverage(c: BindingCoverage | undefined): void;
|
|
103
155
|
export declare function printDepthUnmeasured(u: DepthUnmeasured | undefined): void;
|
|
104
156
|
export declare function printSystemic(outcome: SystemicOutcome): void;
|
|
105
157
|
export declare function printNameMasks(outcome: MaskOutcome | undefined): void;
|
|
@@ -117,7 +169,7 @@ export declare function printNameDrift(findings: NameDriftFinding[] | undefined)
|
|
|
117
169
|
* and never in `--json` mode, where it would corrupt the machine-readable payload.
|
|
118
170
|
* Shared with `run`, which ends on the same verdict.
|
|
119
171
|
*/
|
|
120
|
-
export declare function driftSummaryLine(verb: string, verdict: 'PASS' | 'FIX' | 'BLOCK', c: Report['counts'], nameDrift?: NameDriftFinding[], bindingDrift?: BindingDrift[], depthUnmeasured?: DepthUnmeasured): void;
|
|
172
|
+
export declare function driftSummaryLine(verb: string, verdict: 'PASS' | 'FIX' | 'BLOCK', c: Report['counts'], nameDrift?: NameDriftFinding[], bindingDrift?: BindingDrift[], depthUnmeasured?: DepthUnmeasured, bindingCoverage?: BindingCoverage): void;
|
|
121
173
|
export interface DirDiffCollection {
|
|
122
174
|
reports: Array<{
|
|
123
175
|
name: string;
|
|
@@ -220,6 +272,8 @@ export interface DirReport {
|
|
|
220
272
|
* that the green it just got covers less than the contract does.
|
|
221
273
|
*/
|
|
222
274
|
depthUnmeasured?: DepthUnmeasured;
|
|
275
|
+
/** The CSS-Page-Object check did not run this walk. Emitted, not merely printed. */
|
|
276
|
+
bindingCoverage?: BindingCoverage;
|
|
223
277
|
}
|
|
224
278
|
export declare function aggregateReports(reports: Array<{
|
|
225
279
|
name: string;
|
|
@@ -291,4 +345,24 @@ export interface Report {
|
|
|
291
345
|
* the same way — never "none were held out because none exist".
|
|
292
346
|
*/
|
|
293
347
|
depthUnmeasured?: DepthUnmeasured;
|
|
348
|
+
/**
|
|
349
|
+
* The suite's own selector strings were NOT judged this run, though the inventory holds
|
|
350
|
+
* some. Absent means they were judged — never "there were none to judge", which is what
|
|
351
|
+
* silence used to mean and what made a capture-path `diff` a false green.
|
|
352
|
+
*/
|
|
353
|
+
bindingCoverage?: BindingCoverage;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Why the CSS-Page-Object check did not run.
|
|
357
|
+
*
|
|
358
|
+
* `absent` — no `_resolved/` for this side. Capture-during-run writes contracts and no
|
|
359
|
+
* bindings (it has no browser at merge time), so this is the whole `run` path today.
|
|
360
|
+
* `stale` — one was there and was refused: it describes an older moment than the contract
|
|
361
|
+
* beside it, so using it would compare a live capture against a `map` from before the drift.
|
|
362
|
+
*/
|
|
363
|
+
export interface BindingCoverage {
|
|
364
|
+
reason: 'absent' | 'stale';
|
|
365
|
+
/** How many selector strings the inventory holds — the size of what went unjudged. */
|
|
366
|
+
inventoried: number;
|
|
367
|
+
side: 'baseline' | 'current' | 'both';
|
|
294
368
|
}
|
package/dist/cli/diff.js
CHANGED
|
@@ -41,6 +41,10 @@ exports.pageShotMap = pageShotMap;
|
|
|
41
41
|
exports.buildEvidence = buildEvidence;
|
|
42
42
|
exports.writeDriftReport = writeDriftReport;
|
|
43
43
|
exports.runDiff = runDiff;
|
|
44
|
+
exports.bindingExitCode = bindingExitCode;
|
|
45
|
+
exports.bindingGateRemedy = bindingGateRemedy;
|
|
46
|
+
exports.printBindingStrictExit = printBindingStrictExit;
|
|
47
|
+
exports.printBindingCoverage = printBindingCoverage;
|
|
44
48
|
exports.printDepthUnmeasured = printDepthUnmeasured;
|
|
45
49
|
exports.printSystemic = printSystemic;
|
|
46
50
|
exports.printNameMasks = printNameMasks;
|
|
@@ -376,7 +380,7 @@ async function runDiff(args) {
|
|
|
376
380
|
const report = attachBaselineHrefs((0, nameMask_1.diffWithMasks)(baseline, current, (0, nameMask_1.loadNameMasks)(), usage), baseline);
|
|
377
381
|
// Same rule as the directory walk: the resolutions sit next to the mappings they
|
|
378
382
|
// describe, so a two-file diff reaches them without being told where to look.
|
|
379
|
-
attachBindingDrift(report, files[0], files[1], baseline.page);
|
|
383
|
+
attachBindingDrift(report, files[0], files[1], baseline.page, baseline, current);
|
|
380
384
|
// A rename is harmless to a CSS-anchored suite and fatal to a name-anchored
|
|
381
385
|
// one. Only the test inventory can tell the two apart, so the verdict is
|
|
382
386
|
// raised here rather than inside the shared matcher.
|
|
@@ -388,6 +392,7 @@ async function runDiff(args) {
|
|
|
388
392
|
else {
|
|
389
393
|
printHuman(report, files, usage);
|
|
390
394
|
printDepthUnmeasured(report.depthUnmeasured);
|
|
395
|
+
printBindingCoverage(report.bindingCoverage);
|
|
391
396
|
printNameMasks(report.nameMask);
|
|
392
397
|
printNameDrift(report.nameDrift);
|
|
393
398
|
}
|
|
@@ -405,9 +410,11 @@ async function runDiff(args) {
|
|
|
405
410
|
}
|
|
406
411
|
(0, emit_1.recordDriftRun)('diff', singleAgg, { baseUrl: (0, config_2.configuredBaseUrl)() });
|
|
407
412
|
const failing = report.verdict === 'BLOCK' || (strict && report.verdict === 'FIX');
|
|
408
|
-
process.exitCode = failing
|
|
413
|
+
process.exitCode = bindingExitCode(failing, strict, report.bindingCoverage);
|
|
414
|
+
if (process.exitCode === 2)
|
|
415
|
+
printBindingStrictExit(report.bindingCoverage, json);
|
|
409
416
|
if (!json)
|
|
410
|
-
driftSummaryLine('diff', report.verdict, report.counts, report.nameDrift, report.bindingDrift, report.depthUnmeasured);
|
|
417
|
+
driftSummaryLine('diff', report.verdict, report.counts, report.nameDrift, report.bindingDrift, report.depthUnmeasured, report.bindingCoverage);
|
|
411
418
|
}
|
|
412
419
|
/**
|
|
413
420
|
* What `nameMask` did, in two registers.
|
|
@@ -440,6 +447,111 @@ async function runDiff(args) {
|
|
|
440
447
|
* counts. A verdict that changed without a line saying why is the silent behaviour this
|
|
441
448
|
* package treats as a defect, and here the silence would be a green one.
|
|
442
449
|
*/
|
|
450
|
+
/**
|
|
451
|
+
* The CSS-Page-Object check did not run, and the verdict above does not cover it.
|
|
452
|
+
*
|
|
453
|
+
* Printed wherever a verdict is: this is the difference between "your selectors are fine"
|
|
454
|
+
* and "nobody looked at your selectors", and the second one wearing the first one's green
|
|
455
|
+
* is the failure mode this whole package exists to refuse.
|
|
456
|
+
*/
|
|
457
|
+
/**
|
|
458
|
+
* How to get a gate that DOES cover the suite's selectors — which depends on whether this
|
|
459
|
+
* project has pages to walk.
|
|
460
|
+
*
|
|
461
|
+
* `map && diff` was printed unconditionally, and on the zero-config path `init` now
|
|
462
|
+
* recommends it is a closed loop: `pages` is empty, so `map` exits 1 with "No pages
|
|
463
|
+
* configured" and points back at capture-during-run, which is where the reader already is.
|
|
464
|
+
* A remedy that returns you to your starting point is worse than none — it costs a run to
|
|
465
|
+
* find out, and it teaches that the advice is not to be trusted.
|
|
466
|
+
*/
|
|
467
|
+
/**
|
|
468
|
+
* The exit code when a run left the suite's own selectors unjudged.
|
|
469
|
+
*
|
|
470
|
+
* Transparency fixed the honesty of the message and left the harder question open: what
|
|
471
|
+
* should a CI *do* with it? For a team whose Page Objects are CSS, the check that did not
|
|
472
|
+
* run is the whole gate — so `diff` returned exit 0 while their suite was red, and no flag
|
|
473
|
+
* could say otherwise.
|
|
474
|
+
*
|
|
475
|
+
* **It is `2`, not `1`, and that is the whole design.** The contract in `exitCodes.ts` is
|
|
476
|
+
* explicit: 1 is "answered, and the answer is no"; 2 is "could not answer — NOT a failure
|
|
477
|
+
* of your app, and never a pass". Bindings that were never resolved are the second, exactly.
|
|
478
|
+
* Returning 1 would tell a pipeline the application drifted, which is the mistake this
|
|
479
|
+
* package documents at length — an agent once wrote `non-zero = BLOCK` into a generated
|
|
480
|
+
* workflow and turned "I do not know" into a permanently red build.
|
|
481
|
+
*
|
|
482
|
+
* **Behind `--strict`, and not on by default.** Every capture-path project would otherwise
|
|
483
|
+
* go non-zero on upgrade, over a limitation none of them introduced; a gate nobody can
|
|
484
|
+
* leave on is a gate nobody turns on. `--strict` already means "tighten what counts as
|
|
485
|
+
* failure", so it is the lever that exists rather than a new one to discover.
|
|
486
|
+
*
|
|
487
|
+
* **A real finding outranks an unanswered question.** BLOCK (and `--strict` FIX) still
|
|
488
|
+
* exit 1: something WAS answered and it is bad. Only a run that would otherwise be green
|
|
489
|
+
* reports 2 — a green that covers less than the reader thinks.
|
|
490
|
+
*/
|
|
491
|
+
function bindingExitCode(failing, strict, coverage) {
|
|
492
|
+
if (failing)
|
|
493
|
+
return 1;
|
|
494
|
+
return strict && coverage ? 2 : 0;
|
|
495
|
+
}
|
|
496
|
+
function bindingGateRemedy(hasDeclaredPages) {
|
|
497
|
+
return hasDeclaredPages
|
|
498
|
+
? ' For a gate that covers them: `ia-qa-heal map && ia-qa-heal diff`.'
|
|
499
|
+
: ' To gate them you need declared pages — `map` walks a list, and this project has none:\n' +
|
|
500
|
+
' ia-qa-heal discover --sitemap --apply (reads them from the app)\n' +
|
|
501
|
+
' then ia-qa-heal map && ia-qa-heal diff\n' +
|
|
502
|
+
' Until then this run gates elements and labels, never the selector strings.';
|
|
503
|
+
}
|
|
504
|
+
/** Does this project declare pages? Absent or unreadable config is treated as none. */
|
|
505
|
+
function hasDeclaredPages() {
|
|
506
|
+
try {
|
|
507
|
+
return (0, config_2.loadConfig)().pages.length > 0;
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Why the process is leaving with 2 on a run whose verdict says PASS.
|
|
515
|
+
*
|
|
516
|
+
* An exit code with no sentence behind it is the thing this package refuses everywhere
|
|
517
|
+
* else: the reader sees a green verdict and a non-zero exit and has to guess which is
|
|
518
|
+
* lying. Neither is — they answer different questions.
|
|
519
|
+
*/
|
|
520
|
+
function printBindingStrictExit(c, json) {
|
|
521
|
+
if (!c || json)
|
|
522
|
+
return;
|
|
523
|
+
console.error(`\n⚠️ --strict: exiting 2 because ${c.inventoried} test selector` +
|
|
524
|
+
`${c.inventoried === 1 ? '' : 's'} went unjudged (see above).\n` +
|
|
525
|
+
` 2 means "could not answer", never "your app is broken" — the drift that WAS\n` +
|
|
526
|
+
` measured is PASS. Drop \`--strict\`, or give the run a way to resolve them.`);
|
|
527
|
+
}
|
|
528
|
+
function printBindingCoverage(c) {
|
|
529
|
+
if (!c)
|
|
530
|
+
return;
|
|
531
|
+
const remedy = bindingGateRemedy(hasDeclaredPages());
|
|
532
|
+
// `side` names the side that LACKS a usable resolution, so every sentence below has to
|
|
533
|
+
// read that way round — "the current has one that describes this capture" said the
|
|
534
|
+
// opposite of what the field means.
|
|
535
|
+
const where = c.side === 'both' ? 'neither side has one' : `the ${c.side} side has none`;
|
|
536
|
+
if (c.reason === 'stale') {
|
|
537
|
+
console.log(`\n 🧭 Selector bindings NOT measured — ${where} that describes this capture.\n` +
|
|
538
|
+
` A resolution left by an earlier \`map\` was found and refused: it is older than the\n` +
|
|
539
|
+
` contract beside it, so it would have compared your live capture against the app as\n` +
|
|
540
|
+
` it was BEFORE the drift — a green built out of a stale file.\n` +
|
|
541
|
+
` Your ${c.inventoried} inventoried test selector${c.inventoried === 1 ? '' : 's'} ` +
|
|
542
|
+
`${c.inventoried === 1 ? 'is' : 'are'} not covered by the verdict above.\n` +
|
|
543
|
+
remedy);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
console.log(`\n 🧭 Selector bindings NOT measured — ${where}.\n` +
|
|
547
|
+
` Capture-during-run records contracts, not bindings: resolving a test's own CSS\n` +
|
|
548
|
+
` selectors needs a live DOM, and the merge that writes these contracts has none.\n` +
|
|
549
|
+
` So the CSS-Page-Object check — the one that catches \`.btn-primary\` breaking — is\n` +
|
|
550
|
+
` NOT running here, and your ${c.inventoried} inventoried selector${c.inventoried === 1 ? '' : 's'} ` +
|
|
551
|
+
`${c.inventoried === 1 ? 'is' : 'are'} unjudged.\n` +
|
|
552
|
+
` Label drift and element drift above are real; this is what they do not cover.\n` +
|
|
553
|
+
remedy);
|
|
554
|
+
}
|
|
443
555
|
function printDepthUnmeasured(u) {
|
|
444
556
|
if (!u || u.selectors.length === 0)
|
|
445
557
|
return;
|
|
@@ -526,7 +638,7 @@ function printNameDrift(findings) {
|
|
|
526
638
|
* and never in `--json` mode, where it would corrupt the machine-readable payload.
|
|
527
639
|
* Shared with `run`, which ends on the same verdict.
|
|
528
640
|
*/
|
|
529
|
-
function driftSummaryLine(verb, verdict, c, nameDrift = [], bindingDrift = [], depthUnmeasured) {
|
|
641
|
+
function driftSummaryLine(verb, verdict, c, nameDrift = [], bindingDrift = [], depthUnmeasured, bindingCoverage) {
|
|
530
642
|
const kind = verdict === 'PASS' ? 'ok' : verdict === 'FIX' ? 'fix' : 'block';
|
|
531
643
|
const nameFixable = nameDrift.filter((f) => f.fixable).length;
|
|
532
644
|
const nameManual = nameDrift.length - nameFixable;
|
|
@@ -550,10 +662,16 @@ function driftSummaryLine(verb, verdict, c, nameDrift = [], bindingDrift = [], d
|
|
|
550
662
|
// contract has to say so here — the same rule that makes a page left out of a `run`
|
|
551
663
|
// `stale` rather than a pass.
|
|
552
664
|
const unmeasured = depthUnmeasured?.selectors.length ?? 0;
|
|
665
|
+
// "suite safe" is a claim about the SUITE, and the check that judges the suite's own
|
|
666
|
+
// selectors is exactly the one the capture path does not run. A PASS that says it while
|
|
667
|
+
// N inventoried selectors went unjudged is the sentence a CI stops reading at.
|
|
553
668
|
const headline = verdict === 'PASS'
|
|
554
|
-
?
|
|
555
|
-
? `PASS · no drift in what was measured · ${
|
|
556
|
-
|
|
669
|
+
? bindingCoverage
|
|
670
|
+
? `PASS · no drift in what was measured · ${bindingCoverage.inventoried} test selector` +
|
|
671
|
+
`${bindingCoverage.inventoried === 1 ? '' : 's'} not judged`
|
|
672
|
+
: unmeasured > 0
|
|
673
|
+
? `PASS · no drift in what was measured · ${unmeasured} not measured (depth mismatch)`
|
|
674
|
+
: 'PASS · no drift, suite safe'
|
|
557
675
|
: verdict === 'FIX'
|
|
558
676
|
? `FIX · ${fixCause} → \`fix\``
|
|
559
677
|
: `BLOCK · ${blockCause} — human needed`;
|
|
@@ -623,6 +741,7 @@ async function runDirDiff(baselineDir, currentDir, opts) {
|
|
|
623
741
|
else {
|
|
624
742
|
printAggregate(aggregate, reports, baselineDir, currentDir, usage);
|
|
625
743
|
printDepthUnmeasured(aggregate.depthUnmeasured);
|
|
744
|
+
printBindingCoverage(aggregate.bindingCoverage);
|
|
626
745
|
printSystemic(systemic);
|
|
627
746
|
printNameMasks(aggregate.nameMask);
|
|
628
747
|
printNameDrift(aggregate.nameDrift);
|
|
@@ -644,9 +763,11 @@ async function runDirDiff(baselineDir, currentDir, opts) {
|
|
|
644
763
|
baseUrl: (0, config_2.configuredBaseUrl)(),
|
|
645
764
|
});
|
|
646
765
|
const failing = aggregate.verdict === 'BLOCK' || (opts.strict && aggregate.verdict === 'FIX');
|
|
647
|
-
process.exitCode = failing
|
|
766
|
+
process.exitCode = bindingExitCode(failing, opts.strict, aggregate.bindingCoverage);
|
|
767
|
+
if (process.exitCode === 2)
|
|
768
|
+
printBindingStrictExit(aggregate.bindingCoverage, opts.json);
|
|
648
769
|
if (!opts.json)
|
|
649
|
-
driftSummaryLine('diff', aggregate.verdict, aggregate.totals, aggregate.nameDrift, aggregate.bindingDrift, aggregate.depthUnmeasured);
|
|
770
|
+
driftSummaryLine('diff', aggregate.verdict, aggregate.totals, aggregate.nameDrift, aggregate.bindingDrift, aggregate.depthUnmeasured, aggregate.bindingCoverage);
|
|
650
771
|
}
|
|
651
772
|
/**
|
|
652
773
|
* Judge the suite's own selector strings against the two moments, and fold the result
|
|
@@ -659,9 +780,36 @@ async function runDirDiff(baselineDir, currentDir, opts) {
|
|
|
659
780
|
* `elementNow` is the contract diff's own conclusion, handed in rather than recomputed:
|
|
660
781
|
* a binding verdict must never disagree with the element verdict printed beside it.
|
|
661
782
|
*/
|
|
662
|
-
function attachBindingDrift(report, baselineFile, currentFile, page) {
|
|
663
|
-
const
|
|
664
|
-
|
|
783
|
+
function attachBindingDrift(report, baselineFile, currentFile, page, baseline, current) {
|
|
784
|
+
const loadFor = (file, mapping) => {
|
|
785
|
+
const found = (0, resolution_1.loadResolution)(page, path.join(path.dirname(file), config_1.RESOLVED_DIRNAME));
|
|
786
|
+
if (!found)
|
|
787
|
+
return { resolution: null, stale: false };
|
|
788
|
+
// A resolution left behind by an earlier `map`, sitting next to a contract a live
|
|
789
|
+
// capture just rewrote, describes a page that is 34 seconds out of date — and being
|
|
790
|
+
// identical to the baseline's, it makes the binding diff empty. That is the false
|
|
791
|
+
// green, and it is why this is checked rather than trusted.
|
|
792
|
+
if (mapping && !(0, resolution_1.resolutionMatchesCapture)(found, mapping))
|
|
793
|
+
return { resolution: null, stale: true };
|
|
794
|
+
return { resolution: found, stale: false };
|
|
795
|
+
};
|
|
796
|
+
const b = loadFor(baselineFile, baseline);
|
|
797
|
+
const c = loadFor(currentFile, current);
|
|
798
|
+
const before = b.resolution;
|
|
799
|
+
const after = c.resolution;
|
|
800
|
+
// Not measured, and said so. The inventory is the trigger: a project that never ran
|
|
801
|
+
// `ingest` has no selector strings to judge and is owed no notice — but one that HAS
|
|
802
|
+
// them and gets no binding verdict is being handed a green over the very check the
|
|
803
|
+
// README opens with ("a Page Object written in CSS"). Same rule as `stale` pages and
|
|
804
|
+
// the depth hold-out: a thing nobody looked at is never a pass.
|
|
805
|
+
const inventoried = Object.keys((0, ingest_1.loadUsage)()?.selectors ?? {}).length;
|
|
806
|
+
if (inventoried > 0 && (!before || !after)) {
|
|
807
|
+
report.bindingCoverage = {
|
|
808
|
+
reason: b.stale || c.stale ? 'stale' : 'absent',
|
|
809
|
+
inventoried,
|
|
810
|
+
side: !before && !after ? 'both' : !before ? 'baseline' : 'current',
|
|
811
|
+
};
|
|
812
|
+
}
|
|
665
813
|
if (!before || !after)
|
|
666
814
|
return report;
|
|
667
815
|
const bySelector = new Map(report.rows.map((r) => [r.selector, r]));
|
|
@@ -729,7 +877,7 @@ function collectDirDiff(baselineDir, currentDir) {
|
|
|
729
877
|
const report = attachBaselineHrefs((0, nameMask_1.diffWithMasks)(baseline, current, masks, usage), baseline);
|
|
730
878
|
report.nameDrift = (0, nameDrift_1.assessNameDrift)(report.rows, current.elements, usage);
|
|
731
879
|
report.verdict = (0, nameDrift_1.escalateVerdict)(report.verdict, report.nameDrift);
|
|
732
|
-
return attachBindingDrift(report, baselineFile, currentFile, page);
|
|
880
|
+
return attachBindingDrift(report, baselineFile, currentFile, page, baseline, current);
|
|
733
881
|
};
|
|
734
882
|
const layoutsDir = path.join(baselineDir, '_layouts');
|
|
735
883
|
if (fs.existsSync(layoutsDir)) {
|
|
@@ -828,12 +976,15 @@ function aggregateReports(reports) {
|
|
|
828
976
|
const bindingDrift = reports.flatMap((r) => r.report.bindingDrift ?? []);
|
|
829
977
|
const nameMask = (0, nameMask_1.mergeMaskOutcomes)(reports.map((r) => r.report.nameMask));
|
|
830
978
|
const depthUnmeasured = (0, depthDelta_1.mergeDepthUnmeasured)(reports.map((r) => r.report.depthUnmeasured));
|
|
979
|
+
// One fact about the run, not one per page: every page shares the same _resolved/ dir.
|
|
980
|
+
const bindingCoverage = reports.map((r) => r.report.bindingCoverage).find(Boolean);
|
|
831
981
|
return {
|
|
832
982
|
verdict,
|
|
833
983
|
...(nameDrift.length > 0 ? { nameDrift } : {}),
|
|
834
984
|
...(bindingDrift.length > 0 ? { bindingDrift } : {}),
|
|
835
985
|
...(nameMask ? { nameMask } : {}),
|
|
836
986
|
...(depthUnmeasured ? { depthUnmeasured } : {}),
|
|
987
|
+
...(bindingCoverage ? { bindingCoverage } : {}),
|
|
837
988
|
layoutVerdict,
|
|
838
989
|
layoutCounts,
|
|
839
990
|
pageReports: pageReports.map((r) => ({
|