@xera-ai/core 0.3.0 → 0.4.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 (231) 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 +71 -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 +8351 -369
  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-query.d.ts +2 -0
  24. package/dist/bin-internal/graph-query.d.ts.map +1 -0
  25. package/dist/bin-internal/graph-record-script.d.ts +2 -0
  26. package/dist/bin-internal/graph-record-script.d.ts.map +1 -0
  27. package/dist/bin-internal/graph-record.d.ts +3 -0
  28. package/dist/bin-internal/graph-record.d.ts.map +1 -0
  29. package/dist/bin-internal/graph-snapshot.d.ts +2 -0
  30. package/dist/bin-internal/graph-snapshot.d.ts.map +1 -0
  31. package/dist/bin-internal/heal-prepare.d.ts.map +1 -0
  32. package/dist/bin-internal/index.d.ts.map +1 -0
  33. package/dist/bin-internal/lint.d.ts.map +1 -0
  34. package/dist/bin-internal/normalize.d.ts.map +1 -0
  35. package/dist/bin-internal/post.d.ts.map +1 -0
  36. package/dist/bin-internal/promote.d.ts.map +1 -0
  37. package/dist/bin-internal/report.d.ts.map +1 -0
  38. package/dist/bin-internal/status-cmd.d.ts.map +1 -0
  39. package/dist/bin-internal/typecheck.d.ts.map +1 -0
  40. package/dist/bin-internal/unlock.d.ts.map +1 -0
  41. package/dist/bin-internal/validate-feature.d.ts.map +1 -0
  42. package/dist/bin-internal/verify-prompts.d.ts.map +1 -0
  43. package/dist/classifier/aggregate.d.ts.map +1 -0
  44. package/dist/classifier/history.d.ts.map +1 -0
  45. package/dist/classifier/types.d.ts.map +1 -0
  46. package/dist/config/define.d.ts.map +1 -0
  47. package/dist/config/load.d.ts.map +1 -0
  48. package/dist/config/schema.d.ts +66 -0
  49. package/dist/config/schema.d.ts.map +1 -0
  50. package/dist/eval/paths.d.ts.map +1 -0
  51. package/dist/eval/run-id.d.ts.map +1 -0
  52. package/dist/eval/types.d.ts +203 -0
  53. package/dist/eval/types.d.ts.map +1 -0
  54. package/dist/graph/cost.d.ts +21 -0
  55. package/dist/graph/cost.d.ts.map +1 -0
  56. package/dist/graph/index.d.ts +8 -0
  57. package/dist/graph/index.d.ts.map +1 -0
  58. package/dist/graph/paths.d.ts +10 -0
  59. package/dist/graph/paths.d.ts.map +1 -0
  60. package/dist/graph/schema.d.ts +177 -0
  61. package/dist/graph/schema.d.ts.map +1 -0
  62. package/dist/graph/store.d.ts +14 -0
  63. package/dist/graph/store.d.ts.map +1 -0
  64. package/dist/graph/types.d.ts +151 -0
  65. package/dist/graph/types.d.ts.map +1 -0
  66. package/dist/graph/ulid.d.ts +2 -0
  67. package/dist/graph/ulid.d.ts.map +1 -0
  68. package/dist/{core/src/index.d.ts → index.d.ts} +11 -11
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/jira/client.d.ts.map +1 -0
  71. package/dist/jira/fields.d.ts.map +1 -0
  72. package/dist/jira/mcp-backend.d.ts.map +1 -0
  73. package/dist/jira/rest-backend.d.ts.map +1 -0
  74. package/dist/jira/retry.d.ts.map +1 -0
  75. package/dist/jira/types.d.ts.map +1 -0
  76. package/dist/lock/file-lock.d.ts.map +1 -0
  77. package/dist/logging/ndjson-logger.d.ts.map +1 -0
  78. package/dist/reporter/jira-comment.d.ts.map +1 -0
  79. package/dist/reporter/status-writer.d.ts.map +1 -0
  80. package/dist/src/index.js +339 -318
  81. package/package.json +19 -14
  82. package/src/auth/refresh.ts +1 -0
  83. package/src/bin-internal/doctor.ts +37 -1
  84. package/src/bin-internal/eval-prepare.ts +1 -1
  85. package/src/bin-internal/graph-backfill.ts +43 -0
  86. package/src/bin-internal/graph-query.ts +43 -0
  87. package/src/bin-internal/graph-record-script.ts +191 -0
  88. package/src/bin-internal/graph-record.ts +243 -0
  89. package/src/bin-internal/graph-snapshot.ts +23 -0
  90. package/src/bin-internal/heal-prepare.ts +1 -1
  91. package/src/bin-internal/index.ts +8 -0
  92. package/src/bin-internal/verify-prompts.ts +1 -0
  93. package/src/config/schema.ts +6 -6
  94. package/src/graph/cost.ts +59 -0
  95. package/src/graph/index.ts +15 -0
  96. package/src/graph/paths.ts +27 -0
  97. package/src/graph/schema.ts +135 -0
  98. package/src/graph/store.ts +231 -0
  99. package/src/graph/types.ts +174 -0
  100. package/src/graph/ulid.ts +58 -0
  101. package/src/index.ts +11 -11
  102. package/src/jira/rest-backend.ts +1 -1
  103. package/src/reporter/status-writer.ts +1 -1
  104. package/dist/core/src/adapter/types.d.ts.map +0 -1
  105. package/dist/core/src/artifact/hash.d.ts.map +0 -1
  106. package/dist/core/src/artifact/meta.d.ts +0 -46
  107. package/dist/core/src/artifact/meta.d.ts.map +0 -1
  108. package/dist/core/src/artifact/paths.d.ts.map +0 -1
  109. package/dist/core/src/artifact/status.d.ts +0 -96
  110. package/dist/core/src/artifact/status.d.ts.map +0 -1
  111. package/dist/core/src/auth/encrypt.d.ts.map +0 -1
  112. package/dist/core/src/auth/key.d.ts.map +0 -1
  113. package/dist/core/src/auth/refresh.d.ts.map +0 -1
  114. package/dist/core/src/auth/state.d.ts.map +0 -1
  115. package/dist/core/src/bin-internal/doctor.d.ts.map +0 -1
  116. package/dist/core/src/bin-internal/eval-deterministic.d.ts.map +0 -1
  117. package/dist/core/src/bin-internal/eval-prepare.d.ts.map +0 -1
  118. package/dist/core/src/bin-internal/eval-report.d.ts.map +0 -1
  119. package/dist/core/src/bin-internal/exec.d.ts.map +0 -1
  120. package/dist/core/src/bin-internal/fetch.d.ts.map +0 -1
  121. package/dist/core/src/bin-internal/heal-prepare.d.ts.map +0 -1
  122. package/dist/core/src/bin-internal/index.d.ts.map +0 -1
  123. package/dist/core/src/bin-internal/lint.d.ts.map +0 -1
  124. package/dist/core/src/bin-internal/normalize.d.ts.map +0 -1
  125. package/dist/core/src/bin-internal/post.d.ts.map +0 -1
  126. package/dist/core/src/bin-internal/promote.d.ts.map +0 -1
  127. package/dist/core/src/bin-internal/report.d.ts.map +0 -1
  128. package/dist/core/src/bin-internal/status-cmd.d.ts.map +0 -1
  129. package/dist/core/src/bin-internal/typecheck.d.ts.map +0 -1
  130. package/dist/core/src/bin-internal/unlock.d.ts.map +0 -1
  131. package/dist/core/src/bin-internal/validate-feature.d.ts.map +0 -1
  132. package/dist/core/src/bin-internal/verify-prompts.d.ts.map +0 -1
  133. package/dist/core/src/classifier/aggregate.d.ts.map +0 -1
  134. package/dist/core/src/classifier/history.d.ts.map +0 -1
  135. package/dist/core/src/classifier/types.d.ts.map +0 -1
  136. package/dist/core/src/config/define.d.ts.map +0 -1
  137. package/dist/core/src/config/load.d.ts.map +0 -1
  138. package/dist/core/src/config/schema.d.ts +0 -326
  139. package/dist/core/src/config/schema.d.ts.map +0 -1
  140. package/dist/core/src/eval/paths.d.ts.map +0 -1
  141. package/dist/core/src/eval/run-id.d.ts.map +0 -1
  142. package/dist/core/src/eval/types.d.ts +0 -551
  143. package/dist/core/src/eval/types.d.ts.map +0 -1
  144. package/dist/core/src/index.d.ts.map +0 -1
  145. package/dist/core/src/jira/client.d.ts.map +0 -1
  146. package/dist/core/src/jira/fields.d.ts.map +0 -1
  147. package/dist/core/src/jira/mcp-backend.d.ts.map +0 -1
  148. package/dist/core/src/jira/rest-backend.d.ts.map +0 -1
  149. package/dist/core/src/jira/retry.d.ts.map +0 -1
  150. package/dist/core/src/jira/types.d.ts.map +0 -1
  151. package/dist/core/src/lock/file-lock.d.ts.map +0 -1
  152. package/dist/core/src/logging/ndjson-logger.d.ts.map +0 -1
  153. package/dist/core/src/reporter/jira-comment.d.ts.map +0 -1
  154. package/dist/core/src/reporter/status-writer.d.ts.map +0 -1
  155. package/dist/web/src/adapter.d.ts +0 -3
  156. package/dist/web/src/adapter.d.ts.map +0 -1
  157. package/dist/web/src/auth-setup/define.d.ts +0 -16
  158. package/dist/web/src/auth-setup/define.d.ts.map +0 -1
  159. package/dist/web/src/auth-setup/playwright-state.d.ts +0 -2
  160. package/dist/web/src/auth-setup/playwright-state.d.ts.map +0 -1
  161. package/dist/web/src/auth-setup/runner.d.ts +0 -12
  162. package/dist/web/src/auth-setup/runner.d.ts.map +0 -1
  163. package/dist/web/src/executor/index.d.ts +0 -18
  164. package/dist/web/src/executor/index.d.ts.map +0 -1
  165. package/dist/web/src/executor/playwright-args.d.ts +0 -7
  166. package/dist/web/src/executor/playwright-args.d.ts.map +0 -1
  167. package/dist/web/src/generator/gherkin-validate.d.ts +0 -9
  168. package/dist/web/src/generator/gherkin-validate.d.ts.map +0 -1
  169. package/dist/web/src/generator/lint.d.ts +0 -9
  170. package/dist/web/src/generator/lint.d.ts.map +0 -1
  171. package/dist/web/src/generator/pom-scan.d.ts +0 -6
  172. package/dist/web/src/generator/pom-scan.d.ts.map +0 -1
  173. package/dist/web/src/generator/promote.d.ts +0 -7
  174. package/dist/web/src/generator/promote.d.ts.map +0 -1
  175. package/dist/web/src/generator/selector-rules.d.ts +0 -10
  176. package/dist/web/src/generator/selector-rules.d.ts.map +0 -1
  177. package/dist/web/src/generator/typecheck.d.ts +0 -11
  178. package/dist/web/src/generator/typecheck.d.ts.map +0 -1
  179. package/dist/web/src/index.d.ts +0 -18
  180. package/dist/web/src/index.d.ts.map +0 -1
  181. package/dist/web/src/trace-normalizer/normalize.d.ts +0 -7
  182. package/dist/web/src/trace-normalizer/normalize.d.ts.map +0 -1
  183. package/dist/web/src/trace-normalizer/parse.d.ts +0 -37
  184. package/dist/web/src/trace-normalizer/parse.d.ts.map +0 -1
  185. package/dist/web/src/trace-normalizer/scrub-rules.d.ts +0 -12
  186. package/dist/web/src/trace-normalizer/scrub-rules.d.ts.map +0 -1
  187. package/dist/web/src/trace-normalizer/scrub.d.ts +0 -29
  188. package/dist/web/src/trace-normalizer/scrub.d.ts.map +0 -1
  189. package/dist/web/src/trace-normalizer/unzip.d.ts +0 -6
  190. package/dist/web/src/trace-normalizer/unzip.d.ts.map +0 -1
  191. /package/dist/{core/src/adapter → adapter}/types.d.ts +0 -0
  192. /package/dist/{core/src/artifact → artifact}/hash.d.ts +0 -0
  193. /package/dist/{core/src/artifact → artifact}/paths.d.ts +0 -0
  194. /package/dist/{core/src/auth → auth}/encrypt.d.ts +0 -0
  195. /package/dist/{core/src/auth → auth}/key.d.ts +0 -0
  196. /package/dist/{core/src/auth → auth}/refresh.d.ts +0 -0
  197. /package/dist/{core/src/bin-internal → bin-internal}/doctor.d.ts +0 -0
  198. /package/dist/{core/src/bin-internal → bin-internal}/eval-deterministic.d.ts +0 -0
  199. /package/dist/{core/src/bin-internal → bin-internal}/eval-prepare.d.ts +0 -0
  200. /package/dist/{core/src/bin-internal → bin-internal}/eval-report.d.ts +0 -0
  201. /package/dist/{core/src/bin-internal → bin-internal}/exec.d.ts +0 -0
  202. /package/dist/{core/src/bin-internal → bin-internal}/fetch.d.ts +0 -0
  203. /package/dist/{core/src/bin-internal → bin-internal}/heal-prepare.d.ts +0 -0
  204. /package/dist/{core/src/bin-internal → bin-internal}/index.d.ts +0 -0
  205. /package/dist/{core/src/bin-internal → bin-internal}/lint.d.ts +0 -0
  206. /package/dist/{core/src/bin-internal → bin-internal}/normalize.d.ts +0 -0
  207. /package/dist/{core/src/bin-internal → bin-internal}/post.d.ts +0 -0
  208. /package/dist/{core/src/bin-internal → bin-internal}/promote.d.ts +0 -0
  209. /package/dist/{core/src/bin-internal → bin-internal}/report.d.ts +0 -0
  210. /package/dist/{core/src/bin-internal → bin-internal}/status-cmd.d.ts +0 -0
  211. /package/dist/{core/src/bin-internal → bin-internal}/typecheck.d.ts +0 -0
  212. /package/dist/{core/src/bin-internal → bin-internal}/unlock.d.ts +0 -0
  213. /package/dist/{core/src/bin-internal → bin-internal}/validate-feature.d.ts +0 -0
  214. /package/dist/{core/src/bin-internal → bin-internal}/verify-prompts.d.ts +0 -0
  215. /package/dist/{core/src/classifier → classifier}/aggregate.d.ts +0 -0
  216. /package/dist/{core/src/classifier → classifier}/history.d.ts +0 -0
  217. /package/dist/{core/src/classifier → classifier}/types.d.ts +0 -0
  218. /package/dist/{core/src/config → config}/define.d.ts +0 -0
  219. /package/dist/{core/src/config → config}/load.d.ts +0 -0
  220. /package/dist/{core/src/eval → eval}/paths.d.ts +0 -0
  221. /package/dist/{core/src/eval → eval}/run-id.d.ts +0 -0
  222. /package/dist/{core/src/jira → jira}/client.d.ts +0 -0
  223. /package/dist/{core/src/jira → jira}/fields.d.ts +0 -0
  224. /package/dist/{core/src/jira → jira}/mcp-backend.d.ts +0 -0
  225. /package/dist/{core/src/jira → jira}/rest-backend.d.ts +0 -0
  226. /package/dist/{core/src/jira → jira}/retry.d.ts +0 -0
  227. /package/dist/{core/src/jira → jira}/types.d.ts +0 -0
  228. /package/dist/{core/src/lock → lock}/file-lock.d.ts +0 -0
  229. /package/dist/{core/src/logging → logging}/ndjson-logger.d.ts +0 -0
  230. /package/dist/{core/src/reporter → reporter}/jira-comment.d.ts +0 -0
  231. /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.0",
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
  }
@@ -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,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
+ }
@@ -0,0 +1,243 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { basename, join } from 'node:path';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { appendEvents } from '../graph/store';
6
+ import type {
7
+ EdgeDiscoveredPayload,
8
+ Event,
9
+ PomPromotedPayload,
10
+ RunClassifiedPayload,
11
+ RunCompletedPayload,
12
+ TicketFetchedPayload,
13
+ } from '../graph/types';
14
+ import { SCHEMA_VERSION } from '../graph/types';
15
+ import { ulid } from '../graph/ulid';
16
+
17
+ function nowIso(): string {
18
+ return new Date().toISOString();
19
+ }
20
+ function sha1(s: string): string {
21
+ return createHash('sha1').update(s).digest('hex');
22
+ }
23
+ function scenarioId(ticket: string, name: string): string {
24
+ return sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, ' ')}`);
25
+ }
26
+ function pomId(filePath: string): string {
27
+ return sha1(basename(filePath));
28
+ }
29
+ function makeEvent<T extends Event['type']>(
30
+ actor: string,
31
+ type: T,
32
+ payload: Extract<Event, { type: T }>['payload'],
33
+ ): Event {
34
+ return {
35
+ event_id: ulid(),
36
+ schema_version: SCHEMA_VERSION,
37
+ ts: nowIso(),
38
+ actor,
39
+ type,
40
+ payload,
41
+ } as Event;
42
+ }
43
+
44
+ interface StoryFrontmatter {
45
+ ticketId: string;
46
+ summary: string;
47
+ storyHash: string;
48
+ acceptanceCriteria?: string[];
49
+ linked_issues?: Array<{
50
+ ticketId: string;
51
+ relation: 'blocks' | 'duplicates' | 'relates' | 'supersedes';
52
+ }>;
53
+ }
54
+
55
+ function readStoryFrontmatter(repoRoot: string, ticket: string): StoryFrontmatter | null {
56
+ const path = join(repoRoot, '.xera', ticket, 'story.md');
57
+ if (!existsSync(path)) return null;
58
+ const raw = readFileSync(path, 'utf8');
59
+ const m = raw.match(/^---\n([\s\S]*?)\n---/);
60
+ if (!m) return null;
61
+ return parseYaml(m[1]!) as StoryFrontmatter;
62
+ }
63
+
64
+ function readGraphInput(repoRoot: string, ticket: string): { modifiesAreas: string[] } {
65
+ const path = join(repoRoot, '.xera', ticket, 'graph-input.json');
66
+ if (!existsSync(path)) return { modifiesAreas: [] };
67
+ try {
68
+ return JSON.parse(readFileSync(path, 'utf8'));
69
+ } catch {
70
+ return { modifiesAreas: [] };
71
+ }
72
+ }
73
+
74
+ export async function recordFetch(repoRoot: string, ticket: string): Promise<number> {
75
+ const fm = readStoryFrontmatter(repoRoot, ticket);
76
+ if (!fm) {
77
+ console.error(`[graph-record fetch] story.md not found for ${ticket}`);
78
+ return 1;
79
+ }
80
+ const { modifiesAreas } = readGraphInput(repoRoot, ticket);
81
+ const events: Event[] = [];
82
+ const fetchedPayload: TicketFetchedPayload = {
83
+ ticketId: fm.ticketId,
84
+ summary: fm.summary,
85
+ ac: fm.acceptanceCriteria ?? [],
86
+ jiraLinks: fm.linked_issues ?? [],
87
+ storyHash: fm.storyHash,
88
+ modifiesAreas,
89
+ };
90
+ events.push(makeEvent('xera-fetch', 'ticket.fetched', fetchedPayload));
91
+ for (const link of fm.linked_issues ?? []) {
92
+ const p: EdgeDiscoveredPayload = {
93
+ kind: 'jira-linked',
94
+ from: fm.ticketId,
95
+ to: link.ticketId,
96
+ source: `jira:${link.relation}`,
97
+ };
98
+ events.push(makeEvent('xera-fetch', 'edge.discovered', p));
99
+ }
100
+ for (const area of modifiesAreas) {
101
+ const p: EdgeDiscoveredPayload = {
102
+ kind: 'modifies',
103
+ from: fm.ticketId,
104
+ to: area,
105
+ source: 'extract-areas',
106
+ };
107
+ events.push(makeEvent('xera-fetch', 'edge.discovered', p));
108
+ }
109
+ appendEvents(repoRoot, events, { skill: 'xera-fetch', ticketId: ticket });
110
+ return 0;
111
+ }
112
+
113
+ async function recordScript(repoRoot: string, ticket: string): Promise<number> {
114
+ const { recordScriptImpl } = await import('./graph-record-script');
115
+ return recordScriptImpl(repoRoot, ticket);
116
+ }
117
+
118
+ async function recordExec(repoRoot: string, ticket: string, runId: string): Promise<number> {
119
+ const reporterPath = join(repoRoot, '.xera', ticket, 'runs', runId, 'reporter.json');
120
+ if (!existsSync(reporterPath)) {
121
+ console.error(`[graph-record exec] reporter.json missing`);
122
+ return 1;
123
+ }
124
+ const data = JSON.parse(readFileSync(reporterPath, 'utf8')) as {
125
+ scenarios: Array<{ name: string; status: 'pass' | 'fail'; runtime: number; traceId?: string }>;
126
+ };
127
+ const events: Event[] = [];
128
+ for (const s of data.scenarios) {
129
+ const p: RunCompletedPayload = {
130
+ scenarioId: scenarioId(ticket, s.name),
131
+ ticketId: ticket,
132
+ runId,
133
+ status: s.status,
134
+ runtime: s.runtime,
135
+ };
136
+ if (s.traceId) p.traceId = s.traceId;
137
+ events.push(makeEvent('xera-exec', 'run.completed', p));
138
+ }
139
+ appendEvents(repoRoot, events, { skill: 'xera-exec', ticketId: ticket });
140
+ return 0;
141
+ }
142
+
143
+ async function recordClassify(repoRoot: string, ticket: string, runId: string): Promise<number> {
144
+ const classifyPath = join(repoRoot, '.xera', ticket, 'runs', runId, 'classifier-output.json');
145
+ if (!existsSync(classifyPath)) {
146
+ console.error(`[graph-record classify] classifier-output.json missing`);
147
+ return 1;
148
+ }
149
+ const data = JSON.parse(readFileSync(classifyPath, 'utf8')) as {
150
+ scenarios: Array<{ name: string; class: string; confidence: 'low' | 'medium' | 'high' }>;
151
+ };
152
+ const events: Event[] = [];
153
+ for (const s of data.scenarios) {
154
+ const p: RunClassifiedPayload = {
155
+ scenarioId: scenarioId(ticket, s.name),
156
+ runId,
157
+ classification: s.class as RunClassifiedPayload['classification'],
158
+ confidence: s.confidence,
159
+ };
160
+ events.push(makeEvent('xera-report', 'run.classified', p));
161
+ }
162
+ appendEvents(repoRoot, events, { skill: 'xera-report', ticketId: ticket });
163
+ return 0;
164
+ }
165
+
166
+ async function recordPromote(repoRoot: string, args: Map<string, string>): Promise<number> {
167
+ const from = args.get('--from');
168
+ const to = args.get('--to');
169
+ const pomIdArg = args.get('--pom-id');
170
+ if (!from || !to) {
171
+ console.error(`[graph-record promote] --from and --to required`);
172
+ return 1;
173
+ }
174
+ const id = pomIdArg ?? pomId(from);
175
+ const p: PomPromotedPayload = { pomId: id, fromPath: from, toPath: to };
176
+ const e = makeEvent('xera-promote', 'pom.promoted', p);
177
+ appendEvents(repoRoot, [e], { skill: 'xera-promote', ticketId: 'shared' });
178
+ return 0;
179
+ }
180
+
181
+ function parseFlags(args: string[]): Map<string, string> {
182
+ const m = new Map<string, string>();
183
+ for (let i = 0; i < args.length; i++) {
184
+ if (args[i]!.startsWith('--')) {
185
+ m.set(args[i]!, args[i + 1] ?? '');
186
+ i++;
187
+ }
188
+ }
189
+ return m;
190
+ }
191
+
192
+ export async function graphRecordCmd(argv: string[]): Promise<number> {
193
+ const [action, ...rest] = argv;
194
+ if (!action) {
195
+ console.error(`Usage: xera-internal graph-record <fetch|script|exec|classify|promote> [args]`);
196
+ return 1;
197
+ }
198
+ const repoRoot = process.cwd();
199
+ switch (action) {
200
+ case 'fetch': {
201
+ const ticket = rest[0];
202
+ if (!ticket) {
203
+ console.error('ticket required');
204
+ return 1;
205
+ }
206
+ return recordFetch(repoRoot, ticket);
207
+ }
208
+ case 'script': {
209
+ const ticket = rest[0];
210
+ if (!ticket) {
211
+ console.error('ticket required');
212
+ return 1;
213
+ }
214
+ return recordScript(repoRoot, ticket);
215
+ }
216
+ case 'exec': {
217
+ const ticket = rest[0];
218
+ const flags = parseFlags(rest);
219
+ const runId = flags.get('--run-id');
220
+ if (!ticket || !runId) {
221
+ console.error('ticket + --run-id required');
222
+ return 1;
223
+ }
224
+ return recordExec(repoRoot, ticket, runId);
225
+ }
226
+ case 'classify': {
227
+ const ticket = rest[0];
228
+ const flags = parseFlags(rest);
229
+ const runId = flags.get('--run-id');
230
+ if (!ticket || !runId) {
231
+ console.error('ticket + --run-id required');
232
+ return 1;
233
+ }
234
+ return recordClassify(repoRoot, ticket, runId);
235
+ }
236
+ case 'promote': {
237
+ return recordPromote(repoRoot, parseFlags(rest));
238
+ }
239
+ default:
240
+ console.error(`Unknown action: ${action}`);
241
+ return 1;
242
+ }
243
+ }
@@ -0,0 +1,23 @@
1
+ import { deriveSnapshot, isSnapshotStale, loadAllEvents, writeSnapshot } from '../graph/store';
2
+
3
+ export async function graphSnapshotCmd(argv: string[]): Promise<number> {
4
+ const check = argv.includes('--check');
5
+ const noRebuild = argv.includes('--no-rebuild');
6
+ const repoRoot = process.cwd();
7
+ const stale = isSnapshotStale(repoRoot);
8
+ if (check) {
9
+ if (!stale) return 0;
10
+ if (noRebuild) {
11
+ console.error('[graph-snapshot] stale');
12
+ return 1;
13
+ }
14
+ // fall through to rebuild
15
+ }
16
+ const events = loadAllEvents(repoRoot);
17
+ const snap = deriveSnapshot(events);
18
+ writeSnapshot(repoRoot, snap);
19
+ if (check && stale) {
20
+ console.log(`[graph-snapshot] rebuilt (${events.length} events)`);
21
+ }
22
+ return 0;
23
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { scrubFreeText } from '@xera-ai/web';
4
4
  import { unzipSync } from 'fflate';
@@ -4,6 +4,10 @@ import { evalPrepareCmd } from './eval-prepare';
4
4
  import { evalReportCmd } from './eval-report';
5
5
  import { execCmd } from './exec';
6
6
  import { fetchCmd } from './fetch';
7
+ import { graphBackfillCmd } from './graph-backfill';
8
+ import { graphQueryCmd } from './graph-query';
9
+ import { graphRecordCmd } from './graph-record';
10
+ import { graphSnapshotCmd } from './graph-snapshot';
7
11
  import { healPrepareCmd } from './heal-prepare';
8
12
  import { lintCmd } from './lint';
9
13
  import { normalizeCmd } from './normalize';
@@ -23,6 +27,10 @@ const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
23
27
  'eval-report': evalReportCmd,
24
28
  exec: execCmd,
25
29
  fetch: fetchCmd,
30
+ 'graph-backfill': graphBackfillCmd,
31
+ 'graph-query': graphQueryCmd,
32
+ 'graph-record': graphRecordCmd,
33
+ 'graph-snapshot': graphSnapshotCmd,
26
34
  'heal-prepare': healPrepareCmd,
27
35
  lint: lintCmd,
28
36
  normalize: normalizeCmd,