@xera-ai/core 0.4.2 → 0.4.3

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.
@@ -9457,6 +9457,306 @@ async function graphQueryCmd(argv) {
9457
9457
  // src/bin-internal/index.ts
9458
9458
  init_graph_record();
9459
9459
 
9460
+ // src/bin-internal/graph-render.ts
9461
+ import { mkdirSync as mkdirSync11, renameSync as renameSync2, writeFileSync as writeFileSync10 } from "fs";
9462
+ import { dirname as dirname7, join as join17 } from "path";
9463
+
9464
+ // src/graph/render.ts
9465
+ import { readFileSync as readFileSync17 } from "fs";
9466
+ import { dirname as dirname6, join as join16 } from "path";
9467
+ import { fileURLToPath } from "url";
9468
+ var COLORS = {
9469
+ ticket: "#3B82F6",
9470
+ scenarioPass: "#10B981",
9471
+ scenarioFail: "#EF4444",
9472
+ pom: "#F59E0B",
9473
+ area: "#6B7280",
9474
+ failure: "#EF4444",
9475
+ edgeModifies: "#EF4444",
9476
+ edgeDefault: "#9CA3AF",
9477
+ edgeJira: "#3B82F6",
9478
+ edgeSimilar: "#A855F7"
9479
+ };
9480
+ function ticketsAfter(since, fetchedAt) {
9481
+ if (!since)
9482
+ return true;
9483
+ return Date.parse(fetchedAt) >= Date.parse(since);
9484
+ }
9485
+ function scenariosAfter(since, generatedAt) {
9486
+ if (!since)
9487
+ return true;
9488
+ return Date.parse(generatedAt) >= Date.parse(since);
9489
+ }
9490
+ function buildTicketNode(snap, ticketId) {
9491
+ const t = snap.tickets[ticketId];
9492
+ const usageCount = snap.edges.filter((e) => e.kind === "tests" && e.from === ticketId).length;
9493
+ const node = {
9494
+ id: t.id,
9495
+ label: t.id,
9496
+ group: "Ticket",
9497
+ color: COLORS.ticket,
9498
+ shape: "dot",
9499
+ size: 10 + Math.min(usageCount * 2, 20),
9500
+ title: `${t.id} \u2014 ${t.summary}`
9501
+ };
9502
+ return node;
9503
+ }
9504
+ function buildScenarioNode(snap, scenarioId2) {
9505
+ const s = snap.scenarios[scenarioId2];
9506
+ const failed = snap.latest_failures[scenarioId2];
9507
+ const sizeBase = s.priority === "p0" ? 14 : s.priority === "p1" ? 11 : 9;
9508
+ const node = {
9509
+ id: s.id,
9510
+ label: s.name,
9511
+ group: "Scenario",
9512
+ color: failed ? COLORS.scenarioFail : COLORS.scenarioPass,
9513
+ shape: "square",
9514
+ size: sizeBase,
9515
+ title: `${s.ticketId} / ${s.name} [${s.priority.toUpperCase()}]`
9516
+ };
9517
+ return node;
9518
+ }
9519
+ function buildPomNode(snap, pomId2) {
9520
+ const p = snap.poms[pomId2];
9521
+ const usageCount = snap.edges.filter((e) => e.kind === "uses" && e.to === pomId2).length;
9522
+ const node = {
9523
+ id: p.id,
9524
+ label: p.filePath.split("/").pop() ?? p.id,
9525
+ group: "POM",
9526
+ color: COLORS.pom,
9527
+ shape: "diamond",
9528
+ size: 8 + Math.min(usageCount * 2, 16),
9529
+ title: `${p.filePath} (${p.route || "no route"})`
9530
+ };
9531
+ return node;
9532
+ }
9533
+ function buildAreaNode(snap, areaId) {
9534
+ const a = snap.areas[areaId];
9535
+ const node = {
9536
+ id: a.id,
9537
+ label: a.id,
9538
+ group: "SUTArea",
9539
+ color: COLORS.area,
9540
+ shape: "hexagon",
9541
+ size: 12,
9542
+ title: `area: ${a.id}`
9543
+ };
9544
+ return node;
9545
+ }
9546
+ function buildFailureNode(_snap, failure) {
9547
+ const node = {
9548
+ id: failure.id,
9549
+ label: "fail",
9550
+ group: "Failure",
9551
+ color: COLORS.failure,
9552
+ shape: "triangle",
9553
+ size: 10,
9554
+ title: `failure on ${failure.scenarioId} @ ${failure.ts}`
9555
+ };
9556
+ return node;
9557
+ }
9558
+ function buildEdge(edge, idx) {
9559
+ const v = {
9560
+ id: `e-${idx}`,
9561
+ from: edge.from,
9562
+ to: edge.to,
9563
+ label: edge.kind,
9564
+ arrows: "to",
9565
+ width: 1
9566
+ };
9567
+ switch (edge.kind) {
9568
+ case "modifies":
9569
+ v.color = COLORS.edgeModifies;
9570
+ v.dashes = true;
9571
+ v.width = 2;
9572
+ break;
9573
+ case "jira-linked":
9574
+ v.color = COLORS.edgeJira;
9575
+ v.dashes = true;
9576
+ break;
9577
+ case "similar":
9578
+ v.color = COLORS.edgeSimilar;
9579
+ v.dashes = false;
9580
+ v.width = 1 + Math.round((edge.confidence ?? 0) * 3);
9581
+ break;
9582
+ default:
9583
+ v.color = COLORS.edgeDefault;
9584
+ break;
9585
+ }
9586
+ return v;
9587
+ }
9588
+ function bfsFromTicket(snap, ticketId, depth) {
9589
+ const nodeIds = new Set([ticketId]);
9590
+ const edgeIdxs = new Set;
9591
+ let frontier = new Set([ticketId]);
9592
+ for (let d = 0;d < depth; d++) {
9593
+ const next = new Set;
9594
+ snap.edges.forEach((e, i) => {
9595
+ if (frontier.has(e.from) && !nodeIds.has(e.to)) {
9596
+ nodeIds.add(e.to);
9597
+ next.add(e.to);
9598
+ edgeIdxs.add(i);
9599
+ } else if (frontier.has(e.to) && !nodeIds.has(e.from)) {
9600
+ nodeIds.add(e.from);
9601
+ next.add(e.from);
9602
+ edgeIdxs.add(i);
9603
+ } else if (frontier.has(e.from) && nodeIds.has(e.to)) {
9604
+ edgeIdxs.add(i);
9605
+ } else if (frontier.has(e.to) && nodeIds.has(e.from)) {
9606
+ edgeIdxs.add(i);
9607
+ }
9608
+ });
9609
+ frontier = next;
9610
+ if (frontier.size === 0)
9611
+ break;
9612
+ }
9613
+ return { nodeIds, edgeIdxs };
9614
+ }
9615
+ function transformForVisNetwork(snap, opts) {
9616
+ const mode = opts.performanceMode ?? "full";
9617
+ const nodes = [];
9618
+ const edges = [];
9619
+ let includeTickets = new Set;
9620
+ let includeScenarios = new Set;
9621
+ let includePoms = new Set;
9622
+ let includeAreas = new Set;
9623
+ let includeEdgeIdxs = new Set;
9624
+ if (opts.ticketId) {
9625
+ const result = bfsFromTicket(snap, opts.ticketId, opts.depth ?? 2);
9626
+ for (const id of result.nodeIds) {
9627
+ if (snap.tickets[id])
9628
+ includeTickets.add(id);
9629
+ else if (snap.scenarios[id])
9630
+ includeScenarios.add(id);
9631
+ else if (snap.poms[id])
9632
+ includePoms.add(id);
9633
+ else if (snap.areas[id])
9634
+ includeAreas.add(id);
9635
+ }
9636
+ includeEdgeIdxs = result.edgeIdxs;
9637
+ } else {
9638
+ includeTickets = new Set(Object.keys(snap.tickets).filter((id) => ticketsAfter(opts.since, snap.tickets[id].fetchedAt)));
9639
+ includeScenarios = new Set(Object.keys(snap.scenarios).filter((id) => scenariosAfter(opts.since, snap.scenarios[id].generatedAt)));
9640
+ includePoms = new Set(Object.keys(snap.poms));
9641
+ includeAreas = new Set(Object.keys(snap.areas));
9642
+ snap.edges.forEach((_, i) => {
9643
+ includeEdgeIdxs.add(i);
9644
+ });
9645
+ }
9646
+ if (mode === "ticket-only") {
9647
+ includeScenarios.clear();
9648
+ includePoms.clear();
9649
+ includeAreas.clear();
9650
+ }
9651
+ for (const id of includeTickets)
9652
+ nodes.push(buildTicketNode(snap, id));
9653
+ for (const id of includeScenarios)
9654
+ nodes.push(buildScenarioNode(snap, id));
9655
+ for (const id of includePoms)
9656
+ nodes.push(buildPomNode(snap, id));
9657
+ for (const id of includeAreas)
9658
+ nodes.push(buildAreaNode(snap, id));
9659
+ for (const failure of Object.values(snap.latest_failures)) {
9660
+ if (includeScenarios.has(failure.scenarioId)) {
9661
+ nodes.push(buildFailureNode(snap, failure));
9662
+ }
9663
+ }
9664
+ const visibleNodeIds = new Set(nodes.map((n) => n.id));
9665
+ for (const i of includeEdgeIdxs) {
9666
+ const e = snap.edges[i];
9667
+ if (!e)
9668
+ continue;
9669
+ if (!visibleNodeIds.has(e.from) || !visibleNodeIds.has(e.to))
9670
+ continue;
9671
+ edges.push(buildEdge(e, i));
9672
+ }
9673
+ const stats = {
9674
+ tickets: includeTickets.size,
9675
+ scenarios: includeScenarios.size,
9676
+ poms: includePoms.size,
9677
+ areas: includeAreas.size,
9678
+ failures: nodes.filter((n) => n.group === "Failure").length,
9679
+ edges: edges.length
9680
+ };
9681
+ return { nodes, edges, stats };
9682
+ }
9683
+ var __filename2 = fileURLToPath(import.meta.url);
9684
+ var __dirname2 = dirname6(__filename2);
9685
+ var TEMPLATES_DIR = join16(__dirname2, "templates");
9686
+ function loadTemplate(name) {
9687
+ return readFileSync17(join16(TEMPLATES_DIR, name), "utf8");
9688
+ }
9689
+ function statsToHuman(s) {
9690
+ return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
9691
+ }
9692
+ function renderHtml(input) {
9693
+ const template = loadTemplate("graph.html.template");
9694
+ const css = loadTemplate("graph.css");
9695
+ const js = loadTemplate("graph.js");
9696
+ const visNetwork = loadTemplate("vis-network.min.js");
9697
+ const graphJson = JSON.stringify(input.data);
9698
+ const statsHuman = statsToHuman(input.stats);
9699
+ return template.replace("{{CSS}}", () => css).replace("{{STATS}}", () => statsHuman).replace("{{GENERATED_AT}}", () => input.generatedAt).replace("{{VIS_NETWORK_JS}}", () => visNetwork).replace("{{GRAPH_DATA}}", () => graphJson).replace("{{INTERACTION_JS}}", () => js);
9700
+ }
9701
+
9702
+ // src/bin-internal/graph-render.ts
9703
+ init_store();
9704
+ function parseDepth(s) {
9705
+ const n = s ? Number.parseInt(s, 10) : 2;
9706
+ if (n === 1 || n === 3)
9707
+ return n;
9708
+ return 2;
9709
+ }
9710
+ function decidePerformanceMode(nodeCount) {
9711
+ if (nodeCount > 2000)
9712
+ return "text-fallback";
9713
+ if (nodeCount > 500)
9714
+ return "ticket-only";
9715
+ return "full";
9716
+ }
9717
+ async function graphRenderCmd(argv) {
9718
+ let outPath;
9719
+ let ticketId;
9720
+ let since;
9721
+ let depth = 2;
9722
+ for (let i = 0;i < argv.length; i++) {
9723
+ if (argv[i] === "--out")
9724
+ outPath = argv[++i];
9725
+ else if (argv[i] === "--ticket")
9726
+ ticketId = argv[++i];
9727
+ else if (argv[i] === "--since")
9728
+ since = argv[++i];
9729
+ else if (argv[i] === "--depth")
9730
+ depth = parseDepth(argv[++i]);
9731
+ }
9732
+ const repoRoot = process.cwd();
9733
+ const finalPath = outPath ?? join17(repoRoot, ".xera/graph.html");
9734
+ const snap = deriveSnapshot(loadAllEvents(repoRoot));
9735
+ const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
9736
+ const performanceMode = decidePerformanceMode(totalNodeCount);
9737
+ if (performanceMode === "text-fallback") {
9738
+ const txtPath = finalPath.replace(/\.html$/, ".txt");
9739
+ mkdirSync11(dirname7(txtPath), { recursive: true });
9740
+ writeFileSync10(txtPath, `Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.
9741
+ `);
9742
+ console.log(`[graph-render] graph too large (${totalNodeCount} nodes); wrote ${txtPath}`);
9743
+ return 0;
9744
+ }
9745
+ const opts = { depth, performanceMode };
9746
+ if (ticketId)
9747
+ opts.ticketId = ticketId;
9748
+ if (since)
9749
+ opts.since = since;
9750
+ const data = transformForVisNetwork(snap, opts);
9751
+ const html = renderHtml({ data, stats: data.stats, generatedAt: new Date().toISOString() });
9752
+ mkdirSync11(dirname7(finalPath), { recursive: true });
9753
+ const tmpPath = `${finalPath}.tmp`;
9754
+ writeFileSync10(tmpPath, html);
9755
+ renameSync2(tmpPath, finalPath);
9756
+ console.log(`[graph-render] wrote ${finalPath} (${data.stats.tickets} tickets \xB7 ${data.stats.scenarios} scenarios \xB7 ${html.length} bytes)`);
9757
+ return 0;
9758
+ }
9759
+
9460
9760
  // src/bin-internal/graph-snapshot.ts
9461
9761
  init_store();
9462
9762
  async function graphSnapshotCmd(argv) {
@@ -9482,8 +9782,8 @@ async function graphSnapshotCmd(argv) {
9482
9782
  }
9483
9783
 
9484
9784
  // src/bin-internal/heal-prepare.ts
9485
- import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
9486
- import { join as join16 } from "path";
9785
+ import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
9786
+ import { join as join18 } from "path";
9487
9787
  import { scrubFreeText } from "@xera-ai/web";
9488
9788
 
9489
9789
  // ../../node_modules/.bun/fflate@0.8.3/node_modules/fflate/esm/index.mjs
@@ -9913,7 +10213,7 @@ function classifyKind(raw) {
9913
10213
  function extractDomSnapshot(tracePath) {
9914
10214
  if (!existsSync20(tracePath))
9915
10215
  return "";
9916
- const buf = readFileSync17(tracePath);
10216
+ const buf = readFileSync18(tracePath);
9917
10217
  const entries = unzipSync(buf);
9918
10218
  const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
9919
10219
  let chosenKey = null;
@@ -9961,16 +10261,16 @@ function extractDomSnapshot(tracePath) {
9961
10261
  return scrubFreeText(html);
9962
10262
  }
9963
10263
  function findPomLine(ticketDir, rawLocator) {
9964
- const pomDir = join16(ticketDir, "page-objects");
10264
+ const pomDir = join18(ticketDir, "page-objects");
9965
10265
  const candidates = [];
9966
10266
  if (existsSync20(pomDir)) {
9967
10267
  for (const name of readdirSync6(pomDir)) {
9968
10268
  if (name.endsWith(".ts"))
9969
- candidates.push(join16(pomDir, name));
10269
+ candidates.push(join18(pomDir, name));
9970
10270
  }
9971
10271
  }
9972
10272
  for (const file of candidates) {
9973
- const text = readFileSync17(file, "utf8");
10273
+ const text = readFileSync18(file, "utf8");
9974
10274
  const lines = text.split(`
9975
10275
  `);
9976
10276
  for (let i2 = 0;i2 < lines.length; i2++) {
@@ -10008,13 +10308,13 @@ function findGherkinStep(featureText, rawLocator) {
10008
10308
  }
10009
10309
  function healPrepare(repoRoot, ticket, runId, scenarioName) {
10010
10310
  const paths = resolveArtifactPaths(repoRoot, ticket);
10011
- const classifierPath = join16(paths.ticketDir, "classifier-input.json");
10012
- const classifier = JSON.parse(readFileSync17(classifierPath, "utf8"));
10311
+ const classifierPath = join18(paths.ticketDir, "classifier-input.json");
10312
+ const classifier = JSON.parse(readFileSync18(classifierPath, "utf8"));
10013
10313
  const cls = classifier.scenarios.find((s) => s.name === scenarioName);
10014
10314
  if (!cls)
10015
10315
  throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
10016
- const runDir = join16(paths.runsDir, runId);
10017
- const normalized = JSON.parse(readFileSync17(join16(runDir, "normalized.json"), "utf8"));
10316
+ const runDir = join18(paths.runsDir, runId);
10317
+ const normalized = JSON.parse(readFileSync18(join18(runDir, "normalized.json"), "utf8"));
10018
10318
  const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
10019
10319
  if (!normSc?.failure)
10020
10320
  throw new Error(`no failure recorded for scenario "${scenarioName}"`);
@@ -10025,9 +10325,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
10025
10325
  const raw = m[1].trim();
10026
10326
  const kind = classifyKind(raw);
10027
10327
  const pomLoc = findPomLine(paths.ticketDir, raw);
10028
- const featureText = readFileSync17(paths.featurePath, "utf8");
10328
+ const featureText = readFileSync18(paths.featurePath, "utf8");
10029
10329
  const gherkinStep = findGherkinStep(featureText, raw);
10030
- const domSnapshotAtFailure = extractDomSnapshot(join16(runDir, "trace.zip"));
10330
+ const domSnapshotAtFailure = extractDomSnapshot(join18(runDir, "trace.zip"));
10031
10331
  return {
10032
10332
  ticket,
10033
10333
  runId,
@@ -10047,8 +10347,8 @@ async function healPrepareCmd(argv) {
10047
10347
  try {
10048
10348
  const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
10049
10349
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10050
- const outPath = join16(paths.runsDir, runId, "heal-input.json");
10051
- writeFileSync10(outPath, JSON.stringify(result, null, 2));
10350
+ const outPath = join18(paths.runsDir, runId, "heal-input.json");
10351
+ writeFileSync11(outPath, JSON.stringify(result, null, 2));
10052
10352
  console.log(`[xera:heal-prepare] wrote ${outPath}`);
10053
10353
  return 0;
10054
10354
  } catch (err2) {
@@ -10058,8 +10358,8 @@ async function healPrepareCmd(argv) {
10058
10358
  }
10059
10359
 
10060
10360
  // src/bin-internal/impact-prepare.ts
10061
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
10062
- import { join as join17 } from "path";
10361
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
10362
+ import { join as join19 } from "path";
10063
10363
 
10064
10364
  // src/graph/impact.ts
10065
10365
  var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
@@ -10264,7 +10564,7 @@ function renderImpactMarkdown(report) {
10264
10564
 
10265
10565
  // src/bin-internal/impact-prepare.ts
10266
10566
  init_store();
10267
- function parseDepth(s) {
10567
+ function parseDepth2(s) {
10268
10568
  const n = s ? Number.parseInt(s, 10) : 2;
10269
10569
  if (n === 1 || n === 3)
10270
10570
  return n;
@@ -10286,7 +10586,7 @@ async function impactPrepareCmd(argv) {
10286
10586
  let quiet = false;
10287
10587
  for (let i2 = 1;i2 < argv.length; i2++) {
10288
10588
  if (argv[i2] === "--depth")
10289
- depth = parseDepth(argv[++i2]);
10589
+ depth = parseDepth2(argv[++i2]);
10290
10590
  else if (argv[i2] === "--min-priority")
10291
10591
  minPriority = parseMinPriority(argv[++i2]);
10292
10592
  else if (argv[i2] === "--quiet")
@@ -10309,11 +10609,11 @@ async function impactPrepareCmd(argv) {
10309
10609
  scenarios,
10310
10610
  generatedAt: new Date().toISOString()
10311
10611
  };
10312
- const impactDir = join17(repoRoot, ".xera/impact");
10313
- mkdirSync11(impactDir, { recursive: true });
10314
- writeFileSync11(join17(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10612
+ const impactDir = join19(repoRoot, ".xera/impact");
10613
+ mkdirSync12(impactDir, { recursive: true });
10614
+ writeFileSync12(join19(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10315
10615
  if (!quiet) {
10316
- writeFileSync11(join17(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
10616
+ writeFileSync12(join19(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
10317
10617
  }
10318
10618
  return 0;
10319
10619
  }
@@ -10339,7 +10639,7 @@ async function lintCmd(argv) {
10339
10639
 
10340
10640
  // src/bin-internal/normalize.ts
10341
10641
  import { existsSync as existsSync21, readdirSync as readdirSync7 } from "fs";
10342
- import { join as join18 } from "path";
10642
+ import { join as join20 } from "path";
10343
10643
  import { normalizeRun } from "@xera-ai/web";
10344
10644
  async function normalizeCmd(argv) {
10345
10645
  const ticket = argv[0];
@@ -10354,7 +10654,7 @@ async function normalizeCmd(argv) {
10354
10654
  console.error("[xera:normalize] no run found");
10355
10655
  return 1;
10356
10656
  }
10357
- const runDir = join18(paths.runsDir, runId);
10657
+ const runDir = join20(paths.runsDir, runId);
10358
10658
  if (!existsSync21(runDir)) {
10359
10659
  console.error(`[xera:normalize] runs/${runId} missing`);
10360
10660
  return 1;
@@ -10365,12 +10665,12 @@ async function normalizeCmd(argv) {
10365
10665
  }
10366
10666
 
10367
10667
  // src/bin-internal/post.ts
10368
- import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
10369
- import { join as join19 } from "path";
10668
+ import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
10669
+ import { join as join21 } from "path";
10370
10670
 
10371
10671
  // src/artifact/status.ts
10372
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
10373
- import { dirname as dirname6 } from "path";
10672
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
10673
+ import { dirname as dirname8 } from "path";
10374
10674
  import { z as z7 } from "zod";
10375
10675
  var ClassificationEnum = z7.enum([
10376
10676
  "PASS",
@@ -10406,11 +10706,11 @@ var HISTORY_CAP = 20;
10406
10706
  function readStatus(path) {
10407
10707
  if (!existsSync22(path))
10408
10708
  return null;
10409
- return StatusJsonSchema.parse(JSON.parse(readFileSync18(path, "utf8")));
10709
+ return StatusJsonSchema.parse(JSON.parse(readFileSync19(path, "utf8")));
10410
10710
  }
10411
10711
  function writeStatus(path, status) {
10412
- mkdirSync12(dirname6(path), { recursive: true });
10413
- writeFileSync12(path, JSON.stringify(status, null, 2));
10712
+ mkdirSync13(dirname8(path), { recursive: true });
10713
+ writeFileSync13(path, JSON.stringify(status, null, 2));
10414
10714
  }
10415
10715
  function appendHistory(path, entry) {
10416
10716
  const s = readStatus(path);
@@ -10436,12 +10736,12 @@ async function postCmd(argv) {
10436
10736
  return 0;
10437
10737
  }
10438
10738
  const paths = resolveArtifactPaths(cwd, ticket);
10439
- const draftPath = join19(paths.ticketDir, "jira-comment.draft.md");
10739
+ const draftPath = join21(paths.ticketDir, "jira-comment.draft.md");
10440
10740
  if (!existsSync23(draftPath)) {
10441
10741
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
10442
10742
  return 1;
10443
10743
  }
10444
- const body = readFileSync19(draftPath, "utf8");
10744
+ const body = readFileSync20(draftPath, "utf8");
10445
10745
  const client = await createJiraClient({
10446
10746
  baseUrl: config.jira.baseUrl,
10447
10747
  preferMcp: true,
@@ -10469,8 +10769,8 @@ async function promoteCmd(argv) {
10469
10769
  }
10470
10770
 
10471
10771
  // src/bin-internal/report.ts
10472
- import { existsSync as existsSync25, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "fs";
10473
- import { join as join20 } from "path";
10772
+ import { existsSync as existsSync25, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
10773
+ import { join as join22 } from "path";
10474
10774
 
10475
10775
  // src/classifier/aggregate.ts
10476
10776
  var CLASS_PRIORITY = [
@@ -10642,10 +10942,10 @@ async function reportCmd(argv) {
10642
10942
  return 1;
10643
10943
  }
10644
10944
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10645
- const input = JSON.parse(readFileSync20(inputArg.slice("--input=".length), "utf8"));
10945
+ const input = JSON.parse(readFileSync21(inputArg.slice("--input=".length), "utf8"));
10646
10946
  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")) : {};
10947
+ const decisionsPath = join22(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
10948
+ const decisions = existsSync25(decisionsPath) ? JSON.parse(readFileSync21(decisionsPath, "utf8")) : {};
10649
10949
  const graph = deriveSnapshot(loadAllEvents(process.cwd()));
10650
10950
  const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
10651
10951
  const scenarioIdByName = {};
@@ -10693,8 +10993,8 @@ async function reportCmd(argv) {
10693
10993
  xeraVersion: "0.1.0",
10694
10994
  promptsVersion: "1.0.0"
10695
10995
  });
10696
- const draftPath = join20(paths.ticketDir, "jira-comment.draft.md");
10697
- writeFileSync13(draftPath, md);
10996
+ const draftPath = join22(paths.ticketDir, "jira-comment.draft.md");
10997
+ writeFileSync14(draftPath, md);
10698
10998
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
10699
10999
  return 0;
10700
11000
  }
@@ -10761,7 +11061,7 @@ async function unlockCmd(argv) {
10761
11061
  }
10762
11062
 
10763
11063
  // src/bin-internal/validate-feature.ts
10764
- import { existsSync as existsSync26, readFileSync as readFileSync21 } from "fs";
11064
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
10765
11065
  import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
10766
11066
  async function validateFeatureCmd(argv) {
10767
11067
  const ticket = argv[0];
@@ -10774,7 +11074,7 @@ async function validateFeatureCmd(argv) {
10774
11074
  console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
10775
11075
  return 1;
10776
11076
  }
10777
- const r = validateGherkin2(readFileSync21(paths.featurePath, "utf8"));
11077
+ const r = validateGherkin2(readFileSync22(paths.featurePath, "utf8"));
10778
11078
  if (r.ok) {
10779
11079
  console.log("[xera:validate-feature] ok");
10780
11080
  return 0;
@@ -10794,6 +11094,7 @@ var COMMANDS = {
10794
11094
  fetch: fetchCmd,
10795
11095
  "graph-backfill": graphBackfillCmd,
10796
11096
  "graph-enrich": graphEnrichCmd,
11097
+ "graph-render": graphRenderCmd,
10797
11098
  "graph-query": graphQueryCmd,
10798
11099
  "graph-record": graphRecordCmd,
10799
11100
  "graph-snapshot": graphSnapshotCmd,
@@ -0,0 +1,2 @@
1
+ export declare function graphRenderCmd(argv: string[]): Promise<number>;
2
+ //# sourceMappingURL=graph-render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-render.d.ts","sourceRoot":"","sources":["../../src/bin-internal/graph-render.ts"],"names":[],"mappings":"AAkBA,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmDpE"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin-internal/index.ts"],"names":[],"mappings":"AAoDA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAczD"}
@@ -7,6 +7,8 @@ export { enrichTicket } from './enrich';
7
7
  export type { ImpactEdge, ImpactOpts, ImpactReport, ImpactScenario, } from './impact';
8
8
  export { renderImpactMarkdown, riskScore, walkImpact, } from './impact';
9
9
  export { currentYyyyMm, graphPaths } from './paths';
10
+ export type { GraphStats, RenderHtmlInput, RenderOpts, VisEdge, VisNode } from './render';
11
+ export { renderHtml, transformForVisNetwork } from './render';
10
12
  export { EventSchema, safeParseEvent } from './schema';
11
13
  export { buildSimilarityPrompt } from './similarity';
12
14
  export { appendEvents, computeEventsHash, deriveSnapshot, isSnapshotStale, loadAllEvents, loadSnapshot, writeSnapshot, } from './store';
@@ -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,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"}
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,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAC9D,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"}
@@ -0,0 +1,50 @@
1
+ import type { Snapshot } from './types';
2
+ export interface VisNode {
3
+ id: string;
4
+ label: string;
5
+ group: 'Ticket' | 'Scenario' | 'POM' | 'SUTArea' | 'Failure';
6
+ color?: string;
7
+ shape?: string;
8
+ size?: number;
9
+ title?: string;
10
+ borderWidth?: number;
11
+ }
12
+ export interface VisEdge {
13
+ id?: string;
14
+ from: string;
15
+ to: string;
16
+ label?: string;
17
+ color?: string;
18
+ dashes?: boolean;
19
+ width?: number;
20
+ arrows?: string;
21
+ }
22
+ export interface GraphStats {
23
+ tickets: number;
24
+ scenarios: number;
25
+ poms: number;
26
+ areas: number;
27
+ failures: number;
28
+ edges: number;
29
+ }
30
+ export interface RenderOpts {
31
+ since?: string;
32
+ ticketId?: string;
33
+ depth?: 1 | 2 | 3;
34
+ performanceMode?: 'full' | 'ticket-only' | 'text-fallback';
35
+ }
36
+ export declare function transformForVisNetwork(snap: Snapshot, opts: RenderOpts): {
37
+ nodes: VisNode[];
38
+ edges: VisEdge[];
39
+ stats: GraphStats;
40
+ };
41
+ export interface RenderHtmlInput {
42
+ data: {
43
+ nodes: VisNode[];
44
+ edges: VisEdge[];
45
+ };
46
+ stats: GraphStats;
47
+ generatedAt: string;
48
+ }
49
+ export declare function renderHtml(input: RenderHtmlInput): string;
50
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/graph/render.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAc,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,QAAQ,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,GAAG,aAAa,GAAG,eAAe,CAAC;CAC5D;AAmKD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,UAAU,GACf;IACD,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,EAAE,UAAU,CAAC;CACnB,CA4EA;AAcD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAAC,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;IAC7C,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAgBzD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/core",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",