@xera-ai/core 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) 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 +321 -69
  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/index.d.ts.map +1 -1
  8. package/dist/bin-internal/report.d.ts.map +1 -1
  9. package/dist/bin-internal/verify-prompts.d.ts.map +1 -1
  10. package/dist/classifier/aggregate.d.ts.map +1 -1
  11. package/dist/graph/classify.d.ts +42 -0
  12. package/dist/graph/classify.d.ts.map +1 -0
  13. package/dist/graph/enrich.d.ts +10 -0
  14. package/dist/graph/enrich.d.ts.map +1 -0
  15. package/dist/graph/index.d.ts +5 -0
  16. package/dist/graph/index.d.ts.map +1 -1
  17. package/dist/graph/schema.d.ts +3 -0
  18. package/dist/graph/schema.d.ts.map +1 -1
  19. package/dist/graph/similarity.d.ts +3 -0
  20. package/dist/graph/similarity.d.ts.map +1 -0
  21. package/dist/graph/types.d.ts +1 -1
  22. package/dist/graph/types.d.ts.map +1 -1
  23. package/dist/src/index.js +8 -1
  24. package/package.json +1 -1
  25. package/src/artifact/status.ts +8 -1
  26. package/src/bin-internal/graph-enrich.ts +28 -0
  27. package/src/bin-internal/graph-record.ts +45 -1
  28. package/src/bin-internal/index.ts +2 -0
  29. package/src/bin-internal/report.ts +63 -5
  30. package/src/bin-internal/verify-prompts.ts +2 -0
  31. package/src/classifier/aggregate.ts +1 -0
  32. package/src/graph/classify.ts +126 -0
  33. package/src/graph/enrich.ts +103 -0
  34. package/src/graph/index.ts +15 -0
  35. package/src/graph/schema.ts +8 -1
  36. package/src/graph/similarity.ts +43 -0
  37. package/src/graph/types.ts +7 -2
@@ -5,6 +5,7 @@ declare const ClassificationEnum: z.ZodEnum<{
5
5
  SELECTOR_DRIFT: "SELECTOR_DRIFT";
6
6
  FLAKY: "FLAKY";
7
7
  TEST_BUG: "TEST_BUG";
8
+ TEST_OUTDATED: "TEST_OUTDATED";
8
9
  }>;
9
10
  export declare const HistoryEntrySchema: z.ZodObject<{
10
11
  ts: z.ZodString;
@@ -18,6 +19,7 @@ export declare const HistoryEntrySchema: z.ZodObject<{
18
19
  SELECTOR_DRIFT: "SELECTOR_DRIFT";
19
20
  FLAKY: "FLAKY";
20
21
  TEST_BUG: "TEST_BUG";
22
+ TEST_OUTDATED: "TEST_OUTDATED";
21
23
  }>;
22
24
  }, z.core.$strip>;
23
25
  export declare const StatusJsonSchema: z.ZodObject<{
@@ -33,6 +35,7 @@ export declare const StatusJsonSchema: z.ZodObject<{
33
35
  SELECTOR_DRIFT: "SELECTOR_DRIFT";
34
36
  FLAKY: "FLAKY";
35
37
  TEST_BUG: "TEST_BUG";
38
+ TEST_OUTDATED: "TEST_OUTDATED";
36
39
  }>;
37
40
  confidence: z.ZodEnum<{
38
41
  low: "low";
@@ -57,6 +60,7 @@ export declare const StatusJsonSchema: z.ZodObject<{
57
60
  SELECTOR_DRIFT: "SELECTOR_DRIFT";
58
61
  FLAKY: "FLAKY";
59
62
  TEST_BUG: "TEST_BUG";
63
+ TEST_OUTDATED: "TEST_OUTDATED";
60
64
  }>;
61
65
  }, z.core.$strip>>>;
62
66
  last_jira_comment_id: z.ZodOptional<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/artifact/status.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,QAAA,MAAM,kBAAkB;;;;;;EAAsE,CAAC;AAI/F,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;iBAI7B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAc3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAIhE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAG1D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAGlE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,UAAU,CAQ3E"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/artifact/status.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,QAAA,MAAM,kBAAkB;;;;;;;EAOtB,CAAC;AAIH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;iBAI7B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAc3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAIhE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAG1D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAGlE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,UAAU,CAQ3E"}
@@ -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_"];
@@ -9266,6 +9312,96 @@ async function graphBackfillCmd(argv) {
9266
9312
  return 0;
9267
9313
  }
9268
9314
 
9315
+ // src/graph/enrich.ts
9316
+ init_store();
9317
+ init_ulid();
9318
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
9319
+ import { join as join15 } from "path";
9320
+ import { z as z6 } from "zod";
9321
+ var MAX_SIMILAR_EDGES = 10;
9322
+ var MIN_CONFIDENCE = 0.7;
9323
+ var SimilarEntrySchema = z6.object({
9324
+ ticketId: z6.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
9325
+ confidence: z6.number(),
9326
+ reason: z6.string()
9327
+ });
9328
+ var EnrichmentInputSchema = z6.object({
9329
+ similar: z6.array(SimilarEntrySchema)
9330
+ });
9331
+ var nowIso3 = () => new Date().toISOString();
9332
+ var mk2 = (actor, type, payload) => ({
9333
+ event_id: ulid(),
9334
+ schema_version: SCHEMA_VERSION,
9335
+ ts: nowIso3(),
9336
+ actor,
9337
+ type,
9338
+ payload
9339
+ });
9340
+ async function enrichTicket(repoRoot, ticketId, opts) {
9341
+ const inputPath = join15(repoRoot, ".xera", ticketId, "enrichment-input.json");
9342
+ if (!existsSync19(inputPath)) {
9343
+ throw new Error(`enrichment-input.json not found at ${inputPath}`);
9344
+ }
9345
+ const raw = JSON.parse(readFileSync16(inputPath, "utf8"));
9346
+ const parsed = EnrichmentInputSchema.safeParse(raw);
9347
+ if (!parsed.success) {
9348
+ throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
9349
+ }
9350
+ const snapshot = deriveSnapshot(loadAllEvents(repoRoot));
9351
+ if (!snapshot.tickets[ticketId]) {
9352
+ throw new Error(`ticket ${ticketId} not in graph; run /xera-fetch first`);
9353
+ }
9354
+ if (snapshot.tickets[ticketId].enrichedAt && !opts.force) {
9355
+ return { ticketId, similarCount: 0, enrichedAt: snapshot.tickets[ticketId].enrichedAt };
9356
+ }
9357
+ 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);
9358
+ const events = [];
9359
+ for (const s of validated) {
9360
+ const payload = {
9361
+ kind: "similar",
9362
+ from: ticketId,
9363
+ to: s.ticketId,
9364
+ confidence: s.confidence,
9365
+ source: `llm-similarity:${s.reason.slice(0, 80)}`
9366
+ };
9367
+ events.push(mk2("graph-enrich", "edge.discovered", payload));
9368
+ }
9369
+ const enrichedAt = nowIso3();
9370
+ const enrichedPayload = {
9371
+ ticketId,
9372
+ enrichedAt,
9373
+ similarCount: validated.length
9374
+ };
9375
+ events.push(mk2("graph-enrich", "ticket.enriched", enrichedPayload));
9376
+ appendEvents(repoRoot, events, { skill: "graph-enrich", ticketId });
9377
+ return { ticketId, similarCount: validated.length, enrichedAt };
9378
+ }
9379
+
9380
+ // src/bin-internal/graph-enrich.ts
9381
+ async function graphEnrichCmd(argv) {
9382
+ let ticket;
9383
+ let force = false;
9384
+ for (let i = 0;i < argv.length; i++) {
9385
+ if (argv[i] === "--ticket")
9386
+ ticket = argv[++i];
9387
+ else if (argv[i] === "--force")
9388
+ force = true;
9389
+ }
9390
+ const repoRoot = process.cwd();
9391
+ if (!ticket) {
9392
+ console.error("[graph-enrich] usage: graph-enrich --ticket <id> [--force]");
9393
+ return 1;
9394
+ }
9395
+ try {
9396
+ const result = await enrichTicket(repoRoot, ticket, { force });
9397
+ console.log(`[graph-enrich] ${ticket} enriched (${result.similarCount} similar edges, at ${result.enrichedAt})`);
9398
+ return 0;
9399
+ } catch (e) {
9400
+ console.error(`[graph-enrich] ${ticket} failed: ${e.message}`);
9401
+ return 1;
9402
+ }
9403
+ }
9404
+
9269
9405
  // src/bin-internal/graph-query.ts
9270
9406
  init_store();
9271
9407
  function filterByTicket(snap, ticket) {
@@ -9339,8 +9475,8 @@ async function graphSnapshotCmd(argv) {
9339
9475
  }
9340
9476
 
9341
9477
  // 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";
9478
+ import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
9479
+ import { join as join16 } from "path";
9344
9480
  import { scrubFreeText } from "@xera-ai/web";
9345
9481
 
9346
9482
  // ../../node_modules/.bun/fflate@0.8.3/node_modules/fflate/esm/index.mjs
@@ -9687,15 +9823,15 @@ function strFromU8(dat, latin1) {
9687
9823
  var slzh = function(d, b) {
9688
9824
  return b + 30 + b2(d, b + 26) + b2(d, b + 28);
9689
9825
  };
9690
- var zh = function(d, b, z6) {
9826
+ var zh = function(d, b, z7) {
9691
9827
  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];
9828
+ 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
9829
  return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
9694
9830
  };
9695
- var z64hs = function(d, b, l, z6, sc, su, off) {
9831
+ var z64hs = function(d, b, l, z7, sc, su, off) {
9696
9832
  var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
9697
9833
  var nf = nsc + nsu + noff;
9698
- if (z6 && nf) {
9834
+ if (z7 && nf) {
9699
9835
  for (;b + 4 < e; b += 4 + b2(d, b + 2)) {
9700
9836
  if (b2(d, b) == 1) {
9701
9837
  return [
@@ -9706,7 +9842,7 @@ var z64hs = function(d, b, l, z6, sc, su, off) {
9706
9842
  ];
9707
9843
  }
9708
9844
  }
9709
- if (z6 < 2)
9845
+ if (z7 < 2)
9710
9846
  err(13);
9711
9847
  }
9712
9848
  return [sc, su, off, 0];
@@ -9722,18 +9858,18 @@ function unzipSync(data, opts) {
9722
9858
  if (!c)
9723
9859
  return {};
9724
9860
  var o = b4(data, e + 16);
9725
- var z6 = b4(data, e - 20) == 117853008;
9726
- if (z6) {
9861
+ var z7 = b4(data, e - 20) == 117853008;
9862
+ if (z7) {
9727
9863
  var ze = b4(data, e - 12);
9728
- z6 = b4(data, ze) == 101075792;
9729
- if (z6) {
9864
+ z7 = b4(data, ze) == 101075792;
9865
+ if (z7) {
9730
9866
  c = b4(data, ze + 32);
9731
9867
  o = b4(data, ze + 48);
9732
9868
  }
9733
9869
  }
9734
9870
  var fltr = opts && opts.filter;
9735
9871
  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);
9872
+ 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
9873
  o = no;
9738
9874
  if (!fltr || fltr({
9739
9875
  name: fn,
@@ -9768,9 +9904,9 @@ function classifyKind(raw) {
9768
9904
  return "other";
9769
9905
  }
9770
9906
  function extractDomSnapshot(tracePath) {
9771
- if (!existsSync19(tracePath))
9907
+ if (!existsSync20(tracePath))
9772
9908
  return "";
9773
- const buf = readFileSync16(tracePath);
9909
+ const buf = readFileSync17(tracePath);
9774
9910
  const entries = unzipSync(buf);
9775
9911
  const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
9776
9912
  let chosenKey = null;
@@ -9818,16 +9954,16 @@ function extractDomSnapshot(tracePath) {
9818
9954
  return scrubFreeText(html);
9819
9955
  }
9820
9956
  function findPomLine(ticketDir, rawLocator) {
9821
- const pomDir = join15(ticketDir, "page-objects");
9957
+ const pomDir = join16(ticketDir, "page-objects");
9822
9958
  const candidates = [];
9823
- if (existsSync19(pomDir)) {
9959
+ if (existsSync20(pomDir)) {
9824
9960
  for (const name of readdirSync6(pomDir)) {
9825
9961
  if (name.endsWith(".ts"))
9826
- candidates.push(join15(pomDir, name));
9962
+ candidates.push(join16(pomDir, name));
9827
9963
  }
9828
9964
  }
9829
9965
  for (const file of candidates) {
9830
- const text = readFileSync16(file, "utf8");
9966
+ const text = readFileSync17(file, "utf8");
9831
9967
  const lines = text.split(`
9832
9968
  `);
9833
9969
  for (let i2 = 0;i2 < lines.length; i2++) {
@@ -9865,13 +10001,13 @@ function findGherkinStep(featureText, rawLocator) {
9865
10001
  }
9866
10002
  function healPrepare(repoRoot, ticket, runId, scenarioName) {
9867
10003
  const paths = resolveArtifactPaths(repoRoot, ticket);
9868
- const classifierPath = join15(paths.ticketDir, "classifier-input.json");
9869
- const classifier = JSON.parse(readFileSync16(classifierPath, "utf8"));
10004
+ const classifierPath = join16(paths.ticketDir, "classifier-input.json");
10005
+ const classifier = JSON.parse(readFileSync17(classifierPath, "utf8"));
9870
10006
  const cls = classifier.scenarios.find((s) => s.name === scenarioName);
9871
10007
  if (!cls)
9872
10008
  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"));
10009
+ const runDir = join16(paths.runsDir, runId);
10010
+ const normalized = JSON.parse(readFileSync17(join16(runDir, "normalized.json"), "utf8"));
9875
10011
  const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
9876
10012
  if (!normSc?.failure)
9877
10013
  throw new Error(`no failure recorded for scenario "${scenarioName}"`);
@@ -9882,9 +10018,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
9882
10018
  const raw = m[1].trim();
9883
10019
  const kind = classifyKind(raw);
9884
10020
  const pomLoc = findPomLine(paths.ticketDir, raw);
9885
- const featureText = readFileSync16(paths.featurePath, "utf8");
10021
+ const featureText = readFileSync17(paths.featurePath, "utf8");
9886
10022
  const gherkinStep = findGherkinStep(featureText, raw);
9887
- const domSnapshotAtFailure = extractDomSnapshot(join15(runDir, "trace.zip"));
10023
+ const domSnapshotAtFailure = extractDomSnapshot(join16(runDir, "trace.zip"));
9888
10024
  return {
9889
10025
  ticket,
9890
10026
  runId,
@@ -9904,7 +10040,7 @@ async function healPrepareCmd(argv) {
9904
10040
  try {
9905
10041
  const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
9906
10042
  const paths = resolveArtifactPaths(process.cwd(), ticket);
9907
- const outPath = join15(paths.runsDir, runId, "heal-input.json");
10043
+ const outPath = join16(paths.runsDir, runId, "heal-input.json");
9908
10044
  writeFileSync10(outPath, JSON.stringify(result, null, 2));
9909
10045
  console.log(`[xera:heal-prepare] wrote ${outPath}`);
9910
10046
  return 0;
@@ -9934,8 +10070,8 @@ async function lintCmd(argv) {
9934
10070
  }
9935
10071
 
9936
10072
  // src/bin-internal/normalize.ts
9937
- import { existsSync as existsSync20, readdirSync as readdirSync7 } from "fs";
9938
- import { join as join16 } from "path";
10073
+ import { existsSync as existsSync21, readdirSync as readdirSync7 } from "fs";
10074
+ import { join as join17 } from "path";
9939
10075
  import { normalizeRun } from "@xera-ai/web";
9940
10076
  async function normalizeCmd(argv) {
9941
10077
  const ticket = argv[0];
@@ -9950,8 +10086,8 @@ async function normalizeCmd(argv) {
9950
10086
  console.error("[xera:normalize] no run found");
9951
10087
  return 1;
9952
10088
  }
9953
- const runDir = join16(paths.runsDir, runId);
9954
- if (!existsSync20(runDir)) {
10089
+ const runDir = join17(paths.runsDir, runId);
10090
+ if (!existsSync21(runDir)) {
9955
10091
  console.error(`[xera:normalize] runs/${runId} missing`);
9956
10092
  return 1;
9957
10093
  }
@@ -9961,41 +10097,48 @@ async function normalizeCmd(argv) {
9961
10097
  }
9962
10098
 
9963
10099
  // src/bin-internal/post.ts
9964
- import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
9965
- import { join as join17 } from "path";
10100
+ import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
10101
+ import { join as join18 } from "path";
9966
10102
 
9967
10103
  // src/artifact/status.ts
9968
- import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
10104
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
9969
10105
  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(),
10106
+ import { z as z7 } from "zod";
10107
+ var ClassificationEnum = z7.enum([
10108
+ "PASS",
10109
+ "REAL_BUG",
10110
+ "SELECTOR_DRIFT",
10111
+ "FLAKY",
10112
+ "TEST_BUG",
10113
+ "TEST_OUTDATED"
10114
+ ]);
10115
+ var ResultEnum = z7.enum(["PASS", "FAIL"]);
10116
+ var ConfidenceEnum = z7.enum(["low", "medium", "high"]);
10117
+ var HistoryEntrySchema = z7.object({
10118
+ ts: z7.string(),
9976
10119
  result: ResultEnum,
9977
10120
  class: ClassificationEnum
9978
10121
  });
9979
- var StatusJsonSchema = z6.object({
9980
- ticket: z6.string(),
9981
- lastRun: z6.string(),
10122
+ var StatusJsonSchema = z7.object({
10123
+ ticket: z7.string(),
10124
+ lastRun: z7.string(),
9982
10125
  result: ResultEnum,
9983
10126
  classification: ClassificationEnum,
9984
10127
  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()
10128
+ scenarios: z7.object({
10129
+ total: z7.number().int().nonnegative(),
10130
+ passed: z7.number().int().nonnegative(),
10131
+ failed: z7.number().int().nonnegative(),
10132
+ skipped: z7.number().int().nonnegative()
9990
10133
  }),
9991
- history: z6.array(HistoryEntrySchema).default([]),
9992
- last_jira_comment_id: z6.string().optional()
10134
+ history: z7.array(HistoryEntrySchema).default([]),
10135
+ last_jira_comment_id: z7.string().optional()
9993
10136
  });
9994
10137
  var HISTORY_CAP = 20;
9995
10138
  function readStatus(path) {
9996
- if (!existsSync21(path))
10139
+ if (!existsSync22(path))
9997
10140
  return null;
9998
- return StatusJsonSchema.parse(JSON.parse(readFileSync17(path, "utf8")));
10141
+ return StatusJsonSchema.parse(JSON.parse(readFileSync18(path, "utf8")));
9999
10142
  }
10000
10143
  function writeStatus(path, status) {
10001
10144
  mkdirSync11(dirname6(path), { recursive: true });
@@ -10025,12 +10168,12 @@ async function postCmd(argv) {
10025
10168
  return 0;
10026
10169
  }
10027
10170
  const paths = resolveArtifactPaths(cwd, ticket);
10028
- const draftPath = join17(paths.ticketDir, "jira-comment.draft.md");
10029
- if (!existsSync22(draftPath)) {
10171
+ const draftPath = join18(paths.ticketDir, "jira-comment.draft.md");
10172
+ if (!existsSync23(draftPath)) {
10030
10173
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
10031
10174
  return 1;
10032
10175
  }
10033
- const body = readFileSync18(draftPath, "utf8");
10176
+ const body = readFileSync19(draftPath, "utf8");
10034
10177
  const client = await createJiraClient({
10035
10178
  baseUrl: config.jira.baseUrl,
10036
10179
  preferMcp: true,
@@ -10058,12 +10201,13 @@ async function promoteCmd(argv) {
10058
10201
  }
10059
10202
 
10060
10203
  // src/bin-internal/report.ts
10061
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
10062
- import { join as join18 } from "path";
10204
+ import { existsSync as existsSync25, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
10205
+ import { join as join19 } from "path";
10063
10206
 
10064
10207
  // src/classifier/aggregate.ts
10065
10208
  var CLASS_PRIORITY = [
10066
10209
  "REAL_BUG",
10210
+ "TEST_OUTDATED",
10067
10211
  "TEST_BUG",
10068
10212
  "SELECTOR_DRIFT",
10069
10213
  "FLAKY",
@@ -10089,6 +10233,80 @@ function aggregateScenarios(scenarios) {
10089
10233
  return { overall: chosen, overallConfidence: minConf, scenarios };
10090
10234
  }
10091
10235
 
10236
+ // src/graph/classify.ts
10237
+ var DEFAULT_THRESHOLD = 0.7;
10238
+ var SHORT_CIRCUIT = ["FLAKY", "PASS"];
10239
+ function findCandidateTickets(graph, scenario) {
10240
+ const poms = graph.edges.filter((e) => e.kind === "uses" && e.from === scenario.id).map((e) => e.to);
10241
+ if (poms.length === 0)
10242
+ return [];
10243
+ const areas = graph.edges.filter((e) => e.kind === "covers" && poms.includes(e.from)).map((e) => e.to);
10244
+ if (areas.length === 0)
10245
+ return [];
10246
+ const ticketIds = graph.edges.filter((e) => e.kind === "modifies" && areas.includes(e.to)).map((e) => e.from);
10247
+ const seen = new Set;
10248
+ const out = [];
10249
+ for (const id of ticketIds) {
10250
+ if (seen.has(id))
10251
+ continue;
10252
+ seen.add(id);
10253
+ if (id === scenario.ticketId)
10254
+ continue;
10255
+ const t = graph.tickets[id];
10256
+ if (!t)
10257
+ continue;
10258
+ if (t.fetchedAt <= scenario.generatedAt)
10259
+ continue;
10260
+ out.push(t);
10261
+ }
10262
+ return out;
10263
+ }
10264
+ async function enhanceClassification(input, graph, decideOutdated, options = {}) {
10265
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
10266
+ if (SHORT_CIRCUIT.includes(input.traceClassification)) {
10267
+ return { classification: input.traceClassification, confidence: 1 };
10268
+ }
10269
+ const scenario = graph.scenarios[input.scenarioId];
10270
+ if (!scenario)
10271
+ return { classification: input.traceClassification, confidence: 1 };
10272
+ const candidates = findCandidateTickets(graph, scenario);
10273
+ if (candidates.length === 0) {
10274
+ return { classification: input.traceClassification, confidence: 1 };
10275
+ }
10276
+ const candidateEvidence = candidates.map((t) => {
10277
+ const area = graph.edges.find((e) => e.kind === "modifies" && e.from === t.id)?.to ?? "";
10278
+ const ev = { ticketId: t.id, summary: t.summary, modifiedArea: area };
10279
+ if (t.ac[0])
10280
+ ev.relevantAcRef = t.ac[0];
10281
+ return ev;
10282
+ });
10283
+ const decision = await decideOutdated({ scenario, candidates });
10284
+ if (decision.classification === "TEST_OUTDATED" && decision.confidence >= threshold) {
10285
+ const evidence = {
10286
+ candidateTickets: candidateEvidence,
10287
+ reasoning: decision.evidence.reasoning,
10288
+ proposedAction: "regenerate-scenario"
10289
+ };
10290
+ if (decision.evidence.expectedByTest)
10291
+ evidence.expectedByTest = decision.evidence.expectedByTest;
10292
+ if (decision.evidence.actualInApp)
10293
+ evidence.actualInApp = decision.evidence.actualInApp;
10294
+ return {
10295
+ classification: "TEST_OUTDATED",
10296
+ confidence: decision.confidence,
10297
+ evidence
10298
+ };
10299
+ }
10300
+ return {
10301
+ classification: input.traceClassification,
10302
+ confidence: 1,
10303
+ evidence: { candidateTickets: candidateEvidence }
10304
+ };
10305
+ }
10306
+
10307
+ // src/bin-internal/report.ts
10308
+ init_store();
10309
+
10092
10310
  // src/reporter/jira-comment.ts
10093
10311
  function buildJiraComment(input) {
10094
10312
  const passed = input.scenarios.filter((s) => s.outcome === "PASS").length;
@@ -10119,11 +10337,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
10119
10337
  }
10120
10338
 
10121
10339
  // src/reporter/status-writer.ts
10122
- import { existsSync as existsSync23 } from "fs";
10340
+ import { existsSync as existsSync24 } from "fs";
10123
10341
  function writeStatusFromClassification(path, input) {
10124
10342
  const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
10125
10343
  const entry = { ts: input.runTs, result, class: input.classification.overall };
10126
- if (!existsSync23(path)) {
10344
+ if (!existsSync24(path)) {
10127
10345
  writeStatus(path, {
10128
10346
  ticket: input.ticket,
10129
10347
  lastRun: input.runTs,
@@ -10156,25 +10374,58 @@ async function reportCmd(argv) {
10156
10374
  return 1;
10157
10375
  }
10158
10376
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10159
- const input = JSON.parse(readFileSync19(inputArg.slice("--input=".length), "utf8"));
10377
+ const input = JSON.parse(readFileSync20(inputArg.slice("--input=".length), "utf8"));
10160
10378
  const aggregated = aggregateScenarios(input.scenarios);
10379
+ const decisionsPath = join19(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
10380
+ const decisions = existsSync25(decisionsPath) ? JSON.parse(readFileSync20(decisionsPath, "utf8")) : {};
10381
+ const graph = deriveSnapshot(loadAllEvents(process.cwd()));
10382
+ const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
10383
+ const scenarioIdByName = {};
10384
+ for (const [id, node] of Object.entries(graph.scenarios)) {
10385
+ if (node.ticketId === ticket) {
10386
+ scenarioIdByName[normalizeScenarioName(node.name)] = id;
10387
+ }
10388
+ }
10389
+ const enhancedScenarios = await Promise.all(aggregated.scenarios.map(async (s) => {
10390
+ if (s.outcome !== "FAIL")
10391
+ return s;
10392
+ const scenarioId2 = scenarioIdByName[normalizeScenarioName(s.name)];
10393
+ if (!scenarioId2)
10394
+ return s;
10395
+ const decision = decisions[scenarioId2];
10396
+ const decideOutdated = async () => decision ?? {
10397
+ classification: "BUG",
10398
+ confidence: 0,
10399
+ evidence: { reasoning: "no LLM decision" }
10400
+ };
10401
+ const enhanced = await enhanceClassification({ scenarioId: scenarioId2, traceClassification: s.class }, graph, decideOutdated);
10402
+ if (enhanced.classification !== s.class) {
10403
+ return {
10404
+ ...s,
10405
+ class: enhanced.classification,
10406
+ rationale: `${s.rationale} | TEST_OUTDATED override (conf ${enhanced.confidence})`
10407
+ };
10408
+ }
10409
+ return s;
10410
+ }));
10411
+ const reAggregated = aggregateScenarios(enhancedScenarios);
10161
10412
  const ts = new Date().toISOString();
10162
10413
  writeStatusFromClassification(paths.statusPath, {
10163
10414
  ticket,
10164
10415
  runTs: ts,
10165
- classification: aggregated,
10416
+ classification: reAggregated,
10166
10417
  scenarioCounts: input.scenarioCounts
10167
10418
  });
10168
10419
  const md = buildJiraComment({
10169
10420
  ticket,
10170
10421
  runId: input.runId,
10171
- overall: aggregated.overall,
10172
- overallConfidence: aggregated.overallConfidence,
10173
- scenarios: aggregated.scenarios,
10422
+ overall: reAggregated.overall,
10423
+ overallConfidence: reAggregated.overallConfidence,
10424
+ scenarios: reAggregated.scenarios,
10174
10425
  xeraVersion: "0.1.0",
10175
10426
  promptsVersion: "1.0.0"
10176
10427
  });
10177
- const draftPath = join18(paths.ticketDir, "jira-comment.draft.md");
10428
+ const draftPath = join19(paths.ticketDir, "jira-comment.draft.md");
10178
10429
  writeFileSync12(draftPath, md);
10179
10430
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
10180
10431
  return 0;
@@ -10242,7 +10493,7 @@ async function unlockCmd(argv) {
10242
10493
  }
10243
10494
 
10244
10495
  // src/bin-internal/validate-feature.ts
10245
- import { existsSync as existsSync24, readFileSync as readFileSync20 } from "fs";
10496
+ import { existsSync as existsSync26, readFileSync as readFileSync21 } from "fs";
10246
10497
  import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
10247
10498
  async function validateFeatureCmd(argv) {
10248
10499
  const ticket = argv[0];
@@ -10251,11 +10502,11 @@ async function validateFeatureCmd(argv) {
10251
10502
  return 1;
10252
10503
  }
10253
10504
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10254
- if (!existsSync24(paths.featurePath)) {
10505
+ if (!existsSync26(paths.featurePath)) {
10255
10506
  console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
10256
10507
  return 1;
10257
10508
  }
10258
- const r = validateGherkin2(readFileSync20(paths.featurePath, "utf8"));
10509
+ const r = validateGherkin2(readFileSync21(paths.featurePath, "utf8"));
10259
10510
  if (r.ok) {
10260
10511
  console.log("[xera:validate-feature] ok");
10261
10512
  return 0;
@@ -10274,6 +10525,7 @@ var COMMANDS = {
10274
10525
  exec: execCmd,
10275
10526
  fetch: fetchCmd,
10276
10527
  "graph-backfill": graphBackfillCmd,
10528
+ "graph-enrich": graphEnrichCmd,
10277
10529
  "graph-query": graphQueryCmd,
10278
10530
  "graph-record": graphRecordCmd,
10279
10531
  "graph-snapshot": graphSnapshotCmd,
@@ -0,0 +1,2 @@
1
+ export declare function graphEnrichCmd(argv: string[]): Promise<number>;
2
+ //# sourceMappingURL=graph-enrich.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-enrich.d.ts","sourceRoot":"","sources":["../../src/bin-internal/graph-enrich.ts"],"names":[],"mappings":"AAEA,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBpE"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph-record.d.ts","sourceRoot":"","sources":["../../src/bin-internal/graph-record.ts"],"names":[],"mappings":"AAyEA,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqCnF;AAiFD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmDpE"}
1
+ {"version":3,"file":"graph-record.d.ts","sourceRoot":"","sources":["../../src/bin-internal/graph-record.ts"],"names":[],"mappings":"AA2EA,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqCnF;AAiFD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA6FpE"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin-internal/index.ts"],"names":[],"mappings":"AA8CA,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":"AAgDA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAczD"}