@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
@@ -0,0 +1,287 @@
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
+ Classification,
8
+ ClassificationDisputedPayload,
9
+ EdgeDiscoveredPayload,
10
+ Event,
11
+ PomPromotedPayload,
12
+ RunClassifiedPayload,
13
+ RunCompletedPayload,
14
+ TicketFetchedPayload,
15
+ } from '../graph/types';
16
+ import { SCHEMA_VERSION } from '../graph/types';
17
+ import { ulid } from '../graph/ulid';
18
+
19
+ function nowIso(): string {
20
+ return new Date().toISOString();
21
+ }
22
+ function sha1(s: string): string {
23
+ return createHash('sha1').update(s).digest('hex');
24
+ }
25
+ function scenarioId(ticket: string, name: string): string {
26
+ return sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, ' ')}`);
27
+ }
28
+ function pomId(filePath: string): string {
29
+ return sha1(basename(filePath));
30
+ }
31
+ function makeEvent<T extends Event['type']>(
32
+ actor: string,
33
+ type: T,
34
+ payload: Extract<Event, { type: T }>['payload'],
35
+ ): Event {
36
+ return {
37
+ event_id: ulid(),
38
+ schema_version: SCHEMA_VERSION,
39
+ ts: nowIso(),
40
+ actor,
41
+ type,
42
+ payload,
43
+ } as Event;
44
+ }
45
+
46
+ interface StoryFrontmatter {
47
+ ticketId: string;
48
+ summary: string;
49
+ storyHash: string;
50
+ acceptanceCriteria?: string[];
51
+ linked_issues?: Array<{
52
+ ticketId: string;
53
+ relation: 'blocks' | 'duplicates' | 'relates' | 'supersedes';
54
+ }>;
55
+ }
56
+
57
+ function readStoryFrontmatter(repoRoot: string, ticket: string): StoryFrontmatter | null {
58
+ const path = join(repoRoot, '.xera', ticket, 'story.md');
59
+ if (!existsSync(path)) return null;
60
+ const raw = readFileSync(path, 'utf8');
61
+ const m = raw.match(/^---\n([\s\S]*?)\n---/);
62
+ if (!m) return null;
63
+ return parseYaml(m[1]!) as StoryFrontmatter;
64
+ }
65
+
66
+ function readGraphInput(repoRoot: string, ticket: string): { modifiesAreas: string[] } {
67
+ const path = join(repoRoot, '.xera', ticket, 'graph-input.json');
68
+ if (!existsSync(path)) return { modifiesAreas: [] };
69
+ try {
70
+ return JSON.parse(readFileSync(path, 'utf8'));
71
+ } catch {
72
+ return { modifiesAreas: [] };
73
+ }
74
+ }
75
+
76
+ export async function recordFetch(repoRoot: string, ticket: string): Promise<number> {
77
+ const fm = readStoryFrontmatter(repoRoot, ticket);
78
+ if (!fm) {
79
+ console.error(`[graph-record fetch] story.md not found for ${ticket}`);
80
+ return 1;
81
+ }
82
+ const { modifiesAreas } = readGraphInput(repoRoot, ticket);
83
+ const events: Event[] = [];
84
+ const fetchedPayload: TicketFetchedPayload = {
85
+ ticketId: fm.ticketId,
86
+ summary: fm.summary,
87
+ ac: fm.acceptanceCriteria ?? [],
88
+ jiraLinks: fm.linked_issues ?? [],
89
+ storyHash: fm.storyHash,
90
+ modifiesAreas,
91
+ };
92
+ events.push(makeEvent('xera-fetch', 'ticket.fetched', fetchedPayload));
93
+ for (const link of fm.linked_issues ?? []) {
94
+ const p: EdgeDiscoveredPayload = {
95
+ kind: 'jira-linked',
96
+ from: fm.ticketId,
97
+ to: link.ticketId,
98
+ source: `jira:${link.relation}`,
99
+ };
100
+ events.push(makeEvent('xera-fetch', 'edge.discovered', p));
101
+ }
102
+ for (const area of modifiesAreas) {
103
+ const p: EdgeDiscoveredPayload = {
104
+ kind: 'modifies',
105
+ from: fm.ticketId,
106
+ to: area,
107
+ source: 'extract-areas',
108
+ };
109
+ events.push(makeEvent('xera-fetch', 'edge.discovered', p));
110
+ }
111
+ appendEvents(repoRoot, events, { skill: 'xera-fetch', ticketId: ticket });
112
+ return 0;
113
+ }
114
+
115
+ async function recordScript(repoRoot: string, ticket: string): Promise<number> {
116
+ const { recordScriptImpl } = await import('./graph-record-script');
117
+ return recordScriptImpl(repoRoot, ticket);
118
+ }
119
+
120
+ async function recordExec(repoRoot: string, ticket: string, runId: string): Promise<number> {
121
+ const reporterPath = join(repoRoot, '.xera', ticket, 'runs', runId, 'reporter.json');
122
+ if (!existsSync(reporterPath)) {
123
+ console.error(`[graph-record exec] reporter.json missing`);
124
+ return 1;
125
+ }
126
+ const data = JSON.parse(readFileSync(reporterPath, 'utf8')) as {
127
+ scenarios: Array<{ name: string; status: 'pass' | 'fail'; runtime: number; traceId?: string }>;
128
+ };
129
+ const events: Event[] = [];
130
+ for (const s of data.scenarios) {
131
+ const p: RunCompletedPayload = {
132
+ scenarioId: scenarioId(ticket, s.name),
133
+ ticketId: ticket,
134
+ runId,
135
+ status: s.status,
136
+ runtime: s.runtime,
137
+ };
138
+ if (s.traceId) p.traceId = s.traceId;
139
+ events.push(makeEvent('xera-exec', 'run.completed', p));
140
+ }
141
+ appendEvents(repoRoot, events, { skill: 'xera-exec', ticketId: ticket });
142
+ return 0;
143
+ }
144
+
145
+ async function recordClassify(repoRoot: string, ticket: string, runId: string): Promise<number> {
146
+ const classifyPath = join(repoRoot, '.xera', ticket, 'runs', runId, 'classifier-output.json');
147
+ if (!existsSync(classifyPath)) {
148
+ console.error(`[graph-record classify] classifier-output.json missing`);
149
+ return 1;
150
+ }
151
+ const data = JSON.parse(readFileSync(classifyPath, 'utf8')) as {
152
+ scenarios: Array<{ name: string; class: string; confidence: 'low' | 'medium' | 'high' }>;
153
+ };
154
+ const events: Event[] = [];
155
+ for (const s of data.scenarios) {
156
+ const p: RunClassifiedPayload = {
157
+ scenarioId: scenarioId(ticket, s.name),
158
+ runId,
159
+ classification: s.class as RunClassifiedPayload['classification'],
160
+ confidence: s.confidence,
161
+ };
162
+ events.push(makeEvent('xera-report', 'run.classified', p));
163
+ }
164
+ appendEvents(repoRoot, events, { skill: 'xera-report', ticketId: ticket });
165
+ return 0;
166
+ }
167
+
168
+ async function recordPromote(repoRoot: string, args: Map<string, string>): Promise<number> {
169
+ const from = args.get('--from');
170
+ const to = args.get('--to');
171
+ const pomIdArg = args.get('--pom-id');
172
+ if (!from || !to) {
173
+ console.error(`[graph-record promote] --from and --to required`);
174
+ return 1;
175
+ }
176
+ const id = pomIdArg ?? pomId(from);
177
+ const p: PomPromotedPayload = { pomId: id, fromPath: from, toPath: to };
178
+ const e = makeEvent('xera-promote', 'pom.promoted', p);
179
+ appendEvents(repoRoot, [e], { skill: 'xera-promote', ticketId: 'shared' });
180
+ return 0;
181
+ }
182
+
183
+ function parseFlags(args: string[]): Map<string, string> {
184
+ const m = new Map<string, string>();
185
+ for (let i = 0; i < args.length; i++) {
186
+ if (args[i]!.startsWith('--')) {
187
+ m.set(args[i]!, args[i + 1] ?? '');
188
+ i++;
189
+ }
190
+ }
191
+ return m;
192
+ }
193
+
194
+ export async function graphRecordCmd(argv: string[]): Promise<number> {
195
+ const [action, ...rest] = argv;
196
+ if (!action) {
197
+ console.error(
198
+ `Usage: xera-internal graph-record <fetch|script|exec|classify|promote|dispute> [args]`,
199
+ );
200
+ return 1;
201
+ }
202
+ const repoRoot = process.cwd();
203
+ switch (action) {
204
+ case 'fetch': {
205
+ const ticket = rest[0];
206
+ if (!ticket) {
207
+ console.error('ticket required');
208
+ return 1;
209
+ }
210
+ return recordFetch(repoRoot, ticket);
211
+ }
212
+ case 'script': {
213
+ const ticket = rest[0];
214
+ if (!ticket) {
215
+ console.error('ticket required');
216
+ return 1;
217
+ }
218
+ return recordScript(repoRoot, ticket);
219
+ }
220
+ case 'exec': {
221
+ const ticket = rest[0];
222
+ const flags = parseFlags(rest);
223
+ const runId = flags.get('--run-id');
224
+ if (!ticket || !runId) {
225
+ console.error('ticket + --run-id required');
226
+ return 1;
227
+ }
228
+ return recordExec(repoRoot, ticket, runId);
229
+ }
230
+ case 'classify': {
231
+ const ticket = rest[0];
232
+ const flags = parseFlags(rest);
233
+ const runId = flags.get('--run-id');
234
+ if (!ticket || !runId) {
235
+ console.error('ticket + --run-id required');
236
+ return 1;
237
+ }
238
+ return recordClassify(repoRoot, ticket, runId);
239
+ }
240
+ case 'promote': {
241
+ return recordPromote(repoRoot, parseFlags(rest));
242
+ }
243
+ case 'dispute': {
244
+ const flags = parseFlags(rest);
245
+ const runId = flags.get('--run-id');
246
+ const scenarioIdArg = flags.get('--scenario-id');
247
+ const from = flags.get('--from');
248
+ const to = flags.get('--to');
249
+ const actor = flags.get('--actor');
250
+ const reason = flags.get('--reason');
251
+ if (!runId || !scenarioIdArg || !from || !to || !actor) {
252
+ console.error(
253
+ '[graph-record dispute] required: --run-id --scenario-id --from --to --actor [--reason]',
254
+ );
255
+ return 1;
256
+ }
257
+ const validClass = [
258
+ 'REAL_BUG',
259
+ 'TEST_BUG',
260
+ 'SELECTOR_DRIFT',
261
+ 'FLAKY',
262
+ 'PASS',
263
+ 'TEST_OUTDATED',
264
+ ];
265
+ if (!validClass.includes(from) || !validClass.includes(to)) {
266
+ console.error(
267
+ `[graph-record dispute] --from and --to must be one of: ${validClass.join(', ')}`,
268
+ );
269
+ return 1;
270
+ }
271
+ const payload: ClassificationDisputedPayload = {
272
+ runId,
273
+ scenarioId: scenarioIdArg,
274
+ originalClassification: from as Classification,
275
+ disputedTo: to as Classification,
276
+ qaActor: actor,
277
+ };
278
+ if (reason) payload.qaReason = reason;
279
+ const e = makeEvent('xera-report', 'classification.disputed', payload);
280
+ appendEvents(repoRoot, [e], { skill: 'xera-report', ticketId: scenarioIdArg.slice(0, 12) });
281
+ return 0;
282
+ }
283
+ default:
284
+ console.error(`Unknown action: ${action}`);
285
+ return 1;
286
+ }
287
+ }
@@ -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,11 @@ 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 { graphEnrichCmd } from './graph-enrich';
9
+ import { graphQueryCmd } from './graph-query';
10
+ import { graphRecordCmd } from './graph-record';
11
+ import { graphSnapshotCmd } from './graph-snapshot';
7
12
  import { healPrepareCmd } from './heal-prepare';
8
13
  import { lintCmd } from './lint';
9
14
  import { normalizeCmd } from './normalize';
@@ -23,6 +28,11 @@ const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
23
28
  'eval-report': evalReportCmd,
24
29
  exec: execCmd,
25
30
  fetch: fetchCmd,
31
+ 'graph-backfill': graphBackfillCmd,
32
+ 'graph-enrich': graphEnrichCmd,
33
+ 'graph-query': graphQueryCmd,
34
+ 'graph-record': graphRecordCmd,
35
+ 'graph-snapshot': graphSnapshotCmd,
26
36
  'heal-prepare': healPrepareCmd,
27
37
  lint: lintCmd,
28
38
  normalize: normalizeCmd,
@@ -1,8 +1,11 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, 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
5
  import type { ScenarioClassification } from '../classifier/types';
6
+ import type { OutdatedDecision } from '../graph/classify';
7
+ import { enhanceClassification } from '../graph/classify';
8
+ import { deriveSnapshot, loadAllEvents } from '../graph/store';
6
9
  import { buildJiraComment } from '../reporter/jira-comment';
7
10
  import { writeStatusFromClassification } from '../reporter/status-writer';
8
11
 
@@ -23,20 +26,75 @@ export async function reportCmd(argv: string[]): Promise<number> {
23
26
  const input = JSON.parse(readFileSync(inputArg.slice('--input='.length), 'utf8')) as ReportInput;
24
27
 
25
28
  const aggregated = aggregateScenarios(input.scenarios);
29
+
30
+ // v0.6.1: TEST_OUTDATED enhancement.
31
+ // The /xera-report skill writes outdated-decisions.json BEFORE invoking this subcommand,
32
+ // containing { [scenarioId]: { classification, confidence, evidence } } for every
33
+ // failing scenario the skill ran the LLM on. We use those decisions directly via
34
+ // an injected resolver — no Claude call here.
35
+ const decisionsPath = join(paths.ticketDir, 'runs', input.runId, 'outdated-decisions.json');
36
+ const decisions: Record<string, OutdatedDecision> = existsSync(decisionsPath)
37
+ ? (JSON.parse(readFileSync(decisionsPath, 'utf8')) as Record<string, OutdatedDecision>)
38
+ : {};
39
+
40
+ const graph = deriveSnapshot(loadAllEvents(process.cwd()));
41
+
42
+ // Build a lookup: normalized name → scenarioId (graph node id) for this ticket.
43
+ // This mirrors how graph-record-script.ts stores scenarios using sha1(ticket:name),
44
+ // but here we look up by the stored node id so both sha1-keyed and stub-keyed graphs work.
45
+ const normalizeScenarioName = (name: string) => name.trim().toLowerCase().replace(/\s+/g, ' ');
46
+
47
+ const scenarioIdByName: Record<string, string> = {};
48
+ for (const [id, node] of Object.entries(graph.scenarios)) {
49
+ if (node.ticketId === ticket) {
50
+ scenarioIdByName[normalizeScenarioName(node.name)] = id;
51
+ }
52
+ }
53
+
54
+ const enhancedScenarios: ScenarioClassification[] = await Promise.all(
55
+ aggregated.scenarios.map(async (s) => {
56
+ if (s.outcome !== 'FAIL') return s;
57
+ const scenarioId = scenarioIdByName[normalizeScenarioName(s.name)];
58
+ if (!scenarioId) return s;
59
+ const decision = decisions[scenarioId];
60
+ const decideOutdated = async (): Promise<OutdatedDecision> =>
61
+ decision ?? {
62
+ classification: 'BUG' as const,
63
+ confidence: 0,
64
+ evidence: { reasoning: 'no LLM decision' },
65
+ };
66
+ const enhanced = await enhanceClassification(
67
+ { scenarioId, traceClassification: s.class },
68
+ graph,
69
+ decideOutdated,
70
+ );
71
+ if (enhanced.classification !== s.class) {
72
+ return {
73
+ ...s,
74
+ class: enhanced.classification,
75
+ rationale: `${s.rationale} | TEST_OUTDATED override (conf ${enhanced.confidence})`,
76
+ };
77
+ }
78
+ return s;
79
+ }),
80
+ );
81
+
82
+ const reAggregated = aggregateScenarios(enhancedScenarios);
83
+
26
84
  const ts = new Date().toISOString();
27
85
  writeStatusFromClassification(paths.statusPath, {
28
86
  ticket,
29
87
  runTs: ts,
30
- classification: aggregated,
88
+ classification: reAggregated,
31
89
  scenarioCounts: input.scenarioCounts,
32
90
  });
33
91
 
34
92
  const md = buildJiraComment({
35
93
  ticket,
36
94
  runId: input.runId,
37
- overall: aggregated.overall,
38
- overallConfidence: aggregated.overallConfidence,
39
- scenarios: aggregated.scenarios,
95
+ overall: reAggregated.overall,
96
+ overallConfidence: reAggregated.overallConfidence,
97
+ scenarios: reAggregated.scenarios,
40
98
  xeraVersion: '0.1.0',
41
99
  promptsVersion: '1.0.0',
42
100
  });
@@ -10,6 +10,9 @@ const IN_SCOPE_PROMPTS = [
10
10
  'feature-from-story.md',
11
11
  'script-from-feature.md',
12
12
  'heal-locator.md',
13
+ 'extract-areas.md',
14
+ 'similarity-match.md',
15
+ 'classify-outdated.md',
13
16
  ] as const;
14
17
 
15
18
  const REQUIRED_SECTION_HEADING = '## Handling untrusted input';
@@ -2,6 +2,7 @@ import type { ClassifyOutput, Confidence, ScenarioClassification } from './types
2
2
 
3
3
  const CLASS_PRIORITY: Array<ClassifyOutput['overall']> = [
4
4
  'REAL_BUG',
5
+ 'TEST_OUTDATED',
5
6
  'TEST_BUG',
6
7
  'SELECTOR_DRIFT',
7
8
  'FLAKY',
@@ -19,12 +19,12 @@ const WebSchema = z
19
19
  message: 'baseUrl must have at least one environment',
20
20
  }),
21
21
  defaultEnv: z.string(),
22
- auth: AuthSchema.default({}),
22
+ auth: AuthSchema.prefault({}),
23
23
  testData: z
24
24
  .object({
25
25
  users: z.record(z.string(), z.object({ fromAuth: z.string() })).default({}),
26
26
  })
27
- .default({ users: {} }),
27
+ .prefault({}),
28
28
  })
29
29
  .refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
30
30
  message: 'defaultEnv must exist in baseUrl map',
@@ -51,9 +51,9 @@ const AISchema = z
51
51
  lint: z.number().int().min(0).max(5).default(2),
52
52
  validateFeature: z.number().int().min(0).max(5).default(2),
53
53
  })
54
- .default({}),
54
+ .prefault({}),
55
55
  })
56
- .default({});
56
+ .prefault({});
57
57
 
58
58
  const ReportingSchema = z
59
59
  .object({
@@ -64,10 +64,10 @@ const ReportingSchema = z
64
64
  onPass: z.string().nullable().default(null),
65
65
  onFail: z.string().nullable().default(null),
66
66
  })
67
- .default({}),
67
+ .prefault({}),
68
68
  artifactLinks: z.enum(['git', 'local']).default('git'),
69
69
  })
70
- .default({});
70
+ .prefault({});
71
71
 
72
72
  export const XeraConfigSchema = z.object({
73
73
  jira: JiraSchema,
@@ -0,0 +1,126 @@
1
+ import type { Classification, ScenarioNode, Snapshot, TicketNode } from './types';
2
+
3
+ export interface ClassifyInput {
4
+ scenarioId: string;
5
+ traceClassification: Classification;
6
+ }
7
+
8
+ export interface CandidateEvidence {
9
+ ticketId: string;
10
+ summary: string;
11
+ modifiedArea: string;
12
+ relevantAcRef?: string;
13
+ }
14
+
15
+ export interface ClassifyEvidence {
16
+ candidateTickets?: CandidateEvidence[];
17
+ reasoning?: string;
18
+ expectedByTest?: string;
19
+ actualInApp?: string;
20
+ proposedAction?: 'regenerate-scenario' | 'review-and-decide';
21
+ }
22
+
23
+ export interface ClassifyOutput {
24
+ classification: Classification;
25
+ confidence: number;
26
+ evidence?: ClassifyEvidence;
27
+ }
28
+
29
+ export interface OutdatedDecision {
30
+ classification: 'TEST_OUTDATED' | 'BUG' | 'AMBIGUOUS';
31
+ confidence: number;
32
+ evidence: {
33
+ reasoning: string;
34
+ expectedByTest?: string;
35
+ actualInApp?: string;
36
+ relevantAcRef?: string;
37
+ };
38
+ }
39
+
40
+ export type DecideOutdated = (args: {
41
+ scenario: ScenarioNode;
42
+ candidates: TicketNode[];
43
+ }) => Promise<OutdatedDecision>;
44
+
45
+ const DEFAULT_THRESHOLD = 0.7;
46
+ const SHORT_CIRCUIT: Classification[] = ['FLAKY', 'PASS'];
47
+
48
+ export function findCandidateTickets(graph: Snapshot, scenario: ScenarioNode): TicketNode[] {
49
+ const poms = graph.edges
50
+ .filter((e) => e.kind === 'uses' && e.from === scenario.id)
51
+ .map((e) => e.to);
52
+ if (poms.length === 0) return [];
53
+
54
+ const areas = graph.edges
55
+ .filter((e) => e.kind === 'covers' && poms.includes(e.from))
56
+ .map((e) => e.to);
57
+ if (areas.length === 0) return [];
58
+
59
+ const ticketIds = graph.edges
60
+ .filter((e) => e.kind === 'modifies' && areas.includes(e.to))
61
+ .map((e) => e.from);
62
+
63
+ const seen = new Set<string>();
64
+ const out: TicketNode[] = [];
65
+ for (const id of ticketIds) {
66
+ if (seen.has(id)) continue;
67
+ seen.add(id);
68
+ if (id === scenario.ticketId) continue;
69
+ const t = graph.tickets[id];
70
+ if (!t) continue;
71
+ if (t.fetchedAt <= scenario.generatedAt) continue;
72
+ out.push(t);
73
+ }
74
+ return out;
75
+ }
76
+
77
+ export async function enhanceClassification(
78
+ input: ClassifyInput,
79
+ graph: Snapshot,
80
+ decideOutdated: DecideOutdated,
81
+ options: { threshold?: number } = {},
82
+ ): Promise<ClassifyOutput> {
83
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
84
+ if (SHORT_CIRCUIT.includes(input.traceClassification)) {
85
+ return { classification: input.traceClassification, confidence: 1 };
86
+ }
87
+
88
+ const scenario = graph.scenarios[input.scenarioId];
89
+ if (!scenario) return { classification: input.traceClassification, confidence: 1 };
90
+
91
+ const candidates = findCandidateTickets(graph, scenario);
92
+ if (candidates.length === 0) {
93
+ return { classification: input.traceClassification, confidence: 1 };
94
+ }
95
+
96
+ const candidateEvidence: CandidateEvidence[] = candidates.map((t) => {
97
+ const area = graph.edges.find((e) => e.kind === 'modifies' && e.from === t.id)?.to ?? '';
98
+ const ev: CandidateEvidence = { ticketId: t.id, summary: t.summary, modifiedArea: area };
99
+ if (t.ac[0]) ev.relevantAcRef = t.ac[0];
100
+ return ev;
101
+ });
102
+
103
+ const decision = await decideOutdated({ scenario, candidates });
104
+
105
+ if (decision.classification === 'TEST_OUTDATED' && decision.confidence >= threshold) {
106
+ const evidence: ClassifyEvidence = {
107
+ candidateTickets: candidateEvidence,
108
+ reasoning: decision.evidence.reasoning,
109
+ proposedAction: 'regenerate-scenario',
110
+ };
111
+ if (decision.evidence.expectedByTest)
112
+ evidence.expectedByTest = decision.evidence.expectedByTest;
113
+ if (decision.evidence.actualInApp) evidence.actualInApp = decision.evidence.actualInApp;
114
+ return {
115
+ classification: 'TEST_OUTDATED',
116
+ confidence: decision.confidence,
117
+ evidence,
118
+ };
119
+ }
120
+
121
+ return {
122
+ classification: input.traceClassification,
123
+ confidence: 1,
124
+ evidence: { candidateTickets: candidateEvidence },
125
+ };
126
+ }
@@ -0,0 +1,59 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { graphPaths } from './paths';
4
+
5
+ export interface LlmCallLog {
6
+ ts?: string;
7
+ skill: string;
8
+ prompt: string;
9
+ tokensIn: number;
10
+ tokensOut: number;
11
+ model: string;
12
+ costUsd: number;
13
+ }
14
+
15
+ export function logLlmCall(repoRoot: string, call: LlmCallLog): void {
16
+ const paths = graphPaths(repoRoot);
17
+ mkdirSync(dirname(paths.costLog), { recursive: true });
18
+ const record = {
19
+ ts: call.ts ?? new Date().toISOString(),
20
+ skill: call.skill,
21
+ prompt: call.prompt,
22
+ tokens_in: call.tokensIn,
23
+ tokens_out: call.tokensOut,
24
+ model: call.model,
25
+ cost_estimate_usd: call.costUsd,
26
+ };
27
+ appendFileSync(paths.costLog, `${JSON.stringify(record)}\n`);
28
+ }
29
+
30
+ export interface CostSummary {
31
+ totalCalls: number;
32
+ totalUsd: number;
33
+ bySkill: Record<string, { calls: number; usd: number }>;
34
+ windowDays: number;
35
+ }
36
+
37
+ export function summarizeCost(repoRoot: string, daysBack: number): CostSummary {
38
+ const paths = graphPaths(repoRoot);
39
+ const result: CostSummary = { totalCalls: 0, totalUsd: 0, bySkill: {}, windowDays: daysBack };
40
+ if (!existsSync(paths.costLog)) return result;
41
+ const cutoff = Date.now() - daysBack * 86400 * 1000;
42
+ for (const line of readFileSync(paths.costLog, 'utf8').split('\n')) {
43
+ if (!line.trim()) continue;
44
+ let row: { ts: string; skill: string; cost_estimate_usd: number };
45
+ try {
46
+ row = JSON.parse(line);
47
+ } catch {
48
+ continue;
49
+ }
50
+ if (Date.parse(row.ts) < cutoff) continue;
51
+ result.totalCalls++;
52
+ result.totalUsd += row.cost_estimate_usd;
53
+ if (!result.bySkill[row.skill]) result.bySkill[row.skill] = { calls: 0, usd: 0 };
54
+ const s = result.bySkill[row.skill]!;
55
+ s.calls++;
56
+ s.usd += row.cost_estimate_usd;
57
+ }
58
+ return result;
59
+ }