@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
@@ -10,6 +10,7 @@ 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',
13
14
  ] as const;
14
15
 
15
16
  const REQUIRED_SECTION_HEADING = '## Handling untrusted input';
@@ -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,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
+ }
@@ -0,0 +1,15 @@
1
+ export type { CostSummary, LlmCallLog } from './cost';
2
+ export { logLlmCall, summarizeCost } from './cost';
3
+ export { currentYyyyMm, graphPaths } from './paths';
4
+ export { EventSchema, safeParseEvent } from './schema';
5
+ export {
6
+ appendEvents,
7
+ computeEventsHash,
8
+ deriveSnapshot,
9
+ isSnapshotStale,
10
+ loadAllEvents,
11
+ loadSnapshot,
12
+ writeSnapshot,
13
+ } from './store';
14
+ export * from './types';
15
+ 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,135 @@
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(['REAL_BUG', 'TEST_BUG', 'SELECTOR_DRIFT', 'FLAKY', 'PASS']);
75
+
76
+ const runClassified = z
77
+ .object({
78
+ scenarioId: z.string(),
79
+ runId: z.string(),
80
+ classification,
81
+ confidence: z.enum(['low', 'medium', 'high']),
82
+ })
83
+ .passthrough();
84
+
85
+ const classificationDisputed = z
86
+ .object({
87
+ runId: z.string(),
88
+ scenarioId: z.string(),
89
+ originalClassification: classification,
90
+ disputedTo: classification,
91
+ qaActor: z.string(),
92
+ qaReason: z.string().optional(),
93
+ })
94
+ .passthrough();
95
+
96
+ const edgeDiscovered = z
97
+ .object({
98
+ kind: z.enum(['tests', 'uses', 'covers', 'modifies', 'jira-linked', 'similar', 'ran']),
99
+ from: z.string(),
100
+ to: z.string(),
101
+ confidence: z.number().min(0).max(1).optional(),
102
+ source: z.string(),
103
+ })
104
+ .passthrough();
105
+
106
+ const base = {
107
+ event_id: z.string().min(20),
108
+ schema_version: schemaV,
109
+ ts: iso,
110
+ actor: z.string(),
111
+ };
112
+
113
+ export const EventSchema = z.discriminatedUnion('type', [
114
+ z.object({ ...base, type: z.literal('ticket.fetched'), payload: ticketFetched }),
115
+ z.object({ ...base, type: z.literal('ticket.enriched'), payload: ticketEnriched }),
116
+ z.object({ ...base, type: z.literal('scenario.generated'), payload: scenarioGenerated }),
117
+ z.object({ ...base, type: z.literal('pom.generated'), payload: pomGenerated }),
118
+ z.object({ ...base, type: z.literal('pom.promoted'), payload: pomPromoted }),
119
+ z.object({ ...base, type: z.literal('run.completed'), payload: runCompleted }),
120
+ z.object({ ...base, type: z.literal('run.classified'), payload: runClassified }),
121
+ z.object({
122
+ ...base,
123
+ type: z.literal('classification.disputed'),
124
+ payload: classificationDisputed,
125
+ }),
126
+ z.object({ ...base, type: z.literal('edge.discovered'), payload: edgeDiscovered }),
127
+ ]);
128
+
129
+ export function safeParseEvent(
130
+ value: unknown,
131
+ ): { success: true; data: Event } | { success: false; error: z.ZodError } {
132
+ const r = EventSchema.safeParse(value);
133
+ if (r.success) return { success: true, data: r.data as Event };
134
+ return { success: false, error: r.error };
135
+ }
@@ -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
+ }
@@ -0,0 +1,174 @@
1
+ // Schema v1 — see docs/superpowers/specs/2026-05-16-xera-v06-project-knowledge-graph-design.md §3
2
+
3
+ export const SCHEMA_VERSION = 1 as const;
4
+
5
+ export type Priority = 'p0' | 'p1' | 'p2';
6
+ export type ScenarioStatus = 'pass' | 'fail';
7
+ export type EdgeKind = 'tests' | 'uses' | 'covers' | 'modifies' | 'jira-linked' | 'similar' | 'ran';
8
+
9
+ export type Classification = 'REAL_BUG' | 'TEST_BUG' | 'SELECTOR_DRIFT' | 'FLAKY' | 'PASS';
10
+ // Note: TEST_OUTDATED is added in v0.6.1.
11
+
12
+ export interface TicketFetchedPayload {
13
+ ticketId: string;
14
+ summary: string;
15
+ ac: string[];
16
+ jiraLinks: Array<{
17
+ ticketId: string;
18
+ relation: 'blocks' | 'duplicates' | 'relates' | 'supersedes';
19
+ }>;
20
+ storyHash: string;
21
+ modifiesAreas: string[];
22
+ }
23
+
24
+ export interface TicketEnrichedPayload {
25
+ ticketId: string;
26
+ enrichedAt: string;
27
+ similarCount: number;
28
+ }
29
+
30
+ export interface ScenarioGeneratedPayload {
31
+ scenarioId: string;
32
+ ticketId: string;
33
+ name: string;
34
+ gherkin: string;
35
+ priority: Priority;
36
+ featureHash: string;
37
+ generatedAt: string;
38
+ }
39
+
40
+ export interface PomGeneratedPayload {
41
+ pomId: string;
42
+ ticketId: string;
43
+ filePath: string;
44
+ route: string;
45
+ locators: string[];
46
+ scope: 'local' | 'shared';
47
+ }
48
+
49
+ export interface PomPromotedPayload {
50
+ pomId: string;
51
+ fromPath: string;
52
+ toPath: string;
53
+ }
54
+
55
+ export interface RunCompletedPayload {
56
+ scenarioId: string;
57
+ ticketId: string;
58
+ runId: string;
59
+ status: ScenarioStatus;
60
+ traceId?: string;
61
+ runtime: number;
62
+ }
63
+
64
+ export interface RunClassifiedPayload {
65
+ scenarioId: string;
66
+ runId: string;
67
+ classification: Classification;
68
+ confidence: 'low' | 'medium' | 'high';
69
+ }
70
+
71
+ export interface ClassificationDisputedPayload {
72
+ runId: string;
73
+ scenarioId: string;
74
+ originalClassification: Classification;
75
+ disputedTo: Classification;
76
+ qaActor: string;
77
+ qaReason?: string;
78
+ }
79
+
80
+ export interface EdgeDiscoveredPayload {
81
+ kind: EdgeKind;
82
+ from: string;
83
+ to: string;
84
+ confidence?: number;
85
+ source: string;
86
+ }
87
+
88
+ export type EventPayloadMap = {
89
+ 'ticket.fetched': TicketFetchedPayload;
90
+ 'ticket.enriched': TicketEnrichedPayload;
91
+ 'scenario.generated': ScenarioGeneratedPayload;
92
+ 'pom.generated': PomGeneratedPayload;
93
+ 'pom.promoted': PomPromotedPayload;
94
+ 'run.completed': RunCompletedPayload;
95
+ 'run.classified': RunClassifiedPayload;
96
+ 'classification.disputed': ClassificationDisputedPayload;
97
+ 'edge.discovered': EdgeDiscoveredPayload;
98
+ };
99
+
100
+ export type EventType = keyof EventPayloadMap;
101
+
102
+ export type Event = {
103
+ [K in EventType]: {
104
+ event_id: string;
105
+ schema_version: typeof SCHEMA_VERSION;
106
+ ts: string;
107
+ actor: string;
108
+ type: K;
109
+ payload: EventPayloadMap[K];
110
+ };
111
+ }[EventType];
112
+
113
+ export interface TicketNode {
114
+ id: string;
115
+ summary: string;
116
+ ac: string[];
117
+ storyHash: string;
118
+ modifiesAreas: string[];
119
+ fetchedAt: string;
120
+ enrichedAt?: string;
121
+ }
122
+
123
+ export interface ScenarioNode {
124
+ id: string;
125
+ ticketId: string;
126
+ name: string;
127
+ gherkin: string;
128
+ priority: Priority;
129
+ featureHash: string;
130
+ generatedAt: string;
131
+ }
132
+
133
+ export interface PomNode {
134
+ id: string;
135
+ ticketId: string;
136
+ filePath: string;
137
+ route: string;
138
+ locators: string[];
139
+ scope: 'local' | 'shared';
140
+ }
141
+
142
+ export interface AreaNode {
143
+ id: string;
144
+ }
145
+
146
+ export interface FailureNode {
147
+ id: string;
148
+ scenarioId: string;
149
+ runId: string;
150
+ traceId?: string;
151
+ ts: string;
152
+ }
153
+
154
+ export interface EdgeRecord {
155
+ kind: EdgeKind;
156
+ from: string;
157
+ to: string;
158
+ confidence?: number;
159
+ source: string;
160
+ discoveredAt: string;
161
+ }
162
+
163
+ export interface Snapshot {
164
+ schema_version: typeof SCHEMA_VERSION;
165
+ generated_at: string;
166
+ event_count: number;
167
+ events_hash: string;
168
+ tickets: Record<string, TicketNode>;
169
+ scenarios: Record<string, ScenarioNode>;
170
+ poms: Record<string, PomNode>;
171
+ areas: Record<string, AreaNode>;
172
+ edges: EdgeRecord[];
173
+ latest_failures: Record<string, FailureNode>;
174
+ }