@ia-qa/self-healing 1.14.0 → 1.16.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/TUTORIAL.md CHANGED
@@ -4,6 +4,15 @@
4
4
 
5
5
  **You do not need to understand any of the internals to use it.** Follow the steps.
6
6
 
7
+ > **On its own, or with the others.** This package is complete by itself: everything below needs
8
+ > nothing else. It has two siblings, and each adds an answer it cannot give:
9
+ > - **`@ia-qa/pal`** runs the whole round for you — map, check, compare, propose repairs — in one
10
+ > command, and says what it could not see. It is built on this package, so the verdicts are the same.
11
+ > - **`@ia-qa/qa-discovery`** knows every page that *exists*. With a watched run of your suite
12
+ > (`ia-qa-heal run`, or pal's `--suite`), it names **the pages no test visits**.
13
+ >
14
+ > The whole workflow, step by step: [all three together](https://www.ia-qa.com/devtools/pal/tutorial#13-all-three-together).
15
+
7
16
  ---
8
17
 
9
18
  ## Table of contents
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export type { PageNameResolution } from './pageName';
19
19
  export { scanText, ingestPaths, saveUsage, loadUsage, usageFiles, usagePath } from './ingest';
20
20
  export type { Usage, SelectorSite, SelectorShape } from './ingest';
21
21
  export { loadExercised, exercisedPath, contractPageNames, EXERCISED_FILENAME } from './exercised';
22
+ export { saveExercised } from './exercised';
22
23
  export type { ExercisedRecord } from './exercised';
23
24
  export { computeTestDepth, computePageDepth, collectUnattributable, readTestDepth } from './testDepth';
24
25
  export { globToRegex } from './volatile';
@@ -45,6 +46,8 @@ export type { AuditOutcome, AuditSuccess } from './cli/audit';
45
46
  export { detectTestCommand } from './cli/init';
46
47
  export { groupNumericLabelDrift, maskDigits } from './nameHint';
47
48
  export type { NameDriftGroup, RenamedRowLike } from './nameHint';
49
+ export { groupListChurn, listShape } from './listChurn';
50
+ export type { ListChurnGroup, ChurnElementLike, ChurnRowLike } from './listChurn';
48
51
  export { planSetup, writeSetup, readSitemap, isWebAddress } from './ui/setup';
49
52
  export type { SetupRequest, SetupPlan, SetupWrite, SitemapAnswer, LoginMode } from './ui/setup';
50
53
  export type { CheckReport, CheckVerdict } from './checks';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CAPTURE_DIRNAME = exports.captureDir = exports.unionElements = exports.mergeCaptures = exports.globToRegex = exports.readTestDepth = exports.collectUnattributable = exports.computePageDepth = exports.computeTestDepth = exports.EXERCISED_FILENAME = exports.contractPageNames = exports.exercisedPath = exports.loadExercised = exports.usagePath = exports.usageFiles = exports.loadUsage = exports.saveUsage = exports.ingestPaths = exports.scanText = exports.crawlApp = exports.sitemapsFromRobots = exports.isSitemapIndex = exports.extractLocs = exports.discoverFromSitemap = exports.UNSAFE_URL_RE = exports.mergePages = exports.toCandidate = exports.normalizedPath = exports.isSafeCandidateUrl = exports.resolvePageName = exports.pageNameFromUrl = exports.mapUrl = exports.markdownPath = exports.saveMarkdown = exports.renderPageContract = exports.saveMapping = exports.loadMapping = exports.extractInteractiveElements = exports.renderModelList = exports.defaultKeyEnv = exports.findProvider = exports.AI_PROVIDERS = exports.parseModelJson = exports.buildPrompt = exports.prefilter = exports.aiResolve = exports.createAiResolver = exports.llmResolverStub = exports.aiFill = exports.aiClick = void 0;
4
- exports.NAVIGATION_SVG_FILENAME = exports.NAVIGATION_FILENAME = exports.OVERVIEW_FILENAME = exports.navigationSvgPath = exports.navigationPath = exports.overviewPath = exports.writeOverview = exports.navigationGraph = exports.layoutGraph = exports.renderNavigationSvg = exports.renderNavigation = exports.renderOverview = exports.buildOverview = exports.minPagesForLayout = exports.stripLayoutFromPages = exports.extractLayout = exports.isWebAddress = exports.readSitemap = exports.writeSetup = exports.planSetup = exports.maskDigits = exports.groupNumericLabelDrift = exports.detectTestCommand = exports.auditProject = exports.runFix = exports.checkCounts = exports.checkHeadline = exports.checkVerdict = exports.checkProject = exports.dirDiffAggregate = exports.runBaseline = exports.LoginWallError = exports.runMap = exports.baselineDir = exports.mappingDir = exports.setBaseDir = exports.LAYOUT_DIRNAME = exports.layoutPath = exports.layoutDir = exports.resolveSecret = exports.loadConfig = exports.replaceQuoted = exports.collectFiles = exports.applyRewrites = exports.computeRewrites = exports.launchBrowser = void 0;
3
+ exports.captureDir = exports.unionElements = exports.mergeCaptures = exports.globToRegex = exports.readTestDepth = exports.collectUnattributable = exports.computePageDepth = exports.computeTestDepth = exports.saveExercised = exports.EXERCISED_FILENAME = exports.contractPageNames = exports.exercisedPath = exports.loadExercised = exports.usagePath = exports.usageFiles = exports.loadUsage = exports.saveUsage = exports.ingestPaths = exports.scanText = exports.crawlApp = exports.sitemapsFromRobots = exports.isSitemapIndex = exports.extractLocs = exports.discoverFromSitemap = exports.UNSAFE_URL_RE = exports.mergePages = exports.toCandidate = exports.normalizedPath = exports.isSafeCandidateUrl = exports.resolvePageName = exports.pageNameFromUrl = exports.mapUrl = exports.markdownPath = exports.saveMarkdown = exports.renderPageContract = exports.saveMapping = exports.loadMapping = exports.extractInteractiveElements = exports.renderModelList = exports.defaultKeyEnv = exports.findProvider = exports.AI_PROVIDERS = exports.parseModelJson = exports.buildPrompt = exports.prefilter = exports.aiResolve = exports.createAiResolver = exports.llmResolverStub = exports.aiFill = exports.aiClick = void 0;
4
+ exports.NAVIGATION_SVG_FILENAME = exports.NAVIGATION_FILENAME = exports.OVERVIEW_FILENAME = exports.navigationSvgPath = exports.navigationPath = exports.overviewPath = exports.writeOverview = exports.navigationGraph = exports.layoutGraph = exports.renderNavigationSvg = exports.renderNavigation = exports.renderOverview = exports.buildOverview = exports.minPagesForLayout = exports.stripLayoutFromPages = exports.extractLayout = exports.isWebAddress = exports.readSitemap = exports.writeSetup = exports.planSetup = exports.listShape = exports.groupListChurn = exports.maskDigits = exports.groupNumericLabelDrift = exports.detectTestCommand = exports.auditProject = exports.runFix = exports.checkCounts = exports.checkHeadline = exports.checkVerdict = exports.checkProject = exports.dirDiffAggregate = exports.runBaseline = exports.LoginWallError = exports.runMap = exports.baselineDir = exports.mappingDir = exports.setBaseDir = exports.LAYOUT_DIRNAME = exports.layoutPath = exports.layoutDir = exports.resolveSecret = exports.loadConfig = exports.replaceQuoted = exports.collectFiles = exports.applyRewrites = exports.computeRewrites = exports.launchBrowser = exports.CAPTURE_DIRNAME = void 0;
5
5
  var healer_1 = require("./playwright/healer");
6
6
  Object.defineProperty(exports, "aiClick", { enumerable: true, get: function () { return healer_1.aiClick; } });
7
7
  Object.defineProperty(exports, "aiFill", { enumerable: true, get: function () { return healer_1.aiFill; } });
@@ -58,6 +58,10 @@ Object.defineProperty(exports, "loadExercised", { enumerable: true, get: functio
58
58
  Object.defineProperty(exports, "exercisedPath", { enumerable: true, get: function () { return exercised_1.exercisedPath; } });
59
59
  Object.defineProperty(exports, "contractPageNames", { enumerable: true, get: function () { return exercised_1.contractPageNames; } });
60
60
  Object.defineProperty(exports, "EXERCISED_FILENAME", { enumerable: true, get: function () { return exercised_1.EXERCISED_FILENAME; } });
61
+ // The writer too, for `@ia-qa/pal`'s `--suite`: a capture-during-run by another door is the
62
+ // same observation, and a second writer of this format would be a second format.
63
+ var exercised_2 = require("./exercised");
64
+ Object.defineProperty(exports, "saveExercised", { enumerable: true, get: function () { return exercised_2.saveExercised; } });
61
65
  var testDepth_1 = require("./testDepth");
62
66
  Object.defineProperty(exports, "computeTestDepth", { enumerable: true, get: function () { return testDepth_1.computeTestDepth; } });
63
67
  Object.defineProperty(exports, "computePageDepth", { enumerable: true, get: function () { return testDepth_1.computePageDepth; } });
@@ -114,6 +118,10 @@ Object.defineProperty(exports, "detectTestCommand", { enumerable: true, get: fun
114
118
  var nameHint_1 = require("./nameHint");
115
119
  Object.defineProperty(exports, "groupNumericLabelDrift", { enumerable: true, get: function () { return nameHint_1.groupNumericLabelDrift; } });
116
120
  Object.defineProperty(exports, "maskDigits", { enumerable: true, get: function () { return nameHint_1.maskDigits; } });
121
+ // Lists whose items are data — noticed here, so every surface groups them the same way.
122
+ var listChurn_1 = require("./listChurn");
123
+ Object.defineProperty(exports, "groupListChurn", { enumerable: true, get: function () { return listChurn_1.groupListChurn; } });
124
+ Object.defineProperty(exports, "listShape", { enumerable: true, get: function () { return listChurn_1.listShape; } });
117
125
  // The setup door `ui` uses: preview and write through one plan, never overwriting, no secret field.
118
126
  var setup_1 = require("./ui/setup");
119
127
  Object.defineProperty(exports, "planSetup", { enumerable: true, get: function () { return setup_1.planSetup; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Data-list churn — the noise class two field reports named first.
3
+ *
4
+ * A catalogue, a feed, a results table: the list is UI, its items are data. When the data
5
+ * changes, the contract diff is right row by row and useless as a whole — measured on the
6
+ * witness app, one list of eight products rotating half its items produced 5 ambiguous,
7
+ * 1 lost, 6 rebound, 4 healable and 6 added, and a real lost button next to it was one line
8
+ * among eighteen.
9
+ *
10
+ * This module only *notices*, like `nameHint.ts`: the rows, the counts and the verdict are
11
+ * untouched. What a caller does with a group is its own policy.
12
+ *
13
+ * A group is claimed on three facts, and each one exists because a narrower rule was wrong:
14
+ * - **a shape** — elements of one role whose selector differs only by a position
15
+ * (`li:nth-of-type(n)`) or whose link differs only by an id-like path segment
16
+ * (`/item/101`). Shape alone is not enough: inserting one button above three others shifts
17
+ * every position too, and that is the `rebound` this package exists to catch.
18
+ * - **a list, not a toolbar** — at least `MIN_MEMBERS` elements of that shape in the baseline.
19
+ * - **replacement both ways** — at least `MIN_REPLACED` names gone *and* as many new names
20
+ * arrived. An insertion keeps every old name; an emptied list brings no new one. Both stay
21
+ * loud: an empty catalogue is a regression, not data.
22
+ */
23
+ export interface ChurnElementLike {
24
+ role: string;
25
+ name: string;
26
+ selector: string;
27
+ href?: string;
28
+ }
29
+ export interface ChurnRowLike {
30
+ status: string;
31
+ role: string;
32
+ name: string;
33
+ selector: string;
34
+ href?: string;
35
+ }
36
+ export interface ListChurnGroup {
37
+ role: string;
38
+ /** The shape, readable: `a[href="/item/:id"]` or `#latest > li:nth-of-type(*) > button`. */
39
+ shape: string;
40
+ /** Baseline members whose name is nowhere in the current list. */
41
+ gone: string[];
42
+ /** Current members whose name was nowhere in the baseline list. */
43
+ appeared: string[];
44
+ /** Members present on both sides. */
45
+ kept: number;
46
+ /** Baseline selectors of the rows this group accounts for — every non-`ok` row of the shape. */
47
+ selectors: string[];
48
+ }
49
+ export declare const MIN_MEMBERS = 4;
50
+ export declare const MIN_REPLACED = 2;
51
+ /**
52
+ * The shape an element belongs to, or `null` when it has none — most elements.
53
+ * A link is shaped by its path with id-like segments collapsed; anything else by its
54
+ * selector with positions collapsed. A link whose path holds no id-like segment is not
55
+ * shaped at all: `/products` and `/cart` are navigation, never a list.
56
+ */
57
+ export declare function listShape(el: {
58
+ role: string;
59
+ selector: string;
60
+ href?: string;
61
+ }): string | null;
62
+ /** Lists on one page whose items were replaced. Pure; empty on an ordinary page. */
63
+ export declare function groupListChurn(baseline: ChurnElementLike[], current: ChurnElementLike[], rows: ChurnRowLike[]): ListChurnGroup[];
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * Data-list churn — the noise class two field reports named first.
4
+ *
5
+ * A catalogue, a feed, a results table: the list is UI, its items are data. When the data
6
+ * changes, the contract diff is right row by row and useless as a whole — measured on the
7
+ * witness app, one list of eight products rotating half its items produced 5 ambiguous,
8
+ * 1 lost, 6 rebound, 4 healable and 6 added, and a real lost button next to it was one line
9
+ * among eighteen.
10
+ *
11
+ * This module only *notices*, like `nameHint.ts`: the rows, the counts and the verdict are
12
+ * untouched. What a caller does with a group is its own policy.
13
+ *
14
+ * A group is claimed on three facts, and each one exists because a narrower rule was wrong:
15
+ * - **a shape** — elements of one role whose selector differs only by a position
16
+ * (`li:nth-of-type(n)`) or whose link differs only by an id-like path segment
17
+ * (`/item/101`). Shape alone is not enough: inserting one button above three others shifts
18
+ * every position too, and that is the `rebound` this package exists to catch.
19
+ * - **a list, not a toolbar** — at least `MIN_MEMBERS` elements of that shape in the baseline.
20
+ * - **replacement both ways** — at least `MIN_REPLACED` names gone *and* as many new names
21
+ * arrived. An insertion keeps every old name; an emptied list brings no new one. Both stay
22
+ * loud: an empty catalogue is a regression, not data.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.MIN_REPLACED = exports.MIN_MEMBERS = void 0;
26
+ exports.listShape = listShape;
27
+ exports.groupListChurn = groupListChurn;
28
+ exports.MIN_MEMBERS = 4;
29
+ exports.MIN_REPLACED = 2;
30
+ const ID_SEGMENT = /\d/;
31
+ function hrefOf(el) {
32
+ if (el.href)
33
+ return el.href;
34
+ const m = /^a\[href="([^"]*)"\]$/i.exec(el.selector.trim());
35
+ return m ? m[1] : undefined;
36
+ }
37
+ /**
38
+ * The shape an element belongs to, or `null` when it has none — most elements.
39
+ * A link is shaped by its path with id-like segments collapsed; anything else by its
40
+ * selector with positions collapsed. A link whose path holds no id-like segment is not
41
+ * shaped at all: `/products` and `/cart` are navigation, never a list.
42
+ */
43
+ function listShape(el) {
44
+ const href = hrefOf(el);
45
+ if (href !== undefined) {
46
+ const pathOnly = href.split(/[?#]/)[0];
47
+ const segments = pathOnly.split('/');
48
+ if (!segments.some((s) => ID_SEGMENT.test(s)))
49
+ return null;
50
+ return `${el.role} a[href="${segments.map((s) => (ID_SEGMENT.test(s) ? ':id' : s)).join('/')}"]`;
51
+ }
52
+ if (!/:nth-of-type\(\d+\)/.test(el.selector))
53
+ return null;
54
+ return `${el.role} ${el.selector.replace(/:nth-of-type\(\d+\)/g, ':nth-of-type(*)')}`;
55
+ }
56
+ function byShape(elements) {
57
+ const out = new Map();
58
+ for (const el of elements) {
59
+ const shape = listShape(el);
60
+ if (!shape)
61
+ continue;
62
+ out.set(shape, [...(out.get(shape) ?? []), el]);
63
+ }
64
+ return out;
65
+ }
66
+ /** Lists on one page whose items were replaced. Pure; empty on an ordinary page. */
67
+ function groupListChurn(baseline, current, rows) {
68
+ const before = byShape(baseline);
69
+ const after = byShape(current);
70
+ const groups = [];
71
+ for (const [shape, members] of before) {
72
+ if (members.length < exports.MIN_MEMBERS)
73
+ continue;
74
+ const oldNames = new Set(members.map((m) => m.name));
75
+ const newNames = new Set((after.get(shape) ?? []).map((m) => m.name));
76
+ const gone = [...oldNames].filter((n) => !newNames.has(n));
77
+ const appeared = [...newNames].filter((n) => !oldNames.has(n));
78
+ if (gone.length < exports.MIN_REPLACED || appeared.length < exports.MIN_REPLACED)
79
+ continue;
80
+ const selectors = rows.filter((r) => r.status !== 'ok' && listShape(r) === shape).map((r) => r.selector);
81
+ groups.push({
82
+ role: members[0].role,
83
+ shape: shape.slice(members[0].role.length + 1),
84
+ gone,
85
+ appeared,
86
+ kept: [...oldNames].filter((n) => newNames.has(n)).length,
87
+ selectors,
88
+ });
89
+ }
90
+ return groups;
91
+ }
92
+ //# sourceMappingURL=listChurn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listChurn.js","sourceRoot":"","sources":["../src/listChurn.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ia-qa/self-healing",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Your Playwright, Cypress or Selenium tests break when a selector moves — this finds the element again and rewrites the test. Deterministic: no LLM decides whether your build passes. Runs entirely on your machine, with a local MCP server for agents.",
5
5
  "keywords": [
6
6
  "self-healing",