@xera-ai/core 0.4.2 → 0.4.4

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 (35) hide show
  1. package/dist/bin/internal.js +725 -290
  2. package/dist/bin-internal/disputes.d.ts +2 -0
  3. package/dist/bin-internal/disputes.d.ts.map +1 -0
  4. package/dist/bin-internal/doctor.d.ts +1 -1
  5. package/dist/bin-internal/doctor.d.ts.map +1 -1
  6. package/dist/bin-internal/exec.d.ts.map +1 -1
  7. package/dist/bin-internal/graph-record-script.d.ts.map +1 -1
  8. package/dist/bin-internal/graph-render.d.ts +2 -0
  9. package/dist/bin-internal/graph-render.d.ts.map +1 -0
  10. package/dist/bin-internal/index.d.ts.map +1 -1
  11. package/dist/graph/index.d.ts +2 -0
  12. package/dist/graph/index.d.ts.map +1 -1
  13. package/dist/graph/render.d.ts +50 -0
  14. package/dist/graph/render.d.ts.map +1 -0
  15. package/dist/graph/store.d.ts.map +1 -1
  16. package/dist/graph/types.d.ts +1 -0
  17. package/dist/graph/types.d.ts.map +1 -1
  18. package/dist/src/index.js +1 -1
  19. package/package.json +1 -1
  20. package/src/bin-internal/disputes.ts +88 -0
  21. package/src/bin-internal/doctor.ts +13 -1
  22. package/src/bin-internal/exec.ts +3 -0
  23. package/src/bin-internal/graph-record-script.ts +37 -8
  24. package/src/bin-internal/graph-render.ts +70 -0
  25. package/src/bin-internal/index.ts +4 -0
  26. package/src/config/schema.ts +1 -1
  27. package/src/graph/index.ts +2 -0
  28. package/src/graph/render.ts +324 -0
  29. package/src/graph/store.ts +8 -1
  30. package/src/graph/templates/LICENSE-vis-network.txt +176 -0
  31. package/src/graph/templates/graph.css +88 -0
  32. package/src/graph/templates/graph.html.template +31 -0
  33. package/src/graph/templates/graph.js +101 -0
  34. package/src/graph/templates/vis-network.min.js +25000 -0
  35. package/src/graph/types.ts +1 -0
@@ -153,10 +153,10 @@ var init_schema = __esm(() => {
153
153
  // src/graph/store.ts
154
154
  import { createHash } from "crypto";
155
155
  import {
156
- existsSync as existsSync2,
157
- mkdirSync as mkdirSync2,
156
+ existsSync,
157
+ mkdirSync,
158
158
  readdirSync,
159
- readFileSync as readFileSync2,
159
+ readFileSync,
160
160
  renameSync,
161
161
  writeFileSync
162
162
  } from "fs";
@@ -167,7 +167,7 @@ function appendEvents(repoRoot, events, opts) {
167
167
  const paths = graphPaths(repoRoot);
168
168
  const yyyyMm = currentYyyyMm(opts.now);
169
169
  const monthDir = paths.eventsMonthDir(yyyyMm);
170
- mkdirSync2(monthDir, { recursive: true });
170
+ mkdirSync(monthDir, { recursive: true });
171
171
  const ulid = events[0].event_id;
172
172
  const finalPath = paths.eventFile(ulid, opts.skill, opts.ticketId, yyyyMm);
173
173
  const tmpPath = `${finalPath}.tmp`;
@@ -180,7 +180,7 @@ function appendEvents(repoRoot, events, opts) {
180
180
  }
181
181
  function loadAllEvents(repoRoot) {
182
182
  const paths = graphPaths(repoRoot);
183
- if (!existsSync2(paths.eventsDir))
183
+ if (!existsSync(paths.eventsDir))
184
184
  return [];
185
185
  const files = [];
186
186
  for (const monthDir of readdirSync(paths.eventsDir, { withFileTypes: true })) {
@@ -200,7 +200,7 @@ function loadAllEvents(repoRoot) {
200
200
  const events = [];
201
201
  for (const file of files) {
202
202
  try {
203
- const lines = readFileSync2(file, "utf8").split(`
203
+ const lines = readFileSync(file, "utf8").split(`
204
204
  `).filter(Boolean);
205
205
  for (const line of lines) {
206
206
  let parsed;
@@ -326,6 +326,13 @@ function deriveSnapshot(events) {
326
326
  edges.push(ed);
327
327
  break;
328
328
  }
329
+ case "classification.disputed": {
330
+ const existing = latestFailures[e.payload.scenarioId];
331
+ if (existing && existing.runId === e.payload.runId) {
332
+ existing.disputed = true;
333
+ }
334
+ break;
335
+ }
329
336
  default:
330
337
  break;
331
338
  }
@@ -345,17 +352,17 @@ function deriveSnapshot(events) {
345
352
  }
346
353
  function writeSnapshot(repoRoot, snap) {
347
354
  const paths = graphPaths(repoRoot);
348
- mkdirSync2(dirname(paths.snapshotFile), { recursive: true });
355
+ mkdirSync(dirname(paths.snapshotFile), { recursive: true });
349
356
  const tmp = `${paths.snapshotFile}.tmp`;
350
357
  writeFileSync(tmp, JSON.stringify(snap, null, 2));
351
358
  renameSync(tmp, paths.snapshotFile);
352
359
  }
353
360
  function loadSnapshot(repoRoot) {
354
361
  const paths = graphPaths(repoRoot);
355
- if (!existsSync2(paths.snapshotFile))
362
+ if (!existsSync(paths.snapshotFile))
356
363
  return null;
357
364
  try {
358
- return JSON.parse(readFileSync2(paths.snapshotFile, "utf8"));
365
+ return JSON.parse(readFileSync(paths.snapshotFile, "utf8"));
359
366
  } catch {
360
367
  return null;
361
368
  }
@@ -429,21 +436,29 @@ var exports_graph_record_script = {};
429
436
  __export(exports_graph_record_script, {
430
437
  recordScriptImpl: () => recordScriptImpl
431
438
  });
432
- import { createHash as createHash3 } from "crypto";
433
- import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync14 } from "fs";
434
- import { basename, join as join12 } from "path";
439
+ import { createHash as createHash2 } from "crypto";
440
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
441
+ import { basename, join as join3 } from "path";
442
+ function inferPriority(name, gherkin) {
443
+ const haystack = `${name} ${gherkin}`.toLowerCase();
444
+ for (const kw of P0_KEYWORDS) {
445
+ if (haystack.includes(kw))
446
+ return "p0";
447
+ }
448
+ return "p1";
449
+ }
435
450
  function parseFeature(text) {
436
451
  const scenarios = [];
437
452
  const lines = text.split(`
438
453
  `);
439
- let currentTagPriority = "p1";
454
+ let explicitTag = null;
440
455
  let i = 0;
441
456
  while (i < lines.length) {
442
457
  const line = lines[i].trim();
443
458
  if (line.startsWith("@")) {
444
459
  const tag = line.slice(1).split(/\s+/)[0].toLowerCase();
445
460
  if (tag === "p0" || tag === "p1" || tag === "p2")
446
- currentTagPriority = tag;
461
+ explicitTag = tag;
447
462
  i++;
448
463
  continue;
449
464
  }
@@ -453,13 +468,11 @@ function parseFeature(text) {
453
468
  i++;
454
469
  while (i < lines.length && !lines[i].trim().startsWith("Scenario") && !lines[i].trim().startsWith("@"))
455
470
  i++;
456
- scenarios.push({
457
- name,
458
- priority: currentTagPriority,
459
- gherkin: lines.slice(start, i).join(`
460
- `)
461
- });
462
- currentTagPriority = "p1";
471
+ const gherkin = lines.slice(start, i).join(`
472
+ `);
473
+ const priority = explicitTag !== null ? explicitTag : inferPriority(name, gherkin);
474
+ scenarios.push({ name, priority, gherkin });
475
+ explicitTag = null;
463
476
  continue;
464
477
  }
465
478
  i++;
@@ -467,9 +480,9 @@ function parseFeature(text) {
467
480
  return scenarios;
468
481
  }
469
482
  function listPomFiles(dir) {
470
- if (!existsSync16(dir))
483
+ if (!existsSync4(dir))
471
484
  return [];
472
- return readdirSync4(dir).filter((f) => f.endsWith(".ts")).map((f) => join12(dir, f));
485
+ return readdirSync2(dir).filter((f) => f.endsWith(".ts")).map((f) => join3(dir, f));
473
486
  }
474
487
  function extractRoute(pomContent) {
475
488
  const m = pomContent.match(/goto\s*\(\s*['"]([^'"]+)['"]/);
@@ -496,15 +509,15 @@ function extractPomUsage(specContent) {
496
509
  return [...names];
497
510
  }
498
511
  async function recordScriptImpl(repoRoot, ticket) {
499
- const ticketDir = join12(repoRoot, ".xera", ticket);
500
- const featurePath = join12(ticketDir, "feature", `${ticket}.feature`);
501
- const specPath = join12(ticketDir, "tests", `${ticket}.spec.ts`);
502
- const pomDir = join12(ticketDir, "poms");
503
- if (!existsSync16(featurePath)) {
512
+ const ticketDir = join3(repoRoot, ".xera", ticket);
513
+ const featurePath = join3(ticketDir, "feature", `${ticket}.feature`);
514
+ const specPath = join3(ticketDir, "tests", `${ticket}.spec.ts`);
515
+ const pomDir = join3(ticketDir, "poms");
516
+ if (!existsSync4(featurePath)) {
504
517
  console.error(`[graph-record script] feature missing`);
505
518
  return 1;
506
519
  }
507
- const featureText = readFileSync14(featurePath, "utf8");
520
+ const featureText = readFileSync4(featurePath, "utf8");
508
521
  const featureHash = sha1(featureText);
509
522
  const scenarios = parseFeature(featureText);
510
523
  const events = [];
@@ -524,7 +537,7 @@ async function recordScriptImpl(repoRoot, ticket) {
524
537
  const pomFiles = listPomFiles(pomDir);
525
538
  const pomNameToId = new Map;
526
539
  for (const pomFile of pomFiles) {
527
- const content = readFileSync14(pomFile, "utf8");
540
+ const content = readFileSync4(pomFile, "utf8");
528
541
  const id = pId(pomFile);
529
542
  const className = content.match(/export\s+class\s+([A-Z][A-Za-z0-9]*Page)/)?.[1] ?? "";
530
543
  pomNameToId.set(className, id);
@@ -538,8 +551,8 @@ async function recordScriptImpl(repoRoot, ticket) {
538
551
  };
539
552
  events.push(mk("xera-script", "pom.generated", pg));
540
553
  }
541
- if (existsSync16(specPath)) {
542
- const specContent = readFileSync14(specPath, "utf8");
554
+ if (existsSync4(specPath)) {
555
+ const specContent = readFileSync4(specPath, "utf8");
543
556
  const usedPoms = extractPomUsage(specContent);
544
557
  for (const scenario of scenarios) {
545
558
  const scId = sId(ticket, scenario.name);
@@ -571,17 +584,39 @@ async function recordScriptImpl(repoRoot, ticket) {
571
584
  appendEvents(repoRoot, events, { skill: "xera-script", ticketId: ticket });
572
585
  return 0;
573
586
  }
574
- var sha1 = (s) => createHash3("sha1").update(s).digest("hex"), sId = (ticket, name) => sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, " ")}`), pId = (file) => sha1(basename(file)), nowIso = () => new Date().toISOString(), mk = (actor, type, payload) => ({
587
+ var sha1 = (s) => createHash2("sha1").update(s).digest("hex"), sId = (ticket, name) => sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, " ")}`), pId = (file) => sha1(basename(file)), nowIso = () => new Date().toISOString(), mk = (actor, type, payload) => ({
575
588
  event_id: ulid(),
576
589
  schema_version: SCHEMA_VERSION,
577
590
  ts: nowIso(),
578
591
  actor,
579
592
  type,
580
593
  payload
581
- });
594
+ }), P0_KEYWORDS;
582
595
  var init_graph_record_script = __esm(() => {
583
596
  init_store();
584
597
  init_ulid();
598
+ P0_KEYWORDS = [
599
+ "log in",
600
+ "login",
601
+ "sign in",
602
+ "signin",
603
+ "sign up",
604
+ "signup",
605
+ "auth",
606
+ "authentic",
607
+ "payment",
608
+ "pay ",
609
+ "checkout",
610
+ "purchase",
611
+ "charge",
612
+ "password",
613
+ "credential",
614
+ "admin",
615
+ "permission",
616
+ "role",
617
+ "must ",
618
+ "critical"
619
+ ];
585
620
  });
586
621
 
587
622
  // ../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
@@ -7580,14 +7615,14 @@ __export(exports_graph_record, {
7580
7615
  recordFetch: () => recordFetch,
7581
7616
  graphRecordCmd: () => graphRecordCmd
7582
7617
  });
7583
- import { createHash as createHash4 } from "crypto";
7584
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
7585
- import { basename as basename2, join as join13 } from "path";
7618
+ import { createHash as createHash3 } from "crypto";
7619
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
7620
+ import { basename as basename2, join as join4 } from "path";
7586
7621
  function nowIso2() {
7587
7622
  return new Date().toISOString();
7588
7623
  }
7589
7624
  function sha12(s) {
7590
- return createHash4("sha1").update(s).digest("hex");
7625
+ return createHash3("sha1").update(s).digest("hex");
7591
7626
  }
7592
7627
  function scenarioId(ticket, name) {
7593
7628
  return sha12(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, " ")}`);
@@ -7606,21 +7641,21 @@ function makeEvent(actor, type, payload) {
7606
7641
  };
7607
7642
  }
7608
7643
  function readStoryFrontmatter(repoRoot, ticket) {
7609
- const path = join13(repoRoot, ".xera", ticket, "story.md");
7610
- if (!existsSync17(path))
7644
+ const path = join4(repoRoot, ".xera", ticket, "story.md");
7645
+ if (!existsSync5(path))
7611
7646
  return null;
7612
- const raw = readFileSync15(path, "utf8");
7647
+ const raw = readFileSync5(path, "utf8");
7613
7648
  const m = raw.match(/^---\n([\s\S]*?)\n---/);
7614
7649
  if (!m)
7615
7650
  return null;
7616
7651
  return $parse(m[1]);
7617
7652
  }
7618
7653
  function readGraphInput(repoRoot, ticket) {
7619
- const path = join13(repoRoot, ".xera", ticket, "graph-input.json");
7620
- if (!existsSync17(path))
7654
+ const path = join4(repoRoot, ".xera", ticket, "graph-input.json");
7655
+ if (!existsSync5(path))
7621
7656
  return { modifiesAreas: [] };
7622
7657
  try {
7623
- return JSON.parse(readFileSync15(path, "utf8"));
7658
+ return JSON.parse(readFileSync5(path, "utf8"));
7624
7659
  } catch {
7625
7660
  return { modifiesAreas: [] };
7626
7661
  }
@@ -7668,12 +7703,12 @@ async function recordScript(repoRoot, ticket) {
7668
7703
  return recordScriptImpl2(repoRoot, ticket);
7669
7704
  }
7670
7705
  async function recordExec(repoRoot, ticket, runId) {
7671
- const reporterPath = join13(repoRoot, ".xera", ticket, "runs", runId, "reporter.json");
7672
- if (!existsSync17(reporterPath)) {
7706
+ const reporterPath = join4(repoRoot, ".xera", ticket, "runs", runId, "reporter.json");
7707
+ if (!existsSync5(reporterPath)) {
7673
7708
  console.error(`[graph-record exec] reporter.json missing`);
7674
7709
  return 1;
7675
7710
  }
7676
- const data = JSON.parse(readFileSync15(reporterPath, "utf8"));
7711
+ const data = JSON.parse(readFileSync5(reporterPath, "utf8"));
7677
7712
  const events = [];
7678
7713
  for (const s of data.scenarios) {
7679
7714
  const p = {
@@ -7691,12 +7726,12 @@ async function recordExec(repoRoot, ticket, runId) {
7691
7726
  return 0;
7692
7727
  }
7693
7728
  async function recordClassify(repoRoot, ticket, runId) {
7694
- const classifyPath = join13(repoRoot, ".xera", ticket, "runs", runId, "classifier-output.json");
7695
- if (!existsSync17(classifyPath)) {
7729
+ const classifyPath = join4(repoRoot, ".xera", ticket, "runs", runId, "classifier-output.json");
7730
+ if (!existsSync5(classifyPath)) {
7696
7731
  console.error(`[graph-record classify] classifier-output.json missing`);
7697
7732
  return 1;
7698
7733
  }
7699
- const data = JSON.parse(readFileSync15(classifyPath, "utf8"));
7734
+ const data = JSON.parse(readFileSync5(classifyPath, "utf8"));
7700
7735
  const events = [];
7701
7736
  for (const s of data.scenarios) {
7702
7737
  const p = {
@@ -7724,7 +7759,7 @@ async function recordPromote(repoRoot, args) {
7724
7759
  appendEvents(repoRoot, [e], { skill: "xera-promote", ticketId: "shared" });
7725
7760
  return 0;
7726
7761
  }
7727
- function parseFlags2(args) {
7762
+ function parseFlags(args) {
7728
7763
  const m = new Map;
7729
7764
  for (let i = 0;i < args.length; i++) {
7730
7765
  if (args[i].startsWith("--")) {
@@ -7760,7 +7795,7 @@ async function graphRecordCmd(argv) {
7760
7795
  }
7761
7796
  case "exec": {
7762
7797
  const ticket = rest[0];
7763
- const flags = parseFlags2(rest);
7798
+ const flags = parseFlags(rest);
7764
7799
  const runId = flags.get("--run-id");
7765
7800
  if (!ticket || !runId) {
7766
7801
  console.error("ticket + --run-id required");
@@ -7770,7 +7805,7 @@ async function graphRecordCmd(argv) {
7770
7805
  }
7771
7806
  case "classify": {
7772
7807
  const ticket = rest[0];
7773
- const flags = parseFlags2(rest);
7808
+ const flags = parseFlags(rest);
7774
7809
  const runId = flags.get("--run-id");
7775
7810
  if (!ticket || !runId) {
7776
7811
  console.error("ticket + --run-id required");
@@ -7779,10 +7814,10 @@ async function graphRecordCmd(argv) {
7779
7814
  return recordClassify(repoRoot, ticket, runId);
7780
7815
  }
7781
7816
  case "promote": {
7782
- return recordPromote(repoRoot, parseFlags2(rest));
7817
+ return recordPromote(repoRoot, parseFlags(rest));
7783
7818
  }
7784
7819
  case "dispute": {
7785
- const flags = parseFlags2(rest);
7820
+ const flags = parseFlags(rest);
7786
7821
  const runId = flags.get("--run-id");
7787
7822
  const scenarioIdArg = flags.get("--scenario-id");
7788
7823
  const from = flags.get("--from");
@@ -7829,20 +7864,145 @@ var init_graph_record = __esm(() => {
7829
7864
  init_ulid();
7830
7865
  });
7831
7866
 
7867
+ // src/bin-internal/graph-backfill.ts
7868
+ var exports_graph_backfill = {};
7869
+ __export(exports_graph_backfill, {
7870
+ graphBackfillCmd: () => graphBackfillCmd
7871
+ });
7872
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
7873
+ import { join as join5 } from "path";
7874
+ async function backfillTicket(repoRoot, ticket, dryRun) {
7875
+ const storyPath = join5(repoRoot, ".xera", ticket, "story.md");
7876
+ if (!existsSync6(storyPath))
7877
+ return 0;
7878
+ const { recordFetch: recordFetch2 } = await Promise.resolve().then(() => (init_graph_record(), exports_graph_record));
7879
+ if (dryRun) {
7880
+ console.log(`[backfill dry-run] would backfill ${ticket}`);
7881
+ return 0;
7882
+ }
7883
+ await recordScriptImpl(repoRoot, ticket);
7884
+ await recordFetch2(repoRoot, ticket);
7885
+ return 0;
7886
+ }
7887
+ async function graphBackfillCmd(argv) {
7888
+ const dryRun = argv.includes("--dry-run");
7889
+ const repoRoot = process.cwd();
7890
+ const xeraDir = join5(repoRoot, ".xera");
7891
+ if (!existsSync6(xeraDir)) {
7892
+ console.log("[backfill] no .xera/ directory");
7893
+ return 0;
7894
+ }
7895
+ const tickets = [];
7896
+ for (const entry of readdirSync3(xeraDir, { withFileTypes: true })) {
7897
+ if (!entry.isDirectory())
7898
+ continue;
7899
+ if (entry.name === "graph")
7900
+ continue;
7901
+ if (entry.name.startsWith("."))
7902
+ continue;
7903
+ if (!/^[A-Z]+-\d+$/.test(entry.name))
7904
+ continue;
7905
+ tickets.push(entry.name);
7906
+ }
7907
+ console.log(`[backfill] found ${tickets.length} tickets`);
7908
+ for (const t of tickets)
7909
+ await backfillTicket(repoRoot, t, dryRun);
7910
+ console.log(`[backfill] done`);
7911
+ return 0;
7912
+ }
7913
+ var init_graph_backfill = __esm(() => {
7914
+ init_graph_record_script();
7915
+ });
7916
+
7917
+ // src/bin-internal/disputes.ts
7918
+ init_store();
7919
+ function parseDuration(s) {
7920
+ const match = s.match(/^(\d+)([dhm])$/);
7921
+ if (!match)
7922
+ return 0;
7923
+ const n = Number.parseInt(match[1], 10);
7924
+ const unit = match[2];
7925
+ if (unit === "d")
7926
+ return n * 86400 * 1000;
7927
+ if (unit === "h")
7928
+ return n * 3600 * 1000;
7929
+ if (unit === "m")
7930
+ return n * 60 * 1000;
7931
+ return 0;
7932
+ }
7933
+ function eventToRow(e) {
7934
+ const p = e.payload;
7935
+ const row = {
7936
+ ts: e.ts,
7937
+ runId: p.runId,
7938
+ scenarioId: p.scenarioId,
7939
+ originalClassification: p.originalClassification,
7940
+ disputedTo: p.disputedTo,
7941
+ qaActor: p.qaActor
7942
+ };
7943
+ if (p.qaReason)
7944
+ row.qaReason = p.qaReason;
7945
+ return row;
7946
+ }
7947
+ function renderText(rows) {
7948
+ if (rows.length === 0)
7949
+ return `No disputes recorded.
7950
+ `;
7951
+ const lines = [];
7952
+ lines.push(`${rows.length} dispute(s):`);
7953
+ for (const r of rows) {
7954
+ lines.push(` ${r.ts} | ${r.scenarioId} | ${r.originalClassification} \u2192 ${r.disputedTo} | ${r.qaActor}`);
7955
+ if (r.qaReason)
7956
+ lines.push(` reason: ${r.qaReason}`);
7957
+ }
7958
+ return `${lines.join(`
7959
+ `)}
7960
+ `;
7961
+ }
7962
+ async function disputesCmd(argv) {
7963
+ let since;
7964
+ let format = "text";
7965
+ for (let i = 0;i < argv.length; i++) {
7966
+ if (argv[i] === "--since") {
7967
+ since = argv[++i];
7968
+ } else if (argv[i] === "--format") {
7969
+ const v = argv[++i];
7970
+ if (v === "json" || v === "text")
7971
+ format = v;
7972
+ }
7973
+ }
7974
+ const repoRoot = process.cwd();
7975
+ const events = loadAllEvents(repoRoot);
7976
+ const disputes = events.filter((e) => e.type === "classification.disputed");
7977
+ let cutoffMs;
7978
+ if (since) {
7979
+ const sinceMs = parseDuration(since);
7980
+ if (sinceMs > 0)
7981
+ cutoffMs = Date.now() - sinceMs;
7982
+ }
7983
+ const rows = disputes.filter((e) => cutoffMs === undefined || Date.parse(e.ts) >= cutoffMs).map(eventToRow).sort((a, b) => a.ts < b.ts ? 1 : -1);
7984
+ if (format === "json") {
7985
+ process.stdout.write(JSON.stringify(rows, null, 2));
7986
+ } else {
7987
+ process.stdout.write(renderText(rows));
7988
+ }
7989
+ return 0;
7990
+ }
7991
+
7832
7992
  // src/bin-internal/doctor.ts
7833
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
7834
- import { join as join3 } from "path";
7993
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
7994
+ import { join as join6 } from "path";
7835
7995
 
7836
7996
  // src/graph/cost.ts
7837
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
7997
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "fs";
7838
7998
  init_paths();
7839
7999
  function summarizeCost(repoRoot, daysBack) {
7840
8000
  const paths = graphPaths(repoRoot);
7841
8001
  const result = { totalCalls: 0, totalUsd: 0, bySkill: {}, windowDays: daysBack };
7842
- if (!existsSync(paths.costLog))
8002
+ if (!existsSync2(paths.costLog))
7843
8003
  return result;
7844
8004
  const cutoff = Date.now() - daysBack * 86400 * 1000;
7845
- for (const line of readFileSync(paths.costLog, "utf8").split(`
8005
+ for (const line of readFileSync2(paths.costLog, "utf8").split(`
7846
8006
  `)) {
7847
8007
  if (!line.trim())
7848
8008
  continue;
@@ -7941,10 +8101,10 @@ function frontmatterField(content, field) {
7941
8101
  return m?.[1] ?? null;
7942
8102
  }
7943
8103
  function checkGoldenEvalDir(repoRoot) {
7944
- const root = join3(repoRoot, "fixtures/golden-eval");
7945
- if (!existsSync4(root))
8104
+ const root = join6(repoRoot, "fixtures/golden-eval");
8105
+ if (!existsSync7(root))
7946
8106
  return [{ ok: false, message: "fixtures/golden-eval/ does not exist" }];
7947
- const dirs = readdirSync2(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
8107
+ const dirs = readdirSync4(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
7948
8108
  const results = [];
7949
8109
  if (dirs.length < 3) {
7950
8110
  results.push({
@@ -7953,15 +8113,15 @@ function checkGoldenEvalDir(repoRoot) {
7953
8113
  });
7954
8114
  }
7955
8115
  for (const entry of dirs) {
7956
- const dir = join3(root, entry.name);
7957
- const metaPath = join3(dir, "meta.json");
7958
- if (!existsSync4(metaPath)) {
8116
+ const dir = join6(root, entry.name);
8117
+ const metaPath = join6(dir, "meta.json");
8118
+ if (!existsSync7(metaPath)) {
7959
8119
  results.push({ ok: false, message: `${entry.name}: meta.json missing` });
7960
8120
  continue;
7961
8121
  }
7962
8122
  let meta;
7963
8123
  try {
7964
- meta = JSON.parse(readFileSync4(metaPath, "utf8"));
8124
+ meta = JSON.parse(readFileSync6(metaPath, "utf8"));
7965
8125
  } catch (err) {
7966
8126
  results.push({
7967
8127
  ok: false,
@@ -7972,12 +8132,12 @@ function checkGoldenEvalDir(repoRoot) {
7972
8132
  const stages = Array.isArray(meta.stages) ? meta.stages : [];
7973
8133
  if (stages.length === 0)
7974
8134
  results.push({ ok: false, message: `${entry.name}: meta.stages is empty` });
7975
- if (!existsSync4(join3(dir, "story.md")))
8135
+ if (!existsSync7(join6(dir, "story.md")))
7976
8136
  results.push({ ok: false, message: `${entry.name}: story.md missing` });
7977
8137
  for (const stage of stages) {
7978
8138
  const required = REQUIRED_FILES_PER_STAGE[stage] ?? [];
7979
8139
  for (const rel of required) {
7980
- if (!existsSync4(join3(dir, rel))) {
8140
+ if (!existsSync7(join6(dir, rel))) {
7981
8141
  results.push({
7982
8142
  ok: false,
7983
8143
  message: `${meta.id ?? entry.name}: stage "${stage}" declared but ${rel} missing`
@@ -7989,10 +8149,10 @@ function checkGoldenEvalDir(repoRoot) {
7989
8149
  return results;
7990
8150
  }
7991
8151
  function checkRubricPrompt(repoRoot) {
7992
- const path = join3(repoRoot, "packages/prompts/eval-rubric.md");
7993
- if (!existsSync4(path))
8152
+ const path = join6(repoRoot, "packages/prompts/eval-rubric.md");
8153
+ if (!existsSync7(path))
7994
8154
  return [{ ok: false, message: "packages/prompts/eval-rubric.md missing" }];
7995
- const text = readFileSync4(path, "utf8");
8155
+ const text = readFileSync6(path, "utf8");
7996
8156
  const id = frontmatterField(text, "id");
7997
8157
  const version = frontmatterField(text, "version");
7998
8158
  if (id !== "eval-rubric")
@@ -8002,10 +8162,10 @@ function checkRubricPrompt(repoRoot) {
8002
8162
  return [];
8003
8163
  }
8004
8164
  function checkEvalSkill(repoRoot) {
8005
- const path = join3(repoRoot, "packages/skills/xera-eval.md");
8006
- if (!existsSync4(path))
8165
+ const path = join6(repoRoot, "packages/skills/xera-eval.md");
8166
+ if (!existsSync7(path))
8007
8167
  return [{ ok: false, message: "packages/skills/xera-eval.md missing" }];
8008
- const text = readFileSync4(path, "utf8");
8168
+ const text = readFileSync6(path, "utf8");
8009
8169
  if (!frontmatterField(text, "name"))
8010
8170
  return [{ ok: false, message: 'xera-eval.md frontmatter "name" missing' }];
8011
8171
  return [];
@@ -8014,16 +8174,17 @@ function checkPromptInjectionPreamble(repoRoot) {
8014
8174
  return verifyPrompts(repoRoot);
8015
8175
  }
8016
8176
  function checkRootScripts(repoRoot) {
8017
- const path = join3(repoRoot, "package.json");
8018
- if (!existsSync4(path))
8177
+ const path = join6(repoRoot, "package.json");
8178
+ if (!existsSync7(path))
8019
8179
  return [{ ok: false, message: "root package.json missing" }];
8020
- const pkg = JSON.parse(readFileSync4(path, "utf8"));
8180
+ const pkg = JSON.parse(readFileSync6(path, "utf8"));
8021
8181
  const scripts = pkg.scripts ?? {};
8022
8182
  const missing = REQUIRED_SCRIPTS.filter((s) => typeof scripts[s] !== "string");
8023
8183
  return missing.map((s) => ({ ok: false, message: `root package.json missing script: ${s}` }));
8024
8184
  }
8025
- async function doctorCmd(_argv, opts = {}) {
8185
+ async function doctorCmd(argv, opts = {}) {
8026
8186
  const repoRoot = opts.cwd ?? process.cwd();
8187
+ const autoEnrich = argv.includes("--auto-enrich");
8027
8188
  const results = [
8028
8189
  ...checkGoldenEvalDir(repoRoot),
8029
8190
  ...checkRubricPrompt(repoRoot),
@@ -8041,9 +8202,9 @@ async function doctorCmd(_argv, opts = {}) {
8041
8202
  if (top)
8042
8203
  console.log(` Top skill: ${top[0]} (${top[1].calls} calls, $${top[1].usd.toFixed(2)})`);
8043
8204
  }
8044
- const xeraDir = join3(repoRoot, ".xera");
8045
- if (existsSync4(xeraDir)) {
8046
- const ticketDirs = readdirSync2(xeraDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name));
8205
+ const xeraDir = join6(repoRoot, ".xera");
8206
+ if (existsSync7(xeraDir)) {
8207
+ const ticketDirs = readdirSync4(xeraDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name));
8047
8208
  if (ticketDirs.length > 0) {
8048
8209
  const events = loadAllEvents(repoRoot);
8049
8210
  const fetchedTickets = new Set(events.filter((e) => e.type === "ticket.fetched").map((e) => e.payload.ticketId));
@@ -8054,6 +8215,16 @@ async function doctorCmd(_argv, opts = {}) {
8054
8215
  console.log(` These won't participate in v0.6.1+ features (TEST_OUTDATED, /xera-impact).`);
8055
8216
  console.log(` Run: bun run xera:graph-backfill`);
8056
8217
  console.log(` (Use --dry-run to preview.)`);
8218
+ if (autoEnrich) {
8219
+ console.log("[doctor] --auto-enrich: running backfill for unbackfilled tickets...");
8220
+ const { graphBackfillCmd: graphBackfillCmd2 } = await Promise.resolve().then(() => (init_graph_backfill(), exports_graph_backfill));
8221
+ const exitCode = await graphBackfillCmd2([]);
8222
+ if (exitCode === 0) {
8223
+ console.log(`[doctor] auto-enrich: backfilled ${unbackfilled.length} tickets`);
8224
+ } else {
8225
+ console.error("[doctor] auto-enrich: backfill failed");
8226
+ }
8227
+ }
8057
8228
  }
8058
8229
  }
8059
8230
  }
@@ -8067,26 +8238,26 @@ async function doctorCmd(_argv, opts = {}) {
8067
8238
  }
8068
8239
 
8069
8240
  // src/bin-internal/eval-deterministic.ts
8070
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
8071
- import { join as join5 } from "path";
8241
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
8242
+ import { join as join8 } from "path";
8072
8243
  import { validateGherkin } from "@xera-ai/web";
8073
8244
 
8074
8245
  // src/eval/paths.ts
8075
- import { join as join4 } from "path";
8246
+ import { join as join7 } from "path";
8076
8247
  function resolveEvalPaths(cwd, runId) {
8077
- const root = join4(cwd, ".xera", "eval", runId);
8248
+ const root = join7(cwd, ".xera", "eval", runId);
8078
8249
  return {
8079
8250
  root,
8080
- manifest: join4(root, "manifest.json"),
8081
- lock: join4(root, ".lock"),
8082
- deterministicScores: join4(root, "deterministic-scores.json"),
8083
- judgeScores: join4(root, "judge-scores.json"),
8084
- report: join4(root, "report.md"),
8085
- summary: join4(root, "summary.json"),
8086
- inputsDir: join4(root, "inputs"),
8087
- actualDir: join4(root, "actual"),
8088
- ticketInputsDir: (ticket) => join4(root, "inputs", ticket),
8089
- ticketActualDir: (ticket) => join4(root, "actual", ticket)
8251
+ manifest: join7(root, "manifest.json"),
8252
+ lock: join7(root, ".lock"),
8253
+ deterministicScores: join7(root, "deterministic-scores.json"),
8254
+ judgeScores: join7(root, "judge-scores.json"),
8255
+ report: join7(root, "report.md"),
8256
+ summary: join7(root, "summary.json"),
8257
+ inputsDir: join7(root, "inputs"),
8258
+ actualDir: join7(root, "actual"),
8259
+ ticketInputsDir: (ticket) => join7(root, "inputs", ticket),
8260
+ ticketActualDir: (ticket) => join7(root, "actual", ticket)
8090
8261
  };
8091
8262
  }
8092
8263
 
@@ -8171,11 +8342,11 @@ var SummarySchema = z2.object({
8171
8342
 
8172
8343
  // src/bin-internal/eval-deterministic.ts
8173
8344
  function checkFeatureFromStory(actualFeaturePath) {
8174
- if (!existsSync5(actualFeaturePath)) {
8345
+ if (!existsSync8(actualFeaturePath)) {
8175
8346
  return { passed: false, checks: ["validate-feature"], error: "actual missing: test.feature" };
8176
8347
  }
8177
8348
  try {
8178
- const r = validateGherkin(readFileSync5(actualFeaturePath, "utf8"));
8349
+ const r = validateGherkin(readFileSync7(actualFeaturePath, "utf8"));
8179
8350
  if (r.ok)
8180
8351
  return { passed: true, checks: ["validate-feature"] };
8181
8352
  return {
@@ -8188,31 +8359,31 @@ function checkFeatureFromStory(actualFeaturePath) {
8188
8359
  }
8189
8360
  }
8190
8361
  function checkScriptFromFeature(actualTicketDir) {
8191
- const specPath = join5(actualTicketDir, "spec.ts");
8192
- if (!existsSync5(specPath)) {
8362
+ const specPath = join8(actualTicketDir, "spec.ts");
8363
+ if (!existsSync8(specPath)) {
8193
8364
  return { passed: false, checks: ["file-presence"], error: "actual missing: spec.ts" };
8194
8365
  }
8195
8366
  return { passed: true, checks: ["file-presence"] };
8196
8367
  }
8197
8368
  function checkDiagnoseFailure(inputsTicketDir, actualTicketDir) {
8198
- const inputPath = join5(inputsTicketDir, "classifier-input.json");
8199
- const actualPath = join5(actualTicketDir, "classification.json");
8200
- if (!existsSync5(actualPath)) {
8369
+ const inputPath = join8(inputsTicketDir, "classifier-input.json");
8370
+ const actualPath = join8(actualTicketDir, "classification.json");
8371
+ if (!existsSync8(actualPath)) {
8201
8372
  return {
8202
8373
  passed: false,
8203
8374
  checks: ["bucket-match"],
8204
8375
  error: "actual missing: classification.json"
8205
8376
  };
8206
8377
  }
8207
- if (!existsSync5(inputPath)) {
8378
+ if (!existsSync8(inputPath)) {
8208
8379
  return {
8209
8380
  passed: false,
8210
8381
  checks: ["bucket-match"],
8211
8382
  error: "inputs missing: classifier-input.json"
8212
8383
  };
8213
8384
  }
8214
- const golden = JSON.parse(readFileSync5(inputPath, "utf8"));
8215
- const actual = JSON.parse(readFileSync5(actualPath, "utf8"));
8385
+ const golden = JSON.parse(readFileSync7(inputPath, "utf8"));
8386
+ const actual = JSON.parse(readFileSync7(actualPath, "utf8"));
8216
8387
  const goldScens = golden.scenarios ?? [];
8217
8388
  const actScens = actual.scenarios ?? [];
8218
8389
  const mismatches = [];
@@ -8242,11 +8413,11 @@ async function evalDeterministicCmd(argv, opts = {}) {
8242
8413
  return 1;
8243
8414
  }
8244
8415
  const paths = resolveEvalPaths(cwd, runId);
8245
- if (!existsSync5(paths.manifest)) {
8416
+ if (!existsSync8(paths.manifest)) {
8246
8417
  console.error(`[xera:eval-deterministic] missing manifest.json at ${paths.manifest}`);
8247
8418
  return 1;
8248
8419
  }
8249
- const manifest = ManifestSchema.parse(JSON.parse(readFileSync5(paths.manifest, "utf8")));
8420
+ const manifest = ManifestSchema.parse(JSON.parse(readFileSync7(paths.manifest, "utf8")));
8250
8421
  const entries = [];
8251
8422
  for (const [ticket, ticketStages] of Object.entries(manifest.ticket_stages)) {
8252
8423
  for (const stage of ticketStages) {
@@ -8254,7 +8425,7 @@ async function evalDeterministicCmd(argv, opts = {}) {
8254
8425
  const actualDir = paths.ticketActualDir(ticket);
8255
8426
  let result;
8256
8427
  if (stage === "feature-from-story") {
8257
- result = checkFeatureFromStory(join5(actualDir, "test.feature"));
8428
+ result = checkFeatureFromStory(join8(actualDir, "test.feature"));
8258
8429
  } else if (stage === "script-from-feature") {
8259
8430
  result = checkScriptFromFeature(actualDir);
8260
8431
  } else {
@@ -8281,13 +8452,13 @@ async function evalDeterministicCmd(argv, opts = {}) {
8281
8452
  // src/bin-internal/eval-prepare.ts
8282
8453
  import {
8283
8454
  copyFileSync,
8284
- existsSync as existsSync7,
8455
+ existsSync as existsSync10,
8285
8456
  mkdirSync as mkdirSync4,
8286
- readdirSync as readdirSync3,
8287
- readFileSync as readFileSync7,
8457
+ readdirSync as readdirSync5,
8458
+ readFileSync as readFileSync9,
8288
8459
  writeFileSync as writeFileSync4
8289
8460
  } from "fs";
8290
- import { join as join6 } from "path";
8461
+ import { join as join9 } from "path";
8291
8462
 
8292
8463
  // src/eval/run-id.ts
8293
8464
  import { execSync } from "child_process";
@@ -8312,11 +8483,11 @@ function generateRunId(opts = {}) {
8312
8483
  }
8313
8484
 
8314
8485
  // src/lock/file-lock.ts
8315
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
8486
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
8316
8487
  import { hostname } from "os";
8317
8488
  import { dirname as dirname2 } from "path";
8318
8489
  function acquireLock(path, runId) {
8319
- if (existsSync6(path))
8490
+ if (existsSync9(path))
8320
8491
  return false;
8321
8492
  mkdirSync3(dirname2(path), { recursive: true });
8322
8493
  const data = {
@@ -8333,13 +8504,13 @@ function acquireLock(path, runId) {
8333
8504
  }
8334
8505
  }
8335
8506
  function releaseLock(path) {
8336
- if (existsSync6(path))
8507
+ if (existsSync9(path))
8337
8508
  unlinkSync(path);
8338
8509
  }
8339
8510
  function readLock(path) {
8340
- if (!existsSync6(path))
8511
+ if (!existsSync9(path))
8341
8512
  return null;
8342
- return JSON.parse(readFileSync6(path, "utf8"));
8513
+ return JSON.parse(readFileSync8(path, "utf8"));
8343
8514
  }
8344
8515
  function isLockStale(path) {
8345
8516
  const lock = readLock(path);
@@ -8360,7 +8531,7 @@ function forceUnlock(path) {
8360
8531
  }
8361
8532
 
8362
8533
  // src/bin-internal/eval-prepare.ts
8363
- function parseFlags(argv) {
8534
+ function parseFlags2(argv) {
8364
8535
  const flags = { force: false, only_prompt: null, only_ticket: null };
8365
8536
  for (const arg of argv) {
8366
8537
  if (arg === "--force")
@@ -8380,42 +8551,42 @@ function parseFlags(argv) {
8380
8551
  return flags;
8381
8552
  }
8382
8553
  function readPromptVersion(repoRoot, name) {
8383
- const path = join6(repoRoot, "packages/prompts", `${name}.md`);
8384
- if (!existsSync7(path))
8554
+ const path = join9(repoRoot, "packages/prompts", `${name}.md`);
8555
+ if (!existsSync10(path))
8385
8556
  return "0.0.0";
8386
- const text = readFileSync7(path, "utf8");
8557
+ const text = readFileSync9(path, "utf8");
8387
8558
  const m = /^version:\s*(\S+)\s*$/m.exec(text);
8388
8559
  return m?.[1] ?? "0.0.0";
8389
8560
  }
8390
8561
  function discoverEvalTickets(repoRoot) {
8391
- const root = join6(repoRoot, "fixtures/golden-eval");
8392
- if (!existsSync7(root))
8562
+ const root = join9(repoRoot, "fixtures/golden-eval");
8563
+ if (!existsSync10(root))
8393
8564
  return [];
8394
8565
  const out = [];
8395
- for (const entry of readdirSync3(root, { withFileTypes: true })) {
8566
+ for (const entry of readdirSync5(root, { withFileTypes: true })) {
8396
8567
  if (!entry.isDirectory())
8397
8568
  continue;
8398
8569
  if (entry.name === "README.md" || entry.name.startsWith("."))
8399
8570
  continue;
8400
- const dir = join6(root, entry.name);
8401
- const metaPath = join6(dir, "meta.json");
8402
- if (!existsSync7(metaPath))
8571
+ const dir = join9(root, entry.name);
8572
+ const metaPath = join9(dir, "meta.json");
8573
+ if (!existsSync10(metaPath))
8403
8574
  continue;
8404
- const meta = JSON.parse(readFileSync7(metaPath, "utf8"));
8575
+ const meta = JSON.parse(readFileSync9(metaPath, "utf8"));
8405
8576
  out.push({ id: meta.id, dir, stages: meta.stages });
8406
8577
  }
8407
8578
  return out.sort((a, b) => a.id.localeCompare(b.id));
8408
8579
  }
8409
8580
  function discoverClassifierTickets(repoRoot) {
8410
- const root = join6(repoRoot, "fixtures/golden-tickets");
8411
- if (!existsSync7(root))
8581
+ const root = join9(repoRoot, "fixtures/golden-tickets");
8582
+ if (!existsSync10(root))
8412
8583
  return [];
8413
8584
  const out = [];
8414
- for (const entry of readdirSync3(root, { withFileTypes: true })) {
8585
+ for (const entry of readdirSync5(root, { withFileTypes: true })) {
8415
8586
  if (!entry.isFile() || !entry.name.endsWith(".json"))
8416
8587
  continue;
8417
- const path = join6(root, entry.name);
8418
- const data = JSON.parse(readFileSync7(path, "utf8"));
8588
+ const path = join9(root, entry.name);
8589
+ const data = JSON.parse(readFileSync9(path, "utf8"));
8419
8590
  if (typeof data.ticket === "string")
8420
8591
  out.push({ id: data.ticket, path });
8421
8592
  }
@@ -8423,7 +8594,7 @@ function discoverClassifierTickets(repoRoot) {
8423
8594
  }
8424
8595
  async function evalPrepareCmd(argv, opts = {}) {
8425
8596
  const repoRoot = opts.cwd ?? process.cwd();
8426
- const flags = parseFlags(argv);
8597
+ const flags = parseFlags2(argv);
8427
8598
  if ("error" in flags) {
8428
8599
  console.error(`[xera:eval-prepare] ${flags.error}`);
8429
8600
  return 1;
@@ -8474,7 +8645,7 @@ async function evalPrepareCmd(argv, opts = {}) {
8474
8645
  ...opts.getGitSha ? { getGitSha: opts.getGitSha } : {}
8475
8646
  });
8476
8647
  const paths = resolveEvalPaths(repoRoot, runId);
8477
- if (existsSync7(paths.root) && !flags.force) {
8648
+ if (existsSync10(paths.root) && !flags.force) {
8478
8649
  console.error(`[xera:eval-prepare] run dir already exists: ${paths.root}. Pass --force to re-run.`);
8479
8650
  return 1;
8480
8651
  }
@@ -8486,13 +8657,13 @@ async function evalPrepareCmd(argv, opts = {}) {
8486
8657
  const evalT = evalTickets.find((t) => t.id === ticket);
8487
8658
  const classT = classifierTickets.find((t) => t.id === ticket);
8488
8659
  if (evalT) {
8489
- copyFileSync(join6(evalT.dir, "story.md"), join6(ticketInputs, "story.md"));
8490
- const featurePath = join6(evalT.dir, "golden/test.feature");
8491
- if (existsSync7(featurePath))
8492
- copyFileSync(featurePath, join6(ticketInputs, "test.feature"));
8660
+ copyFileSync(join9(evalT.dir, "story.md"), join9(ticketInputs, "story.md"));
8661
+ const featurePath = join9(evalT.dir, "golden/test.feature");
8662
+ if (existsSync10(featurePath))
8663
+ copyFileSync(featurePath, join9(ticketInputs, "test.feature"));
8493
8664
  }
8494
8665
  if (classT) {
8495
- copyFileSync(classT.path, join6(ticketInputs, "classifier-input.json"));
8666
+ copyFileSync(classT.path, join9(ticketInputs, "classifier-input.json"));
8496
8667
  }
8497
8668
  }
8498
8669
  const now = (opts.now ?? (() => new Date))();
@@ -8528,7 +8699,7 @@ async function evalPrepareCmd(argv, opts = {}) {
8528
8699
  }
8529
8700
 
8530
8701
  // src/bin-internal/eval-report.ts
8531
- import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
8702
+ import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
8532
8703
  function scoreJudgment(j) {
8533
8704
  const nonNa = j.dimensions.filter((d) => d.verdict !== "NA");
8534
8705
  if (nonNa.length === 0)
@@ -8583,22 +8754,22 @@ async function evalReportCmd(argv, opts = {}) {
8583
8754
  return 1;
8584
8755
  }
8585
8756
  const paths = resolveEvalPaths(cwd, runId);
8586
- if (!existsSync8(paths.manifest)) {
8757
+ if (!existsSync11(paths.manifest)) {
8587
8758
  console.error(`[xera:eval-report] missing manifest.json at ${paths.manifest}`);
8588
8759
  return 1;
8589
8760
  }
8590
- const manifest = ManifestSchema.parse(JSON.parse(readFileSync8(paths.manifest, "utf8")));
8761
+ const manifest = ManifestSchema.parse(JSON.parse(readFileSync10(paths.manifest, "utf8")));
8591
8762
  try {
8592
8763
  let det;
8593
8764
  let judge;
8594
8765
  try {
8595
- det = DeterministicScoresSchema.parse(JSON.parse(readFileSync8(paths.deterministicScores, "utf8")));
8766
+ det = DeterministicScoresSchema.parse(JSON.parse(readFileSync10(paths.deterministicScores, "utf8")));
8596
8767
  } catch (err) {
8597
8768
  console.error(`[xera:eval-report] invalid deterministic-scores.json: ${err.message}`);
8598
8769
  return 2;
8599
8770
  }
8600
8771
  try {
8601
- judge = JudgeScoresSchema.parse(JSON.parse(readFileSync8(paths.judgeScores, "utf8")));
8772
+ judge = JudgeScoresSchema.parse(JSON.parse(readFileSync10(paths.judgeScores, "utf8")));
8602
8773
  } catch (err) {
8603
8774
  console.error(`[xera:eval-report] invalid judge-scores.json: ${err.message}`);
8604
8775
  return 2;
@@ -8670,40 +8841,40 @@ async function evalReportCmd(argv, opts = {}) {
8670
8841
  }
8671
8842
 
8672
8843
  // src/bin-internal/exec.ts
8673
- import { existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
8674
- import { join as join10 } from "path";
8844
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7 } from "fs";
8845
+ import { join as join13 } from "path";
8675
8846
  import { chromium } from "@playwright/test";
8676
8847
  import { runAuthSetup, runPlaywright, stagePlaywrightState } from "@xera-ai/web";
8677
8848
 
8678
8849
  // src/artifact/paths.ts
8679
- import { join as join7 } from "path";
8850
+ import { join as join10 } from "path";
8680
8851
  var TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
8681
8852
  function resolveArtifactPaths(repoRoot, ticket) {
8682
8853
  if (!TICKET_RE.test(ticket)) {
8683
8854
  throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
8684
8855
  }
8685
- const ticketDir = join7(repoRoot, ".xera", ticket);
8856
+ const ticketDir = join10(repoRoot, ".xera", ticket);
8686
8857
  return {
8687
8858
  ticketDir,
8688
- storyPath: join7(ticketDir, "story.md"),
8689
- featurePath: join7(ticketDir, "test.feature"),
8690
- specPath: join7(ticketDir, "spec.ts"),
8691
- pageObjectsDir: join7(ticketDir, "page-objects"),
8692
- runsDir: join7(ticketDir, "runs"),
8693
- metaPath: join7(ticketDir, "meta.json"),
8694
- statusPath: join7(ticketDir, "status.json"),
8695
- logPath: join7(ticketDir, "xera.log"),
8696
- lockPath: join7(ticketDir, ".lock"),
8697
- authDir: join7(repoRoot, ".xera", ".auth"),
8859
+ storyPath: join10(ticketDir, "story.md"),
8860
+ featurePath: join10(ticketDir, "test.feature"),
8861
+ specPath: join10(ticketDir, "spec.ts"),
8862
+ pageObjectsDir: join10(ticketDir, "page-objects"),
8863
+ runsDir: join10(ticketDir, "runs"),
8864
+ metaPath: join10(ticketDir, "meta.json"),
8865
+ statusPath: join10(ticketDir, "status.json"),
8866
+ logPath: join10(ticketDir, "xera.log"),
8867
+ lockPath: join10(ticketDir, ".lock"),
8868
+ authDir: join10(repoRoot, ".xera", ".auth"),
8698
8869
  runPath: (runId) => {
8699
- const runDir = join7(ticketDir, "runs", runId);
8870
+ const runDir = join10(ticketDir, "runs", runId);
8700
8871
  return {
8701
8872
  runDir,
8702
- reportJsonPath: join7(runDir, "report.json"),
8703
- tracePath: join7(runDir, "trace.zip"),
8704
- normalizedPath: join7(runDir, "normalized.json"),
8705
- screenshotsDir: join7(runDir, "screenshots"),
8706
- videoDir: join7(runDir, "videos")
8873
+ reportJsonPath: join10(runDir, "report.json"),
8874
+ tracePath: join10(runDir, "trace.zip"),
8875
+ normalizedPath: join10(runDir, "normalized.json"),
8876
+ screenshotsDir: join10(runDir, "screenshots"),
8877
+ videoDir: join10(runDir, "videos")
8707
8878
  };
8708
8879
  }
8709
8880
  };
@@ -8714,7 +8885,7 @@ function generateRunId2(now = new Date) {
8714
8885
 
8715
8886
  // src/auth/refresh.ts
8716
8887
  var RE = /^(\d+)([hms])$/;
8717
- function parseDuration(d) {
8888
+ function parseDuration2(d) {
8718
8889
  const m = RE.exec(d);
8719
8890
  if (!m)
8720
8891
  throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
@@ -8729,8 +8900,8 @@ function parseDuration(d) {
8729
8900
  function needsRefresh(entry, policy, now = new Date) {
8730
8901
  if (!entry)
8731
8902
  return true;
8732
- const ttlMs = parseDuration(policy.ttl);
8733
- const bufMs = parseDuration(policy.refreshBuffer);
8903
+ const ttlMs = parseDuration2(policy.ttl);
8904
+ const bufMs = parseDuration2(policy.refreshBuffer);
8734
8905
  const createdAt = new Date(entry.created_at).getTime();
8735
8906
  if (now.getTime() - createdAt > ttlMs)
8736
8907
  return true;
@@ -8741,8 +8912,8 @@ function needsRefresh(entry, policy, now = new Date) {
8741
8912
  }
8742
8913
 
8743
8914
  // src/auth/state.ts
8744
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
8745
- import { join as join8 } from "path";
8915
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
8916
+ import { join as join11 } from "path";
8746
8917
  import { z as z3 } from "zod";
8747
8918
 
8748
8919
  // src/auth/encrypt.ts
@@ -8808,7 +8979,7 @@ var AuthStateEntrySchema = z3.object({
8808
8979
  payload: z3.record(z3.string(), z3.unknown())
8809
8980
  });
8810
8981
  function pathFor(authDir, role) {
8811
- return join8(authDir, `${role}.json`);
8982
+ return join11(authDir, `${role}.json`);
8812
8983
  }
8813
8984
  function writeAuthState(authDir, entry) {
8814
8985
  mkdirSync5(authDir, { recursive: true });
@@ -8817,16 +8988,16 @@ function writeAuthState(authDir, entry) {
8817
8988
  }
8818
8989
  function readAuthState(authDir, role) {
8819
8990
  const p = pathFor(authDir, role);
8820
- if (!existsSync9(p))
8991
+ if (!existsSync12(p))
8821
8992
  return null;
8822
- const txt = readFileSync9(p, "utf8");
8993
+ const txt = readFileSync11(p, "utf8");
8823
8994
  const plain = decrypt(txt, resolveAuthKey());
8824
8995
  return AuthStateEntrySchema.parse(JSON.parse(plain));
8825
8996
  }
8826
8997
 
8827
8998
  // src/config/load.ts
8828
- import { existsSync as existsSync10 } from "fs";
8829
- import { join as join9 } from "path";
8999
+ import { existsSync as existsSync13 } from "fs";
9000
+ import { join as join12 } from "path";
8830
9001
  import { pathToFileURL } from "url";
8831
9002
 
8832
9003
  // src/config/schema.ts
@@ -8885,7 +9056,7 @@ var ReportingSchema = z4.object({
8885
9056
  var RunSchema = z4.object({
8886
9057
  autoImpact: z4.object({
8887
9058
  enabled: z4.boolean().default(true),
8888
- threshold: z4.number().nonnegative().default(6)
9059
+ threshold: z4.number().nonnegative().default(8)
8889
9060
  }).prefault({})
8890
9061
  }).prefault({});
8891
9062
  var XeraConfigSchema = z4.object({
@@ -8899,8 +9070,8 @@ var XeraConfigSchema = z4.object({
8899
9070
 
8900
9071
  // src/config/load.ts
8901
9072
  async function loadConfig(cwd) {
8902
- const path = join9(cwd, "xera.config.ts");
8903
- if (!existsSync10(path)) {
9073
+ const path = join12(cwd, "xera.config.ts");
9074
+ if (!existsSync13(path)) {
8904
9075
  throw new Error(`xera.config.ts not found in ${cwd}`);
8905
9076
  }
8906
9077
  const mod = await import(pathToFileURL(path).href);
@@ -8909,7 +9080,7 @@ async function loadConfig(cwd) {
8909
9080
  }
8910
9081
 
8911
9082
  // src/logging/ndjson-logger.ts
8912
- import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync10 } from "fs";
9083
+ import { appendFileSync as appendFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync12 } from "fs";
8913
9084
  import { dirname as dirname3 } from "path";
8914
9085
 
8915
9086
  class NdjsonLogger {
@@ -8924,9 +9095,9 @@ class NdjsonLogger {
8924
9095
  `);
8925
9096
  }
8926
9097
  static readAll(path) {
8927
- if (!existsSync11(path))
9098
+ if (!existsSync14(path))
8928
9099
  return [];
8929
- const txt = readFileSync10(path, "utf8").trim();
9100
+ const txt = readFileSync12(path, "utf8").trim();
8930
9101
  if (!txt)
8931
9102
  return [];
8932
9103
  return txt.split(`
@@ -8941,6 +9112,8 @@ async function execCmd(argv) {
8941
9112
  console.error("[xera:exec] usage: exec <TICKET>");
8942
9113
  return 1;
8943
9114
  }
9115
+ const grepIdx = argv.indexOf("--grep");
9116
+ const grep = grepIdx > -1 ? argv[grepIdx + 1] : undefined;
8944
9117
  const cwd = process.cwd();
8945
9118
  const config = await loadConfig(cwd);
8946
9119
  const paths = resolveArtifactPaths(cwd, ticket);
@@ -8977,7 +9150,7 @@ async function execCmd(argv) {
8977
9150
  await runAuthSetup({
8978
9151
  role: roleName,
8979
9152
  creds: { email, password },
8980
- setupScriptPath: join10(cwd, config.web.auth.setupScript),
9153
+ setupScriptPath: join13(cwd, config.web.auth.setupScript),
8981
9154
  authDir: paths.authDir,
8982
9155
  browser
8983
9156
  });
@@ -8995,8 +9168,8 @@ async function execCmd(argv) {
8995
9168
  }
8996
9169
  }
8997
9170
  }
8998
- const cfgPath = join10(cwd, "playwright.config.ts");
8999
- if (!existsSync12(cfgPath)) {
9171
+ const cfgPath = join13(cwd, "playwright.config.ts");
9172
+ if (!existsSync15(cfgPath)) {
9000
9173
  console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
9001
9174
  return 1;
9002
9175
  }
@@ -9004,7 +9177,7 @@ async function execCmd(argv) {
9004
9177
  mkdirSync7(runDir, { recursive: true });
9005
9178
  const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
9006
9179
  const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv];
9007
- const reportJsonPath = join10(runDir, "report.json");
9180
+ const reportJsonPath = join13(runDir, "report.json");
9008
9181
  log.log({ step: "exec.start", runId, env: envName, baseURL });
9009
9182
  const r = await runPlaywright({
9010
9183
  specPath: paths.specPath,
@@ -9014,7 +9187,8 @@ async function execCmd(argv) {
9014
9187
  XERA_BASE_URL: baseURL,
9015
9188
  XERA_ENV: envName,
9016
9189
  PLAYWRIGHT_JSON_OUTPUT_NAME: reportJsonPath
9017
- }
9190
+ },
9191
+ ...grep && { grep }
9018
9192
  });
9019
9193
  log.log({ step: "exec.done", runId, exit: r.exitCode, ms: Date.now() - t0 });
9020
9194
  console.log(`[xera:exec] runId=${runId} outcome=${r.outcome}`);
@@ -9029,22 +9203,22 @@ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
9029
9203
  import { dirname as dirname5 } from "path";
9030
9204
 
9031
9205
  // src/artifact/hash.ts
9032
- import { createHash as createHash2 } from "crypto";
9033
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
9206
+ import { createHash as createHash4 } from "crypto";
9207
+ import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
9034
9208
  function hashString(s) {
9035
- return `sha256:${createHash2("sha256").update(s).digest("hex")}`;
9209
+ return `sha256:${createHash4("sha256").update(s).digest("hex")}`;
9036
9210
  }
9037
9211
  function hashFile(path) {
9038
- return hashString(readFileSync11(path, "utf8"));
9212
+ return hashString(readFileSync13(path, "utf8"));
9039
9213
  }
9040
9214
  function hashFileIfExists(path) {
9041
- if (!existsSync13(path))
9215
+ if (!existsSync16(path))
9042
9216
  return null;
9043
9217
  return hashFile(path);
9044
9218
  }
9045
9219
 
9046
9220
  // src/artifact/meta.ts
9047
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
9221
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
9048
9222
  import { dirname as dirname4 } from "path";
9049
9223
  import { z as z5 } from "zod";
9050
9224
  var MetaJsonSchema = z5.object({
@@ -9062,9 +9236,9 @@ var MetaJsonSchema = z5.object({
9062
9236
  script_warnings: z5.array(z5.string()).optional()
9063
9237
  });
9064
9238
  function readMeta(path) {
9065
- if (!existsSync14(path))
9239
+ if (!existsSync17(path))
9066
9240
  return null;
9067
- return MetaJsonSchema.parse(JSON.parse(readFileSync12(path, "utf8")));
9241
+ return MetaJsonSchema.parse(JSON.parse(readFileSync14(path, "utf8")));
9068
9242
  }
9069
9243
  function writeMeta(path, meta) {
9070
9244
  mkdirSync8(dirname4(path), { recursive: true });
@@ -9081,32 +9255,32 @@ function updateMeta(path, patch) {
9081
9255
  }
9082
9256
 
9083
9257
  // src/jira/mcp-backend.ts
9084
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
9258
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
9085
9259
  import { tmpdir } from "os";
9086
- import { join as join11 } from "path";
9260
+ import { join as join14 } from "path";
9087
9261
  var MCP_ENV = "XERA_MCP_JIRA";
9088
9262
  async function createMcpBackend(_baseUrl) {
9089
9263
  if (process.env[MCP_ENV] !== "1")
9090
9264
  return null;
9091
- const tmpDir = join11(tmpdir(), "xera-mcp");
9265
+ const tmpDir = join14(tmpdir(), "xera-mcp");
9092
9266
  mkdirSync9(tmpDir, { recursive: true });
9093
9267
  return {
9094
9268
  backend: "mcp",
9095
9269
  async fetchTicket(key, _fields) {
9096
- const cachePath = join11(tmpDir, `${key}.json`);
9097
- if (!existsSync15(cachePath)) {
9270
+ const cachePath = join14(tmpDir, `${key}.json`);
9271
+ if (!existsSync18(cachePath)) {
9098
9272
  throw new Error(`MCP-mode fetch requires the skill to first call mcp__atlassian__getJiraIssue and write ${cachePath}. ` + `If you are running this directly, unset ${MCP_ENV} to use REST.`);
9099
9273
  }
9100
- const parsed = JSON.parse(readFileSync13(cachePath, "utf8"));
9274
+ const parsed = JSON.parse(readFileSync15(cachePath, "utf8"));
9101
9275
  return parsed;
9102
9276
  },
9103
9277
  async postComment(key, body) {
9104
- const outPath = join11(tmpDir, `${key}.comment.json`);
9278
+ const outPath = join14(tmpDir, `${key}.comment.json`);
9105
9279
  writeFileSync8(outPath, JSON.stringify({ key, body }));
9106
9280
  return { id: "mcp-pending" };
9107
9281
  },
9108
9282
  async transitionStatus(key, statusName) {
9109
- const outPath = join11(tmpDir, `${key}.transition.json`);
9283
+ const outPath = join14(tmpDir, `${key}.transition.json`);
9110
9284
  writeFileSync8(outPath, JSON.stringify({ key, statusName }));
9111
9285
  },
9112
9286
  async listFields(_sampleKey) {
@@ -9275,49 +9449,8 @@ function renderStory(t) {
9275
9449
  `);
9276
9450
  }
9277
9451
 
9278
- // src/bin-internal/graph-backfill.ts
9279
- init_graph_record_script();
9280
- import { existsSync as existsSync18, readdirSync as readdirSync5 } from "fs";
9281
- import { join as join14 } from "path";
9282
- async function backfillTicket(repoRoot, ticket, dryRun) {
9283
- const storyPath = join14(repoRoot, ".xera", ticket, "story.md");
9284
- if (!existsSync18(storyPath))
9285
- return 0;
9286
- const { recordFetch: recordFetch2 } = await Promise.resolve().then(() => (init_graph_record(), exports_graph_record));
9287
- if (dryRun) {
9288
- console.log(`[backfill dry-run] would backfill ${ticket}`);
9289
- return 0;
9290
- }
9291
- await recordScriptImpl(repoRoot, ticket);
9292
- await recordFetch2(repoRoot, ticket);
9293
- return 0;
9294
- }
9295
- async function graphBackfillCmd(argv) {
9296
- const dryRun = argv.includes("--dry-run");
9297
- const repoRoot = process.cwd();
9298
- const xeraDir = join14(repoRoot, ".xera");
9299
- if (!existsSync18(xeraDir)) {
9300
- console.log("[backfill] no .xera/ directory");
9301
- return 0;
9302
- }
9303
- const tickets = [];
9304
- for (const entry of readdirSync5(xeraDir, { withFileTypes: true })) {
9305
- if (!entry.isDirectory())
9306
- continue;
9307
- if (entry.name === "graph")
9308
- continue;
9309
- if (entry.name.startsWith("."))
9310
- continue;
9311
- if (!/^[A-Z]+-\d+$/.test(entry.name))
9312
- continue;
9313
- tickets.push(entry.name);
9314
- }
9315
- console.log(`[backfill] found ${tickets.length} tickets`);
9316
- for (const t of tickets)
9317
- await backfillTicket(repoRoot, t, dryRun);
9318
- console.log(`[backfill] done`);
9319
- return 0;
9320
- }
9452
+ // src/bin-internal/index.ts
9453
+ init_graph_backfill();
9321
9454
 
9322
9455
  // src/graph/enrich.ts
9323
9456
  init_store();
@@ -9421,7 +9554,7 @@ function filterByTicket(snap, ticket) {
9421
9554
  };
9422
9555
  return out;
9423
9556
  }
9424
- function renderText(snap) {
9557
+ function renderText2(snap) {
9425
9558
  const out = [];
9426
9559
  out.push(`Graph snapshot \u2014 ${snap.event_count} events`);
9427
9560
  out.push(`Tickets: ${Object.keys(snap.tickets).length}`);
@@ -9450,13 +9583,313 @@ async function graphQueryCmd(argv) {
9450
9583
  if (format === "json")
9451
9584
  process.stdout.write(JSON.stringify(snap, null, 2));
9452
9585
  else
9453
- process.stdout.write(renderText(snap));
9586
+ process.stdout.write(renderText2(snap));
9454
9587
  return 0;
9455
9588
  }
9456
9589
 
9457
9590
  // src/bin-internal/index.ts
9458
9591
  init_graph_record();
9459
9592
 
9593
+ // src/bin-internal/graph-render.ts
9594
+ import { mkdirSync as mkdirSync11, renameSync as renameSync2, writeFileSync as writeFileSync10 } from "fs";
9595
+ import { dirname as dirname7, join as join17 } from "path";
9596
+
9597
+ // src/graph/render.ts
9598
+ import { readFileSync as readFileSync17 } from "fs";
9599
+ import { dirname as dirname6, join as join16 } from "path";
9600
+ import { fileURLToPath } from "url";
9601
+ var COLORS = {
9602
+ ticket: "#3B82F6",
9603
+ scenarioPass: "#10B981",
9604
+ scenarioFail: "#EF4444",
9605
+ pom: "#F59E0B",
9606
+ area: "#6B7280",
9607
+ failure: "#EF4444",
9608
+ edgeModifies: "#EF4444",
9609
+ edgeDefault: "#9CA3AF",
9610
+ edgeJira: "#3B82F6",
9611
+ edgeSimilar: "#A855F7"
9612
+ };
9613
+ function ticketsAfter(since, fetchedAt) {
9614
+ if (!since)
9615
+ return true;
9616
+ return Date.parse(fetchedAt) >= Date.parse(since);
9617
+ }
9618
+ function scenariosAfter(since, generatedAt) {
9619
+ if (!since)
9620
+ return true;
9621
+ return Date.parse(generatedAt) >= Date.parse(since);
9622
+ }
9623
+ function buildTicketNode(snap, ticketId) {
9624
+ const t = snap.tickets[ticketId];
9625
+ const usageCount = snap.edges.filter((e) => e.kind === "tests" && e.from === ticketId).length;
9626
+ const node = {
9627
+ id: t.id,
9628
+ label: t.id,
9629
+ group: "Ticket",
9630
+ color: COLORS.ticket,
9631
+ shape: "dot",
9632
+ size: 10 + Math.min(usageCount * 2, 20),
9633
+ title: `${t.id} \u2014 ${t.summary}`
9634
+ };
9635
+ return node;
9636
+ }
9637
+ function buildScenarioNode(snap, scenarioId2) {
9638
+ const s = snap.scenarios[scenarioId2];
9639
+ const failed = snap.latest_failures[scenarioId2];
9640
+ const sizeBase = s.priority === "p0" ? 14 : s.priority === "p1" ? 11 : 9;
9641
+ const node = {
9642
+ id: s.id,
9643
+ label: s.name,
9644
+ group: "Scenario",
9645
+ color: failed ? COLORS.scenarioFail : COLORS.scenarioPass,
9646
+ shape: "square",
9647
+ size: sizeBase,
9648
+ title: `${s.ticketId} / ${s.name} [${s.priority.toUpperCase()}]`
9649
+ };
9650
+ return node;
9651
+ }
9652
+ function buildPomNode(snap, pomId2) {
9653
+ const p = snap.poms[pomId2];
9654
+ const usageCount = snap.edges.filter((e) => e.kind === "uses" && e.to === pomId2).length;
9655
+ const node = {
9656
+ id: p.id,
9657
+ label: p.filePath.split("/").pop() ?? p.id,
9658
+ group: "POM",
9659
+ color: COLORS.pom,
9660
+ shape: "diamond",
9661
+ size: 8 + Math.min(usageCount * 2, 16),
9662
+ title: `${p.filePath} (${p.route || "no route"})`
9663
+ };
9664
+ return node;
9665
+ }
9666
+ function buildAreaNode(snap, areaId) {
9667
+ const a = snap.areas[areaId];
9668
+ const node = {
9669
+ id: a.id,
9670
+ label: a.id,
9671
+ group: "SUTArea",
9672
+ color: COLORS.area,
9673
+ shape: "hexagon",
9674
+ size: 12,
9675
+ title: `area: ${a.id}`
9676
+ };
9677
+ return node;
9678
+ }
9679
+ function buildFailureNode(_snap, failure) {
9680
+ const node = {
9681
+ id: failure.id,
9682
+ label: "fail",
9683
+ group: "Failure",
9684
+ color: COLORS.failure,
9685
+ shape: "triangle",
9686
+ size: 10,
9687
+ title: `failure on ${failure.scenarioId} @ ${failure.ts}`
9688
+ };
9689
+ return node;
9690
+ }
9691
+ function buildEdge(edge, idx) {
9692
+ const v = {
9693
+ id: `e-${idx}`,
9694
+ from: edge.from,
9695
+ to: edge.to,
9696
+ label: edge.kind,
9697
+ arrows: "to",
9698
+ width: 1
9699
+ };
9700
+ switch (edge.kind) {
9701
+ case "modifies":
9702
+ v.color = COLORS.edgeModifies;
9703
+ v.dashes = true;
9704
+ v.width = 2;
9705
+ break;
9706
+ case "jira-linked":
9707
+ v.color = COLORS.edgeJira;
9708
+ v.dashes = true;
9709
+ break;
9710
+ case "similar":
9711
+ v.color = COLORS.edgeSimilar;
9712
+ v.dashes = false;
9713
+ v.width = 1 + Math.round((edge.confidence ?? 0) * 3);
9714
+ break;
9715
+ default:
9716
+ v.color = COLORS.edgeDefault;
9717
+ break;
9718
+ }
9719
+ return v;
9720
+ }
9721
+ function bfsFromTicket(snap, ticketId, depth) {
9722
+ const nodeIds = new Set([ticketId]);
9723
+ const edgeIdxs = new Set;
9724
+ let frontier = new Set([ticketId]);
9725
+ for (let d = 0;d < depth; d++) {
9726
+ const next = new Set;
9727
+ snap.edges.forEach((e, i) => {
9728
+ if (frontier.has(e.from) && !nodeIds.has(e.to)) {
9729
+ nodeIds.add(e.to);
9730
+ next.add(e.to);
9731
+ edgeIdxs.add(i);
9732
+ } else if (frontier.has(e.to) && !nodeIds.has(e.from)) {
9733
+ nodeIds.add(e.from);
9734
+ next.add(e.from);
9735
+ edgeIdxs.add(i);
9736
+ } else if (frontier.has(e.from) && nodeIds.has(e.to)) {
9737
+ edgeIdxs.add(i);
9738
+ } else if (frontier.has(e.to) && nodeIds.has(e.from)) {
9739
+ edgeIdxs.add(i);
9740
+ }
9741
+ });
9742
+ frontier = next;
9743
+ if (frontier.size === 0)
9744
+ break;
9745
+ }
9746
+ return { nodeIds, edgeIdxs };
9747
+ }
9748
+ function transformForVisNetwork(snap, opts) {
9749
+ const mode = opts.performanceMode ?? "full";
9750
+ const nodes = [];
9751
+ const edges = [];
9752
+ let includeTickets = new Set;
9753
+ let includeScenarios = new Set;
9754
+ let includePoms = new Set;
9755
+ let includeAreas = new Set;
9756
+ let includeEdgeIdxs = new Set;
9757
+ if (opts.ticketId) {
9758
+ const result = bfsFromTicket(snap, opts.ticketId, opts.depth ?? 2);
9759
+ for (const id of result.nodeIds) {
9760
+ if (snap.tickets[id])
9761
+ includeTickets.add(id);
9762
+ else if (snap.scenarios[id])
9763
+ includeScenarios.add(id);
9764
+ else if (snap.poms[id])
9765
+ includePoms.add(id);
9766
+ else if (snap.areas[id])
9767
+ includeAreas.add(id);
9768
+ }
9769
+ includeEdgeIdxs = result.edgeIdxs;
9770
+ } else {
9771
+ includeTickets = new Set(Object.keys(snap.tickets).filter((id) => ticketsAfter(opts.since, snap.tickets[id].fetchedAt)));
9772
+ includeScenarios = new Set(Object.keys(snap.scenarios).filter((id) => scenariosAfter(opts.since, snap.scenarios[id].generatedAt)));
9773
+ includePoms = new Set(Object.keys(snap.poms));
9774
+ includeAreas = new Set(Object.keys(snap.areas));
9775
+ snap.edges.forEach((_, i) => {
9776
+ includeEdgeIdxs.add(i);
9777
+ });
9778
+ }
9779
+ if (mode === "ticket-only") {
9780
+ includeScenarios.clear();
9781
+ includePoms.clear();
9782
+ includeAreas.clear();
9783
+ }
9784
+ for (const id of includeTickets)
9785
+ nodes.push(buildTicketNode(snap, id));
9786
+ for (const id of includeScenarios)
9787
+ nodes.push(buildScenarioNode(snap, id));
9788
+ for (const id of includePoms)
9789
+ nodes.push(buildPomNode(snap, id));
9790
+ for (const id of includeAreas)
9791
+ nodes.push(buildAreaNode(snap, id));
9792
+ for (const failure of Object.values(snap.latest_failures)) {
9793
+ if (includeScenarios.has(failure.scenarioId)) {
9794
+ nodes.push(buildFailureNode(snap, failure));
9795
+ }
9796
+ }
9797
+ const visibleNodeIds = new Set(nodes.map((n) => n.id));
9798
+ for (const i of includeEdgeIdxs) {
9799
+ const e = snap.edges[i];
9800
+ if (!e)
9801
+ continue;
9802
+ if (!visibleNodeIds.has(e.from) || !visibleNodeIds.has(e.to))
9803
+ continue;
9804
+ edges.push(buildEdge(e, i));
9805
+ }
9806
+ const stats = {
9807
+ tickets: includeTickets.size,
9808
+ scenarios: includeScenarios.size,
9809
+ poms: includePoms.size,
9810
+ areas: includeAreas.size,
9811
+ failures: nodes.filter((n) => n.group === "Failure").length,
9812
+ edges: edges.length
9813
+ };
9814
+ return { nodes, edges, stats };
9815
+ }
9816
+ var __filename2 = fileURLToPath(import.meta.url);
9817
+ var __dirname2 = dirname6(__filename2);
9818
+ var TEMPLATES_DIR = join16(__dirname2, "templates");
9819
+ function loadTemplate(name) {
9820
+ return readFileSync17(join16(TEMPLATES_DIR, name), "utf8");
9821
+ }
9822
+ function statsToHuman(s) {
9823
+ return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
9824
+ }
9825
+ function renderHtml(input) {
9826
+ const template = loadTemplate("graph.html.template");
9827
+ const css = loadTemplate("graph.css");
9828
+ const js = loadTemplate("graph.js");
9829
+ const visNetwork = loadTemplate("vis-network.min.js");
9830
+ const graphJson = JSON.stringify(input.data);
9831
+ const statsHuman = statsToHuman(input.stats);
9832
+ 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);
9833
+ }
9834
+
9835
+ // src/bin-internal/graph-render.ts
9836
+ init_store();
9837
+ function parseDepth(s) {
9838
+ const n = s ? Number.parseInt(s, 10) : 2;
9839
+ if (n === 1 || n === 3)
9840
+ return n;
9841
+ return 2;
9842
+ }
9843
+ function decidePerformanceMode(nodeCount) {
9844
+ if (nodeCount > 2000)
9845
+ return "text-fallback";
9846
+ if (nodeCount > 500)
9847
+ return "ticket-only";
9848
+ return "full";
9849
+ }
9850
+ async function graphRenderCmd(argv) {
9851
+ let outPath;
9852
+ let ticketId;
9853
+ let since;
9854
+ let depth = 2;
9855
+ for (let i = 0;i < argv.length; i++) {
9856
+ if (argv[i] === "--out")
9857
+ outPath = argv[++i];
9858
+ else if (argv[i] === "--ticket")
9859
+ ticketId = argv[++i];
9860
+ else if (argv[i] === "--since")
9861
+ since = argv[++i];
9862
+ else if (argv[i] === "--depth")
9863
+ depth = parseDepth(argv[++i]);
9864
+ }
9865
+ const repoRoot = process.cwd();
9866
+ const finalPath = outPath ?? join17(repoRoot, ".xera/graph.html");
9867
+ const snap = deriveSnapshot(loadAllEvents(repoRoot));
9868
+ const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
9869
+ const performanceMode = decidePerformanceMode(totalNodeCount);
9870
+ if (performanceMode === "text-fallback") {
9871
+ const txtPath = finalPath.replace(/\.html$/, ".txt");
9872
+ mkdirSync11(dirname7(txtPath), { recursive: true });
9873
+ writeFileSync10(txtPath, `Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.
9874
+ `);
9875
+ console.log(`[graph-render] graph too large (${totalNodeCount} nodes); wrote ${txtPath}`);
9876
+ return 0;
9877
+ }
9878
+ const opts = { depth, performanceMode };
9879
+ if (ticketId)
9880
+ opts.ticketId = ticketId;
9881
+ if (since)
9882
+ opts.since = since;
9883
+ const data = transformForVisNetwork(snap, opts);
9884
+ const html = renderHtml({ data, stats: data.stats, generatedAt: new Date().toISOString() });
9885
+ mkdirSync11(dirname7(finalPath), { recursive: true });
9886
+ const tmpPath = `${finalPath}.tmp`;
9887
+ writeFileSync10(tmpPath, html);
9888
+ renameSync2(tmpPath, finalPath);
9889
+ console.log(`[graph-render] wrote ${finalPath} (${data.stats.tickets} tickets \xB7 ${data.stats.scenarios} scenarios \xB7 ${html.length} bytes)`);
9890
+ return 0;
9891
+ }
9892
+
9460
9893
  // src/bin-internal/graph-snapshot.ts
9461
9894
  init_store();
9462
9895
  async function graphSnapshotCmd(argv) {
@@ -9482,8 +9915,8 @@ async function graphSnapshotCmd(argv) {
9482
9915
  }
9483
9916
 
9484
9917
  // 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";
9918
+ import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
9919
+ import { join as join18 } from "path";
9487
9920
  import { scrubFreeText } from "@xera-ai/web";
9488
9921
 
9489
9922
  // ../../node_modules/.bun/fflate@0.8.3/node_modules/fflate/esm/index.mjs
@@ -9913,7 +10346,7 @@ function classifyKind(raw) {
9913
10346
  function extractDomSnapshot(tracePath) {
9914
10347
  if (!existsSync20(tracePath))
9915
10348
  return "";
9916
- const buf = readFileSync17(tracePath);
10349
+ const buf = readFileSync18(tracePath);
9917
10350
  const entries = unzipSync(buf);
9918
10351
  const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
9919
10352
  let chosenKey = null;
@@ -9961,16 +10394,16 @@ function extractDomSnapshot(tracePath) {
9961
10394
  return scrubFreeText(html);
9962
10395
  }
9963
10396
  function findPomLine(ticketDir, rawLocator) {
9964
- const pomDir = join16(ticketDir, "page-objects");
10397
+ const pomDir = join18(ticketDir, "page-objects");
9965
10398
  const candidates = [];
9966
10399
  if (existsSync20(pomDir)) {
9967
10400
  for (const name of readdirSync6(pomDir)) {
9968
10401
  if (name.endsWith(".ts"))
9969
- candidates.push(join16(pomDir, name));
10402
+ candidates.push(join18(pomDir, name));
9970
10403
  }
9971
10404
  }
9972
10405
  for (const file of candidates) {
9973
- const text = readFileSync17(file, "utf8");
10406
+ const text = readFileSync18(file, "utf8");
9974
10407
  const lines = text.split(`
9975
10408
  `);
9976
10409
  for (let i2 = 0;i2 < lines.length; i2++) {
@@ -10008,13 +10441,13 @@ function findGherkinStep(featureText, rawLocator) {
10008
10441
  }
10009
10442
  function healPrepare(repoRoot, ticket, runId, scenarioName) {
10010
10443
  const paths = resolveArtifactPaths(repoRoot, ticket);
10011
- const classifierPath = join16(paths.ticketDir, "classifier-input.json");
10012
- const classifier = JSON.parse(readFileSync17(classifierPath, "utf8"));
10444
+ const classifierPath = join18(paths.ticketDir, "classifier-input.json");
10445
+ const classifier = JSON.parse(readFileSync18(classifierPath, "utf8"));
10013
10446
  const cls = classifier.scenarios.find((s) => s.name === scenarioName);
10014
10447
  if (!cls)
10015
10448
  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"));
10449
+ const runDir = join18(paths.runsDir, runId);
10450
+ const normalized = JSON.parse(readFileSync18(join18(runDir, "normalized.json"), "utf8"));
10018
10451
  const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
10019
10452
  if (!normSc?.failure)
10020
10453
  throw new Error(`no failure recorded for scenario "${scenarioName}"`);
@@ -10025,9 +10458,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
10025
10458
  const raw = m[1].trim();
10026
10459
  const kind = classifyKind(raw);
10027
10460
  const pomLoc = findPomLine(paths.ticketDir, raw);
10028
- const featureText = readFileSync17(paths.featurePath, "utf8");
10461
+ const featureText = readFileSync18(paths.featurePath, "utf8");
10029
10462
  const gherkinStep = findGherkinStep(featureText, raw);
10030
- const domSnapshotAtFailure = extractDomSnapshot(join16(runDir, "trace.zip"));
10463
+ const domSnapshotAtFailure = extractDomSnapshot(join18(runDir, "trace.zip"));
10031
10464
  return {
10032
10465
  ticket,
10033
10466
  runId,
@@ -10047,8 +10480,8 @@ async function healPrepareCmd(argv) {
10047
10480
  try {
10048
10481
  const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
10049
10482
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10050
- const outPath = join16(paths.runsDir, runId, "heal-input.json");
10051
- writeFileSync10(outPath, JSON.stringify(result, null, 2));
10483
+ const outPath = join18(paths.runsDir, runId, "heal-input.json");
10484
+ writeFileSync11(outPath, JSON.stringify(result, null, 2));
10052
10485
  console.log(`[xera:heal-prepare] wrote ${outPath}`);
10053
10486
  return 0;
10054
10487
  } catch (err2) {
@@ -10058,8 +10491,8 @@ async function healPrepareCmd(argv) {
10058
10491
  }
10059
10492
 
10060
10493
  // src/bin-internal/impact-prepare.ts
10061
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
10062
- import { join as join17 } from "path";
10494
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
10495
+ import { join as join19 } from "path";
10063
10496
 
10064
10497
  // src/graph/impact.ts
10065
10498
  var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
@@ -10264,7 +10697,7 @@ function renderImpactMarkdown(report) {
10264
10697
 
10265
10698
  // src/bin-internal/impact-prepare.ts
10266
10699
  init_store();
10267
- function parseDepth(s) {
10700
+ function parseDepth2(s) {
10268
10701
  const n = s ? Number.parseInt(s, 10) : 2;
10269
10702
  if (n === 1 || n === 3)
10270
10703
  return n;
@@ -10286,7 +10719,7 @@ async function impactPrepareCmd(argv) {
10286
10719
  let quiet = false;
10287
10720
  for (let i2 = 1;i2 < argv.length; i2++) {
10288
10721
  if (argv[i2] === "--depth")
10289
- depth = parseDepth(argv[++i2]);
10722
+ depth = parseDepth2(argv[++i2]);
10290
10723
  else if (argv[i2] === "--min-priority")
10291
10724
  minPriority = parseMinPriority(argv[++i2]);
10292
10725
  else if (argv[i2] === "--quiet")
@@ -10309,11 +10742,11 @@ async function impactPrepareCmd(argv) {
10309
10742
  scenarios,
10310
10743
  generatedAt: new Date().toISOString()
10311
10744
  };
10312
- const impactDir = join17(repoRoot, ".xera/impact");
10313
- mkdirSync11(impactDir, { recursive: true });
10314
- writeFileSync11(join17(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10745
+ const impactDir = join19(repoRoot, ".xera/impact");
10746
+ mkdirSync12(impactDir, { recursive: true });
10747
+ writeFileSync12(join19(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10315
10748
  if (!quiet) {
10316
- writeFileSync11(join17(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
10749
+ writeFileSync12(join19(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
10317
10750
  }
10318
10751
  return 0;
10319
10752
  }
@@ -10339,7 +10772,7 @@ async function lintCmd(argv) {
10339
10772
 
10340
10773
  // src/bin-internal/normalize.ts
10341
10774
  import { existsSync as existsSync21, readdirSync as readdirSync7 } from "fs";
10342
- import { join as join18 } from "path";
10775
+ import { join as join20 } from "path";
10343
10776
  import { normalizeRun } from "@xera-ai/web";
10344
10777
  async function normalizeCmd(argv) {
10345
10778
  const ticket = argv[0];
@@ -10354,7 +10787,7 @@ async function normalizeCmd(argv) {
10354
10787
  console.error("[xera:normalize] no run found");
10355
10788
  return 1;
10356
10789
  }
10357
- const runDir = join18(paths.runsDir, runId);
10790
+ const runDir = join20(paths.runsDir, runId);
10358
10791
  if (!existsSync21(runDir)) {
10359
10792
  console.error(`[xera:normalize] runs/${runId} missing`);
10360
10793
  return 1;
@@ -10365,12 +10798,12 @@ async function normalizeCmd(argv) {
10365
10798
  }
10366
10799
 
10367
10800
  // src/bin-internal/post.ts
10368
- import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
10369
- import { join as join19 } from "path";
10801
+ import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
10802
+ import { join as join21 } from "path";
10370
10803
 
10371
10804
  // 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";
10805
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
10806
+ import { dirname as dirname8 } from "path";
10374
10807
  import { z as z7 } from "zod";
10375
10808
  var ClassificationEnum = z7.enum([
10376
10809
  "PASS",
@@ -10406,11 +10839,11 @@ var HISTORY_CAP = 20;
10406
10839
  function readStatus(path) {
10407
10840
  if (!existsSync22(path))
10408
10841
  return null;
10409
- return StatusJsonSchema.parse(JSON.parse(readFileSync18(path, "utf8")));
10842
+ return StatusJsonSchema.parse(JSON.parse(readFileSync19(path, "utf8")));
10410
10843
  }
10411
10844
  function writeStatus(path, status) {
10412
- mkdirSync12(dirname6(path), { recursive: true });
10413
- writeFileSync12(path, JSON.stringify(status, null, 2));
10845
+ mkdirSync13(dirname8(path), { recursive: true });
10846
+ writeFileSync13(path, JSON.stringify(status, null, 2));
10414
10847
  }
10415
10848
  function appendHistory(path, entry) {
10416
10849
  const s = readStatus(path);
@@ -10436,12 +10869,12 @@ async function postCmd(argv) {
10436
10869
  return 0;
10437
10870
  }
10438
10871
  const paths = resolveArtifactPaths(cwd, ticket);
10439
- const draftPath = join19(paths.ticketDir, "jira-comment.draft.md");
10872
+ const draftPath = join21(paths.ticketDir, "jira-comment.draft.md");
10440
10873
  if (!existsSync23(draftPath)) {
10441
10874
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
10442
10875
  return 1;
10443
10876
  }
10444
- const body = readFileSync19(draftPath, "utf8");
10877
+ const body = readFileSync20(draftPath, "utf8");
10445
10878
  const client = await createJiraClient({
10446
10879
  baseUrl: config.jira.baseUrl,
10447
10880
  preferMcp: true,
@@ -10469,8 +10902,8 @@ async function promoteCmd(argv) {
10469
10902
  }
10470
10903
 
10471
10904
  // 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";
10905
+ import { existsSync as existsSync25, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
10906
+ import { join as join22 } from "path";
10474
10907
 
10475
10908
  // src/classifier/aggregate.ts
10476
10909
  var CLASS_PRIORITY = [
@@ -10642,10 +11075,10 @@ async function reportCmd(argv) {
10642
11075
  return 1;
10643
11076
  }
10644
11077
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10645
- const input = JSON.parse(readFileSync20(inputArg.slice("--input=".length), "utf8"));
11078
+ const input = JSON.parse(readFileSync21(inputArg.slice("--input=".length), "utf8"));
10646
11079
  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")) : {};
11080
+ const decisionsPath = join22(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
11081
+ const decisions = existsSync25(decisionsPath) ? JSON.parse(readFileSync21(decisionsPath, "utf8")) : {};
10649
11082
  const graph = deriveSnapshot(loadAllEvents(process.cwd()));
10650
11083
  const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
10651
11084
  const scenarioIdByName = {};
@@ -10693,8 +11126,8 @@ async function reportCmd(argv) {
10693
11126
  xeraVersion: "0.1.0",
10694
11127
  promptsVersion: "1.0.0"
10695
11128
  });
10696
- const draftPath = join20(paths.ticketDir, "jira-comment.draft.md");
10697
- writeFileSync13(draftPath, md);
11129
+ const draftPath = join22(paths.ticketDir, "jira-comment.draft.md");
11130
+ writeFileSync14(draftPath, md);
10698
11131
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
10699
11132
  return 0;
10700
11133
  }
@@ -10761,7 +11194,7 @@ async function unlockCmd(argv) {
10761
11194
  }
10762
11195
 
10763
11196
  // src/bin-internal/validate-feature.ts
10764
- import { existsSync as existsSync26, readFileSync as readFileSync21 } from "fs";
11197
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
10765
11198
  import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
10766
11199
  async function validateFeatureCmd(argv) {
10767
11200
  const ticket = argv[0];
@@ -10774,7 +11207,7 @@ async function validateFeatureCmd(argv) {
10774
11207
  console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
10775
11208
  return 1;
10776
11209
  }
10777
- const r = validateGherkin2(readFileSync21(paths.featurePath, "utf8"));
11210
+ const r = validateGherkin2(readFileSync22(paths.featurePath, "utf8"));
10778
11211
  if (r.ok) {
10779
11212
  console.log("[xera:validate-feature] ok");
10780
11213
  return 0;
@@ -10786,6 +11219,7 @@ async function validateFeatureCmd(argv) {
10786
11219
 
10787
11220
  // src/bin-internal/index.ts
10788
11221
  var COMMANDS = {
11222
+ disputes: disputesCmd,
10789
11223
  doctor: doctorCmd,
10790
11224
  "eval-deterministic": evalDeterministicCmd,
10791
11225
  "eval-prepare": evalPrepareCmd,
@@ -10794,6 +11228,7 @@ var COMMANDS = {
10794
11228
  fetch: fetchCmd,
10795
11229
  "graph-backfill": graphBackfillCmd,
10796
11230
  "graph-enrich": graphEnrichCmd,
11231
+ "graph-render": graphRenderCmd,
10797
11232
  "graph-query": graphQueryCmd,
10798
11233
  "graph-record": graphRecordCmd,
10799
11234
  "graph-snapshot": graphSnapshotCmd,