@xera-ai/core 0.4.1 → 0.4.2

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.
@@ -8882,11 +8882,18 @@ var ReportingSchema = z4.object({
8882
8882
  }).prefault({}),
8883
8883
  artifactLinks: z4.enum(["git", "local"]).default("git")
8884
8884
  }).prefault({});
8885
+ var RunSchema = z4.object({
8886
+ autoImpact: z4.object({
8887
+ enabled: z4.boolean().default(true),
8888
+ threshold: z4.number().nonnegative().default(6)
8889
+ }).prefault({})
8890
+ }).prefault({});
8885
8891
  var XeraConfigSchema = z4.object({
8886
8892
  jira: JiraSchema,
8887
8893
  web: WebSchema,
8888
8894
  ai: AISchema,
8889
8895
  reporting: ReportingSchema,
8896
+ run: RunSchema.prefault({}),
8890
8897
  adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
8891
8898
  });
8892
8899
 
@@ -10050,6 +10057,267 @@ async function healPrepareCmd(argv) {
10050
10057
  }
10051
10058
  }
10052
10059
 
10060
+ // src/bin-internal/impact-prepare.ts
10061
+ import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
10062
+ import { join as join17 } from "path";
10063
+
10064
+ // src/graph/impact.ts
10065
+ var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
10066
+ var EDGE_WEIGHT_FIXED = {
10067
+ modifies: 5,
10068
+ uses: 4,
10069
+ covers: 4
10070
+ };
10071
+ function jiraRelationWeight(source) {
10072
+ if (!source)
10073
+ return 0;
10074
+ if (source.endsWith("blocks"))
10075
+ return 4;
10076
+ if (source.endsWith("duplicates"))
10077
+ return 3;
10078
+ if (source.endsWith("relates"))
10079
+ return 2;
10080
+ if (source.endsWith("supersedes"))
10081
+ return 3;
10082
+ return 1;
10083
+ }
10084
+ function edgeWeight(edge) {
10085
+ if (edge.kind === "modifies")
10086
+ return EDGE_WEIGHT_FIXED.modifies ?? 0;
10087
+ if (edge.kind === "uses" || edge.kind === "covers")
10088
+ return EDGE_WEIGHT_FIXED.uses ?? 0;
10089
+ if (edge.kind === "jira-linked")
10090
+ return jiraRelationWeight(edge.source);
10091
+ if (edge.kind === "similar")
10092
+ return 1 * (edge.confidence ?? 0);
10093
+ return 0;
10094
+ }
10095
+ function riskScore(scenario, daysSinceLastPass) {
10096
+ const pri = PRIORITY_WEIGHT[scenario.priority] * 3;
10097
+ const firstEdge = scenario.edgePath[0];
10098
+ const edgeW = firstEdge ? edgeWeight(firstEdge) : 0;
10099
+ const confW = firstEdge?.confidence !== undefined ? firstEdge.confidence * 2 : 0;
10100
+ const decay = daysSinceLastPass * 0.1;
10101
+ return pri + edgeW + confW - decay;
10102
+ }
10103
+ var PRIORITY_RANK = { p0: 3, p1: 2, p2: 1 };
10104
+ function daysSince(ts) {
10105
+ if (!ts)
10106
+ return 0;
10107
+ const ms = Date.now() - Date.parse(ts);
10108
+ return ms < 0 ? 0 : ms / (86400 * 1000);
10109
+ }
10110
+ function walkImpact(graph, target, opts) {
10111
+ const result = [];
10112
+ const seen = new Set;
10113
+ const targetAreas = new Set(target.modifiesAreas);
10114
+ const pomIds = graph.edges.filter((e) => e.kind === "covers" && targetAreas.has(e.to)).map((e) => e.from);
10115
+ const directScenarios = graph.edges.filter((e) => e.kind === "uses" && pomIds.includes(e.to)).map((e) => e.from);
10116
+ for (const scenarioId2 of directScenarios) {
10117
+ if (seen.has(scenarioId2))
10118
+ continue;
10119
+ const scenario = graph.scenarios[scenarioId2];
10120
+ if (!scenario)
10121
+ continue;
10122
+ if (scenario.ticketId === target.id)
10123
+ continue;
10124
+ const usingPom = graph.edges.find((e) => e.kind === "uses" && e.from === scenarioId2);
10125
+ const modifyEdge = graph.edges.find((e) => e.kind === "modifies" && e.from === target.id && targetAreas.has(e.to));
10126
+ const edgePath = [];
10127
+ if (modifyEdge)
10128
+ edgePath.push({ kind: "modifies", from: modifyEdge.from, to: modifyEdge.to });
10129
+ if (usingPom)
10130
+ edgePath.push({ kind: "uses", from: usingPom.from, to: usingPom.to });
10131
+ seen.add(scenarioId2);
10132
+ const impact = {
10133
+ scenarioId: scenarioId2,
10134
+ ticketId: scenario.ticketId,
10135
+ name: scenario.name,
10136
+ priority: scenario.priority,
10137
+ edgePath,
10138
+ riskScore: 0
10139
+ };
10140
+ impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
10141
+ result.push(impact);
10142
+ }
10143
+ if (opts.depth >= 2) {
10144
+ const linked = graph.edges.filter((e) => e.kind === "jira-linked" && e.from === target.id).map((e) => ({ to: e.to, source: e.source }));
10145
+ for (const link of linked) {
10146
+ const sceneIds = graph.edges.filter((e) => e.kind === "tests" && e.from === link.to).map((e) => e.to);
10147
+ for (const scenarioId2 of sceneIds) {
10148
+ if (seen.has(scenarioId2))
10149
+ continue;
10150
+ const scenario = graph.scenarios[scenarioId2];
10151
+ if (!scenario || scenario.ticketId === target.id)
10152
+ continue;
10153
+ seen.add(scenarioId2);
10154
+ const edge = { kind: "jira-linked", from: target.id, to: link.to };
10155
+ if (link.source !== undefined)
10156
+ edge.source = link.source;
10157
+ const impact = {
10158
+ scenarioId: scenarioId2,
10159
+ ticketId: scenario.ticketId,
10160
+ name: scenario.name,
10161
+ priority: scenario.priority,
10162
+ edgePath: [edge],
10163
+ riskScore: 0
10164
+ };
10165
+ impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
10166
+ result.push(impact);
10167
+ }
10168
+ }
10169
+ }
10170
+ if (opts.depth >= 3) {
10171
+ const similar = graph.edges.filter((e) => e.kind === "similar" && e.from === target.id).map((e) => ({ to: e.to, confidence: e.confidence }));
10172
+ for (const link of similar) {
10173
+ const sceneIds = graph.edges.filter((e) => e.kind === "tests" && e.from === link.to).map((e) => e.to);
10174
+ for (const scenarioId2 of sceneIds) {
10175
+ if (seen.has(scenarioId2))
10176
+ continue;
10177
+ const scenario = graph.scenarios[scenarioId2];
10178
+ if (!scenario || scenario.ticketId === target.id)
10179
+ continue;
10180
+ seen.add(scenarioId2);
10181
+ const edge = { kind: "similar", from: target.id, to: link.to };
10182
+ if (link.confidence !== undefined)
10183
+ edge.confidence = link.confidence;
10184
+ const impact = {
10185
+ scenarioId: scenarioId2,
10186
+ ticketId: scenario.ticketId,
10187
+ name: scenario.name,
10188
+ priority: scenario.priority,
10189
+ edgePath: [edge],
10190
+ riskScore: 0
10191
+ };
10192
+ impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
10193
+ result.push(impact);
10194
+ }
10195
+ }
10196
+ }
10197
+ let filtered = result;
10198
+ if (opts.minPriority) {
10199
+ const min = PRIORITY_RANK[opts.minPriority];
10200
+ filtered = filtered.filter((s) => PRIORITY_RANK[s.priority] >= min);
10201
+ }
10202
+ filtered.sort((a, b) => b.riskScore - a.riskScore);
10203
+ return filtered;
10204
+ }
10205
+ var HIGH_THRESHOLD = 7;
10206
+ var MEDIUM_THRESHOLD = 4;
10207
+ function bucket(score) {
10208
+ if (score >= HIGH_THRESHOLD)
10209
+ return "high";
10210
+ if (score >= MEDIUM_THRESHOLD)
10211
+ return "medium";
10212
+ return "low";
10213
+ }
10214
+ function fmtEdgePath(path) {
10215
+ return path.map((e) => `${e.from} \u2192[${e.kind}]\u2192 ${e.to}`).join(" \xB7 ");
10216
+ }
10217
+ function renderImpactMarkdown(report) {
10218
+ const lines = [];
10219
+ lines.push(`# Impact Analysis \u2014 ${report.targetTicket}`);
10220
+ lines.push("");
10221
+ lines.push(`**Modified areas:** ${report.modifiedAreas.join(", ") || "(none)"}`);
10222
+ lines.push(`**Generated:** ${report.generatedAt}`);
10223
+ lines.push("");
10224
+ if (report.scenarios.length === 0) {
10225
+ lines.push("No prior scenarios in the modified areas. This may be a new feature area.");
10226
+ lines.push("");
10227
+ return lines.join(`
10228
+ `);
10229
+ }
10230
+ const bySeverity = {
10231
+ high: [],
10232
+ medium: [],
10233
+ low: []
10234
+ };
10235
+ for (const s of report.scenarios)
10236
+ bySeverity[bucket(s.riskScore)].push(s);
10237
+ lines.push(`**Total impacted:** ${report.scenarios.length} scenarios (${bySeverity.high.length} high \xB7 ${bySeverity.medium.length} medium \xB7 ${bySeverity.low.length} low)`);
10238
+ lines.push("");
10239
+ for (const [name, scenarios] of [
10240
+ ["High-risk", bySeverity.high],
10241
+ ["Medium-risk", bySeverity.medium],
10242
+ ["Low-risk", bySeverity.low]
10243
+ ]) {
10244
+ if (scenarios.length === 0)
10245
+ continue;
10246
+ lines.push(`## ${name}`);
10247
+ lines.push("");
10248
+ for (const s of scenarios) {
10249
+ lines.push(`### ${s.ticketId} / "${s.name}" [${s.priority.toUpperCase()}] score ${s.riskScore.toFixed(1)}`);
10250
+ lines.push(`- Edge: ${fmtEdgePath(s.edgePath)}`);
10251
+ if (s.lastPassedAt)
10252
+ lines.push(`- Last passed: ${s.lastPassedAt}`);
10253
+ lines.push("");
10254
+ }
10255
+ }
10256
+ lines.push("## Re-run commands");
10257
+ lines.push(`- All: \`bun run xera:exec --from-impact ${report.targetTicket}\``);
10258
+ lines.push(`- P0 only: \`bun run xera:exec --from-impact ${report.targetTicket} --min-priority p0\``);
10259
+ lines.push(`- Select: \`bun run xera:exec --from-impact ${report.targetTicket} --select\``);
10260
+ lines.push("");
10261
+ return lines.join(`
10262
+ `);
10263
+ }
10264
+
10265
+ // src/bin-internal/impact-prepare.ts
10266
+ init_store();
10267
+ function parseDepth(s) {
10268
+ const n = s ? Number.parseInt(s, 10) : 2;
10269
+ if (n === 1 || n === 3)
10270
+ return n;
10271
+ return 2;
10272
+ }
10273
+ function parseMinPriority(s) {
10274
+ if (s === "p0" || s === "p1" || s === "p2")
10275
+ return s;
10276
+ return;
10277
+ }
10278
+ async function impactPrepareCmd(argv) {
10279
+ const ticket = argv[0];
10280
+ if (!ticket || ticket.startsWith("--")) {
10281
+ console.error("[impact-prepare] usage: impact-prepare <TICKET> [--depth 1|2|3] [--min-priority p0|p1|p2] [--quiet]");
10282
+ return 1;
10283
+ }
10284
+ let depth = 2;
10285
+ let minPriority;
10286
+ let quiet = false;
10287
+ for (let i2 = 1;i2 < argv.length; i2++) {
10288
+ if (argv[i2] === "--depth")
10289
+ depth = parseDepth(argv[++i2]);
10290
+ else if (argv[i2] === "--min-priority")
10291
+ minPriority = parseMinPriority(argv[++i2]);
10292
+ else if (argv[i2] === "--quiet")
10293
+ quiet = true;
10294
+ }
10295
+ const repoRoot = process.cwd();
10296
+ const graph = deriveSnapshot(loadAllEvents(repoRoot));
10297
+ const target = graph.tickets[ticket];
10298
+ if (!target) {
10299
+ console.error(`[impact-prepare] ticket ${ticket} not in graph; run /xera-fetch first`);
10300
+ return 2;
10301
+ }
10302
+ const opts = { depth };
10303
+ if (minPriority)
10304
+ opts.minPriority = minPriority;
10305
+ const scenarios = walkImpact(graph, target, opts);
10306
+ const report = {
10307
+ targetTicket: ticket,
10308
+ modifiedAreas: target.modifiesAreas,
10309
+ scenarios,
10310
+ generatedAt: new Date().toISOString()
10311
+ };
10312
+ const impactDir = join17(repoRoot, ".xera/impact");
10313
+ mkdirSync11(impactDir, { recursive: true });
10314
+ writeFileSync11(join17(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10315
+ if (!quiet) {
10316
+ writeFileSync11(join17(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
10317
+ }
10318
+ return 0;
10319
+ }
10320
+
10053
10321
  // src/bin-internal/lint.ts
10054
10322
  import { lintTicket } from "@xera-ai/web";
10055
10323
  async function lintCmd(argv) {
@@ -10071,7 +10339,7 @@ async function lintCmd(argv) {
10071
10339
 
10072
10340
  // src/bin-internal/normalize.ts
10073
10341
  import { existsSync as existsSync21, readdirSync as readdirSync7 } from "fs";
10074
- import { join as join17 } from "path";
10342
+ import { join as join18 } from "path";
10075
10343
  import { normalizeRun } from "@xera-ai/web";
10076
10344
  async function normalizeCmd(argv) {
10077
10345
  const ticket = argv[0];
@@ -10086,7 +10354,7 @@ async function normalizeCmd(argv) {
10086
10354
  console.error("[xera:normalize] no run found");
10087
10355
  return 1;
10088
10356
  }
10089
- const runDir = join17(paths.runsDir, runId);
10357
+ const runDir = join18(paths.runsDir, runId);
10090
10358
  if (!existsSync21(runDir)) {
10091
10359
  console.error(`[xera:normalize] runs/${runId} missing`);
10092
10360
  return 1;
@@ -10098,10 +10366,10 @@ async function normalizeCmd(argv) {
10098
10366
 
10099
10367
  // src/bin-internal/post.ts
10100
10368
  import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
10101
- import { join as join18 } from "path";
10369
+ import { join as join19 } from "path";
10102
10370
 
10103
10371
  // src/artifact/status.ts
10104
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
10372
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
10105
10373
  import { dirname as dirname6 } from "path";
10106
10374
  import { z as z7 } from "zod";
10107
10375
  var ClassificationEnum = z7.enum([
@@ -10141,8 +10409,8 @@ function readStatus(path) {
10141
10409
  return StatusJsonSchema.parse(JSON.parse(readFileSync18(path, "utf8")));
10142
10410
  }
10143
10411
  function writeStatus(path, status) {
10144
- mkdirSync11(dirname6(path), { recursive: true });
10145
- writeFileSync11(path, JSON.stringify(status, null, 2));
10412
+ mkdirSync12(dirname6(path), { recursive: true });
10413
+ writeFileSync12(path, JSON.stringify(status, null, 2));
10146
10414
  }
10147
10415
  function appendHistory(path, entry) {
10148
10416
  const s = readStatus(path);
@@ -10168,7 +10436,7 @@ async function postCmd(argv) {
10168
10436
  return 0;
10169
10437
  }
10170
10438
  const paths = resolveArtifactPaths(cwd, ticket);
10171
- const draftPath = join18(paths.ticketDir, "jira-comment.draft.md");
10439
+ const draftPath = join19(paths.ticketDir, "jira-comment.draft.md");
10172
10440
  if (!existsSync23(draftPath)) {
10173
10441
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
10174
10442
  return 1;
@@ -10201,8 +10469,8 @@ async function promoteCmd(argv) {
10201
10469
  }
10202
10470
 
10203
10471
  // src/bin-internal/report.ts
10204
- import { existsSync as existsSync25, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
10205
- import { join as join19 } from "path";
10472
+ import { existsSync as existsSync25, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "fs";
10473
+ import { join as join20 } from "path";
10206
10474
 
10207
10475
  // src/classifier/aggregate.ts
10208
10476
  var CLASS_PRIORITY = [
@@ -10376,7 +10644,7 @@ async function reportCmd(argv) {
10376
10644
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10377
10645
  const input = JSON.parse(readFileSync20(inputArg.slice("--input=".length), "utf8"));
10378
10646
  const aggregated = aggregateScenarios(input.scenarios);
10379
- const decisionsPath = join19(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
10647
+ const decisionsPath = join20(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
10380
10648
  const decisions = existsSync25(decisionsPath) ? JSON.parse(readFileSync20(decisionsPath, "utf8")) : {};
10381
10649
  const graph = deriveSnapshot(loadAllEvents(process.cwd()));
10382
10650
  const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
@@ -10425,8 +10693,8 @@ async function reportCmd(argv) {
10425
10693
  xeraVersion: "0.1.0",
10426
10694
  promptsVersion: "1.0.0"
10427
10695
  });
10428
- const draftPath = join19(paths.ticketDir, "jira-comment.draft.md");
10429
- writeFileSync12(draftPath, md);
10696
+ const draftPath = join20(paths.ticketDir, "jira-comment.draft.md");
10697
+ writeFileSync13(draftPath, md);
10430
10698
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
10431
10699
  return 0;
10432
10700
  }
@@ -10530,6 +10798,7 @@ var COMMANDS = {
10530
10798
  "graph-record": graphRecordCmd,
10531
10799
  "graph-snapshot": graphSnapshotCmd,
10532
10800
  "heal-prepare": healPrepareCmd,
10801
+ "impact-prepare": impactPrepareCmd,
10533
10802
  lint: lintCmd,
10534
10803
  normalize: normalizeCmd,
10535
10804
  post: postCmd,
@@ -0,0 +1,2 @@
1
+ export declare function impactPrepareCmd(argv: string[]): Promise<number>;
2
+ //# sourceMappingURL=impact-prepare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impact-prepare.d.ts","sourceRoot":"","sources":["../../src/bin-internal/impact-prepare.ts"],"names":[],"mappings":"AAkBA,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CtE"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin-internal/index.ts"],"names":[],"mappings":"AAgDA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAczD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin-internal/index.ts"],"names":[],"mappings":"AAkDA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAczD"}
@@ -60,6 +60,12 @@ export declare const XeraConfigSchema: z.ZodObject<{
60
60
  local: "local";
61
61
  }>>;
62
62
  }, z.core.$strip>>;
63
+ run: z.ZodPrefault<z.ZodPrefault<z.ZodObject<{
64
+ autoImpact: z.ZodPrefault<z.ZodObject<{
65
+ enabled: z.ZodDefault<z.ZodBoolean>;
66
+ threshold: z.ZodDefault<z.ZodNumber>;
67
+ }, z.core.$strip>>;
68
+ }, z.core.$strip>>>;
63
69
  adapters: z.ZodDefault<z.ZodArray<z.ZodString>>;
64
70
  }, z.core.$strip>;
65
71
  export type XeraConfig = z.infer<typeof XeraConfigSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuExB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAM3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkFxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { EdgeKind, Priority, Snapshot, TicketNode } from './types';
2
+ export interface ImpactEdge {
3
+ kind: EdgeKind;
4
+ from: string;
5
+ to: string;
6
+ confidence?: number;
7
+ source?: string;
8
+ }
9
+ export interface ImpactScenario {
10
+ scenarioId: string;
11
+ ticketId: string;
12
+ name: string;
13
+ priority: Priority;
14
+ edgePath: ImpactEdge[];
15
+ riskScore: number;
16
+ lastPassedAt?: string;
17
+ }
18
+ export interface ImpactOpts {
19
+ depth: 1 | 2 | 3;
20
+ minPriority?: Priority;
21
+ }
22
+ export interface ImpactReport {
23
+ targetTicket: string;
24
+ modifiedAreas: string[];
25
+ scenarios: ImpactScenario[];
26
+ generatedAt: string;
27
+ }
28
+ export declare function riskScore(scenario: ImpactScenario, daysSinceLastPass: number): number;
29
+ export declare function walkImpact(graph: Snapshot, target: TicketNode, opts: ImpactOpts): ImpactScenario[];
30
+ export declare function renderImpactMarkdown(report: ImpactReport): string;
31
+ //# sourceMappingURL=impact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impact.d.ts","sourceRoot":"","sources":["../../src/graph/impact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAExE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjB,WAAW,CAAC,EAAE,QAAQ,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB;AA4BD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAOrF;AAUD,wBAAgB,UAAU,CACxB,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,GACf,cAAc,EAAE,CAkHlB;AAeD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAqDjE"}
@@ -4,6 +4,8 @@ export type { CostSummary, LlmCallLog } from './cost';
4
4
  export { logLlmCall, summarizeCost } from './cost';
5
5
  export type { EnrichOptions, EnrichResult } from './enrich';
6
6
  export { enrichTicket } from './enrich';
7
+ export type { ImpactEdge, ImpactOpts, ImpactReport, ImpactScenario, } from './impact';
8
+ export { renderImpactMarkdown, riskScore, walkImpact, } from './impact';
7
9
  export { currentYyyyMm, graphPaths } from './paths';
8
10
  export { EventSchema, safeParseEvent } from './schema';
9
11
  export { buildSimilarityPrompt } from './similarity';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/graph/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/graph/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,oBAAoB,EACpB,SAAS,EACT,UAAU,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC"}
package/dist/src/index.js CHANGED
@@ -324,11 +324,18 @@ var ReportingSchema = z4.object({
324
324
  }).prefault({}),
325
325
  artifactLinks: z4.enum(["git", "local"]).default("git")
326
326
  }).prefault({});
327
+ var RunSchema = z4.object({
328
+ autoImpact: z4.object({
329
+ enabled: z4.boolean().default(true),
330
+ threshold: z4.number().nonnegative().default(6)
331
+ }).prefault({})
332
+ }).prefault({});
327
333
  var XeraConfigSchema = z4.object({
328
334
  jira: JiraSchema,
329
335
  web: WebSchema,
330
336
  ai: AISchema,
331
337
  reporting: ReportingSchema,
338
+ run: RunSchema.prefault({}),
332
339
  adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
333
340
  });
334
341
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/core",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -0,0 +1,64 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { ImpactOpts, ImpactReport } from '../graph/impact';
4
+ import { renderImpactMarkdown, walkImpact } from '../graph/impact';
5
+ import { deriveSnapshot, loadAllEvents } from '../graph/store';
6
+ import type { Priority } from '../graph/types';
7
+
8
+ function parseDepth(s: string | undefined): 1 | 2 | 3 {
9
+ const n = s ? Number.parseInt(s, 10) : 2;
10
+ if (n === 1 || n === 3) return n;
11
+ return 2;
12
+ }
13
+
14
+ function parseMinPriority(s: string | undefined): Priority | undefined {
15
+ if (s === 'p0' || s === 'p1' || s === 'p2') return s;
16
+ return undefined;
17
+ }
18
+
19
+ export async function impactPrepareCmd(argv: string[]): Promise<number> {
20
+ const ticket = argv[0];
21
+ if (!ticket || ticket.startsWith('--')) {
22
+ console.error(
23
+ '[impact-prepare] usage: impact-prepare <TICKET> [--depth 1|2|3] [--min-priority p0|p1|p2] [--quiet]',
24
+ );
25
+ return 1;
26
+ }
27
+
28
+ let depth: 1 | 2 | 3 = 2;
29
+ let minPriority: Priority | undefined;
30
+ let quiet = false;
31
+ for (let i = 1; i < argv.length; i++) {
32
+ if (argv[i] === '--depth') depth = parseDepth(argv[++i]);
33
+ else if (argv[i] === '--min-priority') minPriority = parseMinPriority(argv[++i]);
34
+ else if (argv[i] === '--quiet') quiet = true;
35
+ }
36
+
37
+ const repoRoot = process.cwd();
38
+ const graph = deriveSnapshot(loadAllEvents(repoRoot));
39
+ const target = graph.tickets[ticket];
40
+ if (!target) {
41
+ console.error(`[impact-prepare] ticket ${ticket} not in graph; run /xera-fetch first`);
42
+ return 2;
43
+ }
44
+
45
+ const opts: ImpactOpts = { depth };
46
+ if (minPriority) opts.minPriority = minPriority;
47
+
48
+ const scenarios = walkImpact(graph, target, opts);
49
+
50
+ const report: ImpactReport = {
51
+ targetTicket: ticket,
52
+ modifiedAreas: target.modifiesAreas,
53
+ scenarios,
54
+ generatedAt: new Date().toISOString(),
55
+ };
56
+
57
+ const impactDir = join(repoRoot, '.xera/impact');
58
+ mkdirSync(impactDir, { recursive: true });
59
+ writeFileSync(join(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
60
+ if (!quiet) {
61
+ writeFileSync(join(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
62
+ }
63
+ return 0;
64
+ }
@@ -10,6 +10,7 @@ import { graphQueryCmd } from './graph-query';
10
10
  import { graphRecordCmd } from './graph-record';
11
11
  import { graphSnapshotCmd } from './graph-snapshot';
12
12
  import { healPrepareCmd } from './heal-prepare';
13
+ import { impactPrepareCmd } from './impact-prepare';
13
14
  import { lintCmd } from './lint';
14
15
  import { normalizeCmd } from './normalize';
15
16
  import { postCmd } from './post';
@@ -34,6 +35,7 @@ const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
34
35
  'graph-record': graphRecordCmd,
35
36
  'graph-snapshot': graphSnapshotCmd,
36
37
  'heal-prepare': healPrepareCmd,
38
+ 'impact-prepare': impactPrepareCmd,
37
39
  lint: lintCmd,
38
40
  normalize: normalizeCmd,
39
41
  post: postCmd,
@@ -69,11 +69,23 @@ const ReportingSchema = z
69
69
  })
70
70
  .prefault({});
71
71
 
72
+ const RunSchema = z
73
+ .object({
74
+ autoImpact: z
75
+ .object({
76
+ enabled: z.boolean().default(true),
77
+ threshold: z.number().nonnegative().default(6.0),
78
+ })
79
+ .prefault({}),
80
+ })
81
+ .prefault({});
82
+
72
83
  export const XeraConfigSchema = z.object({
73
84
  jira: JiraSchema,
74
85
  web: WebSchema,
75
86
  ai: AISchema,
76
87
  reporting: ReportingSchema,
88
+ run: RunSchema.prefault({}),
77
89
  adapters: z.array(z.string().min(1)).min(1).default(['web']),
78
90
  });
79
91
 
@@ -0,0 +1,262 @@
1
+ import type { EdgeKind, Priority, Snapshot, TicketNode } from './types';
2
+
3
+ export interface ImpactEdge {
4
+ kind: EdgeKind;
5
+ from: string;
6
+ to: string;
7
+ confidence?: number;
8
+ source?: string;
9
+ }
10
+
11
+ export interface ImpactScenario {
12
+ scenarioId: string;
13
+ ticketId: string; // owner of the scenario (NOT the impact target)
14
+ name: string;
15
+ priority: Priority;
16
+ edgePath: ImpactEdge[];
17
+ riskScore: number;
18
+ lastPassedAt?: string;
19
+ }
20
+
21
+ export interface ImpactOpts {
22
+ depth: 1 | 2 | 3;
23
+ minPriority?: Priority;
24
+ }
25
+
26
+ export interface ImpactReport {
27
+ targetTicket: string;
28
+ modifiedAreas: string[];
29
+ scenarios: ImpactScenario[];
30
+ generatedAt: string;
31
+ }
32
+
33
+ const PRIORITY_WEIGHT: Record<Priority, number> = { p0: 3, p1: 2, p2: 1 };
34
+
35
+ const EDGE_WEIGHT_FIXED: Partial<Record<EdgeKind, number>> = {
36
+ modifies: 5, // direct collision via SUT area
37
+ uses: 4, // shared POM
38
+ covers: 4, // shared POM (alt path)
39
+ // 'jira-linked' weight is dynamic — see jiraRelationWeight
40
+ };
41
+
42
+ function jiraRelationWeight(source?: string): number {
43
+ if (!source) return 0;
44
+ if (source.endsWith('blocks')) return 4;
45
+ if (source.endsWith('duplicates')) return 3;
46
+ if (source.endsWith('relates')) return 2;
47
+ if (source.endsWith('supersedes')) return 3;
48
+ return 1;
49
+ }
50
+
51
+ function edgeWeight(edge: ImpactEdge): number {
52
+ if (edge.kind === 'modifies') return EDGE_WEIGHT_FIXED.modifies ?? 0;
53
+ if (edge.kind === 'uses' || edge.kind === 'covers') return EDGE_WEIGHT_FIXED.uses ?? 0;
54
+ if (edge.kind === 'jira-linked') return jiraRelationWeight(edge.source);
55
+ if (edge.kind === 'similar') return 1 * (edge.confidence ?? 0);
56
+ return 0;
57
+ }
58
+
59
+ export function riskScore(scenario: ImpactScenario, daysSinceLastPass: number): number {
60
+ const pri = PRIORITY_WEIGHT[scenario.priority] * 3;
61
+ const firstEdge = scenario.edgePath[0];
62
+ const edgeW = firstEdge ? edgeWeight(firstEdge) : 0;
63
+ const confW = firstEdge?.confidence !== undefined ? firstEdge.confidence * 2 : 0;
64
+ const decay = daysSinceLastPass * 0.1;
65
+ return pri + edgeW + confW - decay;
66
+ }
67
+
68
+ const PRIORITY_RANK: Record<Priority, number> = { p0: 3, p1: 2, p2: 1 };
69
+
70
+ function daysSince(ts: string | undefined): number {
71
+ if (!ts) return 0;
72
+ const ms = Date.now() - Date.parse(ts);
73
+ return ms < 0 ? 0 : ms / (86400 * 1000);
74
+ }
75
+
76
+ export function walkImpact(
77
+ graph: Snapshot,
78
+ target: TicketNode,
79
+ opts: ImpactOpts,
80
+ ): ImpactScenario[] {
81
+ const result: ImpactScenario[] = [];
82
+ const seen = new Set<string>();
83
+
84
+ // Areas the target modifies
85
+ const targetAreas = new Set(target.modifiesAreas);
86
+
87
+ // POMs covering any of those areas
88
+ const pomIds = graph.edges
89
+ .filter((e) => e.kind === 'covers' && targetAreas.has(e.to))
90
+ .map((e) => e.from);
91
+
92
+ // Scenarios using any of those POMs (depth 1 — direct collision)
93
+ const directScenarios = graph.edges
94
+ .filter((e) => e.kind === 'uses' && pomIds.includes(e.to))
95
+ .map((e) => e.from);
96
+
97
+ for (const scenarioId of directScenarios) {
98
+ if (seen.has(scenarioId)) continue;
99
+ const scenario = graph.scenarios[scenarioId];
100
+ if (!scenario) continue;
101
+ if (scenario.ticketId === target.id) continue; // exclude own scenarios
102
+
103
+ const usingPom = graph.edges.find((e) => e.kind === 'uses' && e.from === scenarioId);
104
+ const modifyEdge = graph.edges.find(
105
+ (e) => e.kind === 'modifies' && e.from === target.id && targetAreas.has(e.to),
106
+ );
107
+ const edgePath: ImpactEdge[] = [];
108
+ if (modifyEdge) edgePath.push({ kind: 'modifies', from: modifyEdge.from, to: modifyEdge.to });
109
+ if (usingPom) edgePath.push({ kind: 'uses', from: usingPom.from, to: usingPom.to });
110
+
111
+ seen.add(scenarioId);
112
+ const impact: ImpactScenario = {
113
+ scenarioId,
114
+ ticketId: scenario.ticketId,
115
+ name: scenario.name,
116
+ priority: scenario.priority,
117
+ edgePath,
118
+ riskScore: 0,
119
+ };
120
+ impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId]?.ts));
121
+ result.push(impact);
122
+ }
123
+
124
+ // Depth >= 2: jira-linked tickets contribute their scenarios
125
+ if (opts.depth >= 2) {
126
+ const linked = graph.edges
127
+ .filter((e) => e.kind === 'jira-linked' && e.from === target.id)
128
+ .map((e) => ({ to: e.to, source: e.source }));
129
+ for (const link of linked) {
130
+ const sceneIds = graph.edges
131
+ .filter((e) => e.kind === 'tests' && e.from === link.to)
132
+ .map((e) => e.to);
133
+ for (const scenarioId of sceneIds) {
134
+ if (seen.has(scenarioId)) continue;
135
+ const scenario = graph.scenarios[scenarioId];
136
+ if (!scenario || scenario.ticketId === target.id) continue;
137
+ seen.add(scenarioId);
138
+ const edge: ImpactEdge = { kind: 'jira-linked', from: target.id, to: link.to };
139
+ if (link.source !== undefined) edge.source = link.source;
140
+ const impact: ImpactScenario = {
141
+ scenarioId,
142
+ ticketId: scenario.ticketId,
143
+ name: scenario.name,
144
+ priority: scenario.priority,
145
+ edgePath: [edge],
146
+ riskScore: 0,
147
+ };
148
+ impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId]?.ts));
149
+ result.push(impact);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Depth >= 3: similar tickets contribute their scenarios
155
+ if (opts.depth >= 3) {
156
+ const similar = graph.edges
157
+ .filter((e) => e.kind === 'similar' && e.from === target.id)
158
+ .map((e) => ({ to: e.to, confidence: e.confidence }));
159
+ for (const link of similar) {
160
+ const sceneIds = graph.edges
161
+ .filter((e) => e.kind === 'tests' && e.from === link.to)
162
+ .map((e) => e.to);
163
+ for (const scenarioId of sceneIds) {
164
+ if (seen.has(scenarioId)) continue;
165
+ const scenario = graph.scenarios[scenarioId];
166
+ if (!scenario || scenario.ticketId === target.id) continue;
167
+ seen.add(scenarioId);
168
+ const edge: ImpactEdge = { kind: 'similar', from: target.id, to: link.to };
169
+ if (link.confidence !== undefined) edge.confidence = link.confidence;
170
+ const impact: ImpactScenario = {
171
+ scenarioId,
172
+ ticketId: scenario.ticketId,
173
+ name: scenario.name,
174
+ priority: scenario.priority,
175
+ edgePath: [edge],
176
+ riskScore: 0,
177
+ };
178
+ impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId]?.ts));
179
+ result.push(impact);
180
+ }
181
+ }
182
+ }
183
+
184
+ // Filter by min-priority
185
+ let filtered = result;
186
+ if (opts.minPriority) {
187
+ const min = PRIORITY_RANK[opts.minPriority];
188
+ filtered = filtered.filter((s) => PRIORITY_RANK[s.priority] >= min);
189
+ }
190
+
191
+ // Sort by riskScore descending
192
+ filtered.sort((a, b) => b.riskScore - a.riskScore);
193
+ return filtered;
194
+ }
195
+
196
+ const HIGH_THRESHOLD = 7.0;
197
+ const MEDIUM_THRESHOLD = 4.0;
198
+
199
+ function bucket(score: number): 'high' | 'medium' | 'low' {
200
+ if (score >= HIGH_THRESHOLD) return 'high';
201
+ if (score >= MEDIUM_THRESHOLD) return 'medium';
202
+ return 'low';
203
+ }
204
+
205
+ function fmtEdgePath(path: ImpactEdge[]): string {
206
+ return path.map((e) => `${e.from} →[${e.kind}]→ ${e.to}`).join(' · ');
207
+ }
208
+
209
+ export function renderImpactMarkdown(report: ImpactReport): string {
210
+ const lines: string[] = [];
211
+ lines.push(`# Impact Analysis — ${report.targetTicket}`);
212
+ lines.push('');
213
+ lines.push(`**Modified areas:** ${report.modifiedAreas.join(', ') || '(none)'}`);
214
+ lines.push(`**Generated:** ${report.generatedAt}`);
215
+ lines.push('');
216
+
217
+ if (report.scenarios.length === 0) {
218
+ lines.push('No prior scenarios in the modified areas. This may be a new feature area.');
219
+ lines.push('');
220
+ return lines.join('\n');
221
+ }
222
+
223
+ const bySeverity = {
224
+ high: [] as ImpactScenario[],
225
+ medium: [] as ImpactScenario[],
226
+ low: [] as ImpactScenario[],
227
+ };
228
+ for (const s of report.scenarios) bySeverity[bucket(s.riskScore)].push(s);
229
+
230
+ lines.push(
231
+ `**Total impacted:** ${report.scenarios.length} scenarios (${bySeverity.high.length} high · ${bySeverity.medium.length} medium · ${bySeverity.low.length} low)`,
232
+ );
233
+ lines.push('');
234
+
235
+ for (const [name, scenarios] of [
236
+ ['High-risk', bySeverity.high],
237
+ ['Medium-risk', bySeverity.medium],
238
+ ['Low-risk', bySeverity.low],
239
+ ] as const) {
240
+ if (scenarios.length === 0) continue;
241
+ lines.push(`## ${name}`);
242
+ lines.push('');
243
+ for (const s of scenarios) {
244
+ lines.push(
245
+ `### ${s.ticketId} / "${s.name}" [${s.priority.toUpperCase()}] score ${s.riskScore.toFixed(1)}`,
246
+ );
247
+ lines.push(`- Edge: ${fmtEdgePath(s.edgePath)}`);
248
+ if (s.lastPassedAt) lines.push(`- Last passed: ${s.lastPassedAt}`);
249
+ lines.push('');
250
+ }
251
+ }
252
+
253
+ lines.push('## Re-run commands');
254
+ lines.push(`- All: \`bun run xera:exec --from-impact ${report.targetTicket}\``);
255
+ lines.push(
256
+ `- P0 only: \`bun run xera:exec --from-impact ${report.targetTicket} --min-priority p0\``,
257
+ );
258
+ lines.push(`- Select: \`bun run xera:exec --from-impact ${report.targetTicket} --select\``);
259
+ lines.push('');
260
+
261
+ return lines.join('\n');
262
+ }
@@ -14,6 +14,17 @@ export type { CostSummary, LlmCallLog } from './cost';
14
14
  export { logLlmCall, summarizeCost } from './cost';
15
15
  export type { EnrichOptions, EnrichResult } from './enrich';
16
16
  export { enrichTicket } from './enrich';
17
+ export type {
18
+ ImpactEdge,
19
+ ImpactOpts,
20
+ ImpactReport,
21
+ ImpactScenario,
22
+ } from './impact';
23
+ export {
24
+ renderImpactMarkdown,
25
+ riskScore,
26
+ walkImpact,
27
+ } from './impact';
17
28
  export { currentYyyyMm, graphPaths } from './paths';
18
29
  export { EventSchema, safeParseEvent } from './schema';
19
30
  export { buildSimilarityPrompt } from './similarity';