@xera-ai/core 0.4.0 → 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.
Files changed (46) hide show
  1. package/dist/artifact/status.d.ts +4 -0
  2. package/dist/artifact/status.d.ts.map +1 -1
  3. package/dist/bin/internal.js +593 -72
  4. package/dist/bin-internal/graph-enrich.d.ts +2 -0
  5. package/dist/bin-internal/graph-enrich.d.ts.map +1 -0
  6. package/dist/bin-internal/graph-record.d.ts.map +1 -1
  7. package/dist/bin-internal/impact-prepare.d.ts +2 -0
  8. package/dist/bin-internal/impact-prepare.d.ts.map +1 -0
  9. package/dist/bin-internal/index.d.ts.map +1 -1
  10. package/dist/bin-internal/report.d.ts.map +1 -1
  11. package/dist/bin-internal/verify-prompts.d.ts.map +1 -1
  12. package/dist/classifier/aggregate.d.ts.map +1 -1
  13. package/dist/config/schema.d.ts +6 -0
  14. package/dist/config/schema.d.ts.map +1 -1
  15. package/dist/graph/classify.d.ts +42 -0
  16. package/dist/graph/classify.d.ts.map +1 -0
  17. package/dist/graph/enrich.d.ts +10 -0
  18. package/dist/graph/enrich.d.ts.map +1 -0
  19. package/dist/graph/impact.d.ts +31 -0
  20. package/dist/graph/impact.d.ts.map +1 -0
  21. package/dist/graph/index.d.ts +7 -0
  22. package/dist/graph/index.d.ts.map +1 -1
  23. package/dist/graph/schema.d.ts +3 -0
  24. package/dist/graph/schema.d.ts.map +1 -1
  25. package/dist/graph/similarity.d.ts +3 -0
  26. package/dist/graph/similarity.d.ts.map +1 -0
  27. package/dist/graph/types.d.ts +1 -1
  28. package/dist/graph/types.d.ts.map +1 -1
  29. package/dist/src/index.js +15 -1
  30. package/package.json +1 -1
  31. package/src/artifact/status.ts +8 -1
  32. package/src/bin-internal/graph-enrich.ts +28 -0
  33. package/src/bin-internal/graph-record.ts +45 -1
  34. package/src/bin-internal/impact-prepare.ts +64 -0
  35. package/src/bin-internal/index.ts +4 -0
  36. package/src/bin-internal/report.ts +63 -5
  37. package/src/bin-internal/verify-prompts.ts +2 -0
  38. package/src/classifier/aggregate.ts +1 -0
  39. package/src/config/schema.ts +12 -0
  40. package/src/graph/classify.ts +126 -0
  41. package/src/graph/enrich.ts +103 -0
  42. package/src/graph/impact.ts +262 -0
  43. package/src/graph/index.ts +26 -0
  44. package/src/graph/schema.ts +8 -1
  45. package/src/graph/similarity.ts +43 -0
  46. package/src/graph/types.ts +7 -2
@@ -98,7 +98,14 @@ var init_schema = __esm(() => {
98
98
  traceId: z.string().optional(),
99
99
  runtime: z.number().nonnegative()
100
100
  }).passthrough();
101
- classification = z.enum(["REAL_BUG", "TEST_BUG", "SELECTOR_DRIFT", "FLAKY", "PASS"]);
101
+ classification = z.enum([
102
+ "REAL_BUG",
103
+ "TEST_BUG",
104
+ "SELECTOR_DRIFT",
105
+ "FLAKY",
106
+ "PASS",
107
+ "TEST_OUTDATED"
108
+ ]);
102
109
  runClassified = z.object({
103
110
  scenarioId: z.string(),
104
111
  runId: z.string(),
@@ -7730,7 +7737,7 @@ function parseFlags2(args) {
7730
7737
  async function graphRecordCmd(argv) {
7731
7738
  const [action, ...rest] = argv;
7732
7739
  if (!action) {
7733
- console.error(`Usage: xera-internal graph-record <fetch|script|exec|classify|promote> [args]`);
7740
+ console.error(`Usage: xera-internal graph-record <fetch|script|exec|classify|promote|dispute> [args]`);
7734
7741
  return 1;
7735
7742
  }
7736
7743
  const repoRoot = process.cwd();
@@ -7774,6 +7781,43 @@ async function graphRecordCmd(argv) {
7774
7781
  case "promote": {
7775
7782
  return recordPromote(repoRoot, parseFlags2(rest));
7776
7783
  }
7784
+ case "dispute": {
7785
+ const flags = parseFlags2(rest);
7786
+ const runId = flags.get("--run-id");
7787
+ const scenarioIdArg = flags.get("--scenario-id");
7788
+ const from = flags.get("--from");
7789
+ const to = flags.get("--to");
7790
+ const actor = flags.get("--actor");
7791
+ const reason = flags.get("--reason");
7792
+ if (!runId || !scenarioIdArg || !from || !to || !actor) {
7793
+ console.error("[graph-record dispute] required: --run-id --scenario-id --from --to --actor [--reason]");
7794
+ return 1;
7795
+ }
7796
+ const validClass = [
7797
+ "REAL_BUG",
7798
+ "TEST_BUG",
7799
+ "SELECTOR_DRIFT",
7800
+ "FLAKY",
7801
+ "PASS",
7802
+ "TEST_OUTDATED"
7803
+ ];
7804
+ if (!validClass.includes(from) || !validClass.includes(to)) {
7805
+ console.error(`[graph-record dispute] --from and --to must be one of: ${validClass.join(", ")}`);
7806
+ return 1;
7807
+ }
7808
+ const payload = {
7809
+ runId,
7810
+ scenarioId: scenarioIdArg,
7811
+ originalClassification: from,
7812
+ disputedTo: to,
7813
+ qaActor: actor
7814
+ };
7815
+ if (reason)
7816
+ payload.qaReason = reason;
7817
+ const e = makeEvent("xera-report", "classification.disputed", payload);
7818
+ appendEvents(repoRoot, [e], { skill: "xera-report", ticketId: scenarioIdArg.slice(0, 12) });
7819
+ return 0;
7820
+ }
7777
7821
  default:
7778
7822
  console.error(`Unknown action: ${action}`);
7779
7823
  return 1;
@@ -7831,7 +7875,9 @@ var IN_SCOPE_PROMPTS = [
7831
7875
  "feature-from-story.md",
7832
7876
  "script-from-feature.md",
7833
7877
  "heal-locator.md",
7834
- "extract-areas.md"
7878
+ "extract-areas.md",
7879
+ "similarity-match.md",
7880
+ "classify-outdated.md"
7835
7881
  ];
7836
7882
  var REQUIRED_SECTION_HEADING = "## Handling untrusted input";
7837
7883
  var REQUIRED_KEYWORDS = ["UNTRUSTED", "injection-follow", "<XR_"];
@@ -8836,11 +8882,18 @@ var ReportingSchema = z4.object({
8836
8882
  }).prefault({}),
8837
8883
  artifactLinks: z4.enum(["git", "local"]).default("git")
8838
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({});
8839
8891
  var XeraConfigSchema = z4.object({
8840
8892
  jira: JiraSchema,
8841
8893
  web: WebSchema,
8842
8894
  ai: AISchema,
8843
8895
  reporting: ReportingSchema,
8896
+ run: RunSchema.prefault({}),
8844
8897
  adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
8845
8898
  });
8846
8899
 
@@ -9266,6 +9319,96 @@ async function graphBackfillCmd(argv) {
9266
9319
  return 0;
9267
9320
  }
9268
9321
 
9322
+ // src/graph/enrich.ts
9323
+ init_store();
9324
+ init_ulid();
9325
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
9326
+ import { join as join15 } from "path";
9327
+ import { z as z6 } from "zod";
9328
+ var MAX_SIMILAR_EDGES = 10;
9329
+ var MIN_CONFIDENCE = 0.7;
9330
+ var SimilarEntrySchema = z6.object({
9331
+ ticketId: z6.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
9332
+ confidence: z6.number(),
9333
+ reason: z6.string()
9334
+ });
9335
+ var EnrichmentInputSchema = z6.object({
9336
+ similar: z6.array(SimilarEntrySchema)
9337
+ });
9338
+ var nowIso3 = () => new Date().toISOString();
9339
+ var mk2 = (actor, type, payload) => ({
9340
+ event_id: ulid(),
9341
+ schema_version: SCHEMA_VERSION,
9342
+ ts: nowIso3(),
9343
+ actor,
9344
+ type,
9345
+ payload
9346
+ });
9347
+ async function enrichTicket(repoRoot, ticketId, opts) {
9348
+ const inputPath = join15(repoRoot, ".xera", ticketId, "enrichment-input.json");
9349
+ if (!existsSync19(inputPath)) {
9350
+ throw new Error(`enrichment-input.json not found at ${inputPath}`);
9351
+ }
9352
+ const raw = JSON.parse(readFileSync16(inputPath, "utf8"));
9353
+ const parsed = EnrichmentInputSchema.safeParse(raw);
9354
+ if (!parsed.success) {
9355
+ throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
9356
+ }
9357
+ const snapshot = deriveSnapshot(loadAllEvents(repoRoot));
9358
+ if (!snapshot.tickets[ticketId]) {
9359
+ throw new Error(`ticket ${ticketId} not in graph; run /xera-fetch first`);
9360
+ }
9361
+ if (snapshot.tickets[ticketId].enrichedAt && !opts.force) {
9362
+ return { ticketId, similarCount: 0, enrichedAt: snapshot.tickets[ticketId].enrichedAt };
9363
+ }
9364
+ const validated = parsed.data.similar.map((s) => ({ ...s, confidence: Math.max(0, Math.min(1, s.confidence)) })).filter((s) => s.confidence >= MIN_CONFIDENCE).filter((s) => snapshot.tickets[s.ticketId] !== undefined).filter((s) => s.ticketId !== ticketId).slice(0, MAX_SIMILAR_EDGES);
9365
+ const events = [];
9366
+ for (const s of validated) {
9367
+ const payload = {
9368
+ kind: "similar",
9369
+ from: ticketId,
9370
+ to: s.ticketId,
9371
+ confidence: s.confidence,
9372
+ source: `llm-similarity:${s.reason.slice(0, 80)}`
9373
+ };
9374
+ events.push(mk2("graph-enrich", "edge.discovered", payload));
9375
+ }
9376
+ const enrichedAt = nowIso3();
9377
+ const enrichedPayload = {
9378
+ ticketId,
9379
+ enrichedAt,
9380
+ similarCount: validated.length
9381
+ };
9382
+ events.push(mk2("graph-enrich", "ticket.enriched", enrichedPayload));
9383
+ appendEvents(repoRoot, events, { skill: "graph-enrich", ticketId });
9384
+ return { ticketId, similarCount: validated.length, enrichedAt };
9385
+ }
9386
+
9387
+ // src/bin-internal/graph-enrich.ts
9388
+ async function graphEnrichCmd(argv) {
9389
+ let ticket;
9390
+ let force = false;
9391
+ for (let i = 0;i < argv.length; i++) {
9392
+ if (argv[i] === "--ticket")
9393
+ ticket = argv[++i];
9394
+ else if (argv[i] === "--force")
9395
+ force = true;
9396
+ }
9397
+ const repoRoot = process.cwd();
9398
+ if (!ticket) {
9399
+ console.error("[graph-enrich] usage: graph-enrich --ticket <id> [--force]");
9400
+ return 1;
9401
+ }
9402
+ try {
9403
+ const result = await enrichTicket(repoRoot, ticket, { force });
9404
+ console.log(`[graph-enrich] ${ticket} enriched (${result.similarCount} similar edges, at ${result.enrichedAt})`);
9405
+ return 0;
9406
+ } catch (e) {
9407
+ console.error(`[graph-enrich] ${ticket} failed: ${e.message}`);
9408
+ return 1;
9409
+ }
9410
+ }
9411
+
9269
9412
  // src/bin-internal/graph-query.ts
9270
9413
  init_store();
9271
9414
  function filterByTicket(snap, ticket) {
@@ -9339,8 +9482,8 @@ async function graphSnapshotCmd(argv) {
9339
9482
  }
9340
9483
 
9341
9484
  // src/bin-internal/heal-prepare.ts
9342
- import { existsSync as existsSync19, readdirSync as readdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
9343
- import { join as join15 } from "path";
9485
+ import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
9486
+ import { join as join16 } from "path";
9344
9487
  import { scrubFreeText } from "@xera-ai/web";
9345
9488
 
9346
9489
  // ../../node_modules/.bun/fflate@0.8.3/node_modules/fflate/esm/index.mjs
@@ -9687,15 +9830,15 @@ function strFromU8(dat, latin1) {
9687
9830
  var slzh = function(d, b) {
9688
9831
  return b + 30 + b2(d, b + 26) + b2(d, b + 28);
9689
9832
  };
9690
- var zh = function(d, b, z6) {
9833
+ var zh = function(d, b, z7) {
9691
9834
  var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
9692
- var _a2 = z64hs(d, es, efl, z6, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
9835
+ var _a2 = z64hs(d, es, efl, z7, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
9693
9836
  return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
9694
9837
  };
9695
- var z64hs = function(d, b, l, z6, sc, su, off) {
9838
+ var z64hs = function(d, b, l, z7, sc, su, off) {
9696
9839
  var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
9697
9840
  var nf = nsc + nsu + noff;
9698
- if (z6 && nf) {
9841
+ if (z7 && nf) {
9699
9842
  for (;b + 4 < e; b += 4 + b2(d, b + 2)) {
9700
9843
  if (b2(d, b) == 1) {
9701
9844
  return [
@@ -9706,7 +9849,7 @@ var z64hs = function(d, b, l, z6, sc, su, off) {
9706
9849
  ];
9707
9850
  }
9708
9851
  }
9709
- if (z6 < 2)
9852
+ if (z7 < 2)
9710
9853
  err(13);
9711
9854
  }
9712
9855
  return [sc, su, off, 0];
@@ -9722,18 +9865,18 @@ function unzipSync(data, opts) {
9722
9865
  if (!c)
9723
9866
  return {};
9724
9867
  var o = b4(data, e + 16);
9725
- var z6 = b4(data, e - 20) == 117853008;
9726
- if (z6) {
9868
+ var z7 = b4(data, e - 20) == 117853008;
9869
+ if (z7) {
9727
9870
  var ze = b4(data, e - 12);
9728
- z6 = b4(data, ze) == 101075792;
9729
- if (z6) {
9871
+ z7 = b4(data, ze) == 101075792;
9872
+ if (z7) {
9730
9873
  c = b4(data, ze + 32);
9731
9874
  o = b4(data, ze + 48);
9732
9875
  }
9733
9876
  }
9734
9877
  var fltr = opts && opts.filter;
9735
9878
  for (var i2 = 0;i2 < c; ++i2) {
9736
- var _a2 = zh(data, o, z6), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
9879
+ var _a2 = zh(data, o, z7), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
9737
9880
  o = no;
9738
9881
  if (!fltr || fltr({
9739
9882
  name: fn,
@@ -9768,9 +9911,9 @@ function classifyKind(raw) {
9768
9911
  return "other";
9769
9912
  }
9770
9913
  function extractDomSnapshot(tracePath) {
9771
- if (!existsSync19(tracePath))
9914
+ if (!existsSync20(tracePath))
9772
9915
  return "";
9773
- const buf = readFileSync16(tracePath);
9916
+ const buf = readFileSync17(tracePath);
9774
9917
  const entries = unzipSync(buf);
9775
9918
  const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
9776
9919
  let chosenKey = null;
@@ -9818,16 +9961,16 @@ function extractDomSnapshot(tracePath) {
9818
9961
  return scrubFreeText(html);
9819
9962
  }
9820
9963
  function findPomLine(ticketDir, rawLocator) {
9821
- const pomDir = join15(ticketDir, "page-objects");
9964
+ const pomDir = join16(ticketDir, "page-objects");
9822
9965
  const candidates = [];
9823
- if (existsSync19(pomDir)) {
9966
+ if (existsSync20(pomDir)) {
9824
9967
  for (const name of readdirSync6(pomDir)) {
9825
9968
  if (name.endsWith(".ts"))
9826
- candidates.push(join15(pomDir, name));
9969
+ candidates.push(join16(pomDir, name));
9827
9970
  }
9828
9971
  }
9829
9972
  for (const file of candidates) {
9830
- const text = readFileSync16(file, "utf8");
9973
+ const text = readFileSync17(file, "utf8");
9831
9974
  const lines = text.split(`
9832
9975
  `);
9833
9976
  for (let i2 = 0;i2 < lines.length; i2++) {
@@ -9865,13 +10008,13 @@ function findGherkinStep(featureText, rawLocator) {
9865
10008
  }
9866
10009
  function healPrepare(repoRoot, ticket, runId, scenarioName) {
9867
10010
  const paths = resolveArtifactPaths(repoRoot, ticket);
9868
- const classifierPath = join15(paths.ticketDir, "classifier-input.json");
9869
- const classifier = JSON.parse(readFileSync16(classifierPath, "utf8"));
10011
+ const classifierPath = join16(paths.ticketDir, "classifier-input.json");
10012
+ const classifier = JSON.parse(readFileSync17(classifierPath, "utf8"));
9870
10013
  const cls = classifier.scenarios.find((s) => s.name === scenarioName);
9871
10014
  if (!cls)
9872
10015
  throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
9873
- const runDir = join15(paths.runsDir, runId);
9874
- const normalized = JSON.parse(readFileSync16(join15(runDir, "normalized.json"), "utf8"));
10016
+ const runDir = join16(paths.runsDir, runId);
10017
+ const normalized = JSON.parse(readFileSync17(join16(runDir, "normalized.json"), "utf8"));
9875
10018
  const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
9876
10019
  if (!normSc?.failure)
9877
10020
  throw new Error(`no failure recorded for scenario "${scenarioName}"`);
@@ -9882,9 +10025,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
9882
10025
  const raw = m[1].trim();
9883
10026
  const kind = classifyKind(raw);
9884
10027
  const pomLoc = findPomLine(paths.ticketDir, raw);
9885
- const featureText = readFileSync16(paths.featurePath, "utf8");
10028
+ const featureText = readFileSync17(paths.featurePath, "utf8");
9886
10029
  const gherkinStep = findGherkinStep(featureText, raw);
9887
- const domSnapshotAtFailure = extractDomSnapshot(join15(runDir, "trace.zip"));
10030
+ const domSnapshotAtFailure = extractDomSnapshot(join16(runDir, "trace.zip"));
9888
10031
  return {
9889
10032
  ticket,
9890
10033
  runId,
@@ -9904,7 +10047,7 @@ async function healPrepareCmd(argv) {
9904
10047
  try {
9905
10048
  const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
9906
10049
  const paths = resolveArtifactPaths(process.cwd(), ticket);
9907
- const outPath = join15(paths.runsDir, runId, "heal-input.json");
10050
+ const outPath = join16(paths.runsDir, runId, "heal-input.json");
9908
10051
  writeFileSync10(outPath, JSON.stringify(result, null, 2));
9909
10052
  console.log(`[xera:heal-prepare] wrote ${outPath}`);
9910
10053
  return 0;
@@ -9914,6 +10057,267 @@ async function healPrepareCmd(argv) {
9914
10057
  }
9915
10058
  }
9916
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
+
9917
10321
  // src/bin-internal/lint.ts
9918
10322
  import { lintTicket } from "@xera-ai/web";
9919
10323
  async function lintCmd(argv) {
@@ -9934,8 +10338,8 @@ async function lintCmd(argv) {
9934
10338
  }
9935
10339
 
9936
10340
  // src/bin-internal/normalize.ts
9937
- import { existsSync as existsSync20, readdirSync as readdirSync7 } from "fs";
9938
- import { join as join16 } from "path";
10341
+ import { existsSync as existsSync21, readdirSync as readdirSync7 } from "fs";
10342
+ import { join as join18 } from "path";
9939
10343
  import { normalizeRun } from "@xera-ai/web";
9940
10344
  async function normalizeCmd(argv) {
9941
10345
  const ticket = argv[0];
@@ -9950,8 +10354,8 @@ async function normalizeCmd(argv) {
9950
10354
  console.error("[xera:normalize] no run found");
9951
10355
  return 1;
9952
10356
  }
9953
- const runDir = join16(paths.runsDir, runId);
9954
- if (!existsSync20(runDir)) {
10357
+ const runDir = join18(paths.runsDir, runId);
10358
+ if (!existsSync21(runDir)) {
9955
10359
  console.error(`[xera:normalize] runs/${runId} missing`);
9956
10360
  return 1;
9957
10361
  }
@@ -9961,45 +10365,52 @@ async function normalizeCmd(argv) {
9961
10365
  }
9962
10366
 
9963
10367
  // src/bin-internal/post.ts
9964
- import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
9965
- import { join as join17 } from "path";
10368
+ import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
10369
+ import { join as join19 } from "path";
9966
10370
 
9967
10371
  // src/artifact/status.ts
9968
- import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
10372
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
9969
10373
  import { dirname as dirname6 } from "path";
9970
- import { z as z6 } from "zod";
9971
- var ClassificationEnum = z6.enum(["PASS", "REAL_BUG", "SELECTOR_DRIFT", "FLAKY", "TEST_BUG"]);
9972
- var ResultEnum = z6.enum(["PASS", "FAIL"]);
9973
- var ConfidenceEnum = z6.enum(["low", "medium", "high"]);
9974
- var HistoryEntrySchema = z6.object({
9975
- ts: z6.string(),
10374
+ import { z as z7 } from "zod";
10375
+ var ClassificationEnum = z7.enum([
10376
+ "PASS",
10377
+ "REAL_BUG",
10378
+ "SELECTOR_DRIFT",
10379
+ "FLAKY",
10380
+ "TEST_BUG",
10381
+ "TEST_OUTDATED"
10382
+ ]);
10383
+ var ResultEnum = z7.enum(["PASS", "FAIL"]);
10384
+ var ConfidenceEnum = z7.enum(["low", "medium", "high"]);
10385
+ var HistoryEntrySchema = z7.object({
10386
+ ts: z7.string(),
9976
10387
  result: ResultEnum,
9977
10388
  class: ClassificationEnum
9978
10389
  });
9979
- var StatusJsonSchema = z6.object({
9980
- ticket: z6.string(),
9981
- lastRun: z6.string(),
10390
+ var StatusJsonSchema = z7.object({
10391
+ ticket: z7.string(),
10392
+ lastRun: z7.string(),
9982
10393
  result: ResultEnum,
9983
10394
  classification: ClassificationEnum,
9984
10395
  confidence: ConfidenceEnum,
9985
- scenarios: z6.object({
9986
- total: z6.number().int().nonnegative(),
9987
- passed: z6.number().int().nonnegative(),
9988
- failed: z6.number().int().nonnegative(),
9989
- skipped: z6.number().int().nonnegative()
10396
+ scenarios: z7.object({
10397
+ total: z7.number().int().nonnegative(),
10398
+ passed: z7.number().int().nonnegative(),
10399
+ failed: z7.number().int().nonnegative(),
10400
+ skipped: z7.number().int().nonnegative()
9990
10401
  }),
9991
- history: z6.array(HistoryEntrySchema).default([]),
9992
- last_jira_comment_id: z6.string().optional()
10402
+ history: z7.array(HistoryEntrySchema).default([]),
10403
+ last_jira_comment_id: z7.string().optional()
9993
10404
  });
9994
10405
  var HISTORY_CAP = 20;
9995
10406
  function readStatus(path) {
9996
- if (!existsSync21(path))
10407
+ if (!existsSync22(path))
9997
10408
  return null;
9998
- return StatusJsonSchema.parse(JSON.parse(readFileSync17(path, "utf8")));
10409
+ return StatusJsonSchema.parse(JSON.parse(readFileSync18(path, "utf8")));
9999
10410
  }
10000
10411
  function writeStatus(path, status) {
10001
- mkdirSync11(dirname6(path), { recursive: true });
10002
- writeFileSync11(path, JSON.stringify(status, null, 2));
10412
+ mkdirSync12(dirname6(path), { recursive: true });
10413
+ writeFileSync12(path, JSON.stringify(status, null, 2));
10003
10414
  }
10004
10415
  function appendHistory(path, entry) {
10005
10416
  const s = readStatus(path);
@@ -10025,12 +10436,12 @@ async function postCmd(argv) {
10025
10436
  return 0;
10026
10437
  }
10027
10438
  const paths = resolveArtifactPaths(cwd, ticket);
10028
- const draftPath = join17(paths.ticketDir, "jira-comment.draft.md");
10029
- if (!existsSync22(draftPath)) {
10439
+ const draftPath = join19(paths.ticketDir, "jira-comment.draft.md");
10440
+ if (!existsSync23(draftPath)) {
10030
10441
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
10031
10442
  return 1;
10032
10443
  }
10033
- const body = readFileSync18(draftPath, "utf8");
10444
+ const body = readFileSync19(draftPath, "utf8");
10034
10445
  const client = await createJiraClient({
10035
10446
  baseUrl: config.jira.baseUrl,
10036
10447
  preferMcp: true,
@@ -10058,12 +10469,13 @@ async function promoteCmd(argv) {
10058
10469
  }
10059
10470
 
10060
10471
  // src/bin-internal/report.ts
10061
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
10062
- import { join as join18 } from "path";
10472
+ import { existsSync as existsSync25, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "fs";
10473
+ import { join as join20 } from "path";
10063
10474
 
10064
10475
  // src/classifier/aggregate.ts
10065
10476
  var CLASS_PRIORITY = [
10066
10477
  "REAL_BUG",
10478
+ "TEST_OUTDATED",
10067
10479
  "TEST_BUG",
10068
10480
  "SELECTOR_DRIFT",
10069
10481
  "FLAKY",
@@ -10089,6 +10501,80 @@ function aggregateScenarios(scenarios) {
10089
10501
  return { overall: chosen, overallConfidence: minConf, scenarios };
10090
10502
  }
10091
10503
 
10504
+ // src/graph/classify.ts
10505
+ var DEFAULT_THRESHOLD = 0.7;
10506
+ var SHORT_CIRCUIT = ["FLAKY", "PASS"];
10507
+ function findCandidateTickets(graph, scenario) {
10508
+ const poms = graph.edges.filter((e) => e.kind === "uses" && e.from === scenario.id).map((e) => e.to);
10509
+ if (poms.length === 0)
10510
+ return [];
10511
+ const areas = graph.edges.filter((e) => e.kind === "covers" && poms.includes(e.from)).map((e) => e.to);
10512
+ if (areas.length === 0)
10513
+ return [];
10514
+ const ticketIds = graph.edges.filter((e) => e.kind === "modifies" && areas.includes(e.to)).map((e) => e.from);
10515
+ const seen = new Set;
10516
+ const out = [];
10517
+ for (const id of ticketIds) {
10518
+ if (seen.has(id))
10519
+ continue;
10520
+ seen.add(id);
10521
+ if (id === scenario.ticketId)
10522
+ continue;
10523
+ const t = graph.tickets[id];
10524
+ if (!t)
10525
+ continue;
10526
+ if (t.fetchedAt <= scenario.generatedAt)
10527
+ continue;
10528
+ out.push(t);
10529
+ }
10530
+ return out;
10531
+ }
10532
+ async function enhanceClassification(input, graph, decideOutdated, options = {}) {
10533
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
10534
+ if (SHORT_CIRCUIT.includes(input.traceClassification)) {
10535
+ return { classification: input.traceClassification, confidence: 1 };
10536
+ }
10537
+ const scenario = graph.scenarios[input.scenarioId];
10538
+ if (!scenario)
10539
+ return { classification: input.traceClassification, confidence: 1 };
10540
+ const candidates = findCandidateTickets(graph, scenario);
10541
+ if (candidates.length === 0) {
10542
+ return { classification: input.traceClassification, confidence: 1 };
10543
+ }
10544
+ const candidateEvidence = candidates.map((t) => {
10545
+ const area = graph.edges.find((e) => e.kind === "modifies" && e.from === t.id)?.to ?? "";
10546
+ const ev = { ticketId: t.id, summary: t.summary, modifiedArea: area };
10547
+ if (t.ac[0])
10548
+ ev.relevantAcRef = t.ac[0];
10549
+ return ev;
10550
+ });
10551
+ const decision = await decideOutdated({ scenario, candidates });
10552
+ if (decision.classification === "TEST_OUTDATED" && decision.confidence >= threshold) {
10553
+ const evidence = {
10554
+ candidateTickets: candidateEvidence,
10555
+ reasoning: decision.evidence.reasoning,
10556
+ proposedAction: "regenerate-scenario"
10557
+ };
10558
+ if (decision.evidence.expectedByTest)
10559
+ evidence.expectedByTest = decision.evidence.expectedByTest;
10560
+ if (decision.evidence.actualInApp)
10561
+ evidence.actualInApp = decision.evidence.actualInApp;
10562
+ return {
10563
+ classification: "TEST_OUTDATED",
10564
+ confidence: decision.confidence,
10565
+ evidence
10566
+ };
10567
+ }
10568
+ return {
10569
+ classification: input.traceClassification,
10570
+ confidence: 1,
10571
+ evidence: { candidateTickets: candidateEvidence }
10572
+ };
10573
+ }
10574
+
10575
+ // src/bin-internal/report.ts
10576
+ init_store();
10577
+
10092
10578
  // src/reporter/jira-comment.ts
10093
10579
  function buildJiraComment(input) {
10094
10580
  const passed = input.scenarios.filter((s) => s.outcome === "PASS").length;
@@ -10119,11 +10605,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
10119
10605
  }
10120
10606
 
10121
10607
  // src/reporter/status-writer.ts
10122
- import { existsSync as existsSync23 } from "fs";
10608
+ import { existsSync as existsSync24 } from "fs";
10123
10609
  function writeStatusFromClassification(path, input) {
10124
10610
  const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
10125
10611
  const entry = { ts: input.runTs, result, class: input.classification.overall };
10126
- if (!existsSync23(path)) {
10612
+ if (!existsSync24(path)) {
10127
10613
  writeStatus(path, {
10128
10614
  ticket: input.ticket,
10129
10615
  lastRun: input.runTs,
@@ -10156,26 +10642,59 @@ async function reportCmd(argv) {
10156
10642
  return 1;
10157
10643
  }
10158
10644
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10159
- const input = JSON.parse(readFileSync19(inputArg.slice("--input=".length), "utf8"));
10645
+ const input = JSON.parse(readFileSync20(inputArg.slice("--input=".length), "utf8"));
10160
10646
  const aggregated = aggregateScenarios(input.scenarios);
10647
+ const decisionsPath = join20(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
10648
+ const decisions = existsSync25(decisionsPath) ? JSON.parse(readFileSync20(decisionsPath, "utf8")) : {};
10649
+ const graph = deriveSnapshot(loadAllEvents(process.cwd()));
10650
+ const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
10651
+ const scenarioIdByName = {};
10652
+ for (const [id, node] of Object.entries(graph.scenarios)) {
10653
+ if (node.ticketId === ticket) {
10654
+ scenarioIdByName[normalizeScenarioName(node.name)] = id;
10655
+ }
10656
+ }
10657
+ const enhancedScenarios = await Promise.all(aggregated.scenarios.map(async (s) => {
10658
+ if (s.outcome !== "FAIL")
10659
+ return s;
10660
+ const scenarioId2 = scenarioIdByName[normalizeScenarioName(s.name)];
10661
+ if (!scenarioId2)
10662
+ return s;
10663
+ const decision = decisions[scenarioId2];
10664
+ const decideOutdated = async () => decision ?? {
10665
+ classification: "BUG",
10666
+ confidence: 0,
10667
+ evidence: { reasoning: "no LLM decision" }
10668
+ };
10669
+ const enhanced = await enhanceClassification({ scenarioId: scenarioId2, traceClassification: s.class }, graph, decideOutdated);
10670
+ if (enhanced.classification !== s.class) {
10671
+ return {
10672
+ ...s,
10673
+ class: enhanced.classification,
10674
+ rationale: `${s.rationale} | TEST_OUTDATED override (conf ${enhanced.confidence})`
10675
+ };
10676
+ }
10677
+ return s;
10678
+ }));
10679
+ const reAggregated = aggregateScenarios(enhancedScenarios);
10161
10680
  const ts = new Date().toISOString();
10162
10681
  writeStatusFromClassification(paths.statusPath, {
10163
10682
  ticket,
10164
10683
  runTs: ts,
10165
- classification: aggregated,
10684
+ classification: reAggregated,
10166
10685
  scenarioCounts: input.scenarioCounts
10167
10686
  });
10168
10687
  const md = buildJiraComment({
10169
10688
  ticket,
10170
10689
  runId: input.runId,
10171
- overall: aggregated.overall,
10172
- overallConfidence: aggregated.overallConfidence,
10173
- scenarios: aggregated.scenarios,
10690
+ overall: reAggregated.overall,
10691
+ overallConfidence: reAggregated.overallConfidence,
10692
+ scenarios: reAggregated.scenarios,
10174
10693
  xeraVersion: "0.1.0",
10175
10694
  promptsVersion: "1.0.0"
10176
10695
  });
10177
- const draftPath = join18(paths.ticketDir, "jira-comment.draft.md");
10178
- writeFileSync12(draftPath, md);
10696
+ const draftPath = join20(paths.ticketDir, "jira-comment.draft.md");
10697
+ writeFileSync13(draftPath, md);
10179
10698
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
10180
10699
  return 0;
10181
10700
  }
@@ -10242,7 +10761,7 @@ async function unlockCmd(argv) {
10242
10761
  }
10243
10762
 
10244
10763
  // src/bin-internal/validate-feature.ts
10245
- import { existsSync as existsSync24, readFileSync as readFileSync20 } from "fs";
10764
+ import { existsSync as existsSync26, readFileSync as readFileSync21 } from "fs";
10246
10765
  import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
10247
10766
  async function validateFeatureCmd(argv) {
10248
10767
  const ticket = argv[0];
@@ -10251,11 +10770,11 @@ async function validateFeatureCmd(argv) {
10251
10770
  return 1;
10252
10771
  }
10253
10772
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10254
- if (!existsSync24(paths.featurePath)) {
10773
+ if (!existsSync26(paths.featurePath)) {
10255
10774
  console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
10256
10775
  return 1;
10257
10776
  }
10258
- const r = validateGherkin2(readFileSync20(paths.featurePath, "utf8"));
10777
+ const r = validateGherkin2(readFileSync21(paths.featurePath, "utf8"));
10259
10778
  if (r.ok) {
10260
10779
  console.log("[xera:validate-feature] ok");
10261
10780
  return 0;
@@ -10274,10 +10793,12 @@ var COMMANDS = {
10274
10793
  exec: execCmd,
10275
10794
  fetch: fetchCmd,
10276
10795
  "graph-backfill": graphBackfillCmd,
10796
+ "graph-enrich": graphEnrichCmd,
10277
10797
  "graph-query": graphQueryCmd,
10278
10798
  "graph-record": graphRecordCmd,
10279
10799
  "graph-snapshot": graphSnapshotCmd,
10280
10800
  "heal-prepare": healPrepareCmd,
10801
+ "impact-prepare": impactPrepareCmd,
10281
10802
  lint: lintCmd,
10282
10803
  normalize: normalizeCmd,
10283
10804
  post: postCmd,