@jevitate/cli 0.9.0 → 0.11.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.
Files changed (190) hide show
  1. package/dist/anchor-baselines.d.ts +26 -0
  2. package/dist/anchor-baselines.js +25 -0
  3. package/dist/anchor-baselines.js.map +1 -0
  4. package/dist/approval-provenance.d.ts +32 -2
  5. package/dist/approval-provenance.js +92 -5
  6. package/dist/approval-provenance.js.map +1 -1
  7. package/dist/approved-demo.d.ts +86 -0
  8. package/dist/approved-demo.js +184 -0
  9. package/dist/approved-demo.js.map +1 -0
  10. package/dist/bin.js +13352 -7050
  11. package/dist/build-info.generated.js +2 -2
  12. package/dist/campaign-api.d.ts +15 -0
  13. package/dist/campaign-api.js +67 -6
  14. package/dist/campaign-api.js.map +1 -1
  15. package/dist/campaign-cli.js +7 -2
  16. package/dist/campaign-cli.js.map +1 -1
  17. package/dist/campaign-preflight.d.ts +76 -0
  18. package/dist/campaign-preflight.js +117 -0
  19. package/dist/campaign-preflight.js.map +1 -0
  20. package/dist/catalog-analysis.js +14 -0
  21. package/dist/catalog-analysis.js.map +1 -1
  22. package/dist/catalog-api.js +12 -4
  23. package/dist/catalog-api.js.map +1 -1
  24. package/dist/catalog-bundle-api.d.ts +83 -0
  25. package/dist/catalog-bundle-api.js +309 -0
  26. package/dist/catalog-bundle-api.js.map +1 -0
  27. package/dist/catalog-bundle-findings.d.ts +64 -0
  28. package/dist/catalog-bundle-findings.js +326 -0
  29. package/dist/catalog-bundle-findings.js.map +1 -0
  30. package/dist/catalog-bundle.d.ts +296 -0
  31. package/dist/catalog-bundle.js +777 -0
  32. package/dist/catalog-bundle.js.map +1 -0
  33. package/dist/catalog-cli.d.ts +6 -0
  34. package/dist/catalog-cli.js +181 -5
  35. package/dist/catalog-cli.js.map +1 -1
  36. package/dist/catalog-refs.d.ts +42 -0
  37. package/dist/catalog-refs.js +188 -0
  38. package/dist/catalog-refs.js.map +1 -0
  39. package/dist/catalog-review.d.ts +3 -1
  40. package/dist/catalog-review.js +61 -0
  41. package/dist/catalog-review.js.map +1 -1
  42. package/dist/catalog.d.ts +27 -3
  43. package/dist/catalog.js +46 -2
  44. package/dist/catalog.js.map +1 -1
  45. package/dist/check-api.js +180 -9
  46. package/dist/check-api.js.map +1 -1
  47. package/dist/check-cli.d.ts +2 -0
  48. package/dist/check-cli.js +96 -2
  49. package/dist/check-cli.js.map +1 -1
  50. package/dist/check-execute.d.ts +18 -0
  51. package/dist/check-execute.js +56 -3
  52. package/dist/check-execute.js.map +1 -1
  53. package/dist/check-types.d.ts +105 -0
  54. package/dist/check-types.js.map +1 -1
  55. package/dist/cli-output.d.ts +6 -0
  56. package/dist/cli-output.js +38 -1
  57. package/dist/cli-output.js.map +1 -1
  58. package/dist/cli-shared.d.ts +10 -0
  59. package/dist/cli-shared.js +0 -4
  60. package/dist/cli-shared.js.map +1 -1
  61. package/dist/cold-start-report.d.ts +32 -0
  62. package/dist/cold-start-report.js +31 -0
  63. package/dist/cold-start-report.js.map +1 -0
  64. package/dist/defect-evidence.js +10 -5
  65. package/dist/defect-evidence.js.map +1 -1
  66. package/dist/demo-aspect-api.d.ts +11 -0
  67. package/dist/demo-aspect-api.js +27 -2
  68. package/dist/demo-aspect-api.js.map +1 -1
  69. package/dist/demo-aspect-cli.js +5 -0
  70. package/dist/demo-aspect-cli.js.map +1 -1
  71. package/dist/demo-capture.d.ts +79 -2
  72. package/dist/demo-capture.js +504 -12
  73. package/dist/demo-capture.js.map +1 -1
  74. package/dist/environments.d.ts +8 -1
  75. package/dist/environments.js +14 -3
  76. package/dist/environments.js.map +1 -1
  77. package/dist/exit-codes.js +16 -0
  78. package/dist/exit-codes.js.map +1 -1
  79. package/dist/explore-adversarial.js +2 -2
  80. package/dist/explore-adversarial.js.map +1 -1
  81. package/dist/explore-shared.d.ts +11 -0
  82. package/dist/explore-shared.js +9 -0
  83. package/dist/explore-shared.js.map +1 -1
  84. package/dist/findings-filing.js +7 -3
  85. package/dist/findings-filing.js.map +1 -1
  86. package/dist/forge-verify.d.ts +224 -0
  87. package/dist/forge-verify.js +649 -0
  88. package/dist/forge-verify.js.map +1 -0
  89. package/dist/github-issue-filer.js +15 -9
  90. package/dist/github-issue-filer.js.map +1 -1
  91. package/dist/init-codeowners.d.ts +19 -0
  92. package/dist/init-codeowners.js +66 -0
  93. package/dist/init-codeowners.js.map +1 -1
  94. package/dist/job-draft-api.d.ts +52 -0
  95. package/dist/job-draft-api.js +110 -0
  96. package/dist/job-draft-api.js.map +1 -0
  97. package/dist/journeeze-api.d.ts +77 -0
  98. package/dist/journeeze-api.js +371 -0
  99. package/dist/journeeze-api.js.map +1 -0
  100. package/dist/journeeze-connect.d.ts +156 -0
  101. package/dist/journeeze-connect.js +357 -0
  102. package/dist/journeeze-connect.js.map +1 -0
  103. package/dist/journey-anchor-api.d.ts +37 -0
  104. package/dist/journey-anchor-api.js +58 -0
  105. package/dist/journey-anchor-api.js.map +1 -0
  106. package/dist/journey-anchor-cli.d.ts +17 -0
  107. package/dist/journey-anchor-cli.js +104 -0
  108. package/dist/journey-anchor-cli.js.map +1 -0
  109. package/dist/journey-anchor-gate.d.ts +52 -0
  110. package/dist/journey-anchor-gate.js +101 -0
  111. package/dist/journey-anchor-gate.js.map +1 -0
  112. package/dist/journey-api.d.ts +30 -1
  113. package/dist/journey-api.js +118 -16
  114. package/dist/journey-api.js.map +1 -1
  115. package/dist/journey-cli.js +93 -10
  116. package/dist/journey-cli.js.map +1 -1
  117. package/dist/journey-demo-api.d.ts +29 -0
  118. package/dist/journey-demo-api.js +29 -7
  119. package/dist/journey-demo-api.js.map +1 -1
  120. package/dist/journey-heal.js +1 -1
  121. package/dist/journey-heal.js.map +1 -1
  122. package/dist/journey-lint-sarif.d.ts +3 -2
  123. package/dist/journey-lint-sarif.js +11 -2
  124. package/dist/journey-lint-sarif.js.map +1 -1
  125. package/dist/journey-migrate-api.d.ts +54 -0
  126. package/dist/journey-migrate-api.js +64 -0
  127. package/dist/journey-migrate-api.js.map +1 -0
  128. package/dist/journey-network-checks.d.ts +11 -0
  129. package/dist/journey-network-checks.js +36 -13
  130. package/dist/journey-network-checks.js.map +1 -1
  131. package/dist/journey-proposal-store.d.ts +9 -0
  132. package/dist/journey-proposal-store.js +23 -0
  133. package/dist/journey-proposal-store.js.map +1 -1
  134. package/dist/journey-result-record.d.ts +2 -0
  135. package/dist/journey-result-record.js +5 -0
  136. package/dist/journey-result-record.js.map +1 -1
  137. package/dist/journey-review-api.js +9 -1
  138. package/dist/journey-review-api.js.map +1 -1
  139. package/dist/journey-review.d.ts +11 -1
  140. package/dist/journey-review.js +77 -5
  141. package/dist/journey-review.js.map +1 -1
  142. package/dist/journey-stale-api.d.ts +40 -0
  143. package/dist/journey-stale-api.js +57 -0
  144. package/dist/journey-stale-api.js.map +1 -0
  145. package/dist/load-api.js +4 -2
  146. package/dist/load-api.js.map +1 -1
  147. package/dist/locator-health-api.d.ts +101 -0
  148. package/dist/locator-health-api.js +203 -0
  149. package/dist/locator-health-api.js.map +1 -0
  150. package/dist/locator-health.d.ts +195 -0
  151. package/dist/locator-health.js +419 -0
  152. package/dist/locator-health.js.map +1 -0
  153. package/dist/mcp-api.d.ts +3 -1
  154. package/dist/mcp-api.js +21 -2
  155. package/dist/mcp-api.js.map +1 -1
  156. package/dist/mcp-cli-tools.d.ts +4 -1
  157. package/dist/mcp-cli-tools.js +82 -10
  158. package/dist/mcp-cli-tools.js.map +1 -1
  159. package/dist/openrouter-call.d.ts +23 -10
  160. package/dist/openrouter-call.js +14 -11
  161. package/dist/openrouter-call.js.map +1 -1
  162. package/dist/png-pixels.d.ts +12 -0
  163. package/dist/png-pixels.js +96 -0
  164. package/dist/png-pixels.js.map +1 -0
  165. package/dist/project-config.d.ts +29 -0
  166. package/dist/project-config.js +76 -0
  167. package/dist/project-config.js.map +1 -0
  168. package/dist/regression-api.js +2 -1
  169. package/dist/regression-api.js.map +1 -1
  170. package/dist/server-log-verify.js +2 -1
  171. package/dist/server-log-verify.js.map +1 -1
  172. package/dist/signal-triage.js +7 -3
  173. package/dist/signal-triage.js.map +1 -1
  174. package/dist/source-run-api.js +2 -2
  175. package/dist/source-run-api.js.map +1 -1
  176. package/dist/verify-fix-api.d.ts +29 -0
  177. package/dist/verify-fix-api.js +43 -2
  178. package/dist/verify-fix-api.js.map +1 -1
  179. package/dist/verify-fix-cli.js +4 -0
  180. package/dist/verify-fix-cli.js.map +1 -1
  181. package/dist/zip-store.d.ts +15 -0
  182. package/dist/zip-store.js +93 -0
  183. package/dist/zip-store.js.map +1 -0
  184. package/package.json +12 -10
  185. package/skills/jevitate-ci-check/SKILL.md +16 -0
  186. package/skills/jevitate-demo/SKILL.md +4 -1
  187. package/skills/jevitate-getting-started/SKILL.md +7 -2
  188. package/skills/jevitate-run-journey/SKILL.md +15 -0
  189. package/skills/jevitate-test-campaign/SKILL.md +26 -0
  190. package/skills/jevitate-verify-fix/SKILL.md +5 -0
@@ -0,0 +1,101 @@
1
+ import { type Journey } from "@jevitate/journey";
2
+ import type { LocatorHealthSummary } from "./check-types.js";
3
+ import { type CompactLocatorHealth, type LocatorHealthDetail, type LocatorHealthTrend, type LocatorLevels, type LocatorSuggestion, type ResolvedStepInput } from "./locator-health.js";
4
+ import { DEFAULT_TEST_ID_ATTRIBUTES } from "./project-config.js";
5
+ /**
6
+ * #470 — locator health: how stable each recorded step's target is against the team's test-id
7
+ * convention (the selector-ladder rung it resolves by; `data-tflow-id` never counts, #468), why a
8
+ * brittle one is brittle, and the fix for the app. Advisory everywhere (result.json, report,
9
+ * check.json warnings, the review sheet); `check` gates only past the team's opt-in threshold
10
+ * `--max-brittle-steps <n>`. The analysis itself is pure (`locator-health.ts`); this module loads.
11
+ *
12
+ * - `jevitate locator-health [--journey <id> | --run <result.json>] [--baseline <file>]` / MCP
13
+ * `locator_health` (read-only): every promoted Journey by default, one Journey, or the steps of one
14
+ * run result (the rung each step actually resolved by, when the run recorded it).
15
+ * - The test-id attribute list is project config (`testIdAttributes` in `<repo>/.jevitate/project.json`;
16
+ * default `data-testid`, `data-test`; teams add e.g. `data-cy`, `data-qa`) — never a flag.
17
+ */
18
+ export { DEFAULT_TEST_ID_ATTRIBUTES };
19
+ export interface LocatorHealthRequest {
20
+ /** The journeys dir (`--dir`, else the repo's `.jevitate/journeys`). */
21
+ readonly journeysDir: string;
22
+ /** The project data dir whose config holds `testIdAttributes`; null outside a project (defaults apply). */
23
+ readonly projectDir: string | null;
24
+ /** `--journey <id>`: just this Journey (exclusive with `runResult`). */
25
+ readonly journeyId?: string;
26
+ /** `--run <result.json>`: the steps of this run result (exclusive with `journeyId`). */
27
+ readonly runResult?: string;
28
+ /**
29
+ * `--baseline <file>`: a previous `locator-health --json` output (or a run's result.json) to report
30
+ * the trend against — steps improved / regressed per Journey.
31
+ */
32
+ readonly baseline?: string;
33
+ }
34
+ export interface LocatorHealthJourney {
35
+ readonly id: string;
36
+ readonly health: LocatorHealthDetail;
37
+ /** Against `--baseline`, when the baseline has this Journey. */
38
+ readonly trend?: LocatorHealthTrend;
39
+ }
40
+ export interface LocatorHealthReport {
41
+ readonly source: {
42
+ readonly kind: "journeys";
43
+ } | {
44
+ readonly kind: "journey";
45
+ readonly id: string;
46
+ } | {
47
+ readonly kind: "run";
48
+ readonly path: string;
49
+ };
50
+ /** The convention applied (from project config, else DEFAULT_TEST_ID_ATTRIBUTES). */
51
+ readonly testIdAttributes: readonly string[];
52
+ readonly journeys: readonly LocatorHealthJourney[];
53
+ readonly summary: LocatorHealthSummary & {
54
+ readonly levels: LocatorLevels;
55
+ readonly line: string;
56
+ };
57
+ /** The work list for the app: one fix per element, de-duplicated across steps and Journeys. */
58
+ readonly suggestions: readonly LocatorSuggestion[];
59
+ /** Totals against `--baseline`. */
60
+ readonly trend?: LocatorHealthTrend & {
61
+ readonly baseline: string;
62
+ readonly line: string;
63
+ };
64
+ }
65
+ export declare class LocatorHealthInputError extends Error {
66
+ readonly code = "E_LOCATOR_HEALTH_INPUT";
67
+ constructor(message: string);
68
+ }
69
+ /** The project's test-id convention (throws `ProjectConfigError` on a malformed config). */
70
+ export declare function testIdAttributesOf(projectDir: string | null): readonly string[];
71
+ /**
72
+ * The convention for a writer that must not fail on it (a run's result.json): the project's, else the
73
+ * default when the config is malformed — `jevitate locator-health` is where a bad config is reported.
74
+ */
75
+ export declare function testIdAttributesOrDefault(projectDir?: string | null): readonly string[];
76
+ /** A Journey's (static) or a run's (with `resolved`) locator health. */
77
+ export declare function journeyLocatorHealth(journey: Journey, testIdAttributes: readonly string[], resolved?: readonly ResolvedStepInput[]): LocatorHealthDetail;
78
+ /**
79
+ * What a Journey run's result.json carries (`result.locatorHealth`): the compact health of the run —
80
+ * the rung each step resolved by when the run recorded it. Undefined when no step acts on a target.
81
+ */
82
+ export declare function runLocatorHealth(journey: Journey, resolved: readonly ResolvedStepInput[] | undefined, testIdAttributes?: readonly string[]): CompactLocatorHealth | undefined;
83
+ /** Read-only: never changes a Journey and never gates (the exit code is 0 on any report). */
84
+ export declare function locatorHealth(req: LocatorHealthRequest): Promise<LocatorHealthReport>;
85
+ /** The human rendering (no `--json`): per Journey and its brittle steps, the totals, the trend and the work list. */
86
+ export declare function renderLocatorHealth(r: LocatorHealthReport, opts?: {
87
+ readonly top?: number;
88
+ }): string;
89
+ /**
90
+ * `check --max-brittle-steps <n>`: the opt-in gate — a Journey item with more than `n` brittle steps
91
+ * is a gating finding. Called by check-cli before the suite runs; `exceedsBrittleSteps(health, n)`
92
+ * (locator-health.ts) is the per-item verdict.
93
+ *
94
+ * Contract for check integration: add `maxBrittleSteps?: number` to `RunCheckOptions` (check-types.ts)
95
+ * and have check-cli pass `gate.maxBrittleSteps` through to `runCheck`; check.json reports it in
96
+ * `LocatorHealthSummary.maxBrittleSteps` / `exceeded`.
97
+ */
98
+ export interface BrittleStepGate {
99
+ readonly maxBrittleSteps: number;
100
+ }
101
+ export declare function brittleStepGate(maxBrittleSteps: number): BrittleStepGate;
@@ -0,0 +1,203 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { FsJourneyStore } from "@jevitate/journey";
3
+ import { analyzeRecording, combineHealth, compactHealth, compareLocatorHealth, trendLine, } from "./locator-health.js";
4
+ import { DEFAULT_TEST_ID_ATTRIBUTES, loadProjectConfig } from "./project-config.js";
5
+ import { findProjectDir } from "./project-dir.js";
6
+ /**
7
+ * #470 — locator health: how stable each recorded step's target is against the team's test-id
8
+ * convention (the selector-ladder rung it resolves by; `data-tflow-id` never counts, #468), why a
9
+ * brittle one is brittle, and the fix for the app. Advisory everywhere (result.json, report,
10
+ * check.json warnings, the review sheet); `check` gates only past the team's opt-in threshold
11
+ * `--max-brittle-steps <n>`. The analysis itself is pure (`locator-health.ts`); this module loads.
12
+ *
13
+ * - `jevitate locator-health [--journey <id> | --run <result.json>] [--baseline <file>]` / MCP
14
+ * `locator_health` (read-only): every promoted Journey by default, one Journey, or the steps of one
15
+ * run result (the rung each step actually resolved by, when the run recorded it).
16
+ * - The test-id attribute list is project config (`testIdAttributes` in `<repo>/.jevitate/project.json`;
17
+ * default `data-testid`, `data-test`; teams add e.g. `data-cy`, `data-qa`) — never a flag.
18
+ */
19
+ export { DEFAULT_TEST_ID_ATTRIBUTES };
20
+ export class LocatorHealthInputError extends Error {
21
+ code = "E_LOCATOR_HEALTH_INPUT";
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "LocatorHealthInputError";
25
+ }
26
+ }
27
+ /** The project's test-id convention (throws `ProjectConfigError` on a malformed config). */
28
+ export function testIdAttributesOf(projectDir) {
29
+ return loadProjectConfig(projectDir).testIdAttributes;
30
+ }
31
+ /**
32
+ * The convention for a writer that must not fail on it (a run's result.json): the project's, else the
33
+ * default when the config is malformed — `jevitate locator-health` is where a bad config is reported.
34
+ */
35
+ export function testIdAttributesOrDefault(projectDir = findProjectDir()) {
36
+ try {
37
+ return testIdAttributesOf(projectDir);
38
+ }
39
+ catch {
40
+ return DEFAULT_TEST_ID_ATTRIBUTES;
41
+ }
42
+ }
43
+ /** A Journey's (static) or a run's (with `resolved`) locator health. */
44
+ export function journeyLocatorHealth(journey, testIdAttributes, resolved) {
45
+ return analyzeRecording(journey.recording, { testIdAttributes, journeyId: journey.metadata.id, ...(resolved === undefined ? {} : { resolved }) });
46
+ }
47
+ /**
48
+ * What a Journey run's result.json carries (`result.locatorHealth`): the compact health of the run —
49
+ * the rung each step resolved by when the run recorded it. Undefined when no step acts on a target.
50
+ */
51
+ export function runLocatorHealth(journey, resolved, testIdAttributes = testIdAttributesOrDefault()) {
52
+ const health = journeyLocatorHealth(journey, testIdAttributes, resolved);
53
+ return health.steps.length === 0 ? undefined : compactHealth(health);
54
+ }
55
+ function isRecord(v) {
56
+ return v !== null && typeof v === "object" && !Array.isArray(v);
57
+ }
58
+ async function readJson(path, what) {
59
+ let raw;
60
+ try {
61
+ raw = await readFile(path, "utf8");
62
+ }
63
+ catch (err) {
64
+ throw new LocatorHealthInputError(`${what}: cannot read ${path} (${err instanceof Error ? err.message : String(err)})`);
65
+ }
66
+ try {
67
+ return JSON.parse(raw);
68
+ }
69
+ catch {
70
+ throw new LocatorHealthInputError(`${what}: ${path} is not JSON`);
71
+ }
72
+ }
73
+ /** A run result's journey id and resolutions (`{result: {mode: "journey", journeyId, resolved}}`). */
74
+ function runOf(json, path, what) {
75
+ const result = isRecord(json) && isRecord(json.result) ? json.result : isRecord(json) ? json : undefined;
76
+ const journeyId = result !== undefined && typeof result.journeyId === "string" ? result.journeyId : undefined;
77
+ if (result === undefined || journeyId === undefined)
78
+ throw new LocatorHealthInputError(`${what}: ${path} is not a Journey run result (no result.journeyId)`);
79
+ const resolved = Array.isArray(result.resolved)
80
+ ? result.resolved.filter((r) => isRecord(r) && typeof r.index === "number" && typeof r.rung === "string")
81
+ : undefined;
82
+ return { journeyId, ...(resolved === undefined ? {} : { resolved }), ...(result.locatorHealth === undefined ? {} : { health: result.locatorHealth }) };
83
+ }
84
+ function asTrendInput(h) {
85
+ if (!isRecord(h) || typeof h.brittle !== "number" || !Array.isArray(h.steps))
86
+ return undefined;
87
+ return h;
88
+ }
89
+ /** Per-Journey baseline healths: a `locator-health --json` output (envelope or report) or a run result. */
90
+ function baselineOf(json, path) {
91
+ const out = new Map();
92
+ const report = isRecord(json) && isRecord(json.data) ? json.data : json;
93
+ if (isRecord(report) && Array.isArray(report.journeys)) {
94
+ for (const j of report.journeys) {
95
+ if (!isRecord(j) || typeof j.id !== "string")
96
+ continue;
97
+ const h = asTrendInput(j.health);
98
+ if (h !== undefined)
99
+ out.set(j.id, h);
100
+ }
101
+ return out;
102
+ }
103
+ const run = runOf(json, path, "--baseline");
104
+ const h = asTrendInput(run.health);
105
+ if (h === undefined)
106
+ throw new LocatorHealthInputError(`--baseline: ${path} carries no locator health (a locator-health --json output, or a run result that recorded it)`);
107
+ out.set(run.journeyId, h);
108
+ return out;
109
+ }
110
+ async function loadJourney(store, id, dir) {
111
+ const j = await store.get(id);
112
+ if (j === null)
113
+ throw new LocatorHealthInputError(`no Journey '${id}' in ${dir}`);
114
+ return j;
115
+ }
116
+ function sumTrends(trends) {
117
+ const sum = (k) => trends.reduce((n, t) => n + t[k], 0);
118
+ return {
119
+ improved: sum("improved"),
120
+ regressed: sum("regressed"),
121
+ unchanged: sum("unchanged"),
122
+ added: sum("added"),
123
+ removed: sum("removed"),
124
+ brittleDelta: sum("brittleDelta"),
125
+ changes: trends.flatMap((t) => t.changes),
126
+ };
127
+ }
128
+ /** Read-only: never changes a Journey and never gates (the exit code is 0 on any report). */
129
+ export async function locatorHealth(req) {
130
+ const testIdAttributes = testIdAttributesOf(req.projectDir);
131
+ const store = new FsJourneyStore(req.journeysDir);
132
+ let journeys = [];
133
+ let source;
134
+ if (req.runResult !== undefined) {
135
+ const run = runOf(await readJson(req.runResult, "--run"), req.runResult, "--run");
136
+ const journey = await loadJourney(store, run.journeyId, req.journeysDir);
137
+ journeys.push({ id: journey.metadata.id, health: journeyLocatorHealth(journey, testIdAttributes, run.resolved) });
138
+ source = { kind: "run", path: req.runResult };
139
+ }
140
+ else if (req.journeyId !== undefined) {
141
+ const journey = await loadJourney(store, req.journeyId, req.journeysDir);
142
+ journeys.push({ id: journey.metadata.id, health: journeyLocatorHealth(journey, testIdAttributes) });
143
+ source = { kind: "journey", id: req.journeyId };
144
+ }
145
+ else {
146
+ const listed = (await store.list()).filter((m) => m.promoted).sort((a, b) => a.id.localeCompare(b.id));
147
+ for (const m of listed) {
148
+ const journey = await store.get(m.id);
149
+ if (journey !== null)
150
+ journeys.push({ id: journey.metadata.id, health: journeyLocatorHealth(journey, testIdAttributes) });
151
+ }
152
+ source = { kind: "journeys" };
153
+ }
154
+ let trend;
155
+ if (req.baseline !== undefined) {
156
+ const base = baselineOf(await readJson(req.baseline, "--baseline"), req.baseline);
157
+ journeys = journeys.map((j) => {
158
+ const b = base.get(j.id);
159
+ return b === undefined ? j : { ...j, trend: compareLocatorHealth(j.health, b) };
160
+ });
161
+ const total = sumTrends(journeys.flatMap((j) => (j.trend === undefined ? [] : [j.trend])));
162
+ trend = { ...total, baseline: req.baseline, line: trendLine(total) };
163
+ }
164
+ const all = combineHealth(journeys.map((j) => j.health));
165
+ return {
166
+ source,
167
+ testIdAttributes,
168
+ journeys,
169
+ summary: { journeys: journeys.length, stable: all.stable, brittle: all.brittle, testIdAttributes, levels: all.levels, line: all.line },
170
+ suggestions: all.suggestions,
171
+ ...(trend === undefined ? {} : { trend }),
172
+ };
173
+ }
174
+ /** The human rendering (no `--json`): per Journey and its brittle steps, the totals, the trend and the work list. */
175
+ export function renderLocatorHealth(r, opts = {}) {
176
+ const lines = [];
177
+ for (const j of r.journeys) {
178
+ lines.push(`${String(j.health.brittle).padStart(4)} brittle / ${j.health.stable} stable ${j.id} (${j.health.line})${j.trend === undefined ? "" : ` [${trendLine(j.trend)}]`}`);
179
+ for (const s of j.health.steps) {
180
+ if (s.stability === "stable")
181
+ continue;
182
+ lines.push(` step ${s.index + 1}${s.stepId === undefined ? "" : ` (${s.stepId})`} ${s.kind} ${s.locator} — ${s.rung}, ${s.level}: ${s.reasons.join("; ")}`);
183
+ }
184
+ }
185
+ lines.push(`${r.summary.line} across ${r.summary.journeys} Journey(s) (test ids: ${r.testIdAttributes.join(", ")})`);
186
+ if (r.trend !== undefined)
187
+ lines.push(r.trend.line);
188
+ const top = opts.top ?? 10;
189
+ if (r.suggestions.length > 0) {
190
+ lines.push(`fixes for the app (${r.suggestions.length} element(s)):`);
191
+ for (const s of r.suggestions.slice(0, top))
192
+ lines.push(` - ${s.fix} [${s.occurrences.length} step(s): ${s.reasons.join("; ")}]`);
193
+ if (r.suggestions.length > top)
194
+ lines.push(` … ${r.suggestions.length - top} more (--json for all)`);
195
+ }
196
+ return `${lines.join("\n")}\n`;
197
+ }
198
+ export function brittleStepGate(maxBrittleSteps) {
199
+ if (!Number.isInteger(maxBrittleSteps) || maxBrittleSteps < 0)
200
+ throw new RangeError(`--max-brittle-steps must be a whole number ≥ 0, got ${maxBrittleSteps}`);
201
+ return { maxBrittleSteps };
202
+ }
203
+ //# sourceMappingURL=locator-health-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locator-health-api.js","sourceRoot":"","sources":["../src/locator-health-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,195 @@
1
+ import type { Recording, Step, TargetDescriptor } from "@jevitate/recording";
2
+ import type { LocatorHealth, LocatorHealthStep, LocatorRung } from "./check-types.js";
3
+ /**
4
+ * #470 — locator health, the pure core: for every step that acts on a target, the selector rung it
5
+ * resolves by, how robust that rung is, WHY it is brittle, and the fix for the app under test. A step
6
+ * is `stable` only when it resolves by a test id from the team's convention (`testIdAttributes`;
7
+ * `data-tflow-id` never counts, #468) that does not look generated; anything else is `brittle` and
8
+ * gets a suggestion — a work list for the codebase, de-duplicated by element.
9
+ *
10
+ * Static (a Journey's recorded descriptors) or dynamic (a run's `resolved` — the rung that actually
11
+ * matched at replay, `@jevitate/interpreter` `StepResolution`). No I/O: `locator-health-api.ts` loads
12
+ * Journeys, results and config; check/report/the review sheet call these helpers.
13
+ */
14
+ /** The ladder's own grade for a rung (the recorder's `Stability`): how likely it is to survive change. */
15
+ export type LocatorLevel = "high" | "medium" | "low";
16
+ /** Why a step's locator is brittle — the stable vocabulary (machine-readable `code`, human `text`). */
17
+ export type BrittleReasonCode = "no-test-id" | "test-id-not-in-convention" | "tflow-id-not-a-test-id" | "generated-value" | "no-accessible-name" | "duplicate-name" | "positional-css" | "text-copy";
18
+ /** A run's resolution of one step (structurally `@jevitate/interpreter` `StepResolution`). */
19
+ export interface ResolvedStepInput {
20
+ readonly index: number;
21
+ readonly stepId?: string;
22
+ readonly rung: string;
23
+ readonly testIdAttr?: string;
24
+ readonly ordinal?: number;
25
+ readonly candidates?: number;
26
+ }
27
+ /** One step's verdict, in detail (a `LocatorHealthStep`, plus what the reports and the review sheet show). */
28
+ export interface LocatorHealthStepDetail extends LocatorHealthStep {
29
+ readonly kind: Step["kind"];
30
+ /** The ladder grade of the rung used. */
31
+ readonly level: LocatorLevel;
32
+ /** `recorded`: from the Journey's descriptor; `resolved`: the rung that matched at replay. */
33
+ readonly source: "recorded" | "resolved";
34
+ /** The route (path) of the page the step acts on, when known. */
35
+ readonly route?: string;
36
+ /** How the step finds its target, e.g. `role=button name="Save"`, `css=main > div:nth-of-type(2)`. */
37
+ readonly locator: string;
38
+ readonly reasonCodes: readonly BrittleReasonCode[];
39
+ /** The key of this step's suggestion (`LocatorSuggestion.key`); absent when stable. */
40
+ readonly suggestion?: string;
41
+ }
42
+ /** Where a brittle element is used. */
43
+ export interface LocatorOccurrence {
44
+ readonly journeyId?: string;
45
+ readonly index: number;
46
+ readonly stepId?: string;
47
+ }
48
+ /** One actionable fix for the app, per element (route + role + name), however many steps use it. */
49
+ export interface LocatorSuggestion {
50
+ /** The de-duplication key: `route|role|name` (or `route|css|selector` for an unnamed element). */
51
+ readonly key: string;
52
+ readonly route?: string;
53
+ readonly role?: string;
54
+ readonly name?: string;
55
+ /** The element in words, e.g. `the "Save" button`. */
56
+ readonly element: string;
57
+ /** A short outerHTML excerpt, when the caller had one (a live page); never a field's value. */
58
+ readonly html?: string;
59
+ /** The attribute the fix adds (the convention's first). */
60
+ readonly attribute: string;
61
+ /** The suggested test id (kebab-case, from the name and route). */
62
+ readonly testId: string;
63
+ /** The fix, e.g. `add data-testid="save-contact" to the "Save" button on /contacts/new`. */
64
+ readonly fix: string;
65
+ readonly reasons: readonly string[];
66
+ readonly occurrences: readonly LocatorOccurrence[];
67
+ }
68
+ export interface LocatorLevels {
69
+ readonly high: number;
70
+ readonly medium: number;
71
+ readonly low: number;
72
+ }
73
+ /** A Journey's (or a run's) locator health in detail — a `LocatorHealth` with the levels, line and fixes. */
74
+ export interface LocatorHealthDetail extends LocatorHealth {
75
+ readonly steps: readonly LocatorHealthStepDetail[];
76
+ readonly levels: LocatorLevels;
77
+ /** e.g. `7/9 steps on stable locators; 2 brittle (high 7 · medium 1 · low 1)`. */
78
+ readonly line: string;
79
+ readonly suggestions: readonly LocatorSuggestion[];
80
+ }
81
+ export interface AnalyzeOptions {
82
+ /** The team's test-id convention (project config `testIdAttributes`). */
83
+ readonly testIdAttributes: readonly string[];
84
+ /** Tags each suggestion's occurrences (a multi-Journey report). */
85
+ readonly journeyId?: string;
86
+ /** A run's resolutions: the rung that actually matched, per step (overrides the recorded rung). */
87
+ readonly resolved?: readonly ResolvedStepInput[];
88
+ /** Short outerHTML excerpts by flat step index, when the caller had a live page. */
89
+ readonly html?: ReadonlyMap<number, string>;
90
+ }
91
+ /** The target a step acts on, or null (navigate, press, assert, a forEach's items are its own). */
92
+ export declare function stepTarget(step: Step): TargetDescriptor | null;
93
+ interface Verdict {
94
+ readonly rung: LocatorRung;
95
+ readonly level: LocatorLevel;
96
+ readonly codes: BrittleReasonCode[];
97
+ }
98
+ /**
99
+ * One target's verdict against the convention. `via` (a run's resolution) replaces the recorded rung
100
+ * when present; everything else is read from the descriptor.
101
+ */
102
+ export declare function classifyTarget(d: TargetDescriptor, testIdAttributes: readonly string[], via?: ResolvedStepInput): Verdict;
103
+ /** `"Save contact"` → `save-contact` (ASCII letters and digits, at most 40 chars). */
104
+ export declare function kebabCase(s: string): string;
105
+ /** The path of a page URL (`https://x/contacts/new?a=1` → `/contacts/new`); a path is kept as is. */
106
+ export declare function routeOf(url: string | undefined): string | undefined;
107
+ /**
108
+ * The suggested test id: the element's name in kebab-case, with the route's noun when the name is a
109
+ * single word (`Save` on /contacts/new → `save-contact`); an unnamed element is named by its route
110
+ * and tag (`contacts-div`).
111
+ */
112
+ export declare function suggestTestId(name: string | undefined, route: string | undefined, fallbackTag: string): string;
113
+ /** The element's identity across steps and runs: route + role + name (else its css). */
114
+ export declare function suggestionKey(d: TargetDescriptor, route: string | undefined): string;
115
+ /** Merges suggestions for the same element (route + role + name): occurrences and reasons combined. */
116
+ export declare function dedupeSuggestions(lists: ReadonlyArray<readonly LocatorSuggestion[]>): LocatorSuggestion[];
117
+ export declare function emptyLevels(): LocatorLevels;
118
+ /** `7/9 steps on stable locators; 2 brittle (high 7 · medium 1 · low 1)`. */
119
+ export declare function healthLine(h: {
120
+ readonly stable: number;
121
+ readonly brittle: number;
122
+ readonly levels: LocatorLevels;
123
+ }): string;
124
+ /**
125
+ * A recording's locator health: every step with a target (flat index, as the interpreter numbers
126
+ * them), against the convention; with `resolved`, the rung each step actually matched at replay.
127
+ */
128
+ export declare function analyzeRecording(rec: Recording, opts: AnalyzeOptions): LocatorHealthDetail;
129
+ /** Totals over several healths (a report, a check): counts, levels and the de-duplicated work list. */
130
+ export declare function combineHealth(healths: readonly LocatorHealthDetail[]): {
131
+ stable: number;
132
+ brittle: number;
133
+ levels: LocatorLevels;
134
+ line: string;
135
+ suggestions: LocatorSuggestion[];
136
+ };
137
+ /** `check --max-brittle-steps <n>`: true when a Journey item's brittle steps exceed the opt-in threshold. */
138
+ export declare function exceedsBrittleSteps(health: Pick<LocatorHealth, "brittle">, maxBrittleSteps: number | undefined): boolean;
139
+ export interface LocatorTrendStep {
140
+ /** `stepId`, else `#<index>`. */
141
+ readonly key: string;
142
+ readonly from: {
143
+ readonly rung: LocatorRung;
144
+ readonly level: LocatorLevel;
145
+ readonly stability: "stable" | "brittle";
146
+ };
147
+ readonly to: {
148
+ readonly rung: LocatorRung;
149
+ readonly level: LocatorLevel;
150
+ readonly stability: "stable" | "brittle";
151
+ };
152
+ }
153
+ /** Against a baseline: steps whose locator improved / regressed (stability first, then level). */
154
+ export interface LocatorHealthTrend {
155
+ readonly improved: number;
156
+ readonly regressed: number;
157
+ readonly unchanged: number;
158
+ /** Steps only in the current (added) or only in the baseline (removed). */
159
+ readonly added: number;
160
+ readonly removed: number;
161
+ readonly brittleDelta: number;
162
+ readonly changes: readonly (LocatorTrendStep & {
163
+ readonly direction: "improved" | "regressed";
164
+ })[];
165
+ }
166
+ type TrendInputStep = Pick<LocatorHealthStep, "stepId" | "index" | "rung" | "stability"> & {
167
+ readonly level?: LocatorLevel;
168
+ };
169
+ /**
170
+ * The trend of `current` against `baseline` (a previous report or run of the same Journey), keyed by
171
+ * step id (else index): a Journey re-recorded or healed after the app added test ids shows its
172
+ * improvement; a step that fell back to a brittle rung shows as a regression.
173
+ */
174
+ export declare function compareLocatorHealth(current: {
175
+ readonly brittle: number;
176
+ readonly steps: readonly TrendInputStep[];
177
+ }, baseline: {
178
+ readonly brittle: number;
179
+ readonly steps: readonly TrendInputStep[];
180
+ }): LocatorHealthTrend;
181
+ /** `trend vs baseline: 2 improved, 1 regressed (brittle -1)`. */
182
+ export declare function trendLine(t: LocatorHealthTrend): string;
183
+ /** What a run's result.json / a check item carries: counts, the line, levels, per-step verdicts, top fixes. */
184
+ export interface CompactLocatorHealth extends LocatorHealth {
185
+ readonly levels: LocatorLevels;
186
+ readonly line: string;
187
+ readonly steps: readonly LocatorHealthStep[];
188
+ readonly suggestions: ReadonlyArray<Pick<LocatorSuggestion, "key" | "element" | "fix" | "reasons"> & {
189
+ readonly route?: string;
190
+ readonly steps: number;
191
+ }>;
192
+ }
193
+ /** The compact form of a detail: per-step rung/stability/reasons, and the top `top` fixes. */
194
+ export declare function compactHealth(h: Pick<LocatorHealthDetail, "stable" | "brittle" | "steps" | "levels" | "line" | "suggestions">, top?: number): CompactLocatorHealth;
195
+ export {};