@xera-ai/core 0.3.0 → 0.4.1

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 (246) hide show
  1. package/bin/internal.ts +1 -0
  2. package/dist/adapter/types.d.ts.map +1 -0
  3. package/dist/artifact/hash.d.ts.map +1 -0
  4. package/dist/artifact/meta.d.ts +20 -0
  5. package/dist/artifact/meta.d.ts.map +1 -0
  6. package/dist/artifact/paths.d.ts.map +1 -0
  7. package/dist/artifact/status.d.ts +75 -0
  8. package/dist/artifact/status.d.ts.map +1 -0
  9. package/dist/auth/encrypt.d.ts.map +1 -0
  10. package/dist/auth/key.d.ts.map +1 -0
  11. package/dist/auth/refresh.d.ts.map +1 -0
  12. package/dist/{core/src/auth → auth}/state.d.ts +5 -14
  13. package/dist/auth/state.d.ts.map +1 -0
  14. package/dist/bin/internal.js +8607 -373
  15. package/dist/bin-internal/doctor.d.ts.map +1 -0
  16. package/dist/bin-internal/eval-deterministic.d.ts.map +1 -0
  17. package/dist/bin-internal/eval-prepare.d.ts.map +1 -0
  18. package/dist/bin-internal/eval-report.d.ts.map +1 -0
  19. package/dist/bin-internal/exec.d.ts.map +1 -0
  20. package/dist/bin-internal/fetch.d.ts.map +1 -0
  21. package/dist/bin-internal/graph-backfill.d.ts +2 -0
  22. package/dist/bin-internal/graph-backfill.d.ts.map +1 -0
  23. package/dist/bin-internal/graph-enrich.d.ts +2 -0
  24. package/dist/bin-internal/graph-enrich.d.ts.map +1 -0
  25. package/dist/bin-internal/graph-query.d.ts +2 -0
  26. package/dist/bin-internal/graph-query.d.ts.map +1 -0
  27. package/dist/bin-internal/graph-record-script.d.ts +2 -0
  28. package/dist/bin-internal/graph-record-script.d.ts.map +1 -0
  29. package/dist/bin-internal/graph-record.d.ts +3 -0
  30. package/dist/bin-internal/graph-record.d.ts.map +1 -0
  31. package/dist/bin-internal/graph-snapshot.d.ts +2 -0
  32. package/dist/bin-internal/graph-snapshot.d.ts.map +1 -0
  33. package/dist/bin-internal/heal-prepare.d.ts.map +1 -0
  34. package/dist/bin-internal/index.d.ts.map +1 -0
  35. package/dist/bin-internal/lint.d.ts.map +1 -0
  36. package/dist/bin-internal/normalize.d.ts.map +1 -0
  37. package/dist/bin-internal/post.d.ts.map +1 -0
  38. package/dist/bin-internal/promote.d.ts.map +1 -0
  39. package/dist/bin-internal/report.d.ts.map +1 -0
  40. package/dist/bin-internal/status-cmd.d.ts.map +1 -0
  41. package/dist/bin-internal/typecheck.d.ts.map +1 -0
  42. package/dist/bin-internal/unlock.d.ts.map +1 -0
  43. package/dist/bin-internal/validate-feature.d.ts.map +1 -0
  44. package/dist/bin-internal/verify-prompts.d.ts.map +1 -0
  45. package/dist/classifier/aggregate.d.ts.map +1 -0
  46. package/dist/classifier/history.d.ts.map +1 -0
  47. package/dist/classifier/types.d.ts.map +1 -0
  48. package/dist/config/define.d.ts.map +1 -0
  49. package/dist/config/load.d.ts.map +1 -0
  50. package/dist/config/schema.d.ts +66 -0
  51. package/dist/config/schema.d.ts.map +1 -0
  52. package/dist/eval/paths.d.ts.map +1 -0
  53. package/dist/eval/run-id.d.ts.map +1 -0
  54. package/dist/eval/types.d.ts +203 -0
  55. package/dist/eval/types.d.ts.map +1 -0
  56. package/dist/graph/classify.d.ts +42 -0
  57. package/dist/graph/classify.d.ts.map +1 -0
  58. package/dist/graph/cost.d.ts +21 -0
  59. package/dist/graph/cost.d.ts.map +1 -0
  60. package/dist/graph/enrich.d.ts +10 -0
  61. package/dist/graph/enrich.d.ts.map +1 -0
  62. package/dist/graph/index.d.ts +13 -0
  63. package/dist/graph/index.d.ts.map +1 -0
  64. package/dist/graph/paths.d.ts +10 -0
  65. package/dist/graph/paths.d.ts.map +1 -0
  66. package/dist/graph/schema.d.ts +180 -0
  67. package/dist/graph/schema.d.ts.map +1 -0
  68. package/dist/graph/similarity.d.ts +3 -0
  69. package/dist/graph/similarity.d.ts.map +1 -0
  70. package/dist/graph/store.d.ts +14 -0
  71. package/dist/graph/store.d.ts.map +1 -0
  72. package/dist/graph/types.d.ts +151 -0
  73. package/dist/graph/types.d.ts.map +1 -0
  74. package/dist/graph/ulid.d.ts +2 -0
  75. package/dist/graph/ulid.d.ts.map +1 -0
  76. package/dist/{core/src/index.d.ts → index.d.ts} +11 -11
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/jira/client.d.ts.map +1 -0
  79. package/dist/jira/fields.d.ts.map +1 -0
  80. package/dist/jira/mcp-backend.d.ts.map +1 -0
  81. package/dist/jira/rest-backend.d.ts.map +1 -0
  82. package/dist/jira/retry.d.ts.map +1 -0
  83. package/dist/jira/types.d.ts.map +1 -0
  84. package/dist/lock/file-lock.d.ts.map +1 -0
  85. package/dist/logging/ndjson-logger.d.ts.map +1 -0
  86. package/dist/reporter/jira-comment.d.ts.map +1 -0
  87. package/dist/reporter/status-writer.d.ts.map +1 -0
  88. package/dist/src/index.js +346 -318
  89. package/package.json +19 -14
  90. package/src/artifact/status.ts +8 -1
  91. package/src/auth/refresh.ts +1 -0
  92. package/src/bin-internal/doctor.ts +37 -1
  93. package/src/bin-internal/eval-prepare.ts +1 -1
  94. package/src/bin-internal/graph-backfill.ts +43 -0
  95. package/src/bin-internal/graph-enrich.ts +28 -0
  96. package/src/bin-internal/graph-query.ts +43 -0
  97. package/src/bin-internal/graph-record-script.ts +191 -0
  98. package/src/bin-internal/graph-record.ts +287 -0
  99. package/src/bin-internal/graph-snapshot.ts +23 -0
  100. package/src/bin-internal/heal-prepare.ts +1 -1
  101. package/src/bin-internal/index.ts +10 -0
  102. package/src/bin-internal/report.ts +63 -5
  103. package/src/bin-internal/verify-prompts.ts +3 -0
  104. package/src/classifier/aggregate.ts +1 -0
  105. package/src/config/schema.ts +6 -6
  106. package/src/graph/classify.ts +126 -0
  107. package/src/graph/cost.ts +59 -0
  108. package/src/graph/enrich.ts +103 -0
  109. package/src/graph/index.ts +30 -0
  110. package/src/graph/paths.ts +27 -0
  111. package/src/graph/schema.ts +142 -0
  112. package/src/graph/similarity.ts +43 -0
  113. package/src/graph/store.ts +231 -0
  114. package/src/graph/types.ts +179 -0
  115. package/src/graph/ulid.ts +58 -0
  116. package/src/index.ts +11 -11
  117. package/src/jira/rest-backend.ts +1 -1
  118. package/src/reporter/status-writer.ts +1 -1
  119. package/dist/core/src/adapter/types.d.ts.map +0 -1
  120. package/dist/core/src/artifact/hash.d.ts.map +0 -1
  121. package/dist/core/src/artifact/meta.d.ts +0 -46
  122. package/dist/core/src/artifact/meta.d.ts.map +0 -1
  123. package/dist/core/src/artifact/paths.d.ts.map +0 -1
  124. package/dist/core/src/artifact/status.d.ts +0 -96
  125. package/dist/core/src/artifact/status.d.ts.map +0 -1
  126. package/dist/core/src/auth/encrypt.d.ts.map +0 -1
  127. package/dist/core/src/auth/key.d.ts.map +0 -1
  128. package/dist/core/src/auth/refresh.d.ts.map +0 -1
  129. package/dist/core/src/auth/state.d.ts.map +0 -1
  130. package/dist/core/src/bin-internal/doctor.d.ts.map +0 -1
  131. package/dist/core/src/bin-internal/eval-deterministic.d.ts.map +0 -1
  132. package/dist/core/src/bin-internal/eval-prepare.d.ts.map +0 -1
  133. package/dist/core/src/bin-internal/eval-report.d.ts.map +0 -1
  134. package/dist/core/src/bin-internal/exec.d.ts.map +0 -1
  135. package/dist/core/src/bin-internal/fetch.d.ts.map +0 -1
  136. package/dist/core/src/bin-internal/heal-prepare.d.ts.map +0 -1
  137. package/dist/core/src/bin-internal/index.d.ts.map +0 -1
  138. package/dist/core/src/bin-internal/lint.d.ts.map +0 -1
  139. package/dist/core/src/bin-internal/normalize.d.ts.map +0 -1
  140. package/dist/core/src/bin-internal/post.d.ts.map +0 -1
  141. package/dist/core/src/bin-internal/promote.d.ts.map +0 -1
  142. package/dist/core/src/bin-internal/report.d.ts.map +0 -1
  143. package/dist/core/src/bin-internal/status-cmd.d.ts.map +0 -1
  144. package/dist/core/src/bin-internal/typecheck.d.ts.map +0 -1
  145. package/dist/core/src/bin-internal/unlock.d.ts.map +0 -1
  146. package/dist/core/src/bin-internal/validate-feature.d.ts.map +0 -1
  147. package/dist/core/src/bin-internal/verify-prompts.d.ts.map +0 -1
  148. package/dist/core/src/classifier/aggregate.d.ts.map +0 -1
  149. package/dist/core/src/classifier/history.d.ts.map +0 -1
  150. package/dist/core/src/classifier/types.d.ts.map +0 -1
  151. package/dist/core/src/config/define.d.ts.map +0 -1
  152. package/dist/core/src/config/load.d.ts.map +0 -1
  153. package/dist/core/src/config/schema.d.ts +0 -326
  154. package/dist/core/src/config/schema.d.ts.map +0 -1
  155. package/dist/core/src/eval/paths.d.ts.map +0 -1
  156. package/dist/core/src/eval/run-id.d.ts.map +0 -1
  157. package/dist/core/src/eval/types.d.ts +0 -551
  158. package/dist/core/src/eval/types.d.ts.map +0 -1
  159. package/dist/core/src/index.d.ts.map +0 -1
  160. package/dist/core/src/jira/client.d.ts.map +0 -1
  161. package/dist/core/src/jira/fields.d.ts.map +0 -1
  162. package/dist/core/src/jira/mcp-backend.d.ts.map +0 -1
  163. package/dist/core/src/jira/rest-backend.d.ts.map +0 -1
  164. package/dist/core/src/jira/retry.d.ts.map +0 -1
  165. package/dist/core/src/jira/types.d.ts.map +0 -1
  166. package/dist/core/src/lock/file-lock.d.ts.map +0 -1
  167. package/dist/core/src/logging/ndjson-logger.d.ts.map +0 -1
  168. package/dist/core/src/reporter/jira-comment.d.ts.map +0 -1
  169. package/dist/core/src/reporter/status-writer.d.ts.map +0 -1
  170. package/dist/web/src/adapter.d.ts +0 -3
  171. package/dist/web/src/adapter.d.ts.map +0 -1
  172. package/dist/web/src/auth-setup/define.d.ts +0 -16
  173. package/dist/web/src/auth-setup/define.d.ts.map +0 -1
  174. package/dist/web/src/auth-setup/playwright-state.d.ts +0 -2
  175. package/dist/web/src/auth-setup/playwright-state.d.ts.map +0 -1
  176. package/dist/web/src/auth-setup/runner.d.ts +0 -12
  177. package/dist/web/src/auth-setup/runner.d.ts.map +0 -1
  178. package/dist/web/src/executor/index.d.ts +0 -18
  179. package/dist/web/src/executor/index.d.ts.map +0 -1
  180. package/dist/web/src/executor/playwright-args.d.ts +0 -7
  181. package/dist/web/src/executor/playwright-args.d.ts.map +0 -1
  182. package/dist/web/src/generator/gherkin-validate.d.ts +0 -9
  183. package/dist/web/src/generator/gherkin-validate.d.ts.map +0 -1
  184. package/dist/web/src/generator/lint.d.ts +0 -9
  185. package/dist/web/src/generator/lint.d.ts.map +0 -1
  186. package/dist/web/src/generator/pom-scan.d.ts +0 -6
  187. package/dist/web/src/generator/pom-scan.d.ts.map +0 -1
  188. package/dist/web/src/generator/promote.d.ts +0 -7
  189. package/dist/web/src/generator/promote.d.ts.map +0 -1
  190. package/dist/web/src/generator/selector-rules.d.ts +0 -10
  191. package/dist/web/src/generator/selector-rules.d.ts.map +0 -1
  192. package/dist/web/src/generator/typecheck.d.ts +0 -11
  193. package/dist/web/src/generator/typecheck.d.ts.map +0 -1
  194. package/dist/web/src/index.d.ts +0 -18
  195. package/dist/web/src/index.d.ts.map +0 -1
  196. package/dist/web/src/trace-normalizer/normalize.d.ts +0 -7
  197. package/dist/web/src/trace-normalizer/normalize.d.ts.map +0 -1
  198. package/dist/web/src/trace-normalizer/parse.d.ts +0 -37
  199. package/dist/web/src/trace-normalizer/parse.d.ts.map +0 -1
  200. package/dist/web/src/trace-normalizer/scrub-rules.d.ts +0 -12
  201. package/dist/web/src/trace-normalizer/scrub-rules.d.ts.map +0 -1
  202. package/dist/web/src/trace-normalizer/scrub.d.ts +0 -29
  203. package/dist/web/src/trace-normalizer/scrub.d.ts.map +0 -1
  204. package/dist/web/src/trace-normalizer/unzip.d.ts +0 -6
  205. package/dist/web/src/trace-normalizer/unzip.d.ts.map +0 -1
  206. /package/dist/{core/src/adapter → adapter}/types.d.ts +0 -0
  207. /package/dist/{core/src/artifact → artifact}/hash.d.ts +0 -0
  208. /package/dist/{core/src/artifact → artifact}/paths.d.ts +0 -0
  209. /package/dist/{core/src/auth → auth}/encrypt.d.ts +0 -0
  210. /package/dist/{core/src/auth → auth}/key.d.ts +0 -0
  211. /package/dist/{core/src/auth → auth}/refresh.d.ts +0 -0
  212. /package/dist/{core/src/bin-internal → bin-internal}/doctor.d.ts +0 -0
  213. /package/dist/{core/src/bin-internal → bin-internal}/eval-deterministic.d.ts +0 -0
  214. /package/dist/{core/src/bin-internal → bin-internal}/eval-prepare.d.ts +0 -0
  215. /package/dist/{core/src/bin-internal → bin-internal}/eval-report.d.ts +0 -0
  216. /package/dist/{core/src/bin-internal → bin-internal}/exec.d.ts +0 -0
  217. /package/dist/{core/src/bin-internal → bin-internal}/fetch.d.ts +0 -0
  218. /package/dist/{core/src/bin-internal → bin-internal}/heal-prepare.d.ts +0 -0
  219. /package/dist/{core/src/bin-internal → bin-internal}/index.d.ts +0 -0
  220. /package/dist/{core/src/bin-internal → bin-internal}/lint.d.ts +0 -0
  221. /package/dist/{core/src/bin-internal → bin-internal}/normalize.d.ts +0 -0
  222. /package/dist/{core/src/bin-internal → bin-internal}/post.d.ts +0 -0
  223. /package/dist/{core/src/bin-internal → bin-internal}/promote.d.ts +0 -0
  224. /package/dist/{core/src/bin-internal → bin-internal}/report.d.ts +0 -0
  225. /package/dist/{core/src/bin-internal → bin-internal}/status-cmd.d.ts +0 -0
  226. /package/dist/{core/src/bin-internal → bin-internal}/typecheck.d.ts +0 -0
  227. /package/dist/{core/src/bin-internal → bin-internal}/unlock.d.ts +0 -0
  228. /package/dist/{core/src/bin-internal → bin-internal}/validate-feature.d.ts +0 -0
  229. /package/dist/{core/src/bin-internal → bin-internal}/verify-prompts.d.ts +0 -0
  230. /package/dist/{core/src/classifier → classifier}/aggregate.d.ts +0 -0
  231. /package/dist/{core/src/classifier → classifier}/history.d.ts +0 -0
  232. /package/dist/{core/src/classifier → classifier}/types.d.ts +0 -0
  233. /package/dist/{core/src/config → config}/define.d.ts +0 -0
  234. /package/dist/{core/src/config → config}/load.d.ts +0 -0
  235. /package/dist/{core/src/eval → eval}/paths.d.ts +0 -0
  236. /package/dist/{core/src/eval → eval}/run-id.d.ts +0 -0
  237. /package/dist/{core/src/jira → jira}/client.d.ts +0 -0
  238. /package/dist/{core/src/jira → jira}/fields.d.ts +0 -0
  239. /package/dist/{core/src/jira → jira}/mcp-backend.d.ts +0 -0
  240. /package/dist/{core/src/jira → jira}/rest-backend.d.ts +0 -0
  241. /package/dist/{core/src/jira → jira}/retry.d.ts +0 -0
  242. /package/dist/{core/src/jira → jira}/types.d.ts +0 -0
  243. /package/dist/{core/src/lock → lock}/file-lock.d.ts +0 -0
  244. /package/dist/{core/src/logging → logging}/ndjson-logger.d.ts +0 -0
  245. /package/dist/{core/src/reporter → reporter}/jira-comment.d.ts +0 -0
  246. /package/dist/{core/src/reporter → reporter}/status-writer.d.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -17,17 +17,22 @@
17
17
  }
18
18
  },
19
19
  "bin": {
20
- "xera-internal": "./dist/bin/internal.js"
21
- },
22
- "files": ["dist", "src", "bin"],
23
- "scripts": {
24
- "build": "bun build ./src/index.ts ./bin/internal.ts --outdir ./dist --target bun --external @playwright/test --external @xera-ai/web --external zod",
25
- "typecheck": "tsc --noEmit"
26
- },
27
- "dependencies": {
28
- "zod": "3.23.8",
29
- "@xera-ai/web": "^0.1.5",
30
- "@playwright/test": "1.48.0",
31
- "fflate": "0.8.2"
32
- }
20
+ "xera-internal": "./dist/bin/internal.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "bin"
26
+ ],
27
+ "scripts": {
28
+ "build": "bun build ./src/index.ts ./bin/internal.ts --outdir ./dist --target bun --external @playwright/test --external @xera-ai/web --external zod",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "dependencies": {
32
+ "zod": "4.4.3",
33
+ "@xera-ai/web": "^0.2.0",
34
+ "@playwright/test": "1.60.0",
35
+ "fflate": "0.8.3",
36
+ "yaml": "2.9.0"
37
+ }
33
38
  }
@@ -2,7 +2,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { z } from 'zod';
4
4
 
5
- const ClassificationEnum = z.enum(['PASS', 'REAL_BUG', 'SELECTOR_DRIFT', 'FLAKY', 'TEST_BUG']);
5
+ const ClassificationEnum = z.enum([
6
+ 'PASS',
7
+ 'REAL_BUG',
8
+ 'SELECTOR_DRIFT',
9
+ 'FLAKY',
10
+ 'TEST_BUG',
11
+ 'TEST_OUTDATED',
12
+ ]);
6
13
  const ResultEnum = z.enum(['PASS', 'FAIL']);
7
14
  const ConfidenceEnum = z.enum(['low', 'medium', 'high']);
8
15
 
@@ -1,4 +1,5 @@
1
1
  import type { AuthStateEntry } from './state';
2
+
2
3
  export type { AuthStateEntry } from './state';
3
4
 
4
5
  const RE = /^(\d+)([hms])$/;
@@ -1,6 +1,8 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import type { Stage } from '../eval/types';
4
+ import { summarizeCost } from '../graph/cost';
5
+ import { loadAllEvents } from '../graph/store';
4
6
  import { verifyPrompts } from './verify-prompts';
5
7
 
6
8
  export interface DoctorOpts {
@@ -124,6 +126,40 @@ export async function doctorCmd(_argv: string[], opts: DoctorOpts = {}): Promise
124
126
  ...checkPromptInjectionPreamble(repoRoot),
125
127
  ...checkRootScripts(repoRoot),
126
128
  ];
129
+ // Cost summary (past 7 days)
130
+ const cost = summarizeCost(repoRoot, 7);
131
+ if (cost.totalCalls > 0) {
132
+ console.log('');
133
+ console.log('LLM cost (past 7 days):');
134
+ console.log(` Total calls: ${cost.totalCalls}`);
135
+ console.log(` Estimated: $${cost.totalUsd.toFixed(2)} USD`);
136
+ const top = Object.entries(cost.bySkill).sort((a, b) => b[1].usd - a[1].usd)[0];
137
+ if (top)
138
+ console.log(` Top skill: ${top[0]} (${top[1].calls} calls, $${top[1].usd.toFixed(2)})`);
139
+ }
140
+
141
+ // Backfill detection
142
+ const xeraDir = join(repoRoot, '.xera');
143
+ if (existsSync(xeraDir)) {
144
+ const ticketDirs = readdirSync(xeraDir, { withFileTypes: true }).filter(
145
+ (e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name),
146
+ );
147
+ if (ticketDirs.length > 0) {
148
+ const events = loadAllEvents(repoRoot);
149
+ const fetchedTickets = new Set(
150
+ events.filter((e) => e.type === 'ticket.fetched').map((e) => e.payload.ticketId),
151
+ );
152
+ const unbackfilled = ticketDirs.map((d) => d.name).filter((t) => !fetchedTickets.has(t));
153
+ if (unbackfilled.length > 0) {
154
+ console.log('');
155
+ console.log(`⚠ Graph: ${unbackfilled.length} ticket(s) not yet in graph.`);
156
+ console.log(` These won't participate in v0.6.1+ features (TEST_OUTDATED, /xera-impact).`);
157
+ console.log(` Run: bun run xera:graph-backfill`);
158
+ console.log(` (Use --dry-run to preview.)`);
159
+ }
160
+ }
161
+ }
162
+
127
163
  if (results.length === 0) {
128
164
  console.log('[xera:doctor] ok');
129
165
  return 0;
@@ -2,8 +2,8 @@ import {
2
2
  copyFileSync,
3
3
  existsSync,
4
4
  mkdirSync,
5
- readFileSync,
6
5
  readdirSync,
6
+ readFileSync,
7
7
  writeFileSync,
8
8
  } from 'node:fs';
9
9
  import { join } from 'node:path';
@@ -0,0 +1,43 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { recordScriptImpl } from './graph-record-script';
4
+
5
+ async function backfillTicket(repoRoot: string, ticket: string, dryRun: boolean): Promise<number> {
6
+ // 1) Synthesize ticket.fetched from story.md (use story.md mtime)
7
+ const storyPath = join(repoRoot, '.xera', ticket, 'story.md');
8
+ if (!existsSync(storyPath)) return 0;
9
+
10
+ const { recordFetch } = await import('./graph-record');
11
+ // Re-use the same code path; in dry-run we don't actually call appendEvents.
12
+ // For simplicity in v0.6.0, dry-run lists ticket count and returns.
13
+ if (dryRun) {
14
+ console.log(`[backfill dry-run] would backfill ${ticket}`);
15
+ return 0;
16
+ }
17
+ // recordScriptImpl handles scenario/POM extraction.
18
+ await recordScriptImpl(repoRoot, ticket);
19
+ await recordFetch(repoRoot, ticket);
20
+ return 0;
21
+ }
22
+
23
+ export async function graphBackfillCmd(argv: string[]): Promise<number> {
24
+ const dryRun = argv.includes('--dry-run');
25
+ const repoRoot = process.cwd();
26
+ const xeraDir = join(repoRoot, '.xera');
27
+ if (!existsSync(xeraDir)) {
28
+ console.log('[backfill] no .xera/ directory');
29
+ return 0;
30
+ }
31
+ const tickets: string[] = [];
32
+ for (const entry of readdirSync(xeraDir, { withFileTypes: true })) {
33
+ if (!entry.isDirectory()) continue;
34
+ if (entry.name === 'graph') continue;
35
+ if (entry.name.startsWith('.')) continue;
36
+ if (!/^[A-Z]+-\d+$/.test(entry.name)) continue;
37
+ tickets.push(entry.name);
38
+ }
39
+ console.log(`[backfill] found ${tickets.length} tickets`);
40
+ for (const t of tickets) await backfillTicket(repoRoot, t, dryRun);
41
+ console.log(`[backfill] done`);
42
+ return 0;
43
+ }
@@ -0,0 +1,28 @@
1
+ import { enrichTicket } from '../graph/enrich';
2
+
3
+ export async function graphEnrichCmd(argv: string[]): Promise<number> {
4
+ let ticket: string | undefined;
5
+ let force = false;
6
+ for (let i = 0; i < argv.length; i++) {
7
+ if (argv[i] === '--ticket') ticket = argv[++i];
8
+ else if (argv[i] === '--force') force = true;
9
+ }
10
+
11
+ const repoRoot = process.cwd();
12
+
13
+ if (!ticket) {
14
+ console.error('[graph-enrich] usage: graph-enrich --ticket <id> [--force]');
15
+ return 1;
16
+ }
17
+
18
+ try {
19
+ const result = await enrichTicket(repoRoot, ticket, { force });
20
+ console.log(
21
+ `[graph-enrich] ${ticket} enriched (${result.similarCount} similar edges, at ${result.enrichedAt})`,
22
+ );
23
+ return 0;
24
+ } catch (e) {
25
+ console.error(`[graph-enrich] ${ticket} failed: ${(e as Error).message}`);
26
+ return 1;
27
+ }
28
+ }
@@ -0,0 +1,43 @@
1
+ import { deriveSnapshot, loadAllEvents } from '../graph/store';
2
+ import type { Snapshot } from '../graph/types';
3
+
4
+ function filterByTicket(snap: Snapshot, ticket: string): Snapshot {
5
+ const out: Snapshot = {
6
+ ...snap,
7
+ tickets: snap.tickets[ticket] ? { [ticket]: snap.tickets[ticket]! } : {},
8
+ scenarios: Object.fromEntries(
9
+ Object.entries(snap.scenarios).filter(([, s]) => s.ticketId === ticket),
10
+ ),
11
+ poms: Object.fromEntries(Object.entries(snap.poms).filter(([, p]) => p.ticketId === ticket)),
12
+ edges: snap.edges.filter((e) => e.from === ticket || e.to === ticket),
13
+ };
14
+ return out;
15
+ }
16
+
17
+ function renderText(snap: Snapshot): string {
18
+ const out: string[] = [];
19
+ out.push(`Graph snapshot — ${snap.event_count} events`);
20
+ out.push(`Tickets: ${Object.keys(snap.tickets).length}`);
21
+ out.push(`Scenarios: ${Object.keys(snap.scenarios).length}`);
22
+ out.push(`POMs: ${Object.keys(snap.poms).length}`);
23
+ out.push(`Edges: ${snap.edges.length}`);
24
+ for (const t of Object.values(snap.tickets)) {
25
+ out.push(` ${t.id} — ${t.summary}`);
26
+ }
27
+ return out.join('\n');
28
+ }
29
+
30
+ export async function graphQueryCmd(argv: string[]): Promise<number> {
31
+ let ticket: string | undefined;
32
+ let format: 'text' | 'json' = 'text';
33
+ for (let i = 0; i < argv.length; i++) {
34
+ if (argv[i] === '--ticket') ticket = argv[++i];
35
+ else if (argv[i] === '--format') format = argv[++i] as 'text' | 'json';
36
+ }
37
+ const repoRoot = process.cwd();
38
+ let snap = deriveSnapshot(loadAllEvents(repoRoot));
39
+ if (ticket) snap = filterByTicket(snap, ticket);
40
+ if (format === 'json') process.stdout.write(JSON.stringify(snap, null, 2));
41
+ else process.stdout.write(renderText(snap));
42
+ return 0;
43
+ }
@@ -0,0 +1,191 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ import { basename, join } from 'node:path';
4
+ import { appendEvents } from '../graph/store';
5
+ import type {
6
+ EdgeDiscoveredPayload,
7
+ Event,
8
+ PomGeneratedPayload,
9
+ ScenarioGeneratedPayload,
10
+ } from '../graph/types';
11
+ import { SCHEMA_VERSION } from '../graph/types';
12
+ import { ulid } from '../graph/ulid';
13
+
14
+ const sha1 = (s: string) => createHash('sha1').update(s).digest('hex');
15
+ const sId = (ticket: string, name: string) =>
16
+ sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, ' ')}`);
17
+ const pId = (file: string) => sha1(basename(file));
18
+ const nowIso = () => new Date().toISOString();
19
+ const mk = <T extends Event['type']>(
20
+ actor: string,
21
+ type: T,
22
+ payload: Extract<Event, { type: T }>['payload'],
23
+ ): Event =>
24
+ ({
25
+ event_id: ulid(),
26
+ schema_version: SCHEMA_VERSION,
27
+ ts: nowIso(),
28
+ actor,
29
+ type,
30
+ payload,
31
+ }) as Event;
32
+
33
+ function parseFeature(
34
+ text: string,
35
+ ): Array<{ name: string; priority: 'p0' | 'p1' | 'p2'; gherkin: string }> {
36
+ const scenarios: Array<{ name: string; priority: 'p0' | 'p1' | 'p2'; gherkin: string }> = [];
37
+ const lines = text.split('\n');
38
+ let currentTagPriority: 'p0' | 'p1' | 'p2' = 'p1';
39
+ let i = 0;
40
+ while (i < lines.length) {
41
+ const line = lines[i]!.trim();
42
+ if (line.startsWith('@')) {
43
+ const tag = line.slice(1).split(/\s+/)[0]!.toLowerCase();
44
+ if (tag === 'p0' || tag === 'p1' || tag === 'p2') currentTagPriority = tag;
45
+ i++;
46
+ continue;
47
+ }
48
+ if (line.startsWith('Scenario:') || line.startsWith('Scenario Outline:')) {
49
+ const name = line.replace(/^Scenario( Outline)?:\s*/, '');
50
+ const start = i;
51
+ i++;
52
+ while (
53
+ i < lines.length &&
54
+ !lines[i]!.trim().startsWith('Scenario') &&
55
+ !lines[i]!.trim().startsWith('@')
56
+ )
57
+ i++;
58
+ scenarios.push({
59
+ name,
60
+ priority: currentTagPriority,
61
+ gherkin: lines.slice(start, i).join('\n'),
62
+ });
63
+ currentTagPriority = 'p1';
64
+ continue;
65
+ }
66
+ i++;
67
+ }
68
+ return scenarios;
69
+ }
70
+
71
+ function listPomFiles(dir: string): string[] {
72
+ if (!existsSync(dir)) return [];
73
+ return readdirSync(dir)
74
+ .filter((f) => f.endsWith('.ts'))
75
+ .map((f) => join(dir, f));
76
+ }
77
+
78
+ function extractRoute(pomContent: string): string {
79
+ const m = pomContent.match(/goto\s*\(\s*['"]([^'"]+)['"]/);
80
+ return m ? m[1]! : '';
81
+ }
82
+
83
+ function extractLocators(pomContent: string): string[] {
84
+ const out: string[] = [];
85
+ const re = /\b(getByRole|getByLabel|getByText|getByTestId|locator)\s*\(\s*([^)]+)\)/g;
86
+ let m = re.exec(pomContent);
87
+ while (m !== null) {
88
+ out.push(`${m[1]}(${m[2]})`);
89
+ m = re.exec(pomContent);
90
+ }
91
+ return out;
92
+ }
93
+
94
+ function extractPomUsage(specContent: string): string[] {
95
+ const names = new Set<string>();
96
+ const re = /new\s+([A-Z][A-Za-z0-9]*Page)\s*\(/g;
97
+ let m = re.exec(specContent);
98
+ while (m !== null) {
99
+ names.add(m[1]!);
100
+ m = re.exec(specContent);
101
+ }
102
+ return [...names];
103
+ }
104
+
105
+ export async function recordScriptImpl(repoRoot: string, ticket: string): Promise<number> {
106
+ const ticketDir = join(repoRoot, '.xera', ticket);
107
+ const featurePath = join(ticketDir, 'feature', `${ticket}.feature`);
108
+ const specPath = join(ticketDir, 'tests', `${ticket}.spec.ts`);
109
+ const pomDir = join(ticketDir, 'poms');
110
+
111
+ if (!existsSync(featurePath)) {
112
+ console.error(`[graph-record script] feature missing`);
113
+ return 1;
114
+ }
115
+
116
+ const featureText = readFileSync(featurePath, 'utf8');
117
+ const featureHash = sha1(featureText);
118
+ const scenarios = parseFeature(featureText);
119
+
120
+ const events: Event[] = [];
121
+ for (const s of scenarios) {
122
+ const id = sId(ticket, s.name);
123
+ const p: ScenarioGeneratedPayload = {
124
+ scenarioId: id,
125
+ ticketId: ticket,
126
+ name: s.name,
127
+ gherkin: s.gherkin,
128
+ priority: s.priority,
129
+ featureHash,
130
+ generatedAt: nowIso(),
131
+ };
132
+ events.push(mk('xera-script', 'scenario.generated', p));
133
+ }
134
+
135
+ const pomFiles = listPomFiles(pomDir);
136
+ const pomNameToId = new Map<string, string>();
137
+ for (const pomFile of pomFiles) {
138
+ const content = readFileSync(pomFile, 'utf8');
139
+ const id = pId(pomFile);
140
+ const className = content.match(/export\s+class\s+([A-Z][A-Za-z0-9]*Page)/)?.[1] ?? '';
141
+ pomNameToId.set(className, id);
142
+ const pg: PomGeneratedPayload = {
143
+ pomId: id,
144
+ ticketId: ticket,
145
+ filePath: pomFile.replace(`${repoRoot}/`, ''),
146
+ route: extractRoute(content),
147
+ locators: extractLocators(content),
148
+ scope: 'local',
149
+ };
150
+ events.push(mk('xera-script', 'pom.generated', pg));
151
+ }
152
+
153
+ if (existsSync(specPath)) {
154
+ const specContent = readFileSync(specPath, 'utf8');
155
+ const usedPoms = extractPomUsage(specContent);
156
+ for (const scenario of scenarios) {
157
+ const scId = sId(ticket, scenario.name);
158
+ for (const pomName of usedPoms) {
159
+ const pid = pomNameToId.get(pomName);
160
+ if (!pid) continue;
161
+ const ep: EdgeDiscoveredPayload = {
162
+ kind: 'uses',
163
+ from: scId,
164
+ to: pid,
165
+ source: 'xera-script',
166
+ };
167
+ events.push(mk('xera-script', 'edge.discovered', ep));
168
+ }
169
+ }
170
+ }
171
+
172
+ for (const [, id] of pomNameToId) {
173
+ const pom = events.find(
174
+ (e) => e.type === 'pom.generated' && (e.payload as PomGeneratedPayload).pomId === id,
175
+ );
176
+ if (!pom) continue;
177
+ const route = (pom.payload as PomGeneratedPayload).route;
178
+ if (!route) continue;
179
+ const slug =
180
+ route
181
+ .replace(/^\//, '')
182
+ .split('/')[0]!
183
+ .replace(/[^a-z0-9-]/gi, '-')
184
+ .toLowerCase() || 'root';
185
+ const ep: EdgeDiscoveredPayload = { kind: 'covers', from: id, to: slug, source: 'xera-script' };
186
+ events.push(mk('xera-script', 'edge.discovered', ep));
187
+ }
188
+
189
+ appendEvents(repoRoot, events, { skill: 'xera-script', ticketId: ticket });
190
+ return 0;
191
+ }