@xera-ai/core 0.1.7 → 0.3.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 (210) hide show
  1. package/dist/bin/internal.js +1932 -623
  2. package/dist/{adapter → core/src/adapter}/types.d.ts +1 -1
  3. package/dist/core/src/adapter/types.d.ts.map +1 -0
  4. package/dist/core/src/artifact/hash.d.ts.map +1 -0
  5. package/dist/core/src/artifact/meta.d.ts.map +1 -0
  6. package/dist/core/src/artifact/paths.d.ts.map +1 -0
  7. package/dist/core/src/artifact/status.d.ts.map +1 -0
  8. package/dist/core/src/auth/encrypt.d.ts.map +1 -0
  9. package/dist/core/src/auth/key.d.ts.map +1 -0
  10. package/dist/core/src/auth/refresh.d.ts.map +1 -0
  11. package/dist/core/src/auth/state.d.ts.map +1 -0
  12. package/dist/core/src/bin-internal/doctor.d.ts +5 -0
  13. package/dist/core/src/bin-internal/doctor.d.ts.map +1 -0
  14. package/dist/core/src/bin-internal/eval-deterministic.d.ts +5 -0
  15. package/dist/core/src/bin-internal/eval-deterministic.d.ts.map +1 -0
  16. package/dist/core/src/bin-internal/eval-prepare.d.ts +7 -0
  17. package/dist/core/src/bin-internal/eval-prepare.d.ts.map +1 -0
  18. package/dist/core/src/bin-internal/eval-report.d.ts +5 -0
  19. package/dist/core/src/bin-internal/eval-report.d.ts.map +1 -0
  20. package/dist/core/src/bin-internal/exec.d.ts.map +1 -0
  21. package/dist/core/src/bin-internal/fetch.d.ts.map +1 -0
  22. package/dist/core/src/bin-internal/heal-prepare.d.ts +19 -0
  23. package/dist/core/src/bin-internal/heal-prepare.d.ts.map +1 -0
  24. package/dist/core/src/bin-internal/index.d.ts.map +1 -0
  25. package/dist/core/src/bin-internal/lint.d.ts.map +1 -0
  26. package/dist/core/src/bin-internal/normalize.d.ts.map +1 -0
  27. package/dist/core/src/bin-internal/post.d.ts.map +1 -0
  28. package/dist/core/src/bin-internal/promote.d.ts.map +1 -0
  29. package/dist/core/src/bin-internal/report.d.ts.map +1 -0
  30. package/dist/core/src/bin-internal/status-cmd.d.ts.map +1 -0
  31. package/dist/core/src/bin-internal/typecheck.d.ts.map +1 -0
  32. package/dist/core/src/bin-internal/unlock.d.ts.map +1 -0
  33. package/dist/core/src/bin-internal/validate-feature.d.ts.map +1 -0
  34. package/dist/core/src/bin-internal/verify-prompts.d.ts +7 -0
  35. package/dist/core/src/bin-internal/verify-prompts.d.ts.map +1 -0
  36. package/dist/core/src/classifier/aggregate.d.ts.map +1 -0
  37. package/dist/core/src/classifier/history.d.ts.map +1 -0
  38. package/dist/core/src/classifier/types.d.ts.map +1 -0
  39. package/dist/core/src/config/define.d.ts.map +1 -0
  40. package/dist/core/src/config/load.d.ts.map +1 -0
  41. package/dist/{config → core/src/config}/schema.d.ts.map +1 -1
  42. package/dist/core/src/eval/paths.d.ts +15 -0
  43. package/dist/core/src/eval/paths.d.ts.map +1 -0
  44. package/dist/core/src/eval/run-id.d.ts +6 -0
  45. package/dist/core/src/eval/run-id.d.ts.map +1 -0
  46. package/dist/core/src/eval/types.d.ts +551 -0
  47. package/dist/core/src/eval/types.d.ts.map +1 -0
  48. package/dist/core/src/index.d.ts.map +1 -0
  49. package/dist/core/src/jira/client.d.ts.map +1 -0
  50. package/dist/core/src/jira/fields.d.ts.map +1 -0
  51. package/dist/core/src/jira/mcp-backend.d.ts.map +1 -0
  52. package/dist/core/src/jira/rest-backend.d.ts.map +1 -0
  53. package/dist/core/src/jira/retry.d.ts.map +1 -0
  54. package/dist/core/src/jira/types.d.ts.map +1 -0
  55. package/dist/core/src/lock/file-lock.d.ts.map +1 -0
  56. package/dist/core/src/logging/ndjson-logger.d.ts.map +1 -0
  57. package/dist/core/src/reporter/jira-comment.d.ts.map +1 -0
  58. package/dist/core/src/reporter/status-writer.d.ts.map +1 -0
  59. package/dist/src/index.js +19 -12
  60. package/dist/web/src/adapter.d.ts +3 -0
  61. package/dist/web/src/adapter.d.ts.map +1 -0
  62. package/dist/web/src/auth-setup/define.d.ts +16 -0
  63. package/dist/web/src/auth-setup/define.d.ts.map +1 -0
  64. package/dist/web/src/auth-setup/playwright-state.d.ts +2 -0
  65. package/dist/web/src/auth-setup/playwright-state.d.ts.map +1 -0
  66. package/dist/web/src/auth-setup/runner.d.ts +12 -0
  67. package/dist/web/src/auth-setup/runner.d.ts.map +1 -0
  68. package/dist/web/src/executor/index.d.ts +18 -0
  69. package/dist/web/src/executor/index.d.ts.map +1 -0
  70. package/dist/web/src/executor/playwright-args.d.ts +7 -0
  71. package/dist/web/src/executor/playwright-args.d.ts.map +1 -0
  72. package/dist/web/src/generator/gherkin-validate.d.ts +9 -0
  73. package/dist/web/src/generator/gherkin-validate.d.ts.map +1 -0
  74. package/dist/web/src/generator/lint.d.ts +9 -0
  75. package/dist/web/src/generator/lint.d.ts.map +1 -0
  76. package/dist/web/src/generator/pom-scan.d.ts +6 -0
  77. package/dist/web/src/generator/pom-scan.d.ts.map +1 -0
  78. package/dist/web/src/generator/promote.d.ts +7 -0
  79. package/dist/web/src/generator/promote.d.ts.map +1 -0
  80. package/dist/web/src/generator/selector-rules.d.ts +10 -0
  81. package/dist/web/src/generator/selector-rules.d.ts.map +1 -0
  82. package/dist/web/src/generator/typecheck.d.ts +11 -0
  83. package/dist/web/src/generator/typecheck.d.ts.map +1 -0
  84. package/dist/web/src/index.d.ts +18 -0
  85. package/dist/web/src/index.d.ts.map +1 -0
  86. package/dist/web/src/trace-normalizer/normalize.d.ts +7 -0
  87. package/dist/web/src/trace-normalizer/normalize.d.ts.map +1 -0
  88. package/dist/web/src/trace-normalizer/parse.d.ts +37 -0
  89. package/dist/web/src/trace-normalizer/parse.d.ts.map +1 -0
  90. package/dist/web/src/trace-normalizer/scrub-rules.d.ts +12 -0
  91. package/dist/web/src/trace-normalizer/scrub-rules.d.ts.map +1 -0
  92. package/dist/web/src/trace-normalizer/scrub.d.ts +29 -0
  93. package/dist/web/src/trace-normalizer/scrub.d.ts.map +1 -0
  94. package/dist/web/src/trace-normalizer/unzip.d.ts +6 -0
  95. package/dist/web/src/trace-normalizer/unzip.d.ts.map +1 -0
  96. package/package.json +3 -2
  97. package/src/adapter/types.ts +5 -2
  98. package/src/artifact/meta.ts +1 -1
  99. package/src/artifact/status.ts +1 -1
  100. package/src/auth/encrypt.ts +2 -2
  101. package/src/auth/key.ts +1 -2
  102. package/src/auth/refresh.ts +4 -1
  103. package/src/auth/state.ts +2 -2
  104. package/src/bin-internal/doctor.ts +133 -0
  105. package/src/bin-internal/eval-deterministic.ts +149 -0
  106. package/src/bin-internal/eval-prepare.ts +214 -0
  107. package/src/bin-internal/eval-report.ts +177 -0
  108. package/src/bin-internal/exec.ts +28 -15
  109. package/src/bin-internal/fetch.ts +21 -10
  110. package/src/bin-internal/heal-prepare.ts +230 -0
  111. package/src/bin-internal/index.ts +25 -11
  112. package/src/bin-internal/lint.ts +11 -4
  113. package/src/bin-internal/normalize.ts +23 -9
  114. package/src/bin-internal/post.ts +10 -4
  115. package/src/bin-internal/report.ts +3 -3
  116. package/src/bin-internal/status-cmd.ts +11 -3
  117. package/src/bin-internal/typecheck.ts +9 -3
  118. package/src/bin-internal/unlock.ts +12 -4
  119. package/src/bin-internal/validate-feature.ts +14 -5
  120. package/src/bin-internal/verify-prompts.ts +59 -0
  121. package/src/classifier/aggregate.ts +13 -6
  122. package/src/config/define.ts +3 -1
  123. package/src/config/load.ts +1 -1
  124. package/src/config/schema.ts +43 -37
  125. package/src/eval/paths.ts +32 -0
  126. package/src/eval/run-id.ts +30 -0
  127. package/src/eval/types.ts +101 -0
  128. package/src/jira/client.ts +4 -2
  129. package/src/jira/fields.ts +4 -2
  130. package/src/jira/mcp-backend.ts +1 -1
  131. package/src/jira/rest-backend.ts +17 -5
  132. package/src/jira/retry.ts +2 -2
  133. package/src/lock/file-lock.ts +2 -2
  134. package/src/logging/ndjson-logger.ts +2 -2
  135. package/src/reporter/jira-comment.ts +13 -7
  136. package/src/reporter/status-writer.ts +2 -2
  137. package/dist/adapter/types.d.ts.map +0 -1
  138. package/dist/artifact/hash.d.ts.map +0 -1
  139. package/dist/artifact/meta.d.ts.map +0 -1
  140. package/dist/artifact/paths.d.ts.map +0 -1
  141. package/dist/artifact/status.d.ts.map +0 -1
  142. package/dist/auth/encrypt.d.ts.map +0 -1
  143. package/dist/auth/key.d.ts.map +0 -1
  144. package/dist/auth/refresh.d.ts.map +0 -1
  145. package/dist/auth/state.d.ts.map +0 -1
  146. package/dist/bin-internal/exec.d.ts.map +0 -1
  147. package/dist/bin-internal/fetch.d.ts.map +0 -1
  148. package/dist/bin-internal/index.d.ts.map +0 -1
  149. package/dist/bin-internal/lint.d.ts.map +0 -1
  150. package/dist/bin-internal/normalize.d.ts.map +0 -1
  151. package/dist/bin-internal/post.d.ts.map +0 -1
  152. package/dist/bin-internal/promote.d.ts.map +0 -1
  153. package/dist/bin-internal/report.d.ts.map +0 -1
  154. package/dist/bin-internal/status-cmd.d.ts.map +0 -1
  155. package/dist/bin-internal/typecheck.d.ts.map +0 -1
  156. package/dist/bin-internal/unlock.d.ts.map +0 -1
  157. package/dist/bin-internal/validate-feature.d.ts.map +0 -1
  158. package/dist/classifier/aggregate.d.ts.map +0 -1
  159. package/dist/classifier/history.d.ts.map +0 -1
  160. package/dist/classifier/types.d.ts.map +0 -1
  161. package/dist/config/define.d.ts.map +0 -1
  162. package/dist/config/load.d.ts.map +0 -1
  163. package/dist/index.d.ts.map +0 -1
  164. package/dist/jira/client.d.ts.map +0 -1
  165. package/dist/jira/fields.d.ts.map +0 -1
  166. package/dist/jira/mcp-backend.d.ts.map +0 -1
  167. package/dist/jira/rest-backend.d.ts.map +0 -1
  168. package/dist/jira/retry.d.ts.map +0 -1
  169. package/dist/jira/types.d.ts.map +0 -1
  170. package/dist/lock/file-lock.d.ts.map +0 -1
  171. package/dist/logging/ndjson-logger.d.ts.map +0 -1
  172. package/dist/reporter/jira-comment.d.ts.map +0 -1
  173. package/dist/reporter/status-writer.d.ts.map +0 -1
  174. /package/dist/{artifact → core/src/artifact}/hash.d.ts +0 -0
  175. /package/dist/{artifact → core/src/artifact}/meta.d.ts +0 -0
  176. /package/dist/{artifact → core/src/artifact}/paths.d.ts +0 -0
  177. /package/dist/{artifact → core/src/artifact}/status.d.ts +0 -0
  178. /package/dist/{auth → core/src/auth}/encrypt.d.ts +0 -0
  179. /package/dist/{auth → core/src/auth}/key.d.ts +0 -0
  180. /package/dist/{auth → core/src/auth}/refresh.d.ts +0 -0
  181. /package/dist/{auth → core/src/auth}/state.d.ts +0 -0
  182. /package/dist/{bin-internal → core/src/bin-internal}/exec.d.ts +0 -0
  183. /package/dist/{bin-internal → core/src/bin-internal}/fetch.d.ts +0 -0
  184. /package/dist/{bin-internal → core/src/bin-internal}/index.d.ts +0 -0
  185. /package/dist/{bin-internal → core/src/bin-internal}/lint.d.ts +0 -0
  186. /package/dist/{bin-internal → core/src/bin-internal}/normalize.d.ts +0 -0
  187. /package/dist/{bin-internal → core/src/bin-internal}/post.d.ts +0 -0
  188. /package/dist/{bin-internal → core/src/bin-internal}/promote.d.ts +0 -0
  189. /package/dist/{bin-internal → core/src/bin-internal}/report.d.ts +0 -0
  190. /package/dist/{bin-internal → core/src/bin-internal}/status-cmd.d.ts +0 -0
  191. /package/dist/{bin-internal → core/src/bin-internal}/typecheck.d.ts +0 -0
  192. /package/dist/{bin-internal → core/src/bin-internal}/unlock.d.ts +0 -0
  193. /package/dist/{bin-internal → core/src/bin-internal}/validate-feature.d.ts +0 -0
  194. /package/dist/{classifier → core/src/classifier}/aggregate.d.ts +0 -0
  195. /package/dist/{classifier → core/src/classifier}/history.d.ts +0 -0
  196. /package/dist/{classifier → core/src/classifier}/types.d.ts +0 -0
  197. /package/dist/{config → core/src/config}/define.d.ts +0 -0
  198. /package/dist/{config → core/src/config}/load.d.ts +0 -0
  199. /package/dist/{config → core/src/config}/schema.d.ts +0 -0
  200. /package/dist/{index.d.ts → core/src/index.d.ts} +0 -0
  201. /package/dist/{jira → core/src/jira}/client.d.ts +0 -0
  202. /package/dist/{jira → core/src/jira}/fields.d.ts +0 -0
  203. /package/dist/{jira → core/src/jira}/mcp-backend.d.ts +0 -0
  204. /package/dist/{jira → core/src/jira}/rest-backend.d.ts +0 -0
  205. /package/dist/{jira → core/src/jira}/retry.d.ts +0 -0
  206. /package/dist/{jira → core/src/jira}/types.d.ts +0 -0
  207. /package/dist/{lock → core/src/lock}/file-lock.d.ts +0 -0
  208. /package/dist/{logging → core/src/logging}/ndjson-logger.d.ts +0 -0
  209. /package/dist/{reporter → core/src/reporter}/jira-comment.d.ts +0 -0
  210. /package/dist/{reporter → core/src/reporter}/status-writer.d.ts +0 -0
@@ -1,17 +1,20 @@
1
- import { resolveArtifactPaths, generateRunId } from '../artifact/paths';
2
- import { acquireLock, releaseLock, isLockStale, readLock, forceUnlock } from '../lock/file-lock';
3
- import { NdjsonLogger } from '../logging/ndjson-logger';
4
- import { loadConfig } from '../config/load';
5
- import { readAuthState } from '../auth/state';
6
- import { needsRefresh } from '../auth/refresh';
7
- import { stagePlaywrightState, runAuthSetup, runPlaywright } from '@xera-ai/web';
8
- import { chromium } from '@playwright/test';
9
- import { mkdirSync, existsSync } from 'node:fs';
1
+ import { existsSync, mkdirSync } from 'node:fs';
10
2
  import { join } from 'node:path';
3
+ import { chromium } from '@playwright/test';
4
+ import { runAuthSetup, runPlaywright, stagePlaywrightState } from '@xera-ai/web';
5
+ import { generateRunId, resolveArtifactPaths } from '../artifact/paths';
6
+ import { needsRefresh } from '../auth/refresh';
7
+ import { readAuthState } from '../auth/state';
8
+ import { loadConfig } from '../config/load';
9
+ import { acquireLock, forceUnlock, isLockStale, readLock, releaseLock } from '../lock/file-lock';
10
+ import { NdjsonLogger } from '../logging/ndjson-logger';
11
11
 
12
12
  export async function execCmd(argv: string[]): Promise<number> {
13
13
  const ticket = argv[0];
14
- if (!ticket) { console.error('[xera:exec] usage: exec <TICKET>'); return 1; }
14
+ if (!ticket) {
15
+ console.error('[xera:exec] usage: exec <TICKET>');
16
+ return 1;
17
+ }
15
18
  const cwd = process.cwd();
16
19
  const config = await loadConfig(cwd);
17
20
  const paths = resolveArtifactPaths(cwd, ticket);
@@ -21,12 +24,16 @@ export async function execCmd(argv: string[]): Promise<number> {
21
24
  // Acquire lock
22
25
  if (!acquireLock(paths.lockPath, runId)) {
23
26
  if (isLockStale(paths.lockPath)) {
24
- console.error(`[xera:exec] stale lock detected; force unlocking. Run \`xera-internal unlock ${ticket}\` to clear manually.`);
27
+ console.error(
28
+ `[xera:exec] stale lock detected; force unlocking. Run \`xera-internal unlock ${ticket}\` to clear manually.`,
29
+ );
25
30
  forceUnlock(paths.lockPath);
26
31
  acquireLock(paths.lockPath, runId);
27
32
  } else {
28
33
  const existing = readLock(paths.lockPath);
29
- console.error(`[xera:exec] another run in progress (PID ${existing?.pid} on ${existing?.hostname}, started ${existing?.started_at}). Wait or run \`xera-internal unlock ${ticket}\`.`);
34
+ console.error(
35
+ `[xera:exec] another run in progress (PID ${existing?.pid} on ${existing?.hostname}, started ${existing?.started_at}). Wait or run \`xera-internal unlock ${ticket}\`.`,
36
+ );
30
37
  return 1;
31
38
  }
32
39
  }
@@ -39,11 +46,18 @@ export async function execCmd(argv: string[]): Promise<number> {
39
46
  try {
40
47
  for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
41
48
  const entry = readAuthState(paths.authDir, roleName);
42
- if (needsRefresh(entry, { ttl: config.web.auth.ttl, refreshBuffer: config.web.auth.refreshBuffer })) {
49
+ if (
50
+ needsRefresh(entry, {
51
+ ttl: config.web.auth.ttl,
52
+ refreshBuffer: config.web.auth.refreshBuffer,
53
+ })
54
+ ) {
43
55
  const email = process.env[roleCreds.envEmail];
44
56
  const password = process.env[roleCreds.envPassword];
45
57
  if (!email || !password) {
46
- console.error(`[xera:exec] missing env ${roleCreds.envEmail} or ${roleCreds.envPassword} for role "${roleName}"`);
58
+ console.error(
59
+ `[xera:exec] missing env ${roleCreds.envEmail} or ${roleCreds.envPassword} for role "${roleName}"`,
60
+ );
47
61
  return 1;
48
62
  }
49
63
  await runAuthSetup({
@@ -113,4 +127,3 @@ export async function execCmd(argv: string[]): Promise<number> {
113
127
  releaseLock(paths.lockPath);
114
128
  }
115
129
  }
116
-
@@ -1,13 +1,15 @@
1
- import { writeFileSync, mkdirSync } from 'node:fs';
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
- import { loadConfig } from '../config/load';
4
- import { resolveArtifactPaths } from '../artifact/paths';
5
3
  import { hashString } from '../artifact/hash';
6
- import { writeMeta, readMeta } from '../artifact/meta';
4
+ import { readMeta, writeMeta } from '../artifact/meta';
5
+ import { resolveArtifactPaths } from '../artifact/paths';
6
+ import { loadConfig } from '../config/load';
7
7
  import { createJiraClient } from '../jira/client';
8
8
  import type { JiraTicket } from '../jira/types';
9
9
 
10
- export interface FetchCmdOpts { cwd?: string; }
10
+ export interface FetchCmdOpts {
11
+ cwd?: string;
12
+ }
11
13
 
12
14
  export async function fetchCmd(argv: string[], opts: FetchCmdOpts = {}): Promise<number> {
13
15
  const cwd = opts.cwd ?? process.cwd();
@@ -31,9 +33,13 @@ export async function fetchCmd(argv: string[], opts: FetchCmdOpts = {}): Promise
31
33
  ? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } }
32
34
  : {}),
33
35
  });
34
- const fieldMap = config.jira.fields.acceptanceCriteria !== undefined
35
- ? { story: config.jira.fields.story, acceptanceCriteria: config.jira.fields.acceptanceCriteria }
36
- : { story: config.jira.fields.story };
36
+ const fieldMap =
37
+ config.jira.fields.acceptanceCriteria !== undefined
38
+ ? {
39
+ story: config.jira.fields.story,
40
+ acceptanceCriteria: config.jira.fields.acceptanceCriteria,
41
+ }
42
+ : { story: config.jira.fields.story };
37
43
  t = await client.fetchTicket(ticket, fieldMap);
38
44
  }
39
45
 
@@ -69,7 +75,7 @@ function renderStory(t: JiraTicket): string {
69
75
  lines.push('## Story', '', story, '');
70
76
  }
71
77
 
72
- if (t.acceptanceCriteria && t.acceptanceCriteria.trim()) {
78
+ if (t.acceptanceCriteria?.trim()) {
73
79
  const ac = t.acceptanceCriteria.trim();
74
80
  if (/^##\s+acceptance\s+criteria\b/i.test(ac)) {
75
81
  lines.push(ac, '');
@@ -78,7 +84,12 @@ function renderStory(t: JiraTicket): string {
78
84
  }
79
85
  }
80
86
  if (t.attachments.length > 0) {
81
- lines.push('## Attachments', '', ...t.attachments.map((a) => `- [${a.filename}](${a.url})`), '');
87
+ lines.push(
88
+ '## Attachments',
89
+ '',
90
+ ...t.attachments.map((a) => `- [${a.filename}](${a.url})`),
91
+ '',
92
+ );
82
93
  }
83
94
  return lines.join('\n');
84
95
  }
@@ -0,0 +1,230 @@
1
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { scrubFreeText } from '@xera-ai/web';
4
+ import { unzipSync } from 'fflate';
5
+ import { resolveArtifactPaths } from '../artifact/paths';
6
+
7
+ export type FailedLocatorKind = 'role' | 'test-id' | 'css-class' | 'text' | 'label' | 'other';
8
+
9
+ export interface HealInput {
10
+ ticket: string;
11
+ runId: string;
12
+ scenarioName: string;
13
+ failedLocator: {
14
+ raw: string;
15
+ kind: FailedLocatorKind;
16
+ pomFile: string;
17
+ pomLine: number;
18
+ pomLineContent: string;
19
+ pomMethodName: string;
20
+ };
21
+ gherkinStep: string;
22
+ domSnapshotAtFailure: string;
23
+ }
24
+
25
+ interface ClassifierInput {
26
+ runId: string;
27
+ scenarios: Array<{
28
+ name: string;
29
+ outcome: string;
30
+ class: string;
31
+ confidence: string;
32
+ rationale: string;
33
+ }>;
34
+ }
35
+
36
+ interface NormalizedRunFile {
37
+ runId: string;
38
+ scenarios: Array<{
39
+ name: string;
40
+ outcome: 'PASS' | 'FAIL' | 'SKIPPED';
41
+ failure?: { errorMessage?: string };
42
+ }>;
43
+ }
44
+
45
+ const LOCATOR_LINE_RE = /^Locator:\s*(.+)$/m;
46
+
47
+ function classifyKind(raw: string): FailedLocatorKind {
48
+ if (/^getByRole\b/.test(raw)) return 'role';
49
+ if (/^getByTestId\b/.test(raw)) return 'test-id';
50
+ if (/^getByLabel\b/.test(raw)) return 'label';
51
+ if (/^getByText\b/.test(raw)) return 'text';
52
+ if (/^locator\(\s*['"`]\s*\.[A-Za-z_-]/.test(raw)) return 'css-class';
53
+ return 'other';
54
+ }
55
+
56
+ function extractDomSnapshot(tracePath: string): string {
57
+ if (!existsSync(tracePath)) return '';
58
+ const buf = readFileSync(tracePath);
59
+ const entries = unzipSync(buf);
60
+
61
+ // Strategy: parse the .trace JSONL event file to find the last frame-snapshot
62
+ // event, then extract the HTML resource it references. This gives us the DOM
63
+ // snapshot closest to the failure point rather than a lexicographic approximation.
64
+ // Falls back to last .html by lex sort if the .trace file is missing or unparseable.
65
+ const traceKey = Object.keys(entries).find((name) => name.endsWith('.trace'));
66
+ let chosenKey: string | null = null;
67
+
68
+ if (traceKey) {
69
+ const traceText = new TextDecoder().decode(entries[traceKey]!);
70
+ const lines = traceText.split('\n').filter(Boolean);
71
+ // Walk events in REVERSE order to find the most recent frame-snapshot.
72
+ for (let i = lines.length - 1; i >= 0; i--) {
73
+ try {
74
+ const evt = JSON.parse(lines[i]!);
75
+ const isSnapshot = evt.type === 'frame-snapshot' || evt.type === 'snapshot';
76
+ if (!isSnapshot) continue;
77
+ const snap = evt.snapshot ?? {};
78
+ // Try direct resource reference (older format).
79
+ const resourceName: unknown = snap.resourceName;
80
+ if (typeof resourceName === 'string') {
81
+ if (entries[resourceName]) {
82
+ chosenKey = resourceName;
83
+ break;
84
+ }
85
+ // Some traces store .html files under resources/ with the resourceName as the basename.
86
+ const guessed = `resources/${resourceName.replace(/^resources\//, '')}`;
87
+ if (entries[guessed]) {
88
+ chosenKey = guessed;
89
+ break;
90
+ }
91
+ }
92
+ // Try snapshot name → look for matching .html in resources/.
93
+ const snapshotName: unknown = snap.snapshotName;
94
+ if (typeof snapshotName === 'string') {
95
+ const directGuess = Object.keys(entries).find(
96
+ (k) => k.endsWith('.html') && k.includes(snapshotName),
97
+ );
98
+ if (directGuess) {
99
+ chosenKey = directGuess;
100
+ break;
101
+ }
102
+ }
103
+ } catch {
104
+ // Skip unparseable trace lines.
105
+ }
106
+ }
107
+ }
108
+
109
+ // Fallback: last .html by lexicographic sort (existing heuristic).
110
+ if (!chosenKey) {
111
+ const htmlKeys = Object.keys(entries)
112
+ .filter((name) => name.endsWith('.html'))
113
+ .sort();
114
+ chosenKey = htmlKeys[htmlKeys.length - 1] ?? null;
115
+ }
116
+
117
+ if (!chosenKey) return '';
118
+ const html = new TextDecoder().decode(entries[chosenKey]!);
119
+ return scrubFreeText(html);
120
+ }
121
+
122
+ function findPomLine(
123
+ ticketDir: string,
124
+ rawLocator: string,
125
+ ): { pomFile: string; pomLine: number; pomLineContent: string; pomMethodName: string } {
126
+ const pomDir = join(ticketDir, 'page-objects');
127
+ const candidates: string[] = [];
128
+ if (existsSync(pomDir)) {
129
+ for (const name of readdirSync(pomDir)) {
130
+ if (name.endsWith('.ts')) candidates.push(join(pomDir, name));
131
+ }
132
+ }
133
+ for (const file of candidates) {
134
+ const text = readFileSync(file, 'utf8');
135
+ const lines = text.split('\n');
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const line = lines[i]!;
138
+ if (line.includes(rawLocator)) {
139
+ const methodMatch = /^\s*(\w+)\s*=/.exec(line);
140
+ return {
141
+ pomFile: file,
142
+ pomLine: i + 1,
143
+ pomLineContent: line,
144
+ pomMethodName: methodMatch?.[1] ?? '<anonymous>',
145
+ };
146
+ }
147
+ }
148
+ }
149
+ throw new Error(`POM line not found for locator: ${rawLocator}`);
150
+ }
151
+
152
+ function findGherkinStep(featureText: string, rawLocator: string): string {
153
+ // Best-effort: find the first step line that mentions a quoted string
154
+ // appearing in the locator (e.g. a button name). Falls back to the
155
+ // first When/Then line if no match.
156
+ const quoteMatch = /['"`]([^'"`]{2,})['"`]/.exec(rawLocator);
157
+ if (quoteMatch) {
158
+ const needle = quoteMatch[1]!;
159
+ for (const line of featureText.split('\n')) {
160
+ if (line.includes(needle) && /^\s*(When|Then|And|Given)\b/.test(line)) {
161
+ return line.trim();
162
+ }
163
+ }
164
+ }
165
+ for (const line of featureText.split('\n')) {
166
+ if (/^\s*(When|Then)\b/.test(line)) return line.trim();
167
+ }
168
+ return '';
169
+ }
170
+
171
+ export function healPrepare(
172
+ repoRoot: string,
173
+ ticket: string,
174
+ runId: string,
175
+ scenarioName: string,
176
+ ): HealInput {
177
+ const paths = resolveArtifactPaths(repoRoot, ticket);
178
+ const classifierPath = join(paths.ticketDir, 'classifier-input.json');
179
+ const classifier: ClassifierInput = JSON.parse(readFileSync(classifierPath, 'utf8'));
180
+ const cls = classifier.scenarios.find((s) => s.name === scenarioName);
181
+ if (!cls) throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
182
+
183
+ const runDir = join(paths.runsDir, runId);
184
+ const normalized: NormalizedRunFile = JSON.parse(
185
+ readFileSync(join(runDir, 'normalized.json'), 'utf8'),
186
+ );
187
+ const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
188
+ if (!normSc?.failure) throw new Error(`no failure recorded for scenario "${scenarioName}"`);
189
+ const errorMessage = normSc.failure.errorMessage ?? '';
190
+ const m = LOCATOR_LINE_RE.exec(errorMessage);
191
+ if (!m) throw new Error(`cannot extract locator from errorMessage: ${errorMessage.slice(0, 80)}`);
192
+ const raw = m[1]!.trim();
193
+ const kind = classifyKind(raw);
194
+
195
+ const pomLoc = findPomLine(paths.ticketDir, raw);
196
+
197
+ const featureText = readFileSync(paths.featurePath, 'utf8');
198
+ const gherkinStep = findGherkinStep(featureText, raw);
199
+
200
+ const domSnapshotAtFailure = extractDomSnapshot(join(runDir, 'trace.zip'));
201
+
202
+ return {
203
+ ticket,
204
+ runId,
205
+ scenarioName,
206
+ failedLocator: { raw, kind, ...pomLoc },
207
+ gherkinStep,
208
+ domSnapshotAtFailure,
209
+ };
210
+ }
211
+
212
+ export async function healPrepareCmd(argv: string[]): Promise<number> {
213
+ const [ticket, runId, ...scenarioParts] = argv;
214
+ if (!ticket || !runId || scenarioParts.length === 0) {
215
+ console.error('[xera:heal-prepare] usage: heal-prepare <TICKET> <RUN_ID> <SCENARIO_NAME>');
216
+ return 1;
217
+ }
218
+ const scenarioName = scenarioParts.join(' ');
219
+ try {
220
+ const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
221
+ const paths = resolveArtifactPaths(process.cwd(), ticket);
222
+ const outPath = join(paths.runsDir, runId, 'heal-input.json');
223
+ writeFileSync(outPath, JSON.stringify(result, null, 2));
224
+ console.log(`[xera:heal-prepare] wrote ${outPath}`);
225
+ return 0;
226
+ } catch (err) {
227
+ console.error(`[xera:heal-prepare] ${(err as Error).message}`);
228
+ return 1;
229
+ }
230
+ }
@@ -1,33 +1,47 @@
1
+ import { doctorCmd } from './doctor';
2
+ import { evalDeterministicCmd } from './eval-deterministic';
3
+ import { evalPrepareCmd } from './eval-prepare';
4
+ import { evalReportCmd } from './eval-report';
5
+ import { execCmd } from './exec';
1
6
  import { fetchCmd } from './fetch';
2
- import { validateFeatureCmd } from './validate-feature';
3
- import { typecheckCmd } from './typecheck';
7
+ import { healPrepareCmd } from './heal-prepare';
4
8
  import { lintCmd } from './lint';
5
- import { execCmd } from './exec';
6
9
  import { normalizeCmd } from './normalize';
7
- import { reportCmd } from './report';
8
10
  import { postCmd } from './post';
11
+ import { promoteCmd } from './promote';
12
+ import { reportCmd } from './report';
9
13
  import { statusCmd } from './status-cmd';
14
+ import { typecheckCmd } from './typecheck';
10
15
  import { unlockCmd } from './unlock';
11
- import { promoteCmd } from './promote';
16
+ import { validateFeatureCmd } from './validate-feature';
17
+ import { verifyPromptsCmd } from './verify-prompts';
12
18
 
13
19
  const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
20
+ doctor: doctorCmd,
21
+ 'eval-deterministic': evalDeterministicCmd,
22
+ 'eval-prepare': evalPrepareCmd,
23
+ 'eval-report': evalReportCmd,
24
+ exec: execCmd,
14
25
  fetch: fetchCmd,
15
- 'validate-feature': validateFeatureCmd,
16
- typecheck: typecheckCmd,
26
+ 'heal-prepare': healPrepareCmd,
17
27
  lint: lintCmd,
18
- exec: execCmd,
19
28
  normalize: normalizeCmd,
20
- report: reportCmd,
21
29
  post: postCmd,
30
+ promote: promoteCmd,
31
+ report: reportCmd,
22
32
  status: statusCmd,
33
+ typecheck: typecheckCmd,
23
34
  unlock: unlockCmd,
24
- promote: promoteCmd,
35
+ 'validate-feature': validateFeatureCmd,
36
+ 'verify-prompts': verifyPromptsCmd,
25
37
  };
26
38
 
27
39
  export async function run(argv: string[]): Promise<number> {
28
40
  const [cmd, ...rest] = argv;
29
41
  if (!cmd || !COMMANDS[cmd]) {
30
- console.error(`Usage: xera-internal <command> [args...]\nCommands: ${Object.keys(COMMANDS).join(', ')}`);
42
+ console.error(
43
+ `Usage: xera-internal <command> [args...]\nCommands: ${Object.keys(COMMANDS).join(', ')}`,
44
+ );
31
45
  return 1;
32
46
  }
33
47
  try {
@@ -1,12 +1,19 @@
1
- import { resolveArtifactPaths } from '../artifact/paths';
2
1
  import { lintTicket } from '@xera-ai/web';
2
+ import { resolveArtifactPaths } from '../artifact/paths';
3
3
 
4
4
  export async function lintCmd(argv: string[]): Promise<number> {
5
5
  const ticket = argv[0];
6
- if (!ticket) { console.error('[xera:lint] usage: lint <TICKET>'); return 1; }
6
+ if (!ticket) {
7
+ console.error('[xera:lint] usage: lint <TICKET>');
8
+ return 1;
9
+ }
7
10
  const paths = resolveArtifactPaths(process.cwd(), ticket);
8
11
  const r = await lintTicket(paths.ticketDir);
9
- if (r.ok) { console.log('[xera:lint] ok'); return 0; }
10
- for (const w of r.warnings) console.error(`[xera:lint] ${w.file}:${w.line} [${w.rule}] ${w.message}`);
12
+ if (r.ok) {
13
+ console.log('[xera:lint] ok');
14
+ return 0;
15
+ }
16
+ for (const w of r.warnings)
17
+ console.error(`[xera:lint] ${w.file}:${w.line} [${w.rule}] ${w.message}`);
11
18
  return 2;
12
19
  }
@@ -1,20 +1,34 @@
1
- import { resolveArtifactPaths } from '../artifact/paths';
2
- import { normalizeRun } from '@xera-ai/web';
3
- import { readdirSync, existsSync } from 'node:fs';
1
+ import { existsSync, readdirSync } from 'node:fs';
4
2
  import { join } from 'node:path';
3
+ import { normalizeRun } from '@xera-ai/web';
4
+ import { resolveArtifactPaths } from '../artifact/paths';
5
5
 
6
6
  export async function normalizeCmd(argv: string[]): Promise<number> {
7
7
  const ticket = argv[0];
8
- if (!ticket) { console.error('[xera:normalize] usage: normalize <TICKET> [--run=<runId>]'); return 1; }
8
+ if (!ticket) {
9
+ console.error('[xera:normalize] usage: normalize <TICKET> [--run=<runId>]');
10
+ return 1;
11
+ }
9
12
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10
- const runArg = argv.find(a => a.startsWith('--run='));
13
+ const runArg = argv.find((a) => a.startsWith('--run='));
11
14
  const runId = runArg
12
15
  ? runArg.split('=')[1]!
13
- : readdirSync(paths.runsDir).filter(n => !n.startsWith('.')).sort().pop()!;
14
- if (!runId) { console.error('[xera:normalize] no run found'); return 1; }
16
+ : readdirSync(paths.runsDir)
17
+ .filter((n) => !n.startsWith('.'))
18
+ .sort()
19
+ .pop()!;
20
+ if (!runId) {
21
+ console.error('[xera:normalize] no run found');
22
+ return 1;
23
+ }
15
24
  const runDir = join(paths.runsDir, runId);
16
- if (!existsSync(runDir)) { console.error(`[xera:normalize] runs/${runId} missing`); return 1; }
25
+ if (!existsSync(runDir)) {
26
+ console.error(`[xera:normalize] runs/${runId} missing`);
27
+ return 1;
28
+ }
17
29
  const r = await normalizeRun({ runId, runDir });
18
- console.log(`[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`);
30
+ console.log(
31
+ `[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`,
32
+ );
19
33
  return 0;
20
34
  }
@@ -1,13 +1,16 @@
1
- import { readFileSync, existsSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { resolveArtifactPaths } from '../artifact/paths';
4
- import { loadConfig } from '../config/load';
5
4
  import { readStatus, writeStatus } from '../artifact/status';
5
+ import { loadConfig } from '../config/load';
6
6
  import { createJiraClient } from '../jira/client';
7
7
 
8
8
  export async function postCmd(argv: string[]): Promise<number> {
9
9
  const ticket = argv[0];
10
- if (!ticket) { console.error('[xera:post] usage: post <TICKET>'); return 1; }
10
+ if (!ticket) {
11
+ console.error('[xera:post] usage: post <TICKET>');
12
+ return 1;
13
+ }
11
14
  const cwd = process.cwd();
12
15
  const config = await loadConfig(cwd);
13
16
  if (!config.reporting.postToJira) {
@@ -16,7 +19,10 @@ export async function postCmd(argv: string[]): Promise<number> {
16
19
  }
17
20
  const paths = resolveArtifactPaths(cwd, ticket);
18
21
  const draftPath = join(paths.ticketDir, 'jira-comment.draft.md');
19
- if (!existsSync(draftPath)) { console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`); return 1; }
22
+ if (!existsSync(draftPath)) {
23
+ console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
24
+ return 1;
25
+ }
20
26
  const body = readFileSync(draftPath, 'utf8');
21
27
 
22
28
  const client = await createJiraClient({
@@ -2,9 +2,9 @@ import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { resolveArtifactPaths } from '../artifact/paths';
4
4
  import { aggregateScenarios } from '../classifier/aggregate';
5
- import { writeStatusFromClassification } from '../reporter/status-writer';
6
- import { buildJiraComment } from '../reporter/jira-comment';
7
5
  import type { ScenarioClassification } from '../classifier/types';
6
+ import { buildJiraComment } from '../reporter/jira-comment';
7
+ import { writeStatusFromClassification } from '../reporter/status-writer';
8
8
 
9
9
  interface ReportInput {
10
10
  scenarios: ScenarioClassification[];
@@ -14,7 +14,7 @@ interface ReportInput {
14
14
 
15
15
  export async function reportCmd(argv: string[]): Promise<number> {
16
16
  const ticket = argv[0];
17
- const inputArg = argv.find(a => a.startsWith('--input='));
17
+ const inputArg = argv.find((a) => a.startsWith('--input='));
18
18
  if (!ticket || !inputArg) {
19
19
  console.error('[xera:report] usage: report <TICKET> --input=<classifier-output.json>');
20
20
  return 1;
@@ -3,11 +3,19 @@ import { readStatus } from '../artifact/status';
3
3
 
4
4
  export async function statusCmd(argv: string[]): Promise<number> {
5
5
  const ticket = argv[0];
6
- if (!ticket) { console.error('[xera:status] usage: status <TICKET>'); return 1; }
6
+ if (!ticket) {
7
+ console.error('[xera:status] usage: status <TICKET>');
8
+ return 1;
9
+ }
7
10
  const paths = resolveArtifactPaths(process.cwd(), ticket);
8
11
  const s = readStatus(paths.statusPath);
9
- if (!s) { console.log(`[xera:status] no status yet for ${ticket}`); return 0; }
10
- console.log(`${ticket}: ${s.result} (${s.classification}, conf=${s.confidence}) ${s.scenarios.passed}/${s.scenarios.total} passed, last run ${s.lastRun}`);
12
+ if (!s) {
13
+ console.log(`[xera:status] no status yet for ${ticket}`);
14
+ return 0;
15
+ }
16
+ console.log(
17
+ `${ticket}: ${s.result} (${s.classification}, conf=${s.confidence}) — ${s.scenarios.passed}/${s.scenarios.total} passed, last run ${s.lastRun}`,
18
+ );
11
19
  for (const h of s.history.slice(0, 5)) console.log(` ${h.ts} ${h.result} ${h.class}`);
12
20
  return 0;
13
21
  }
@@ -1,12 +1,18 @@
1
- import { resolveArtifactPaths } from '../artifact/paths';
2
1
  import { typecheckTicket } from '@xera-ai/web';
2
+ import { resolveArtifactPaths } from '../artifact/paths';
3
3
 
4
4
  export async function typecheckCmd(argv: string[]): Promise<number> {
5
5
  const ticket = argv[0];
6
- if (!ticket) { console.error('[xera:typecheck] usage: typecheck <TICKET>'); return 1; }
6
+ if (!ticket) {
7
+ console.error('[xera:typecheck] usage: typecheck <TICKET>');
8
+ return 1;
9
+ }
7
10
  const paths = resolveArtifactPaths(process.cwd(), ticket);
8
11
  const r = await typecheckTicket(paths.ticketDir);
9
- if (r.ok) { console.log('[xera:typecheck] ok'); return 0; }
12
+ if (r.ok) {
13
+ console.log('[xera:typecheck] ok');
14
+ return 0;
15
+ }
10
16
  for (const e of r.errors) console.error(`[xera:typecheck] ${e}`);
11
17
  return 2;
12
18
  }
@@ -1,15 +1,23 @@
1
1
  import { resolveArtifactPaths } from '../artifact/paths';
2
- import { isLockStale, readLock, forceUnlock } from '../lock/file-lock';
2
+ import { forceUnlock, isLockStale, readLock } from '../lock/file-lock';
3
3
 
4
4
  export async function unlockCmd(argv: string[]): Promise<number> {
5
5
  const ticket = argv[0];
6
- if (!ticket) { console.error('[xera:unlock] usage: unlock <TICKET> [--force]'); return 1; }
6
+ if (!ticket) {
7
+ console.error('[xera:unlock] usage: unlock <TICKET> [--force]');
8
+ return 1;
9
+ }
7
10
  const paths = resolveArtifactPaths(process.cwd(), ticket);
8
11
  const lock = readLock(paths.lockPath);
9
- if (!lock) { console.log(`[xera:unlock] no lock for ${ticket}`); return 0; }
12
+ if (!lock) {
13
+ console.log(`[xera:unlock] no lock for ${ticket}`);
14
+ return 0;
15
+ }
10
16
  const force = argv.includes('--force');
11
17
  if (!force && !isLockStale(paths.lockPath)) {
12
- console.error(`[xera:unlock] lock is held by PID ${lock.pid} on ${lock.hostname} (active). Pass --force to override.`);
18
+ console.error(
19
+ `[xera:unlock] lock is held by PID ${lock.pid} on ${lock.hostname} (active). Pass --force to override.`,
20
+ );
13
21
  return 1;
14
22
  }
15
23
  forceUnlock(paths.lockPath);
@@ -1,14 +1,23 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { resolveArtifactPaths } from '../artifact/paths';
1
+ import { existsSync, readFileSync } from 'node:fs';
3
2
  import { validateGherkin } from '@xera-ai/web';
3
+ import { resolveArtifactPaths } from '../artifact/paths';
4
4
 
5
5
  export async function validateFeatureCmd(argv: string[]): Promise<number> {
6
6
  const ticket = argv[0];
7
- if (!ticket) { console.error('[xera:validate-feature] usage: validate-feature <TICKET>'); return 1; }
7
+ if (!ticket) {
8
+ console.error('[xera:validate-feature] usage: validate-feature <TICKET>');
9
+ return 1;
10
+ }
8
11
  const paths = resolveArtifactPaths(process.cwd(), ticket);
9
- if (!existsSync(paths.featurePath)) { console.error(`[xera:validate-feature] missing ${paths.featurePath}`); return 1; }
12
+ if (!existsSync(paths.featurePath)) {
13
+ console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
14
+ return 1;
15
+ }
10
16
  const r = validateGherkin(readFileSync(paths.featurePath, 'utf8'));
11
- if (r.ok) { console.log('[xera:validate-feature] ok'); return 0; }
17
+ if (r.ok) {
18
+ console.log('[xera:validate-feature] ok');
19
+ return 0;
20
+ }
12
21
  for (const e of r.errors) console.error(`[xera:validate-feature] line ${e.line}: ${e.message}`);
13
22
  return 2;
14
23
  }