@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,103 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { appendEvents, deriveSnapshot, loadAllEvents } from './store';
5
+ import type { EdgeDiscoveredPayload, Event, TicketEnrichedPayload } from './types';
6
+ import { SCHEMA_VERSION } from './types';
7
+ import { ulid } from './ulid';
8
+
9
+ const MAX_SIMILAR_EDGES = 10;
10
+ const MIN_CONFIDENCE = 0.7;
11
+
12
+ const SimilarEntrySchema = z.object({
13
+ ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
14
+ confidence: z.number(),
15
+ reason: z.string(),
16
+ });
17
+
18
+ const EnrichmentInputSchema = z.object({
19
+ similar: z.array(SimilarEntrySchema),
20
+ });
21
+
22
+ export interface EnrichOptions {
23
+ force?: boolean;
24
+ }
25
+
26
+ export interface EnrichResult {
27
+ ticketId: string;
28
+ similarCount: number;
29
+ enrichedAt: string;
30
+ }
31
+
32
+ const nowIso = () => new Date().toISOString();
33
+
34
+ const mk = <T extends Event['type']>(
35
+ actor: string,
36
+ type: T,
37
+ payload: Extract<Event, { type: T }>['payload'],
38
+ ): Event =>
39
+ ({
40
+ event_id: ulid(),
41
+ schema_version: SCHEMA_VERSION,
42
+ ts: nowIso(),
43
+ actor,
44
+ type,
45
+ payload,
46
+ }) as Event;
47
+
48
+ export async function enrichTicket(
49
+ repoRoot: string,
50
+ ticketId: string,
51
+ opts: EnrichOptions,
52
+ ): Promise<EnrichResult> {
53
+ const inputPath = join(repoRoot, '.xera', ticketId, 'enrichment-input.json');
54
+ if (!existsSync(inputPath)) {
55
+ throw new Error(`enrichment-input.json not found at ${inputPath}`);
56
+ }
57
+
58
+ const raw = JSON.parse(readFileSync(inputPath, 'utf8'));
59
+ const parsed = EnrichmentInputSchema.safeParse(raw);
60
+ if (!parsed.success) {
61
+ throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
62
+ }
63
+
64
+ const snapshot = deriveSnapshot(loadAllEvents(repoRoot));
65
+ if (!snapshot.tickets[ticketId]) {
66
+ throw new Error(`ticket ${ticketId} not in graph; run /xera-fetch first`);
67
+ }
68
+
69
+ if (snapshot.tickets[ticketId]!.enrichedAt && !opts.force) {
70
+ return { ticketId, similarCount: 0, enrichedAt: snapshot.tickets[ticketId]!.enrichedAt! };
71
+ }
72
+
73
+ const validated = parsed.data.similar
74
+ .map((s) => ({ ...s, confidence: Math.max(0, Math.min(1, s.confidence)) }))
75
+ .filter((s) => s.confidence >= MIN_CONFIDENCE)
76
+ .filter((s) => snapshot.tickets[s.ticketId] !== undefined)
77
+ .filter((s) => s.ticketId !== ticketId)
78
+ .slice(0, MAX_SIMILAR_EDGES);
79
+
80
+ const events: Event[] = [];
81
+ for (const s of validated) {
82
+ const payload: EdgeDiscoveredPayload = {
83
+ kind: 'similar',
84
+ from: ticketId,
85
+ to: s.ticketId,
86
+ confidence: s.confidence,
87
+ source: `llm-similarity:${s.reason.slice(0, 80)}`,
88
+ };
89
+ events.push(mk('graph-enrich', 'edge.discovered', payload));
90
+ }
91
+
92
+ const enrichedAt = nowIso();
93
+ const enrichedPayload: TicketEnrichedPayload = {
94
+ ticketId,
95
+ enrichedAt,
96
+ similarCount: validated.length,
97
+ };
98
+ events.push(mk('graph-enrich', 'ticket.enriched', enrichedPayload));
99
+
100
+ appendEvents(repoRoot, events, { skill: 'graph-enrich', ticketId });
101
+
102
+ return { ticketId, similarCount: validated.length, enrichedAt };
103
+ }
@@ -0,0 +1,30 @@
1
+ export type {
2
+ CandidateEvidence,
3
+ ClassifyEvidence,
4
+ ClassifyInput,
5
+ ClassifyOutput,
6
+ DecideOutdated,
7
+ OutdatedDecision,
8
+ } from './classify';
9
+ export {
10
+ enhanceClassification,
11
+ findCandidateTickets,
12
+ } from './classify';
13
+ export type { CostSummary, LlmCallLog } from './cost';
14
+ export { logLlmCall, summarizeCost } from './cost';
15
+ export type { EnrichOptions, EnrichResult } from './enrich';
16
+ export { enrichTicket } from './enrich';
17
+ export { currentYyyyMm, graphPaths } from './paths';
18
+ export { EventSchema, safeParseEvent } from './schema';
19
+ export { buildSimilarityPrompt } from './similarity';
20
+ export {
21
+ appendEvents,
22
+ computeEventsHash,
23
+ deriveSnapshot,
24
+ isSnapshotStale,
25
+ loadAllEvents,
26
+ loadSnapshot,
27
+ writeSnapshot,
28
+ } from './store';
29
+ export * from './types';
30
+ export { ulid } from './ulid';
@@ -0,0 +1,27 @@
1
+ import { join } from 'node:path';
2
+
3
+ export interface GraphPaths {
4
+ eventsDir: string;
5
+ snapshotFile: string;
6
+ costLog: string;
7
+ eventsMonthDir(yyyyMm: string): string;
8
+ eventFile(ulid: string, skill: string, ticketId: string, yyyyMm: string): string;
9
+ }
10
+
11
+ export function graphPaths(repoRoot: string): GraphPaths {
12
+ const eventsDir = join(repoRoot, '.xera/graph/events');
13
+ return {
14
+ eventsDir,
15
+ snapshotFile: join(repoRoot, '.xera/graph/snapshot.json'),
16
+ costLog: join(repoRoot, '.xera/cost-log.jsonl'),
17
+ eventsMonthDir: (yyyyMm) => join(eventsDir, yyyyMm),
18
+ eventFile: (ulid, skill, ticketId, yyyyMm) =>
19
+ join(eventsDir, yyyyMm, `${ulid}-${skill}-${ticketId}.jsonl`),
20
+ };
21
+ }
22
+
23
+ export function currentYyyyMm(now: Date = new Date()): string {
24
+ const y = now.getUTCFullYear();
25
+ const m = String(now.getUTCMonth() + 1).padStart(2, '0');
26
+ return `${y}-${m}`;
27
+ }
@@ -0,0 +1,142 @@
1
+ import { z } from 'zod';
2
+ import type { Event } from './types';
3
+ import { SCHEMA_VERSION } from './types';
4
+
5
+ const schemaV = z.literal(SCHEMA_VERSION);
6
+ const iso = z.string().datetime({ offset: false });
7
+
8
+ const ticketFetched = z
9
+ .object({
10
+ ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
11
+ summary: z.string(),
12
+ ac: z.array(z.string()),
13
+ jiraLinks: z.array(
14
+ z.object({
15
+ ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
16
+ relation: z.enum(['blocks', 'duplicates', 'relates', 'supersedes']),
17
+ }),
18
+ ),
19
+ storyHash: z.string(),
20
+ modifiesAreas: z.array(z.string().regex(/^[a-z0-9-]+$/)),
21
+ })
22
+ .passthrough();
23
+
24
+ const ticketEnriched = z
25
+ .object({
26
+ ticketId: z.string(),
27
+ enrichedAt: iso,
28
+ similarCount: z.number().int().nonnegative(),
29
+ })
30
+ .passthrough();
31
+
32
+ const scenarioGenerated = z
33
+ .object({
34
+ scenarioId: z.string(),
35
+ ticketId: z.string(),
36
+ name: z.string(),
37
+ gherkin: z.string(),
38
+ priority: z.enum(['p0', 'p1', 'p2']),
39
+ featureHash: z.string(),
40
+ generatedAt: iso,
41
+ })
42
+ .passthrough();
43
+
44
+ const pomGenerated = z
45
+ .object({
46
+ pomId: z.string(),
47
+ ticketId: z.string(),
48
+ filePath: z.string(),
49
+ route: z.string(),
50
+ locators: z.array(z.string()),
51
+ scope: z.enum(['local', 'shared']),
52
+ })
53
+ .passthrough();
54
+
55
+ const pomPromoted = z
56
+ .object({
57
+ pomId: z.string(),
58
+ fromPath: z.string(),
59
+ toPath: z.string(),
60
+ })
61
+ .passthrough();
62
+
63
+ const runCompleted = z
64
+ .object({
65
+ scenarioId: z.string(),
66
+ ticketId: z.string(),
67
+ runId: z.string(),
68
+ status: z.enum(['pass', 'fail']),
69
+ traceId: z.string().optional(),
70
+ runtime: z.number().nonnegative(),
71
+ })
72
+ .passthrough();
73
+
74
+ const classification = z.enum([
75
+ 'REAL_BUG',
76
+ 'TEST_BUG',
77
+ 'SELECTOR_DRIFT',
78
+ 'FLAKY',
79
+ 'PASS',
80
+ 'TEST_OUTDATED',
81
+ ]);
82
+
83
+ const runClassified = z
84
+ .object({
85
+ scenarioId: z.string(),
86
+ runId: z.string(),
87
+ classification,
88
+ confidence: z.enum(['low', 'medium', 'high']),
89
+ })
90
+ .passthrough();
91
+
92
+ const classificationDisputed = z
93
+ .object({
94
+ runId: z.string(),
95
+ scenarioId: z.string(),
96
+ originalClassification: classification,
97
+ disputedTo: classification,
98
+ qaActor: z.string(),
99
+ qaReason: z.string().optional(),
100
+ })
101
+ .passthrough();
102
+
103
+ const edgeDiscovered = z
104
+ .object({
105
+ kind: z.enum(['tests', 'uses', 'covers', 'modifies', 'jira-linked', 'similar', 'ran']),
106
+ from: z.string(),
107
+ to: z.string(),
108
+ confidence: z.number().min(0).max(1).optional(),
109
+ source: z.string(),
110
+ })
111
+ .passthrough();
112
+
113
+ const base = {
114
+ event_id: z.string().min(20),
115
+ schema_version: schemaV,
116
+ ts: iso,
117
+ actor: z.string(),
118
+ };
119
+
120
+ export const EventSchema = z.discriminatedUnion('type', [
121
+ z.object({ ...base, type: z.literal('ticket.fetched'), payload: ticketFetched }),
122
+ z.object({ ...base, type: z.literal('ticket.enriched'), payload: ticketEnriched }),
123
+ z.object({ ...base, type: z.literal('scenario.generated'), payload: scenarioGenerated }),
124
+ z.object({ ...base, type: z.literal('pom.generated'), payload: pomGenerated }),
125
+ z.object({ ...base, type: z.literal('pom.promoted'), payload: pomPromoted }),
126
+ z.object({ ...base, type: z.literal('run.completed'), payload: runCompleted }),
127
+ z.object({ ...base, type: z.literal('run.classified'), payload: runClassified }),
128
+ z.object({
129
+ ...base,
130
+ type: z.literal('classification.disputed'),
131
+ payload: classificationDisputed,
132
+ }),
133
+ z.object({ ...base, type: z.literal('edge.discovered'), payload: edgeDiscovered }),
134
+ ]);
135
+
136
+ export function safeParseEvent(
137
+ value: unknown,
138
+ ): { success: true; data: Event } | { success: false; error: z.ZodError } {
139
+ const r = EventSchema.safeParse(value);
140
+ if (r.success) return { success: true, data: r.data as Event };
141
+ return { success: false, error: r.error };
142
+ }
@@ -0,0 +1,43 @@
1
+ import type { TicketNode } from './types';
2
+
3
+ const MAX_CANDIDATES = 50;
4
+
5
+ export function buildSimilarityPrompt(target: TicketNode, candidates: TicketNode[]): string {
6
+ const window = candidates.slice(0, MAX_CANDIDATES);
7
+ const candidateBlock = window
8
+ .map((t, i) => {
9
+ const ac = t.ac.length > 0 ? `\n AC: ${t.ac.slice(0, 3).join(' | ')}` : '';
10
+ return `${i + 1}. ${t.id} — ${t.summary}${ac}`;
11
+ })
12
+ .join('\n');
13
+
14
+ const targetAc =
15
+ target.ac.length > 0 ? `\nAC:\n${target.ac.map((a) => ` - ${a}`).join('\n')}` : '';
16
+
17
+ return `You are evaluating whether a NEW ticket is semantically related to any prior tickets in this project's knowledge graph.
18
+
19
+ # NEW TICKET
20
+ ID: ${target.id}
21
+ Summary: ${target.summary}${targetAc}
22
+
23
+ # PRIOR TICKETS (most recent ${window.length} of ${candidates.length})
24
+ ${candidateBlock || '(none yet)'}
25
+
26
+ # Task
27
+ Output a JSON object with shape:
28
+
29
+ \`\`\`json
30
+ {
31
+ "similar": [
32
+ { "ticketId": "<JIRA-KEY>", "confidence": 0.0-1.0, "reason": "<one sentence>" }
33
+ ]
34
+ }
35
+ \`\`\`
36
+
37
+ # Rules
38
+ 1. Only include candidates with confidence ≥ 0.7. Below that, exclude.
39
+ 2. Confidence reflects semantic relatedness (same SUT area, same flow, complementary feature) — NOT just word overlap.
40
+ 3. Cap output at 10 entries even if more candidates pass the threshold; pick the highest-confidence ones.
41
+ 4. If NO candidates are related, return \`{ "similar": [] }\`. Do not invent relationships.
42
+ 5. Output JSON ONLY. No prose, no fences, no commentary.`;
43
+ }
@@ -0,0 +1,231 @@
1
+ import { createHash } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { dirname } from 'node:path';
11
+ import { currentYyyyMm, graphPaths } from './paths';
12
+ import { safeParseEvent } from './schema';
13
+ import type {
14
+ EdgeRecord,
15
+ Event,
16
+ FailureNode,
17
+ PomNode,
18
+ ScenarioNode,
19
+ Snapshot,
20
+ TicketNode,
21
+ } from './types';
22
+ import { SCHEMA_VERSION } from './types';
23
+
24
+ export interface AppendOptions {
25
+ skill: string;
26
+ ticketId: string;
27
+ now?: Date;
28
+ }
29
+
30
+ export function appendEvents(repoRoot: string, events: Event[], opts: AppendOptions): string {
31
+ if (events.length === 0) return '';
32
+ const paths = graphPaths(repoRoot);
33
+ const yyyyMm = currentYyyyMm(opts.now);
34
+ const monthDir = paths.eventsMonthDir(yyyyMm);
35
+ mkdirSync(monthDir, { recursive: true });
36
+ const ulid = events[0]!.event_id;
37
+ const finalPath = paths.eventFile(ulid, opts.skill, opts.ticketId, yyyyMm);
38
+ const tmpPath = `${finalPath}.tmp`;
39
+ const body = `${events.map((e) => JSON.stringify(e)).join('\n')}\n`;
40
+ writeFileSync(tmpPath, body);
41
+ renameSync(tmpPath, finalPath);
42
+ return finalPath;
43
+ }
44
+
45
+ export function loadAllEvents(repoRoot: string): Event[] {
46
+ const paths = graphPaths(repoRoot);
47
+ if (!existsSync(paths.eventsDir)) return [];
48
+ const files: string[] = [];
49
+ for (const monthDir of readdirSync(paths.eventsDir, { withFileTypes: true })) {
50
+ if (!monthDir.isDirectory()) continue;
51
+ const monthPath = paths.eventsMonthDir(monthDir.name);
52
+ for (const f of readdirSync(monthPath)) {
53
+ if (f.endsWith('.jsonl')) files.push(`${monthPath}/${f}`);
54
+ }
55
+ }
56
+ files.sort((a, b) => {
57
+ const ua = a.split('/').pop()!.split('-')[0]!;
58
+ const ub = b.split('/').pop()!.split('-')[0]!;
59
+ return ua < ub ? -1 : ua > ub ? 1 : 0;
60
+ });
61
+ const events: Event[] = [];
62
+ for (const file of files) {
63
+ try {
64
+ const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
65
+ for (const line of lines) {
66
+ let parsed: unknown;
67
+ try {
68
+ parsed = JSON.parse(line);
69
+ } catch {
70
+ console.warn(`[graph.store] skip-line bad-json ${file}`);
71
+ continue;
72
+ }
73
+ const r = safeParseEvent(parsed);
74
+ if (!r.success) {
75
+ console.warn(`[graph.store] skip-line invalid ${file}`);
76
+ continue;
77
+ }
78
+ events.push(r.data);
79
+ }
80
+ } catch (e) {
81
+ console.warn(`[graph.store] skip-file ${file} ${(e as Error).message}`);
82
+ }
83
+ }
84
+ events.sort((a, b) => (a.event_id < b.event_id ? -1 : a.event_id > b.event_id ? 1 : 0));
85
+ return events;
86
+ }
87
+
88
+ export function computeEventsHash(events: Event[]): string {
89
+ const h = createHash('sha256');
90
+ for (const e of events) h.update(e.event_id);
91
+ return `sha256:${h.digest('hex')}`;
92
+ }
93
+
94
+ export function deriveSnapshot(events: Event[]): Snapshot {
95
+ const tickets: Record<string, TicketNode> = {};
96
+ const scenarios: Record<string, ScenarioNode> = {};
97
+ const poms: Record<string, PomNode> = {};
98
+ const areas: Record<string, { id: string }> = {};
99
+ const edges: EdgeRecord[] = [];
100
+ const latestFailures: Record<string, FailureNode> = {};
101
+
102
+ for (const e of events) {
103
+ switch (e.type) {
104
+ case 'ticket.fetched':
105
+ tickets[e.payload.ticketId] = {
106
+ id: e.payload.ticketId,
107
+ summary: e.payload.summary,
108
+ ac: e.payload.ac,
109
+ storyHash: e.payload.storyHash,
110
+ modifiesAreas: e.payload.modifiesAreas,
111
+ fetchedAt: e.ts,
112
+ };
113
+ for (const a of e.payload.modifiesAreas) areas[a] = { id: a };
114
+ for (const link of e.payload.jiraLinks) {
115
+ edges.push({
116
+ kind: 'jira-linked',
117
+ from: e.payload.ticketId,
118
+ to: link.ticketId,
119
+ source: `jira:${link.relation}`,
120
+ discoveredAt: e.ts,
121
+ });
122
+ }
123
+ break;
124
+ case 'ticket.enriched':
125
+ if (tickets[e.payload.ticketId])
126
+ tickets[e.payload.ticketId]!.enrichedAt = e.payload.enrichedAt;
127
+ break;
128
+ case 'scenario.generated':
129
+ scenarios[e.payload.scenarioId] = {
130
+ id: e.payload.scenarioId,
131
+ ticketId: e.payload.ticketId,
132
+ name: e.payload.name,
133
+ gherkin: e.payload.gherkin,
134
+ priority: e.payload.priority,
135
+ featureHash: e.payload.featureHash,
136
+ generatedAt: e.payload.generatedAt,
137
+ };
138
+ edges.push({
139
+ kind: 'tests',
140
+ from: e.payload.ticketId,
141
+ to: e.payload.scenarioId,
142
+ source: 'xera-script',
143
+ discoveredAt: e.ts,
144
+ });
145
+ break;
146
+ case 'pom.generated':
147
+ poms[e.payload.pomId] = {
148
+ id: e.payload.pomId,
149
+ ticketId: e.payload.ticketId,
150
+ filePath: e.payload.filePath,
151
+ route: e.payload.route,
152
+ locators: e.payload.locators,
153
+ scope: e.payload.scope,
154
+ };
155
+ break;
156
+ case 'pom.promoted':
157
+ if (poms[e.payload.pomId]) {
158
+ poms[e.payload.pomId]!.filePath = e.payload.toPath;
159
+ poms[e.payload.pomId]!.scope = 'shared';
160
+ }
161
+ break;
162
+ case 'run.completed':
163
+ if (e.payload.status === 'fail') {
164
+ const fail: FailureNode = {
165
+ id: `${e.payload.runId}:${e.payload.scenarioId}`,
166
+ scenarioId: e.payload.scenarioId,
167
+ runId: e.payload.runId,
168
+ ts: e.ts,
169
+ };
170
+ if (e.payload.traceId) fail.traceId = e.payload.traceId;
171
+ latestFailures[e.payload.scenarioId] = fail;
172
+ } else {
173
+ delete latestFailures[e.payload.scenarioId];
174
+ }
175
+ break;
176
+ case 'edge.discovered': {
177
+ const ed: EdgeRecord = {
178
+ kind: e.payload.kind,
179
+ from: e.payload.from,
180
+ to: e.payload.to,
181
+ source: e.payload.source,
182
+ discoveredAt: e.ts,
183
+ };
184
+ if (e.payload.confidence !== undefined) ed.confidence = e.payload.confidence;
185
+ edges.push(ed);
186
+ break;
187
+ }
188
+ // run.classified and classification.disputed: not materialized in v0.6.0 snapshot
189
+ default:
190
+ break;
191
+ }
192
+ }
193
+
194
+ return {
195
+ schema_version: SCHEMA_VERSION,
196
+ generated_at: new Date().toISOString(),
197
+ event_count: events.length,
198
+ events_hash: computeEventsHash(events),
199
+ tickets,
200
+ scenarios,
201
+ poms,
202
+ areas,
203
+ edges,
204
+ latest_failures: latestFailures,
205
+ };
206
+ }
207
+
208
+ export function writeSnapshot(repoRoot: string, snap: Snapshot): void {
209
+ const paths = graphPaths(repoRoot);
210
+ mkdirSync(dirname(paths.snapshotFile), { recursive: true });
211
+ const tmp = `${paths.snapshotFile}.tmp`;
212
+ writeFileSync(tmp, JSON.stringify(snap, null, 2));
213
+ renameSync(tmp, paths.snapshotFile);
214
+ }
215
+
216
+ export function loadSnapshot(repoRoot: string): Snapshot | null {
217
+ const paths = graphPaths(repoRoot);
218
+ if (!existsSync(paths.snapshotFile)) return null;
219
+ try {
220
+ return JSON.parse(readFileSync(paths.snapshotFile, 'utf8')) as Snapshot;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+
226
+ export function isSnapshotStale(repoRoot: string): boolean {
227
+ const snap = loadSnapshot(repoRoot);
228
+ if (!snap) return true;
229
+ const liveHash = computeEventsHash(loadAllEvents(repoRoot));
230
+ return snap.events_hash !== liveHash;
231
+ }