@xera-ai/core 0.9.7 → 0.10.0

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 (34) hide show
  1. package/dist/bin/internal.js +1415 -534
  2. package/dist/bin/templates/LICENSE-vis-network.txt +2 -0
  3. package/dist/bin/templates/coverage-panel.html.fragment +20 -0
  4. package/dist/bin/templates/graph.css +379 -43
  5. package/dist/bin/templates/graph.html.template +35 -15
  6. package/dist/bin/templates/graph.js +458 -56
  7. package/dist/bin/templates/vis-network.min.js +3 -24976
  8. package/dist/src/index.js +6 -0
  9. package/package.json +3 -3
  10. package/src/bin-internal/ac-coverage-backfill-finalize.ts +90 -0
  11. package/src/bin-internal/ac-coverage-backfill-prepare.ts +72 -0
  12. package/src/bin-internal/coverage-prepare.ts +123 -0
  13. package/src/bin-internal/fill-gap-finalize.ts +115 -0
  14. package/src/bin-internal/fill-gap-prepare.ts +150 -0
  15. package/src/bin-internal/graph-render.ts +32 -4
  16. package/src/bin-internal/index.ts +10 -0
  17. package/src/bin-internal/verify-prompts.ts +2 -0
  18. package/src/config/schema.ts +9 -0
  19. package/src/coverage/index.ts +29 -0
  20. package/src/coverage/report.ts +206 -0
  21. package/src/coverage/risk.ts +69 -0
  22. package/src/coverage/status.ts +76 -0
  23. package/src/coverage/types.ts +11 -0
  24. package/src/coverage/why.ts +122 -0
  25. package/src/graph/render.ts +16 -2
  26. package/src/graph/schema.ts +54 -1
  27. package/src/graph/store.ts +96 -6
  28. package/src/graph/templates/LICENSE-vis-network.txt +2 -0
  29. package/src/graph/templates/coverage-panel.html.fragment +20 -0
  30. package/src/graph/templates/graph.css +379 -43
  31. package/src/graph/templates/graph.html.template +35 -15
  32. package/src/graph/templates/graph.js +458 -56
  33. package/src/graph/templates/vis-network.min.js +3 -24976
  34. package/src/graph/types.ts +56 -1
@@ -404,15 +404,15 @@ var require_main = __commonJS((exports, module) => {
404
404
  });
405
405
 
406
406
  // src/graph/paths.ts
407
- import { join as join3 } from "path";
407
+ import { join } from "path";
408
408
  function graphPaths(repoRoot) {
409
- const eventsDir = join3(repoRoot, ".xera/graph/events");
409
+ const eventsDir = join(repoRoot, ".xera/graph/events");
410
410
  return {
411
411
  eventsDir,
412
- snapshotFile: join3(repoRoot, ".xera/graph/snapshot.json"),
413
- costLog: join3(repoRoot, ".xera/cost-log.jsonl"),
414
- eventsMonthDir: (yyyyMm) => join3(eventsDir, yyyyMm),
415
- eventFile: (ulid, skill, ticketId, yyyyMm) => join3(eventsDir, yyyyMm, `${ulid}-${skill}-${ticketId}.jsonl`)
412
+ snapshotFile: join(repoRoot, ".xera/graph/snapshot.json"),
413
+ costLog: join(repoRoot, ".xera/cost-log.jsonl"),
414
+ eventsMonthDir: (yyyyMm) => join(eventsDir, yyyyMm),
415
+ eventFile: (ulid, skill, ticketId, yyyyMm) => join(eventsDir, yyyyMm, `${ulid}-${skill}-${ticketId}.jsonl`)
416
416
  };
417
417
  }
418
418
  function currentYyyyMm(now = new Date) {
@@ -426,64 +426,65 @@ var init_paths = () => {};
426
426
  var SCHEMA_VERSION = 1;
427
427
 
428
428
  // src/graph/schema.ts
429
- import { z as z2 } from "zod";
429
+ import { z } from "zod";
430
430
  function safeParseEvent(value) {
431
431
  const r = EventSchema.safeParse(value);
432
432
  if (r.success)
433
433
  return { success: true, data: r.data };
434
434
  return { success: false, error: r.error };
435
435
  }
436
- var schemaV, iso, ticketFetched, ticketEnriched, scenarioGenerated, pomGenerated, pomPromoted, runCompleted, classification, runClassified, classificationDisputed, edgeDiscovered, base, EventSchema;
436
+ var schemaV, iso, ticketFetched, ticketEnriched, scenarioGenerated, pomGenerated, pomPromoted, runCompleted, classification, runClassified, classificationDisputed, edgeDiscovered, coverageSnapshot, acCoverageBackfilled, base, EventSchema;
437
437
  var init_schema = __esm(() => {
438
- schemaV = z2.literal(SCHEMA_VERSION);
439
- iso = z2.string().datetime({ offset: false });
440
- ticketFetched = z2.object({
441
- ticketId: z2.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
442
- summary: z2.string(),
443
- ac: z2.array(z2.string()),
444
- jiraLinks: z2.array(z2.object({
445
- ticketId: z2.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
446
- relation: z2.enum(["blocks", "duplicates", "relates", "supersedes"])
438
+ schemaV = z.literal(SCHEMA_VERSION);
439
+ iso = z.string().datetime({ offset: false });
440
+ ticketFetched = z.object({
441
+ ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
442
+ summary: z.string(),
443
+ ac: z.array(z.string()),
444
+ jiraLinks: z.array(z.object({
445
+ ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
446
+ relation: z.enum(["blocks", "duplicates", "relates", "supersedes"])
447
447
  })),
448
- storyHash: z2.string(),
449
- modifiesAreas: z2.array(z2.string().regex(/^[a-z0-9-]+$/))
448
+ storyHash: z.string(),
449
+ modifiesAreas: z.array(z.string().regex(/^[a-z0-9-]+$/))
450
450
  }).passthrough();
451
- ticketEnriched = z2.object({
452
- ticketId: z2.string(),
451
+ ticketEnriched = z.object({
452
+ ticketId: z.string(),
453
453
  enrichedAt: iso,
454
- similarCount: z2.number().int().nonnegative()
454
+ similarCount: z.number().int().nonnegative()
455
455
  }).passthrough();
456
- scenarioGenerated = z2.object({
457
- scenarioId: z2.string(),
458
- ticketId: z2.string(),
459
- name: z2.string(),
460
- gherkin: z2.string(),
461
- priority: z2.enum(["p0", "p1", "p2"]),
462
- featureHash: z2.string(),
463
- generatedAt: iso
456
+ scenarioGenerated = z.object({
457
+ scenarioId: z.string(),
458
+ ticketId: z.string(),
459
+ name: z.string(),
460
+ gherkin: z.string(),
461
+ priority: z.enum(["p0", "p1", "p2"]),
462
+ featureHash: z.string(),
463
+ generatedAt: iso,
464
+ satisfiesAcs: z.array(z.number().int().nonnegative()).optional()
464
465
  }).passthrough();
465
- pomGenerated = z2.object({
466
- pomId: z2.string(),
467
- ticketId: z2.string(),
468
- filePath: z2.string(),
469
- route: z2.string(),
470
- locators: z2.array(z2.string()),
471
- scope: z2.enum(["local", "shared"])
466
+ pomGenerated = z.object({
467
+ pomId: z.string(),
468
+ ticketId: z.string(),
469
+ filePath: z.string(),
470
+ route: z.string(),
471
+ locators: z.array(z.string()),
472
+ scope: z.enum(["local", "shared"])
472
473
  }).passthrough();
473
- pomPromoted = z2.object({
474
- pomId: z2.string(),
475
- fromPath: z2.string(),
476
- toPath: z2.string()
474
+ pomPromoted = z.object({
475
+ pomId: z.string(),
476
+ fromPath: z.string(),
477
+ toPath: z.string()
477
478
  }).passthrough();
478
- runCompleted = z2.object({
479
- scenarioId: z2.string(),
480
- ticketId: z2.string(),
481
- runId: z2.string(),
482
- status: z2.enum(["pass", "fail"]),
483
- traceId: z2.string().optional(),
484
- runtime: z2.number().nonnegative()
479
+ runCompleted = z.object({
480
+ scenarioId: z.string(),
481
+ ticketId: z.string(),
482
+ runId: z.string(),
483
+ status: z.enum(["pass", "fail"]),
484
+ traceId: z.string().optional(),
485
+ runtime: z.number().nonnegative()
485
486
  }).passthrough();
486
- classification = z2.enum([
487
+ classification = z.enum([
487
488
  "REAL_BUG",
488
489
  "TEST_BUG",
489
490
  "SELECTOR_DRIFT",
@@ -494,54 +495,94 @@ var init_schema = __esm(() => {
494
495
  "RATE_LIMITED",
495
496
  "AUTH_EXPIRED"
496
497
  ]);
497
- runClassified = z2.object({
498
- scenarioId: z2.string(),
499
- runId: z2.string(),
498
+ runClassified = z.object({
499
+ scenarioId: z.string(),
500
+ runId: z.string(),
500
501
  classification,
501
- confidence: z2.enum(["low", "medium", "high"])
502
+ confidence: z.enum(["low", "medium", "high"])
502
503
  }).passthrough();
503
- classificationDisputed = z2.object({
504
- runId: z2.string(),
505
- scenarioId: z2.string(),
504
+ classificationDisputed = z.object({
505
+ runId: z.string(),
506
+ scenarioId: z.string(),
506
507
  originalClassification: classification,
507
508
  disputedTo: classification,
508
- qaActor: z2.string(),
509
- qaReason: z2.string().optional()
509
+ qaActor: z.string(),
510
+ qaReason: z.string().optional()
511
+ }).passthrough();
512
+ edgeDiscovered = z.object({
513
+ kind: z.enum([
514
+ "tests",
515
+ "uses",
516
+ "covers",
517
+ "modifies",
518
+ "jira-linked",
519
+ "similar",
520
+ "ran",
521
+ "satisfies"
522
+ ]),
523
+ from: z.string(),
524
+ to: z.string(),
525
+ confidence: z.number().min(0).max(1).optional(),
526
+ source: z.string()
510
527
  }).passthrough();
511
- edgeDiscovered = z2.object({
512
- kind: z2.enum(["tests", "uses", "covers", "modifies", "jira-linked", "similar", "ran"]),
513
- from: z2.string(),
514
- to: z2.string(),
515
- confidence: z2.number().min(0).max(1).optional(),
516
- source: z2.string()
528
+ coverageSnapshot = z.object({
529
+ ts: iso,
530
+ windowDays: z.number().int().positive(),
531
+ areas: z.array(z.object({
532
+ id: z.string().regex(/^[a-z0-9-]+$/),
533
+ status: z.enum(["UNCOVERED", "STALE", "COVERED"]),
534
+ risk: z.number().nonnegative(),
535
+ breakdown: z.object({
536
+ recentTickets: z.number().int().nonnegative(),
537
+ recentBugs: z.number().int().nonnegative(),
538
+ criticalBoost: z.union([z.literal(1), z.literal(2)])
539
+ })
540
+ })),
541
+ tickets: z.array(z.object({
542
+ id: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
543
+ acCount: z.number().int().nonnegative(),
544
+ satisfiedCount: z.number().int().nonnegative(),
545
+ gapScore: z.number().nonnegative()
546
+ }))
547
+ }).passthrough();
548
+ acCoverageBackfilled = z.object({
549
+ ts: iso,
550
+ ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
551
+ mappings: z.array(z.object({
552
+ scenarioId: z.string().min(1),
553
+ satisfiesAcs: z.array(z.number().int().nonnegative()),
554
+ confidence: z.number().min(0).max(1)
555
+ }))
517
556
  }).passthrough();
518
557
  base = {
519
- event_id: z2.string().min(20),
558
+ event_id: z.string().min(20),
520
559
  schema_version: schemaV,
521
560
  ts: iso,
522
- actor: z2.string()
561
+ actor: z.string()
523
562
  };
524
- EventSchema = z2.discriminatedUnion("type", [
525
- z2.object({ ...base, type: z2.literal("ticket.fetched"), payload: ticketFetched }),
526
- z2.object({ ...base, type: z2.literal("ticket.enriched"), payload: ticketEnriched }),
527
- z2.object({ ...base, type: z2.literal("scenario.generated"), payload: scenarioGenerated }),
528
- z2.object({ ...base, type: z2.literal("pom.generated"), payload: pomGenerated }),
529
- z2.object({ ...base, type: z2.literal("pom.promoted"), payload: pomPromoted }),
530
- z2.object({ ...base, type: z2.literal("run.completed"), payload: runCompleted }),
531
- z2.object({ ...base, type: z2.literal("run.classified"), payload: runClassified }),
532
- z2.object({
563
+ EventSchema = z.discriminatedUnion("type", [
564
+ z.object({ ...base, type: z.literal("ticket.fetched"), payload: ticketFetched }),
565
+ z.object({ ...base, type: z.literal("ticket.enriched"), payload: ticketEnriched }),
566
+ z.object({ ...base, type: z.literal("scenario.generated"), payload: scenarioGenerated }),
567
+ z.object({ ...base, type: z.literal("pom.generated"), payload: pomGenerated }),
568
+ z.object({ ...base, type: z.literal("pom.promoted"), payload: pomPromoted }),
569
+ z.object({ ...base, type: z.literal("run.completed"), payload: runCompleted }),
570
+ z.object({ ...base, type: z.literal("run.classified"), payload: runClassified }),
571
+ z.object({
533
572
  ...base,
534
- type: z2.literal("classification.disputed"),
573
+ type: z.literal("classification.disputed"),
535
574
  payload: classificationDisputed
536
575
  }),
537
- z2.object({ ...base, type: z2.literal("edge.discovered"), payload: edgeDiscovered })
576
+ z.object({ ...base, type: z.literal("edge.discovered"), payload: edgeDiscovered }),
577
+ z.object({ ...base, type: z.literal("coverage.snapshot"), payload: coverageSnapshot }),
578
+ z.object({ ...base, type: z.literal("ac-coverage.backfilled"), payload: acCoverageBackfilled })
538
579
  ]);
539
580
  });
540
581
 
541
582
  // src/graph/store.ts
542
583
  import { createHash } from "crypto";
543
584
  import {
544
- existsSync as existsSync3,
585
+ existsSync,
545
586
  mkdirSync,
546
587
  readdirSync,
547
588
  readFileSync,
@@ -568,7 +609,7 @@ function appendEvents(repoRoot, events, opts) {
568
609
  }
569
610
  function loadAllEvents(repoRoot) {
570
611
  const paths = graphPaths(repoRoot);
571
- if (!existsSync3(paths.eventsDir))
612
+ if (!existsSync(paths.eventsDir))
572
613
  return [];
573
614
  const files = [];
574
615
  for (const monthDir of readdirSync(paths.eventsDir, { withFileTypes: true })) {
@@ -625,11 +666,14 @@ function deriveSnapshot(events) {
625
666
  const areas = {};
626
667
  const edges = [];
627
668
  const latestFailures = {};
669
+ const acNodes = {};
670
+ const classifications = [];
628
671
  for (const e of events) {
629
672
  switch (e.type) {
630
- case "ticket.fetched":
631
- tickets[e.payload.ticketId] = {
632
- id: e.payload.ticketId,
673
+ case "ticket.fetched": {
674
+ const tid = e.payload.ticketId;
675
+ tickets[tid] = {
676
+ id: tid,
633
677
  summary: e.payload.summary,
634
678
  ac: e.payload.ac,
635
679
  storyHash: e.payload.storyHash,
@@ -641,18 +685,34 @@ function deriveSnapshot(events) {
641
685
  for (const link of e.payload.jiraLinks) {
642
686
  edges.push({
643
687
  kind: "jira-linked",
644
- from: e.payload.ticketId,
688
+ from: tid,
645
689
  to: link.ticketId,
646
690
  source: `jira:${link.relation}`,
647
691
  discoveredAt: e.ts
648
692
  });
649
693
  }
694
+ for (const acId of Object.keys(acNodes)) {
695
+ if (acNodes[acId]?.ticketId === tid)
696
+ delete acNodes[acId];
697
+ }
698
+ e.payload.ac.forEach((text, index) => {
699
+ const acId = `${tid}#ac-${index}`;
700
+ acNodes[acId] = { id: acId, ticketId: tid, index, text };
701
+ });
702
+ for (let i = edges.length - 1;i >= 0; i--) {
703
+ const ed = edges[i];
704
+ if (ed.kind !== "satisfies")
705
+ continue;
706
+ if (acNodes[ed.to] === undefined)
707
+ edges.splice(i, 1);
708
+ }
650
709
  break;
710
+ }
651
711
  case "ticket.enriched":
652
712
  if (tickets[e.payload.ticketId])
653
713
  tickets[e.payload.ticketId].enrichedAt = e.payload.enrichedAt;
654
714
  break;
655
- case "scenario.generated":
715
+ case "scenario.generated": {
656
716
  scenarios[e.payload.scenarioId] = {
657
717
  id: e.payload.scenarioId,
658
718
  ticketId: e.payload.ticketId,
@@ -669,7 +729,29 @@ function deriveSnapshot(events) {
669
729
  source: "xera-script",
670
730
  discoveredAt: e.ts
671
731
  });
732
+ if (e.payload.satisfiesAcs && e.payload.satisfiesAcs.length > 0) {
733
+ for (let i = edges.length - 1;i >= 0; i--) {
734
+ const ed = edges[i];
735
+ if (ed.kind === "satisfies" && ed.from === e.payload.scenarioId && ed.source === "xera-script") {
736
+ edges.splice(i, 1);
737
+ }
738
+ }
739
+ for (const acIdx of e.payload.satisfiesAcs) {
740
+ const acId = `${e.payload.ticketId}#ac-${acIdx}`;
741
+ if (acNodes[acId] === undefined)
742
+ continue;
743
+ edges.push({
744
+ kind: "satisfies",
745
+ from: e.payload.scenarioId,
746
+ to: acId,
747
+ confidence: 1,
748
+ source: "xera-script",
749
+ discoveredAt: e.ts
750
+ });
751
+ }
752
+ }
672
753
  break;
754
+ }
673
755
  case "pom.generated":
674
756
  poms[e.payload.pomId] = {
675
757
  id: e.payload.pomId,
@@ -721,6 +803,40 @@ function deriveSnapshot(events) {
721
803
  }
722
804
  break;
723
805
  }
806
+ case "run.classified":
807
+ classifications.push({
808
+ scenarioId: e.payload.scenarioId,
809
+ classification: e.payload.classification,
810
+ ts: e.ts
811
+ });
812
+ break;
813
+ case "ac-coverage.backfilled": {
814
+ const { ts, ticketId, mappings } = e.payload;
815
+ for (let i = edges.length - 1;i >= 0; i--) {
816
+ const ed = edges[i];
817
+ if (ed.kind === "satisfies" && ed.source === "ac-coverage" && ed.to.startsWith(`${ticketId}#ac-`)) {
818
+ edges.splice(i, 1);
819
+ }
820
+ }
821
+ for (const m of mappings) {
822
+ for (const acIdx of m.satisfiesAcs) {
823
+ const acId = `${ticketId}#ac-${acIdx}`;
824
+ if (acNodes[acId] === undefined)
825
+ continue;
826
+ edges.push({
827
+ kind: "satisfies",
828
+ from: m.scenarioId,
829
+ to: acId,
830
+ confidence: m.confidence,
831
+ source: "ac-coverage",
832
+ discoveredAt: ts
833
+ });
834
+ }
835
+ }
836
+ break;
837
+ }
838
+ case "coverage.snapshot":
839
+ break;
724
840
  default:
725
841
  break;
726
842
  }
@@ -735,7 +851,9 @@ function deriveSnapshot(events) {
735
851
  poms,
736
852
  areas,
737
853
  edges,
738
- latest_failures: latestFailures
854
+ latest_failures: latestFailures,
855
+ acNodes,
856
+ classifications
739
857
  };
740
858
  }
741
859
  function writeSnapshot(repoRoot, snap) {
@@ -747,7 +865,7 @@ function writeSnapshot(repoRoot, snap) {
747
865
  }
748
866
  function loadSnapshot(repoRoot) {
749
867
  const paths = graphPaths(repoRoot);
750
- if (!existsSync3(paths.snapshotFile))
868
+ if (!existsSync(paths.snapshotFile))
751
869
  return null;
752
870
  try {
753
871
  return JSON.parse(readFileSync(paths.snapshotFile, "utf8"));
@@ -825,8 +943,8 @@ __export(exports_graph_record_script, {
825
943
  recordScriptImpl: () => recordScriptImpl
826
944
  });
827
945
  import { createHash as createHash2 } from "crypto";
828
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
829
- import { basename, join as join5 } from "path";
946
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
947
+ import { basename, join as join8 } from "path";
830
948
  function inferPriority(name, gherkin) {
831
949
  const haystack = `${name} ${gherkin}`.toLowerCase();
832
950
  for (const kw of P0_KEYWORDS) {
@@ -868,9 +986,9 @@ function parseFeature(text) {
868
986
  return scenarios;
869
987
  }
870
988
  function listPomFiles(dir) {
871
- if (!existsSync6(dir))
989
+ if (!existsSync7(dir))
872
990
  return [];
873
- return readdirSync2(dir).filter((f) => f.endsWith(".ts")).map((f) => join5(dir, f));
991
+ return readdirSync2(dir).filter((f) => f.endsWith(".ts")).map((f) => join8(dir, f));
874
992
  }
875
993
  function extractRoute(pomContent) {
876
994
  const m = pomContent.match(/goto\s*\(\s*['"]([^'"]+)['"]/);
@@ -897,15 +1015,15 @@ function extractPomUsage(specContent) {
897
1015
  return [...names];
898
1016
  }
899
1017
  async function recordScriptImpl(repoRoot, ticket) {
900
- const ticketDir = join5(repoRoot, ".xera", ticket);
901
- const featurePath = existsSync6(join5(ticketDir, "test.feature")) ? join5(ticketDir, "test.feature") : join5(ticketDir, "feature", `${ticket}.feature`);
902
- const specPath = existsSync6(join5(ticketDir, "spec.ts")) ? join5(ticketDir, "spec.ts") : join5(ticketDir, "tests", `${ticket}.spec.ts`);
903
- const pomDir = existsSync6(join5(ticketDir, "page-objects")) ? join5(ticketDir, "page-objects") : join5(ticketDir, "poms");
904
- if (!existsSync6(featurePath)) {
1018
+ const ticketDir = join8(repoRoot, ".xera", ticket);
1019
+ const featurePath = existsSync7(join8(ticketDir, "test.feature")) ? join8(ticketDir, "test.feature") : join8(ticketDir, "feature", `${ticket}.feature`);
1020
+ const specPath = existsSync7(join8(ticketDir, "spec.ts")) ? join8(ticketDir, "spec.ts") : join8(ticketDir, "tests", `${ticket}.spec.ts`);
1021
+ const pomDir = existsSync7(join8(ticketDir, "page-objects")) ? join8(ticketDir, "page-objects") : join8(ticketDir, "poms");
1022
+ if (!existsSync7(featurePath)) {
905
1023
  console.error(`[graph-record script] feature missing`);
906
1024
  return 1;
907
1025
  }
908
- const featureText = readFileSync4(featurePath, "utf8");
1026
+ const featureText = readFileSync6(featurePath, "utf8");
909
1027
  const featureHash = sha1(featureText);
910
1028
  const scenarios = parseFeature(featureText);
911
1029
  const events = [];
@@ -925,7 +1043,7 @@ async function recordScriptImpl(repoRoot, ticket) {
925
1043
  const pomFiles = listPomFiles(pomDir);
926
1044
  const pomNameToId = new Map;
927
1045
  for (const pomFile of pomFiles) {
928
- const content = readFileSync4(pomFile, "utf8");
1046
+ const content = readFileSync6(pomFile, "utf8");
929
1047
  const id = pId(pomFile);
930
1048
  const className = content.match(/export\s+class\s+([A-Z][A-Za-z0-9]*Page)/)?.[1] ?? "";
931
1049
  pomNameToId.set(className, id);
@@ -939,8 +1057,8 @@ async function recordScriptImpl(repoRoot, ticket) {
939
1057
  };
940
1058
  events.push(mk("xera-script", "pom.generated", pg));
941
1059
  }
942
- if (existsSync6(specPath)) {
943
- const specContent = readFileSync4(specPath, "utf8");
1060
+ if (existsSync7(specPath)) {
1061
+ const specContent = readFileSync6(specPath, "utf8");
944
1062
  const usedPoms = extractPomUsage(specContent);
945
1063
  for (const scenario of scenarios) {
946
1064
  const scId = sId(ticket, scenario.name);
@@ -7998,33 +8116,33 @@ var init_dist = __esm(() => {
7998
8116
  });
7999
8117
 
8000
8118
  // src/artifact/paths.ts
8001
- import { join as join6 } from "path";
8119
+ import { join as join9 } from "path";
8002
8120
  function resolveArtifactPaths(repoRoot, ticket) {
8003
- if (!TICKET_RE.test(ticket)) {
8121
+ if (!TICKET_RE2.test(ticket)) {
8004
8122
  throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
8005
8123
  }
8006
- const ticketDir = join6(repoRoot, ".xera", ticket);
8124
+ const ticketDir = join9(repoRoot, ".xera", ticket);
8007
8125
  return {
8008
8126
  ticketDir,
8009
- storyPath: join6(ticketDir, "story.md"),
8010
- featurePath: join6(ticketDir, "test.feature"),
8011
- specPath: join6(ticketDir, "spec.ts"),
8012
- pageObjectsDir: join6(ticketDir, "page-objects"),
8013
- runsDir: join6(ticketDir, "runs"),
8014
- metaPath: join6(ticketDir, "meta.json"),
8015
- statusPath: join6(ticketDir, "status.json"),
8016
- logPath: join6(ticketDir, "xera.log"),
8017
- lockPath: join6(ticketDir, ".lock"),
8018
- authDir: join6(repoRoot, ".xera", ".auth"),
8127
+ storyPath: join9(ticketDir, "story.md"),
8128
+ featurePath: join9(ticketDir, "test.feature"),
8129
+ specPath: join9(ticketDir, "spec.ts"),
8130
+ pageObjectsDir: join9(ticketDir, "page-objects"),
8131
+ runsDir: join9(ticketDir, "runs"),
8132
+ metaPath: join9(ticketDir, "meta.json"),
8133
+ statusPath: join9(ticketDir, "status.json"),
8134
+ logPath: join9(ticketDir, "xera.log"),
8135
+ lockPath: join9(ticketDir, ".lock"),
8136
+ authDir: join9(repoRoot, ".xera", ".auth"),
8019
8137
  runPath: (runId) => {
8020
- const runDir = join6(ticketDir, "runs", runId);
8138
+ const runDir = join9(ticketDir, "runs", runId);
8021
8139
  return {
8022
8140
  runDir,
8023
- reportJsonPath: join6(runDir, "report.json"),
8024
- tracePath: join6(runDir, "trace.zip"),
8025
- normalizedPath: join6(runDir, "normalized.json"),
8026
- screenshotsDir: join6(runDir, "screenshots"),
8027
- videoDir: join6(runDir, "videos")
8141
+ reportJsonPath: join9(runDir, "report.json"),
8142
+ tracePath: join9(runDir, "trace.zip"),
8143
+ normalizedPath: join9(runDir, "normalized.json"),
8144
+ screenshotsDir: join9(runDir, "screenshots"),
8145
+ videoDir: join9(runDir, "videos")
8028
8146
  };
8029
8147
  }
8030
8148
  };
@@ -8032,9 +8150,9 @@ function resolveArtifactPaths(repoRoot, ticket) {
8032
8150
  function generateRunId(now = new Date) {
8033
8151
  return now.toISOString().replace(/[:.]/g, "-").replace("Z", "");
8034
8152
  }
8035
- var TICKET_RE;
8153
+ var TICKET_RE2;
8036
8154
  var init_paths2 = __esm(() => {
8037
- TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
8155
+ TICKET_RE2 = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
8038
8156
  });
8039
8157
 
8040
8158
  // src/bin-internal/graph-record.ts
@@ -8044,8 +8162,8 @@ __export(exports_graph_record, {
8044
8162
  graphRecordCmd: () => graphRecordCmd
8045
8163
  });
8046
8164
  import { createHash as createHash3 } from "crypto";
8047
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
8048
- import { basename as basename2, join as join7 } from "path";
8165
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
8166
+ import { basename as basename2, join as join10 } from "path";
8049
8167
  function nowIso2() {
8050
8168
  return new Date().toISOString();
8051
8169
  }
@@ -8069,21 +8187,21 @@ function makeEvent(actor, type, payload) {
8069
8187
  };
8070
8188
  }
8071
8189
  function readStoryFrontmatter(repoRoot, ticket) {
8072
- const path = join7(repoRoot, ".xera", ticket, "story.md");
8073
- if (!existsSync7(path))
8190
+ const path = join10(repoRoot, ".xera", ticket, "story.md");
8191
+ if (!existsSync8(path))
8074
8192
  return null;
8075
- const raw = readFileSync5(path, "utf8");
8193
+ const raw = readFileSync7(path, "utf8");
8076
8194
  const m = raw.match(/^---\n([\s\S]*?)\n---/);
8077
8195
  if (!m)
8078
8196
  return null;
8079
8197
  return $parse(m[1]);
8080
8198
  }
8081
8199
  function readGraphInput(repoRoot, ticket) {
8082
- const path = join7(repoRoot, ".xera", ticket, "graph-input.json");
8083
- if (!existsSync7(path))
8200
+ const path = join10(repoRoot, ".xera", ticket, "graph-input.json");
8201
+ if (!existsSync8(path))
8084
8202
  return { modifiesAreas: [] };
8085
8203
  try {
8086
- return JSON.parse(readFileSync5(path, "utf8"));
8204
+ return JSON.parse(readFileSync7(path, "utf8"));
8087
8205
  } catch {
8088
8206
  return { modifiesAreas: [] };
8089
8207
  }
@@ -8132,11 +8250,11 @@ async function recordScript(repoRoot, ticket) {
8132
8250
  }
8133
8251
  async function recordExec(repoRoot, ticket, runId) {
8134
8252
  const { normalizedPath } = resolveArtifactPaths(repoRoot, ticket).runPath(runId);
8135
- if (!existsSync7(normalizedPath)) {
8253
+ if (!existsSync8(normalizedPath)) {
8136
8254
  console.error(`[graph-record exec] normalized.json missing`);
8137
8255
  return 1;
8138
8256
  }
8139
- const data = JSON.parse(readFileSync5(normalizedPath, "utf8"));
8257
+ const data = JSON.parse(readFileSync7(normalizedPath, "utf8"));
8140
8258
  const events = [];
8141
8259
  for (const s of data.scenarios) {
8142
8260
  if (s.outcome === "SKIPPED")
@@ -8155,12 +8273,12 @@ async function recordExec(repoRoot, ticket, runId) {
8155
8273
  }
8156
8274
  async function recordClassify(repoRoot, ticket, runId) {
8157
8275
  const { ticketDir } = resolveArtifactPaths(repoRoot, ticket);
8158
- const classifyPath = join7(ticketDir, "classifier-input.json");
8159
- if (!existsSync7(classifyPath)) {
8276
+ const classifyPath = join10(ticketDir, "classifier-input.json");
8277
+ if (!existsSync8(classifyPath)) {
8160
8278
  console.error(`[graph-record classify] classifier-input.json missing`);
8161
8279
  return 1;
8162
8280
  }
8163
- const data = JSON.parse(readFileSync5(classifyPath, "utf8"));
8281
+ const data = JSON.parse(readFileSync7(classifyPath, "utf8"));
8164
8282
  const events = [];
8165
8283
  for (const s of data.scenarios) {
8166
8284
  const p = {
@@ -8302,11 +8420,11 @@ var exports_graph_backfill = {};
8302
8420
  __export(exports_graph_backfill, {
8303
8421
  graphBackfillCmd: () => graphBackfillCmd
8304
8422
  });
8305
- import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
8306
- import { join as join8 } from "path";
8423
+ import { existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
8424
+ import { join as join11 } from "path";
8307
8425
  async function backfillTicket(repoRoot, ticket, dryRun) {
8308
- const storyPath = join8(repoRoot, ".xera", ticket, "story.md");
8309
- if (!existsSync8(storyPath))
8426
+ const storyPath = join11(repoRoot, ".xera", ticket, "story.md");
8427
+ if (!existsSync9(storyPath))
8310
8428
  return 0;
8311
8429
  const { recordFetch: recordFetch2 } = await Promise.resolve().then(() => (init_graph_record(), exports_graph_record));
8312
8430
  if (dryRun) {
@@ -8320,8 +8438,8 @@ async function backfillTicket(repoRoot, ticket, dryRun) {
8320
8438
  async function graphBackfillCmd(argv) {
8321
8439
  const dryRun = argv.includes("--dry-run");
8322
8440
  const repoRoot = process.cwd();
8323
- const xeraDir = join8(repoRoot, ".xera");
8324
- if (!existsSync8(xeraDir)) {
8441
+ const xeraDir = join11(repoRoot, ".xera");
8442
+ if (!existsSync9(xeraDir)) {
8325
8443
  console.log("[backfill] no .xera/ directory");
8326
8444
  return 0;
8327
8445
  }
@@ -8350,109 +8468,246 @@ var init_graph_backfill = __esm(() => {
8350
8468
  // bin/internal.ts
8351
8469
  var import_dotenv = __toESM(require_main(), 1);
8352
8470
 
8353
- // src/bin-internal/auth-setup.ts
8354
- import { existsSync as existsSync2 } from "fs";
8471
+ // src/bin-internal/ac-coverage-backfill-finalize.ts
8472
+ init_store();
8473
+ init_ulid();
8474
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
8355
8475
  import { join as join2 } from "path";
8476
+ import { z as z2 } from "zod";
8477
+ var DecisionsSchema = z2.object({
8478
+ mappings: z2.array(z2.object({
8479
+ scenarioId: z2.string().min(1),
8480
+ satisfiesAcs: z2.array(z2.number().int().nonnegative()),
8481
+ confidence: z2.number().min(0).max(1)
8482
+ }))
8483
+ });
8484
+ function parseArgs(argv) {
8485
+ const args = {};
8486
+ for (let i = 0;i < argv.length; i++) {
8487
+ const a = argv[i];
8488
+ if (a === "--input") {
8489
+ const v = argv[++i];
8490
+ if (v !== undefined)
8491
+ args.inputFile = v;
8492
+ } else if (a === "--snapshot-ts") {
8493
+ const v = argv[++i];
8494
+ if (v !== undefined)
8495
+ args.snapshotTs = v;
8496
+ } else if (a === "--help-stub") {} else {
8497
+ console.error(`[ac-coverage-backfill-finalize] unknown flag: ${a}`);
8498
+ return args;
8499
+ }
8500
+ }
8501
+ return args;
8502
+ }
8503
+ async function acCoverageBackfillFinalizeCmd(argv) {
8504
+ const args = parseArgs(argv);
8505
+ const cwd = process.cwd();
8506
+ const inputPath = args.inputFile ?? join2(cwd, ".xera/coverage/ac-backfill-decisions.json");
8507
+ if (!existsSync2(inputPath)) {
8508
+ console.error(`[ac-coverage-backfill-finalize] decisions file not found: ${inputPath}`);
8509
+ return 2;
8510
+ }
8511
+ let parsed;
8512
+ try {
8513
+ const raw = JSON.parse(readFileSync2(inputPath, "utf8"));
8514
+ parsed = DecisionsSchema.parse(raw);
8515
+ } catch (e) {
8516
+ console.error(`[ac-coverage-backfill-finalize] invalid decisions: ${e.message}`);
8517
+ return 2;
8518
+ }
8519
+ if (parsed.mappings.length === 0)
8520
+ return 0;
8521
+ const byTicket = {};
8522
+ for (const m of parsed.mappings) {
8523
+ const ticketId = m.scenarioId.split("#")[0];
8524
+ if (!ticketId)
8525
+ continue;
8526
+ if (!byTicket[ticketId])
8527
+ byTicket[ticketId] = [];
8528
+ byTicket[ticketId].push(m);
8529
+ }
8530
+ const ts = args.snapshotTs ?? new Date().toISOString();
8531
+ const now = new Date(ts);
8532
+ for (const [ticketId, mappings] of Object.entries(byTicket)) {
8533
+ const event = {
8534
+ event_id: ulid(),
8535
+ schema_version: SCHEMA_VERSION,
8536
+ ts,
8537
+ actor: "xera-coverage",
8538
+ type: "ac-coverage.backfilled",
8539
+ payload: { ts, ticketId, mappings }
8540
+ };
8541
+ appendEvents(cwd, [event], { skill: "ac-coverage", ticketId, now });
8542
+ }
8543
+ return 0;
8544
+ }
8545
+
8546
+ // src/bin-internal/ac-coverage-backfill-prepare.ts
8547
+ init_store();
8548
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
8549
+ import { join as join3 } from "path";
8550
+ function findUnmapped(snap) {
8551
+ const out = [];
8552
+ for (const ticket of Object.values(snap.tickets)) {
8553
+ if (ticket.ac.length === 0)
8554
+ continue;
8555
+ const ticketScenarios = Object.values(snap.scenarios).filter((s) => s.ticketId === ticket.id);
8556
+ if (ticketScenarios.length === 0)
8557
+ continue;
8558
+ const acsForTicket = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticket.id);
8559
+ const hasAnyEdge = snap.edges.some((e) => e.kind === "satisfies" && acsForTicket.some((ac) => ac.id === e.to));
8560
+ if (hasAnyEdge)
8561
+ continue;
8562
+ out.push({
8563
+ id: ticket.id,
8564
+ summary: ticket.summary,
8565
+ acs: ticket.ac,
8566
+ scenarios: ticketScenarios.map((s) => ({
8567
+ id: s.id,
8568
+ name: s.name,
8569
+ gherkin: s.gherkin
8570
+ }))
8571
+ });
8572
+ }
8573
+ return { tickets: out };
8574
+ }
8575
+ function parseArgs2(argv) {
8576
+ const args = {};
8577
+ for (let i = 0;i < argv.length; i++) {
8578
+ const a = argv[i];
8579
+ if (a === "--output") {
8580
+ const v = argv[++i];
8581
+ if (v !== undefined)
8582
+ args.outputFile = v;
8583
+ } else if (a === "--help-stub") {} else {
8584
+ console.error(`[ac-coverage-backfill-prepare] unknown flag: ${a}`);
8585
+ return args;
8586
+ }
8587
+ }
8588
+ return args;
8589
+ }
8590
+ async function acCoverageBackfillPrepareCmd(argv) {
8591
+ const args = parseArgs2(argv);
8592
+ const cwd = process.cwd();
8593
+ const snap = deriveSnapshot(loadAllEvents(cwd));
8594
+ const input = findUnmapped(snap);
8595
+ const outDir = join3(cwd, ".xera/coverage");
8596
+ mkdirSync2(outDir, { recursive: true });
8597
+ const outPath = args.outputFile ?? join3(outDir, "ac-backfill-input.json");
8598
+ writeFileSync2(outPath, JSON.stringify(input, null, 2));
8599
+ return 0;
8600
+ }
8601
+
8602
+ // src/bin-internal/auth-setup.ts
8603
+ import { existsSync as existsSync4 } from "fs";
8604
+ import { join as join5 } from "path";
8356
8605
  import { pathToFileURL as pathToFileURL2 } from "url";
8357
8606
 
8358
8607
  // src/config/load.ts
8359
- import { existsSync } from "fs";
8360
- import { join } from "path";
8608
+ import { existsSync as existsSync3 } from "fs";
8609
+ import { join as join4 } from "path";
8361
8610
  import { pathToFileURL } from "url";
8362
8611
 
8363
8612
  // src/config/schema.ts
8364
- import { z } from "zod";
8365
- var AuthRoleSchema = z.object({
8366
- envEmail: z.string().min(1),
8367
- envPassword: z.string().min(1)
8613
+ import { z as z3 } from "zod";
8614
+ var AuthRoleSchema = z3.object({
8615
+ envEmail: z3.string().min(1),
8616
+ envPassword: z3.string().min(1)
8368
8617
  });
8369
- var AuthSchema = z.object({
8370
- strategy: z.enum(["storageState", "apiToken", "none"]).default("none"),
8371
- ttl: z.string().default("8h"),
8372
- refreshBuffer: z.string().default("30m"),
8373
- setupScript: z.string().optional(),
8374
- roles: z.record(z.string(), AuthRoleSchema).default({})
8618
+ var AuthSchema = z3.object({
8619
+ strategy: z3.enum(["storageState", "apiToken", "none"]).default("none"),
8620
+ ttl: z3.string().default("8h"),
8621
+ refreshBuffer: z3.string().default("30m"),
8622
+ setupScript: z3.string().optional(),
8623
+ roles: z3.record(z3.string(), AuthRoleSchema).default({})
8375
8624
  });
8376
- var WebSchema = z.object({
8377
- baseUrl: z.record(z.string(), z.string().url()).refine((m) => Object.keys(m).length > 0, {
8625
+ var WebSchema = z3.object({
8626
+ baseUrl: z3.record(z3.string(), z3.string().url()).refine((m) => Object.keys(m).length > 0, {
8378
8627
  message: "baseUrl must have at least one environment"
8379
8628
  }),
8380
- defaultEnv: z.string(),
8629
+ defaultEnv: z3.string(),
8381
8630
  auth: AuthSchema.prefault({}),
8382
- testData: z.object({
8383
- users: z.record(z.string(), z.object({ fromAuth: z.string() })).default({})
8631
+ testData: z3.object({
8632
+ users: z3.record(z3.string(), z3.object({ fromAuth: z3.string() })).default({})
8384
8633
  }).prefault({})
8385
8634
  }).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
8386
8635
  message: "defaultEnv must exist in baseUrl map",
8387
8636
  path: ["defaultEnv"]
8388
8637
  });
8389
- var HttpAuthRoleSchema = z.object({
8390
- tokenEnv: z.string().optional(),
8391
- userEnv: z.string().optional(),
8392
- passEnv: z.string().optional(),
8393
- tokenUrl: z.string().url().optional(),
8394
- clientIdEnv: z.string().optional(),
8395
- clientSecretEnv: z.string().optional(),
8396
- scope: z.string().optional()
8638
+ var HttpAuthRoleSchema = z3.object({
8639
+ tokenEnv: z3.string().optional(),
8640
+ userEnv: z3.string().optional(),
8641
+ passEnv: z3.string().optional(),
8642
+ tokenUrl: z3.string().url().optional(),
8643
+ clientIdEnv: z3.string().optional(),
8644
+ clientSecretEnv: z3.string().optional(),
8645
+ scope: z3.string().optional()
8397
8646
  });
8398
- var HttpAuthSchema = z.object({
8399
- strategy: z.enum(["bearer", "apiKey", "basic", "oauth-cc", "custom", "none"]).default("none"),
8400
- ttl: z.string().default("8h"),
8401
- refreshBuffer: z.string().default("30m"),
8402
- roles: z.record(z.string(), HttpAuthRoleSchema).default({})
8647
+ var HttpAuthSchema = z3.object({
8648
+ strategy: z3.enum(["bearer", "apiKey", "basic", "oauth-cc", "custom", "none"]).default("none"),
8649
+ ttl: z3.string().default("8h"),
8650
+ refreshBuffer: z3.string().default("30m"),
8651
+ roles: z3.record(z3.string(), HttpAuthRoleSchema).default({})
8403
8652
  });
8404
- var HttpSchema = z.object({
8405
- baseUrl: z.record(z.string(), z.string().url()).refine((m) => Object.keys(m).length > 0, {
8653
+ var HttpSchema = z3.object({
8654
+ baseUrl: z3.record(z3.string(), z3.string().url()).refine((m) => Object.keys(m).length > 0, {
8406
8655
  message: "baseUrl must have at least one environment"
8407
8656
  }),
8408
- defaultEnv: z.string(),
8409
- spec: z.string().optional(),
8657
+ defaultEnv: z3.string(),
8658
+ spec: z3.string().optional(),
8410
8659
  auth: HttpAuthSchema.prefault({})
8411
8660
  }).refine((h) => h.baseUrl[h.defaultEnv] !== undefined, {
8412
8661
  message: "defaultEnv must exist in baseUrl map",
8413
8662
  path: ["defaultEnv"]
8414
8663
  });
8415
- var JiraSchema = z.object({
8416
- baseUrl: z.string().url(),
8417
- projectKeys: z.array(z.string().min(1)).min(1),
8418
- fields: z.object({
8419
- story: z.string().min(1),
8420
- acceptanceCriteria: z.string().optional(),
8421
- attachments: z.string().default("attachment")
8664
+ var JiraSchema = z3.object({
8665
+ baseUrl: z3.string().url(),
8666
+ projectKeys: z3.array(z3.string().min(1)).min(1),
8667
+ fields: z3.object({
8668
+ story: z3.string().min(1),
8669
+ acceptanceCriteria: z3.string().optional(),
8670
+ attachments: z3.string().default("attachment")
8422
8671
  })
8423
8672
  });
8424
- var AISchema = z.object({
8425
- livePageSnapshot: z.boolean().default(true),
8426
- confidenceThreshold: z.enum(["low", "medium", "high"]).default("medium"),
8427
- maxRetries: z.object({
8428
- typecheck: z.number().int().min(0).max(5).default(2),
8429
- lint: z.number().int().min(0).max(5).default(2),
8430
- validateFeature: z.number().int().min(0).max(5).default(2)
8673
+ var AISchema = z3.object({
8674
+ livePageSnapshot: z3.boolean().default(true),
8675
+ confidenceThreshold: z3.enum(["low", "medium", "high"]).default("medium"),
8676
+ maxRetries: z3.object({
8677
+ typecheck: z3.number().int().min(0).max(5).default(2),
8678
+ lint: z3.number().int().min(0).max(5).default(2),
8679
+ validateFeature: z3.number().int().min(0).max(5).default(2)
8431
8680
  }).prefault({})
8432
8681
  }).prefault({});
8433
- var ReportingSchema = z.object({
8434
- language: z.enum(["en", "vi"]).default("en"),
8435
- postToJira: z.boolean().default(true),
8436
- transition: z.object({
8437
- onPass: z.string().nullable().default(null),
8438
- onFail: z.string().nullable().default(null)
8682
+ var ReportingSchema = z3.object({
8683
+ language: z3.enum(["en", "vi"]).default("en"),
8684
+ postToJira: z3.boolean().default(true),
8685
+ transition: z3.object({
8686
+ onPass: z3.string().nullable().default(null),
8687
+ onFail: z3.string().nullable().default(null)
8439
8688
  }).prefault({}),
8440
- artifactLinks: z.enum(["git", "local"]).default("git")
8689
+ artifactLinks: z3.enum(["git", "local"]).default("git")
8441
8690
  }).prefault({});
8442
- var RunSchema = z.object({
8443
- autoImpact: z.object({
8444
- enabled: z.boolean().default(true),
8445
- threshold: z.number().nonnegative().default(8)
8691
+ var RunSchema = z3.object({
8692
+ autoImpact: z3.object({
8693
+ enabled: z3.boolean().default(true),
8694
+ threshold: z3.number().nonnegative().default(8)
8446
8695
  }).prefault({})
8447
8696
  }).prefault({});
8448
- var XeraConfigSchema = z.object({
8697
+ var CoverageSchema = z3.object({
8698
+ staleAfterDays: z3.number().int().positive().default(30),
8699
+ criticalAreas: z3.array(z3.string().regex(/^[a-z0-9-]+$/)).default([]),
8700
+ autoSnapshotOnCoverage: z3.boolean().default(true)
8701
+ }).prefault({});
8702
+ var XeraConfigSchema = z3.object({
8449
8703
  jira: JiraSchema,
8450
8704
  web: WebSchema.optional(),
8451
8705
  http: HttpSchema.optional(),
8452
8706
  ai: AISchema,
8453
8707
  reporting: ReportingSchema,
8454
8708
  run: RunSchema.prefault({}),
8455
- adapters: z.array(z.enum(["web", "http"])).min(1).default(["web"])
8709
+ coverage: CoverageSchema,
8710
+ adapters: z3.array(z3.enum(["web", "http"])).min(1).default(["web"])
8456
8711
  }).refine((c) => c.web !== undefined || c.http !== undefined, {
8457
8712
  message: "At least one of `web` or `http` must be configured"
8458
8713
  }).refine((c) => c.adapters.every((a) => (a === "web" ? c.web : c.http) !== undefined), {
@@ -8462,8 +8717,8 @@ var XeraConfigSchema = z.object({
8462
8717
 
8463
8718
  // src/config/load.ts
8464
8719
  async function loadConfig(cwd) {
8465
- const path = join(cwd, "xera.config.ts");
8466
- if (!existsSync(path)) {
8720
+ const path = join4(cwd, "xera.config.ts");
8721
+ if (!existsSync3(path)) {
8467
8722
  throw new Error(`xera.config.ts not found in ${cwd}`);
8468
8723
  }
8469
8724
  const mod = await import(pathToFileURL(path).href);
@@ -8492,8 +8747,8 @@ async function authSetupCmd(argv) {
8492
8747
  const opts = parseOpts(argv);
8493
8748
  const cwd = process.cwd();
8494
8749
  const config = await loadConfig(cwd);
8495
- const authSetupScript = join2(cwd, "shared", "auth-setup.ts");
8496
- if (!existsSync2(authSetupScript)) {
8750
+ const authSetupScript = join5(cwd, "shared", "auth-setup.ts");
8751
+ if (!existsSync4(authSetupScript)) {
8497
8752
  console.error(`[xera:auth-setup] auth-setup.ts not found at ${authSetupScript}. Run 'bunx @xera-ai/cli init' first.`);
8498
8753
  return 1;
8499
8754
  }
@@ -8519,7 +8774,7 @@ async function authSetupCmd(argv) {
8519
8774
  role: roleName,
8520
8775
  creds: { email, password },
8521
8776
  setupScriptPath: authSetupScript,
8522
- authDir: join2(cwd, ".xera", ".auth"),
8777
+ authDir: join5(cwd, ".xera", ".auth"),
8523
8778
  browser
8524
8779
  });
8525
8780
  console.log(`[xera:auth-setup] \u2713 ${roleName}.json (web)`);
@@ -8540,7 +8795,7 @@ async function authSetupCmd(argv) {
8540
8795
  continue;
8541
8796
  try {
8542
8797
  await runHttpAuthSetup({
8543
- authDir: join2(cwd, ".xera", ".auth"),
8798
+ authDir: join5(cwd, ".xera", ".auth"),
8544
8799
  role: roleName,
8545
8800
  config: config.http,
8546
8801
  setupFn: mod.http,
@@ -8556,6 +8811,401 @@ async function authSetupCmd(argv) {
8556
8811
  return exitCode;
8557
8812
  }
8558
8813
 
8814
+ // src/bin-internal/coverage-prepare.ts
8815
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
8816
+ import { join as join6 } from "path";
8817
+
8818
+ // src/coverage/status.ts
8819
+ function daysBetween(a, b) {
8820
+ return Math.abs(a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24);
8821
+ }
8822
+ function computeScenarioStatus(scenarioId, snap, windowDays, now) {
8823
+ const events = snap.classifications.filter((c) => c.scenarioId === scenarioId).sort((a, b) => a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0);
8824
+ const latest = events[0];
8825
+ if (!latest)
8826
+ return "NOT_PASSING";
8827
+ if (latest.classification !== "PASS")
8828
+ return "NOT_PASSING";
8829
+ if (daysBetween(now, new Date(latest.ts)) > windowDays)
8830
+ return "NOT_PASSING";
8831
+ return "PASSING";
8832
+ }
8833
+ function computeAreaStatus(areaId, snap, windowDays, now) {
8834
+ const coveringPoms = snap.edges.filter((e) => e.kind === "covers" && e.to === areaId).map((e) => e.from);
8835
+ if (coveringPoms.length === 0)
8836
+ return "UNCOVERED";
8837
+ const scenariosInArea = snap.edges.filter((e) => e.kind === "uses" && coveringPoms.includes(e.to)).map((e) => e.from);
8838
+ const anyPassing = scenariosInArea.some((sid) => computeScenarioStatus(sid, snap, windowDays, now) === "PASSING");
8839
+ return anyPassing ? "COVERED" : "STALE";
8840
+ }
8841
+ function computeAcStatus(acId, snap, windowDays, now) {
8842
+ const edges = snap.edges.filter((e) => e.kind === "satisfies" && e.to === acId);
8843
+ if (edges.length === 0)
8844
+ return "UNSATISFIED";
8845
+ const anyPassing = edges.some((e) => computeScenarioStatus(e.from, snap, windowDays, now) === "PASSING");
8846
+ return anyPassing ? "SATISFIED" : "UNSATISFIED";
8847
+ }
8848
+ function computeTicketStatus(ticketId, snap, windowDays, now) {
8849
+ const acIds = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticketId).map((ac) => ac.id);
8850
+ if (acIds.length === 0)
8851
+ return "COMPLETE";
8852
+ const allSatisfied = acIds.every((acId) => computeAcStatus(acId, snap, windowDays, now) === "SATISFIED");
8853
+ return allSatisfied ? "COMPLETE" : "INCOMPLETE";
8854
+ }
8855
+
8856
+ // src/coverage/risk.ts
8857
+ var RISK_WEIGHTS = {
8858
+ criticalBoost: 2,
8859
+ bugClassifications: new Set(["REAL_BUG", "TEST_OUTDATED"]),
8860
+ recencyBoosts: { recent: 2, withinWindow: 1, older: 0.5 },
8861
+ recencyThresholdDays: 7
8862
+ };
8863
+ function daysBetween2(a, b) {
8864
+ return Math.abs(a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24);
8865
+ }
8866
+ function computeAreaRisk(areaId, snap, config, now) {
8867
+ const recentTickets = snap.edges.filter((e) => e.kind === "modifies" && e.to === areaId).map((e) => snap.tickets[e.from]).filter((t) => t !== undefined).filter((t) => daysBetween2(now, new Date(t.fetchedAt)) <= config.staleAfterDays).length;
8868
+ const pomsInArea = snap.edges.filter((e) => e.kind === "covers" && e.to === areaId).map((e) => e.from);
8869
+ const scenariosInArea = new Set(snap.edges.filter((e) => e.kind === "uses" && pomsInArea.includes(e.to)).map((e) => e.from));
8870
+ const recentBugs = snap.classifications.filter((c) => scenariosInArea.has(c.scenarioId)).filter((c) => RISK_WEIGHTS.bugClassifications.has(c.classification)).filter((c) => daysBetween2(now, new Date(c.ts)) <= config.staleAfterDays).length;
8871
+ const criticalBoost = config.criticalAreas.includes(areaId) ? RISK_WEIGHTS.criticalBoost : 1;
8872
+ return recentTickets * criticalBoost + recentBugs;
8873
+ }
8874
+ function computeAcGapScore(ticketId, snap, config, now) {
8875
+ const ticket = snap.tickets[ticketId];
8876
+ if (!ticket)
8877
+ return 0;
8878
+ const acs = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticketId);
8879
+ const unsatisfied = acs.filter((ac) => computeAcStatus(ac.id, snap, config.staleAfterDays, now) === "UNSATISFIED").length;
8880
+ if (unsatisfied === 0)
8881
+ return 0;
8882
+ const days = daysBetween2(now, new Date(ticket.fetchedAt));
8883
+ let boost;
8884
+ if (days <= RISK_WEIGHTS.recencyThresholdDays) {
8885
+ boost = RISK_WEIGHTS.recencyBoosts.recent;
8886
+ } else if (days <= config.staleAfterDays) {
8887
+ boost = RISK_WEIGHTS.recencyBoosts.withinWindow;
8888
+ } else {
8889
+ boost = RISK_WEIGHTS.recencyBoosts.older;
8890
+ }
8891
+ return unsatisfied * boost;
8892
+ }
8893
+
8894
+ // src/coverage/report.ts
8895
+ var STATUS_RANK = {
8896
+ UNCOVERED: 0,
8897
+ STALE: 1,
8898
+ COVERED: 2
8899
+ };
8900
+ function daysBetween3(a, b) {
8901
+ return Math.abs(a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24);
8902
+ }
8903
+ function buildCoverageReport(snap, config, now) {
8904
+ const areas = Object.keys(snap.areas).map((areaId) => {
8905
+ const status = computeAreaStatus(areaId, snap, config.staleAfterDays, now);
8906
+ const risk = computeAreaRisk(areaId, snap, config, now);
8907
+ const recentTickets = snap.edges.filter((e) => e.kind === "modifies" && e.to === areaId).map((e) => snap.tickets[e.from]).filter((t) => t !== undefined).filter((t) => daysBetween3(now, new Date(t.fetchedAt)) <= config.staleAfterDays).length;
8908
+ const pomsInArea = snap.edges.filter((e) => e.kind === "covers" && e.to === areaId).map((e) => e.from);
8909
+ const scenariosInArea = new Set(snap.edges.filter((e) => e.kind === "uses" && pomsInArea.includes(e.to)).map((e) => e.from));
8910
+ const recentBugs = snap.classifications.filter((c) => scenariosInArea.has(c.scenarioId)).filter((c) => RISK_WEIGHTS.bugClassifications.has(c.classification)).filter((c) => daysBetween3(now, new Date(c.ts)) <= config.staleAfterDays).length;
8911
+ const criticalBoost = config.criticalAreas.includes(areaId) ? 2 : 1;
8912
+ return {
8913
+ id: areaId,
8914
+ status,
8915
+ risk,
8916
+ breakdown: { recentTickets, recentBugs, criticalBoost }
8917
+ };
8918
+ });
8919
+ areas.sort((a, b) => {
8920
+ if (STATUS_RANK[a.status] !== STATUS_RANK[b.status]) {
8921
+ return STATUS_RANK[a.status] - STATUS_RANK[b.status];
8922
+ }
8923
+ if (a.status === "COVERED")
8924
+ return a.id.localeCompare(b.id);
8925
+ if (b.risk !== a.risk)
8926
+ return b.risk - a.risk;
8927
+ return a.id.localeCompare(b.id);
8928
+ });
8929
+ const tickets = Object.values(snap.tickets).filter((t) => computeTicketStatus(t.id, snap, config.staleAfterDays, now) === "INCOMPLETE").map((t) => {
8930
+ const acs = Object.values(snap.acNodes).filter((ac) => ac.ticketId === t.id).sort((a, b) => a.index - b.index);
8931
+ const unsatisfiedAcs = acs.filter((ac) => computeAcStatus(ac.id, snap, config.staleAfterDays, now) === "UNSATISFIED").map((ac) => ({ index: ac.index, text: ac.text }));
8932
+ return {
8933
+ id: t.id,
8934
+ summary: t.summary,
8935
+ acCount: acs.length,
8936
+ satisfiedCount: acs.length - unsatisfiedAcs.length,
8937
+ gapScore: computeAcGapScore(t.id, snap, config, now),
8938
+ unsatisfiedAcs
8939
+ };
8940
+ }).sort((a, b) => b.gapScore - a.gapScore || a.id.localeCompare(b.id));
8941
+ return {
8942
+ generatedAt: now.toISOString(),
8943
+ windowDays: config.staleAfterDays,
8944
+ areas,
8945
+ tickets,
8946
+ acBackfillNeeded: needsBackfill(snap)
8947
+ };
8948
+ }
8949
+ function needsBackfill(snap) {
8950
+ for (const ticket of Object.values(snap.tickets)) {
8951
+ const acsForTicket = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticket.id);
8952
+ if (acsForTicket.length === 0)
8953
+ continue;
8954
+ const scenariosForTicket = Object.values(snap.scenarios).filter((s) => s.ticketId === ticket.id);
8955
+ if (scenariosForTicket.length === 0)
8956
+ continue;
8957
+ const hasAnyEdge = snap.edges.some((e) => e.kind === "satisfies" && acsForTicket.some((ac) => ac.id === e.to));
8958
+ if (!hasAnyEdge)
8959
+ return true;
8960
+ }
8961
+ return false;
8962
+ }
8963
+ function pad(s, n) {
8964
+ return s.length >= n ? s : `${s}${" ".repeat(n - s.length)}`;
8965
+ }
8966
+ function renderMarkdown(report, options = {}) {
8967
+ const lines = [];
8968
+ const dateOnly = report.generatedAt.slice(0, 10);
8969
+ lines.push("", `Coverage report \u2014 generated ${dateOnly} \xB7 window ${report.windowDays}d`, "");
8970
+ const uncovered = report.areas.filter((a) => a.status === "UNCOVERED");
8971
+ if (uncovered.length > 0) {
8972
+ lines.push(`UNCOVERED \u2014 ${uncovered.length} area${uncovered.length === 1 ? "" : "s"}, sorted by risk`);
8973
+ lines.push("");
8974
+ uncovered.forEach((a, i) => {
8975
+ const parts = [`${a.breakdown.recentTickets} tickets`];
8976
+ if (a.breakdown.recentBugs > 0)
8977
+ parts.push(`${a.breakdown.recentBugs} bugs`);
8978
+ if (a.breakdown.criticalBoost === 2)
8979
+ parts.push("critical \xD72");
8980
+ lines.push(` #${i + 1} ${pad(a.id, 10)} risk ${a.risk} ${parts.join(" \xB7 ")}`);
8981
+ });
8982
+ lines.push("");
8983
+ }
8984
+ const stale = report.areas.filter((a) => a.status === "STALE");
8985
+ if (stale.length > 0) {
8986
+ lines.push(`STALE \u2014 ${stale.length} area${stale.length === 1 ? "" : "s"}, has tests but no PASS in ${report.windowDays}d`);
8987
+ lines.push("");
8988
+ stale.forEach((a, i) => {
8989
+ lines.push(` #${i + 1} ${pad(a.id, 10)} (see --why ${a.id} for details)`);
8990
+ });
8991
+ lines.push("");
8992
+ }
8993
+ if (report.tickets.length > 0) {
8994
+ lines.push(`AC GAPS \u2014 ${report.tickets.length} ticket${report.tickets.length === 1 ? "" : "s"} with unsatisfied acceptance criteria`);
8995
+ lines.push("");
8996
+ for (const t of report.tickets) {
8997
+ lines.push(` ${t.id} ${t.satisfiedCount}/${t.acCount} ACs covered \xB7 gap_score ${t.gapScore}`);
8998
+ for (const ac of t.unsatisfiedAcs) {
8999
+ lines.push(` \u2717 AC-${ac.index} ${ac.text}`);
9000
+ }
9001
+ lines.push("");
9002
+ }
9003
+ }
9004
+ const covered = report.areas.filter((a) => a.status === "COVERED");
9005
+ if (covered.length > 0) {
9006
+ if (options.includeCovered) {
9007
+ lines.push(`COVERED \u2014 ${covered.length} area${covered.length === 1 ? "" : "s"}`);
9008
+ lines.push("");
9009
+ covered.forEach((a, i) => {
9010
+ lines.push(` #${i + 1} ${pad(a.id, 10)} ok`);
9011
+ });
9012
+ lines.push("");
9013
+ } else {
9014
+ lines.push(`COVERED \u2014 ${covered.length} area${covered.length === 1 ? "" : "s"} (collapsed; show with --all)`);
9015
+ lines.push("");
9016
+ }
9017
+ }
9018
+ return lines.join(`
9019
+ `);
9020
+ }
9021
+ // src/coverage/why.ts
9022
+ function daysBetween4(a, b) {
9023
+ return Math.abs(a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24);
9024
+ }
9025
+ function pad2(s, n) {
9026
+ return s.length >= n ? s : `${s}${" ".repeat(n - s.length)}`;
9027
+ }
9028
+ function buildWhyArea(areaId, snap, config, now) {
9029
+ if (snap.areas[areaId] === undefined)
9030
+ return `Unknown area: ${areaId}
9031
+ `;
9032
+ const status = computeAreaStatus(areaId, snap, config.staleAfterDays, now);
9033
+ const isCritical = config.criticalAreas.includes(areaId);
9034
+ const heading = isCritical ? `${status}, critical` : status;
9035
+ const risk = computeAreaRisk(areaId, snap, config, now);
9036
+ const recentTickets = snap.edges.filter((e) => e.kind === "modifies" && e.to === areaId).map((e) => snap.tickets[e.from]).filter((t) => t !== undefined).filter((t) => daysBetween4(now, new Date(t.fetchedAt)) <= config.staleAfterDays);
9037
+ const pomsInArea = snap.edges.filter((e) => e.kind === "covers" && e.to === areaId).map((e) => e.from);
9038
+ const scenariosInArea = new Set(snap.edges.filter((e) => e.kind === "uses" && pomsInArea.includes(e.to)).map((e) => e.from));
9039
+ const recentBugs = snap.classifications.filter((c) => scenariosInArea.has(c.scenarioId)).filter((c) => RISK_WEIGHTS.bugClassifications.has(c.classification)).filter((c) => daysBetween4(now, new Date(c.ts)) <= config.staleAfterDays);
9040
+ const boost = isCritical ? 2 : 1;
9041
+ const lines = [
9042
+ "",
9043
+ `Area: ${areaId} (${heading})`,
9044
+ "",
9045
+ `Risk score: ${risk}`,
9046
+ " recent_tickets \xD7 critical_boost + recent_bugs",
9047
+ ` = ${recentTickets.length} \xD7 ${boost} + ${recentBugs.length} = ${risk}`,
9048
+ "",
9049
+ `Recent tickets (${recentTickets.length}, last ${config.staleAfterDays}d):`
9050
+ ];
9051
+ for (const t of recentTickets) {
9052
+ lines.push(` ${t.id} ${t.fetchedAt.slice(0, 10)} ${t.summary}`);
9053
+ }
9054
+ if (recentTickets.length === 0)
9055
+ lines.push(" (none)");
9056
+ lines.push("");
9057
+ if (recentBugs.length > 0) {
9058
+ lines.push(`Recent bugs (${recentBugs.length}, last ${config.staleAfterDays}d):`);
9059
+ for (const b of recentBugs) {
9060
+ lines.push(` ${b.ts.slice(0, 10)} ${pad2(b.classification, 14)} scenario ${b.scenarioId}`);
9061
+ }
9062
+ lines.push("");
9063
+ }
9064
+ if (status === "UNCOVERED") {
9065
+ lines.push("No POM covers this area. To draft scenarios:");
9066
+ lines.push(` /xera-fill-gap ${areaId}`);
9067
+ }
9068
+ return `${lines.join(`
9069
+ `)}
9070
+ `;
9071
+ }
9072
+ function buildWhyTicket(ticketId, snap, config, now) {
9073
+ const ticket = snap.tickets[ticketId];
9074
+ if (!ticket)
9075
+ return `Unknown ticket: ${ticketId}
9076
+ `;
9077
+ const status = computeTicketStatus(ticketId, snap, config.staleAfterDays, now);
9078
+ const acs = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticketId).sort((a, b) => a.index - b.index);
9079
+ const satisfiedCount = acs.filter((ac) => computeAcStatus(ac.id, snap, config.staleAfterDays, now) === "SATISFIED").length;
9080
+ const gapScore = computeAcGapScore(ticketId, snap, config, now);
9081
+ const days = daysBetween4(now, new Date(ticket.fetchedAt));
9082
+ let boostLabel;
9083
+ if (days <= RISK_WEIGHTS.recencyThresholdDays)
9084
+ boostLabel = "\xD72.0";
9085
+ else if (days <= config.staleAfterDays)
9086
+ boostLabel = "\xD71.0";
9087
+ else
9088
+ boostLabel = "\xD70.5";
9089
+ const lines = [
9090
+ "",
9091
+ `Ticket: ${ticketId} (${status}, ${satisfiedCount}/${acs.length} ACs covered)`,
9092
+ ` Title: ${ticket.summary}`,
9093
+ ` Fetched: ${ticket.fetchedAt.slice(0, 10)} (${Math.floor(days)}d ago, recency boost ${boostLabel})`,
9094
+ ` AC gap score: ${gapScore}`,
9095
+ "",
9096
+ "Acceptance Criteria:"
9097
+ ];
9098
+ for (const ac of acs) {
9099
+ const acStatus = computeAcStatus(ac.id, snap, config.staleAfterDays, now);
9100
+ const marker = acStatus === "SATISFIED" ? "\u2713" : "\u2717";
9101
+ const satisfyingScenarios = snap.edges.filter((e) => e.kind === "satisfies" && e.to === ac.id).map((e) => e.from);
9102
+ const scenarioRef = satisfyingScenarios.length > 0 ? ` \u2014 scenario "${satisfyingScenarios[0]}"` : "";
9103
+ lines.push(` ${marker} AC-${ac.index} ${ac.text}${scenarioRef}`);
9104
+ }
9105
+ lines.push("");
9106
+ if (status === "INCOMPLETE") {
9107
+ lines.push("To draft scenarios for unsatisfied ACs:");
9108
+ lines.push(` /xera-fill-gap --ticket ${ticketId}`);
9109
+ }
9110
+ return `${lines.join(`
9111
+ `)}
9112
+ `;
9113
+ }
9114
+ // src/bin-internal/coverage-prepare.ts
9115
+ init_store();
9116
+ init_ulid();
9117
+ function parseArgs3(argv) {
9118
+ const args = { emitEvent: true, json: false, all: false };
9119
+ for (let i = 0;i < argv.length; i++) {
9120
+ const a = argv[i];
9121
+ if (a === "--snapshot-ts") {
9122
+ const v = argv[++i];
9123
+ if (v !== undefined)
9124
+ args.snapshotTs = v;
9125
+ } else if (a === "--no-emit-event")
9126
+ args.emitEvent = false;
9127
+ else if (a === "--why") {
9128
+ const v = argv[++i];
9129
+ if (v !== undefined)
9130
+ args.why = v;
9131
+ } else if (a === "--json")
9132
+ args.json = true;
9133
+ else if (a === "--all")
9134
+ args.all = true;
9135
+ else if (a === "--snapshot-file") {
9136
+ const v = argv[++i];
9137
+ if (v !== undefined)
9138
+ args.snapshotFile = v;
9139
+ } else if (a === "--help-stub") {} else {
9140
+ console.error(`[coverage-prepare] unknown flag: ${a}`);
9141
+ return { ...args, emitEvent: false };
9142
+ }
9143
+ }
9144
+ return args;
9145
+ }
9146
+ var TICKET_RE = /^[A-Z][A-Z0-9]*-\d+$/;
9147
+ async function coveragePrepareCmd(argv) {
9148
+ const args = parseArgs3(argv);
9149
+ const cwd = process.cwd();
9150
+ let config;
9151
+ try {
9152
+ config = await loadConfig(cwd);
9153
+ } catch (e) {
9154
+ console.error(`[coverage-prepare] ${e.message}`);
9155
+ return 2;
9156
+ }
9157
+ let snap;
9158
+ if (args.snapshotFile) {
9159
+ snap = JSON.parse(readFileSync3(args.snapshotFile, "utf8"));
9160
+ } else {
9161
+ snap = deriveSnapshot(loadAllEvents(cwd));
9162
+ }
9163
+ const now = args.snapshotTs ? new Date(args.snapshotTs) : new Date;
9164
+ if (args.why) {
9165
+ const out = TICKET_RE.test(args.why) ? buildWhyTicket(args.why, snap, config.coverage, now) : buildWhyArea(args.why, snap, config.coverage, now);
9166
+ process.stdout.write(out);
9167
+ return 0;
9168
+ }
9169
+ const report = buildCoverageReport(snap, config.coverage, now);
9170
+ if (args.json) {
9171
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
9172
+ `);
9173
+ return 0;
9174
+ }
9175
+ const outDir = join6(cwd, ".xera/coverage");
9176
+ mkdirSync3(outDir, { recursive: true });
9177
+ writeFileSync3(join6(outDir, "report.json"), JSON.stringify(report, null, 2));
9178
+ const renderOpts = { includeCovered: args.all };
9179
+ writeFileSync3(join6(outDir, "report.md"), renderMarkdown(report, renderOpts));
9180
+ if (args.emitEvent && config.coverage.autoSnapshotOnCoverage) {
9181
+ const event = {
9182
+ event_id: ulid(),
9183
+ schema_version: 1,
9184
+ ts: now.toISOString(),
9185
+ actor: "xera-coverage",
9186
+ type: "coverage.snapshot",
9187
+ payload: {
9188
+ ts: now.toISOString(),
9189
+ windowDays: config.coverage.staleAfterDays,
9190
+ areas: report.areas.map((a) => ({
9191
+ id: a.id,
9192
+ status: a.status,
9193
+ risk: a.risk,
9194
+ breakdown: a.breakdown
9195
+ })),
9196
+ tickets: report.tickets.map((t) => ({
9197
+ id: t.id,
9198
+ acCount: t.acCount,
9199
+ satisfiedCount: t.satisfiedCount,
9200
+ gapScore: t.gapScore
9201
+ }))
9202
+ }
9203
+ };
9204
+ appendEvents(cwd, [event], { skill: "coverage", ticketId: "session", now });
9205
+ }
9206
+ return 0;
9207
+ }
9208
+
8559
9209
  // src/bin-internal/disputes.ts
8560
9210
  init_store();
8561
9211
  function parseDuration(s) {
@@ -8632,19 +9282,19 @@ async function disputesCmd(argv) {
8632
9282
  }
8633
9283
 
8634
9284
  // src/bin-internal/doctor.ts
8635
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
8636
- import { join as join9 } from "path";
9285
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
9286
+ import { join as join12 } from "path";
8637
9287
 
8638
9288
  // src/graph/cost.ts
8639
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "fs";
9289
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
8640
9290
  init_paths();
8641
9291
  function summarizeCost(repoRoot, daysBack) {
8642
9292
  const paths = graphPaths(repoRoot);
8643
9293
  const result = { totalCalls: 0, totalUsd: 0, bySkill: {}, windowDays: daysBack };
8644
- if (!existsSync4(paths.costLog))
9294
+ if (!existsSync5(paths.costLog))
8645
9295
  return result;
8646
9296
  const cutoff = Date.now() - daysBack * 86400 * 1000;
8647
- for (const line of readFileSync2(paths.costLog, "utf8").split(`
9297
+ for (const line of readFileSync4(paths.costLog, "utf8").split(`
8648
9298
  `)) {
8649
9299
  if (!line.trim())
8650
9300
  continue;
@@ -8671,8 +9321,8 @@ function summarizeCost(repoRoot, daysBack) {
8671
9321
  init_store();
8672
9322
 
8673
9323
  // src/bin-internal/verify-prompts.ts
8674
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
8675
- import { join as join4 } from "path";
9324
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
9325
+ import { join as join7 } from "path";
8676
9326
  var IN_SCOPE_PROMPTS = [
8677
9327
  "feature-from-story.md",
8678
9328
  "script-from-feature-web.md",
@@ -8680,23 +9330,25 @@ var IN_SCOPE_PROMPTS = [
8680
9330
  "heal-locator.md",
8681
9331
  "extract-areas.md",
8682
9332
  "similarity-match.md",
8683
- "classify-outdated.md"
9333
+ "classify-outdated.md",
9334
+ "map-ac-to-scenarios.md",
9335
+ "propose-scenarios.md"
8684
9336
  ];
8685
9337
  var REQUIRED_SECTION_HEADING = "## Handling untrusted input";
8686
9338
  var REQUIRED_KEYWORDS = ["UNTRUSTED", "injection-follow", "<XR_"];
8687
9339
  function verifyPrompts(repoRoot) {
8688
- const promptsDir = join4(repoRoot, "packages/prompts");
9340
+ const promptsDir = join7(repoRoot, "packages/prompts");
8689
9341
  const results = [];
8690
9342
  for (const filename of IN_SCOPE_PROMPTS) {
8691
- const path = join4(promptsDir, filename);
8692
- if (!existsSync5(path)) {
9343
+ const path = join7(promptsDir, filename);
9344
+ if (!existsSync6(path)) {
8693
9345
  results.push({
8694
9346
  ok: false,
8695
9347
  message: `${filename}: file missing at packages/prompts/${filename}`
8696
9348
  });
8697
9349
  continue;
8698
9350
  }
8699
- const text = readFileSync3(path, "utf8");
9351
+ const text = readFileSync5(path, "utf8");
8700
9352
  if (!text.includes(REQUIRED_SECTION_HEADING)) {
8701
9353
  results.push({
8702
9354
  ok: false,
@@ -8744,8 +9396,8 @@ function frontmatterField(content, field) {
8744
9396
  return m?.[1] ?? null;
8745
9397
  }
8746
9398
  function checkGoldenEvalDir(repoRoot) {
8747
- const root = join9(repoRoot, "fixtures/golden-eval");
8748
- if (!existsSync9(root))
9399
+ const root = join12(repoRoot, "fixtures/golden-eval");
9400
+ if (!existsSync10(root))
8749
9401
  return [{ ok: false, message: "fixtures/golden-eval/ does not exist" }];
8750
9402
  const dirs = readdirSync4(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
8751
9403
  const results = [];
@@ -8756,15 +9408,15 @@ function checkGoldenEvalDir(repoRoot) {
8756
9408
  });
8757
9409
  }
8758
9410
  for (const entry of dirs) {
8759
- const dir = join9(root, entry.name);
8760
- const metaPath = join9(dir, "meta.json");
8761
- if (!existsSync9(metaPath)) {
9411
+ const dir = join12(root, entry.name);
9412
+ const metaPath = join12(dir, "meta.json");
9413
+ if (!existsSync10(metaPath)) {
8762
9414
  results.push({ ok: false, message: `${entry.name}: meta.json missing` });
8763
9415
  continue;
8764
9416
  }
8765
9417
  let meta;
8766
9418
  try {
8767
- meta = JSON.parse(readFileSync6(metaPath, "utf8"));
9419
+ meta = JSON.parse(readFileSync8(metaPath, "utf8"));
8768
9420
  } catch (err) {
8769
9421
  results.push({
8770
9422
  ok: false,
@@ -8775,12 +9427,12 @@ function checkGoldenEvalDir(repoRoot) {
8775
9427
  const stages = Array.isArray(meta.stages) ? meta.stages : [];
8776
9428
  if (stages.length === 0)
8777
9429
  results.push({ ok: false, message: `${entry.name}: meta.stages is empty` });
8778
- if (!existsSync9(join9(dir, "story.md")))
9430
+ if (!existsSync10(join12(dir, "story.md")))
8779
9431
  results.push({ ok: false, message: `${entry.name}: story.md missing` });
8780
9432
  for (const stage of stages) {
8781
9433
  const required = REQUIRED_FILES_PER_STAGE[stage] ?? [];
8782
9434
  for (const rel of required) {
8783
- if (!existsSync9(join9(dir, rel))) {
9435
+ if (!existsSync10(join12(dir, rel))) {
8784
9436
  results.push({
8785
9437
  ok: false,
8786
9438
  message: `${meta.id ?? entry.name}: stage "${stage}" declared but ${rel} missing`
@@ -8792,10 +9444,10 @@ function checkGoldenEvalDir(repoRoot) {
8792
9444
  return results;
8793
9445
  }
8794
9446
  function checkRubricPrompt(repoRoot) {
8795
- const path = join9(repoRoot, "packages/prompts/eval-rubric.md");
8796
- if (!existsSync9(path))
9447
+ const path = join12(repoRoot, "packages/prompts/eval-rubric.md");
9448
+ if (!existsSync10(path))
8797
9449
  return [{ ok: false, message: "packages/prompts/eval-rubric.md missing" }];
8798
- const text = readFileSync6(path, "utf8");
9450
+ const text = readFileSync8(path, "utf8");
8799
9451
  const id = frontmatterField(text, "id");
8800
9452
  const version = frontmatterField(text, "version");
8801
9453
  if (id !== "eval-rubric")
@@ -8805,10 +9457,10 @@ function checkRubricPrompt(repoRoot) {
8805
9457
  return [];
8806
9458
  }
8807
9459
  function checkEvalSkill(repoRoot) {
8808
- const path = join9(repoRoot, "packages/skills/xera-eval.md");
8809
- if (!existsSync9(path))
9460
+ const path = join12(repoRoot, "packages/skills/xera-eval.md");
9461
+ if (!existsSync10(path))
8810
9462
  return [{ ok: false, message: "packages/skills/xera-eval.md missing" }];
8811
- const text = readFileSync6(path, "utf8");
9463
+ const text = readFileSync8(path, "utf8");
8812
9464
  if (!frontmatterField(text, "name"))
8813
9465
  return [{ ok: false, message: 'xera-eval.md frontmatter "name" missing' }];
8814
9466
  return [];
@@ -8817,16 +9469,16 @@ function checkPromptInjectionPreamble(repoRoot) {
8817
9469
  return verifyPrompts(repoRoot);
8818
9470
  }
8819
9471
  function checkRootScripts(repoRoot) {
8820
- const path = join9(repoRoot, "package.json");
8821
- if (!existsSync9(path))
9472
+ const path = join12(repoRoot, "package.json");
9473
+ if (!existsSync10(path))
8822
9474
  return [{ ok: false, message: "root package.json missing" }];
8823
- const pkg = JSON.parse(readFileSync6(path, "utf8"));
9475
+ const pkg = JSON.parse(readFileSync8(path, "utf8"));
8824
9476
  const scripts = pkg.scripts ?? {};
8825
9477
  const missing = REQUIRED_SCRIPTS.filter((s) => typeof scripts[s] !== "string");
8826
9478
  return missing.map((s) => ({ ok: false, message: `root package.json missing script: ${s}` }));
8827
9479
  }
8828
9480
  function isXeraMonorepo(repoRoot) {
8829
- return existsSync9(join9(repoRoot, "packages/skills")) && existsSync9(join9(repoRoot, "packages/prompts"));
9481
+ return existsSync10(join12(repoRoot, "packages/skills")) && existsSync10(join12(repoRoot, "packages/prompts"));
8830
9482
  }
8831
9483
  async function doctorCmd(argv, opts = {}) {
8832
9484
  const repoRoot = opts.cwd ?? process.cwd();
@@ -8848,8 +9500,8 @@ async function doctorCmd(argv, opts = {}) {
8848
9500
  if (top)
8849
9501
  console.log(` Top skill: ${top[0]} (${top[1].calls} calls, $${top[1].usd.toFixed(2)})`);
8850
9502
  }
8851
- const xeraDir = join9(repoRoot, ".xera");
8852
- if (existsSync9(xeraDir)) {
9503
+ const xeraDir = join12(repoRoot, ".xera");
9504
+ if (existsSync10(xeraDir)) {
8853
9505
  const ticketDirs = readdirSync4(xeraDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name));
8854
9506
  if (ticketDirs.length > 0) {
8855
9507
  const events = loadAllEvents(repoRoot);
@@ -8884,115 +9536,115 @@ async function doctorCmd(argv, opts = {}) {
8884
9536
  }
8885
9537
 
8886
9538
  // src/bin-internal/eval-deterministic.ts
8887
- import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
8888
- import { join as join11 } from "path";
9539
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
9540
+ import { join as join14 } from "path";
8889
9541
  import { validateGherkin } from "@xera-ai/web";
8890
9542
 
8891
9543
  // src/eval/paths.ts
8892
- import { join as join10 } from "path";
9544
+ import { join as join13 } from "path";
8893
9545
  function resolveEvalPaths(cwd, runId) {
8894
- const root = join10(cwd, ".xera", "eval", runId);
9546
+ const root = join13(cwd, ".xera", "eval", runId);
8895
9547
  return {
8896
9548
  root,
8897
- manifest: join10(root, "manifest.json"),
8898
- lock: join10(root, ".lock"),
8899
- deterministicScores: join10(root, "deterministic-scores.json"),
8900
- judgeScores: join10(root, "judge-scores.json"),
8901
- report: join10(root, "report.md"),
8902
- summary: join10(root, "summary.json"),
8903
- inputsDir: join10(root, "inputs"),
8904
- actualDir: join10(root, "actual"),
8905
- ticketInputsDir: (ticket) => join10(root, "inputs", ticket),
8906
- ticketActualDir: (ticket) => join10(root, "actual", ticket)
9549
+ manifest: join13(root, "manifest.json"),
9550
+ lock: join13(root, ".lock"),
9551
+ deterministicScores: join13(root, "deterministic-scores.json"),
9552
+ judgeScores: join13(root, "judge-scores.json"),
9553
+ report: join13(root, "report.md"),
9554
+ summary: join13(root, "summary.json"),
9555
+ inputsDir: join13(root, "inputs"),
9556
+ actualDir: join13(root, "actual"),
9557
+ ticketInputsDir: (ticket) => join13(root, "inputs", ticket),
9558
+ ticketActualDir: (ticket) => join13(root, "actual", ticket)
8907
9559
  };
8908
9560
  }
8909
9561
 
8910
9562
  // src/eval/types.ts
8911
- import { z as z3 } from "zod";
9563
+ import { z as z4 } from "zod";
8912
9564
  var STAGES = ["feature-from-story", "script-from-feature", "diagnose-failure"];
8913
- var StageSchema = z3.enum(STAGES);
8914
- var VerdictSchema = z3.enum(["PASS", "FAIL", "NA"]);
8915
- var PromptVersionsSchema = z3.object({
8916
- "feature-from-story": z3.string(),
8917
- "script-from-feature": z3.string(),
8918
- "diagnose-failure": z3.string(),
8919
- "eval-rubric": z3.string()
9565
+ var StageSchema = z4.enum(STAGES);
9566
+ var VerdictSchema = z4.enum(["PASS", "FAIL", "NA"]);
9567
+ var PromptVersionsSchema = z4.object({
9568
+ "feature-from-story": z4.string(),
9569
+ "script-from-feature": z4.string(),
9570
+ "diagnose-failure": z4.string(),
9571
+ "eval-rubric": z4.string()
8920
9572
  });
8921
- var ManifestSchema = z3.object({
8922
- run_id: z3.string(),
8923
- started_at: z3.string(),
8924
- git_sha: z3.string(),
8925
- tickets: z3.array(z3.string()).min(1),
8926
- stages: z3.array(StageSchema).min(1),
8927
- ticket_stages: z3.record(z3.string(), z3.array(StageSchema).min(1)),
9573
+ var ManifestSchema = z4.object({
9574
+ run_id: z4.string(),
9575
+ started_at: z4.string(),
9576
+ git_sha: z4.string(),
9577
+ tickets: z4.array(z4.string()).min(1),
9578
+ stages: z4.array(StageSchema).min(1),
9579
+ ticket_stages: z4.record(z4.string(), z4.array(StageSchema).min(1)),
8928
9580
  prompt_versions: PromptVersionsSchema,
8929
- flags: z3.object({
8930
- force: z3.boolean(),
9581
+ flags: z4.object({
9582
+ force: z4.boolean(),
8931
9583
  only_prompt: StageSchema.nullable(),
8932
- only_ticket: z3.string().nullable(),
8933
- judge_only: z3.boolean()
9584
+ only_ticket: z4.string().nullable(),
9585
+ judge_only: z4.boolean()
8934
9586
  })
8935
9587
  });
8936
- var DimensionSchema = z3.object({
8937
- name: z3.string(),
9588
+ var DimensionSchema = z4.object({
9589
+ name: z4.string(),
8938
9590
  verdict: VerdictSchema,
8939
- notes: z3.string()
9591
+ notes: z4.string()
8940
9592
  });
8941
- var JudgmentSchema = z3.object({
9593
+ var JudgmentSchema = z4.object({
8942
9594
  stage: StageSchema,
8943
- ticket: z3.string(),
8944
- dimensions: z3.array(DimensionSchema).min(1)
9595
+ ticket: z4.string(),
9596
+ dimensions: z4.array(DimensionSchema).min(1)
8945
9597
  });
8946
- var JudgeScoresSchema = z3.object({
8947
- run_id: z3.string(),
8948
- judgments: z3.array(JudgmentSchema)
9598
+ var JudgeScoresSchema = z4.object({
9599
+ run_id: z4.string(),
9600
+ judgments: z4.array(JudgmentSchema)
8949
9601
  });
8950
- var DeterministicEntrySchema = z3.object({
8951
- ticket: z3.string(),
9602
+ var DeterministicEntrySchema = z4.object({
9603
+ ticket: z4.string(),
8952
9604
  stage: StageSchema,
8953
- passed: z3.boolean(),
8954
- checks: z3.array(z3.string()),
8955
- error: z3.string().optional()
9605
+ passed: z4.boolean(),
9606
+ checks: z4.array(z4.string()),
9607
+ error: z4.string().optional()
8956
9608
  });
8957
- var DeterministicScoresSchema = z3.object({
8958
- run_id: z3.string(),
8959
- entries: z3.array(DeterministicEntrySchema)
9609
+ var DeterministicScoresSchema = z4.object({
9610
+ run_id: z4.string(),
9611
+ entries: z4.array(DeterministicEntrySchema)
8960
9612
  });
8961
- var ResultSchema = z3.object({
8962
- ticket: z3.string(),
9613
+ var ResultSchema = z4.object({
9614
+ ticket: z4.string(),
8963
9615
  stage: StageSchema,
8964
- deterministic: z3.object({
8965
- passed: z3.boolean(),
8966
- checks: z3.array(z3.string()),
8967
- error: z3.string().optional()
9616
+ deterministic: z4.object({
9617
+ passed: z4.boolean(),
9618
+ checks: z4.array(z4.string()),
9619
+ error: z4.string().optional()
8968
9620
  }),
8969
- judge: z3.object({
8970
- passed: z3.boolean(),
8971
- dimensions: z3.array(DimensionSchema),
8972
- score: z3.number().min(0).max(1)
9621
+ judge: z4.object({
9622
+ passed: z4.boolean(),
9623
+ dimensions: z4.array(DimensionSchema),
9624
+ score: z4.number().min(0).max(1)
8973
9625
  }).nullable(),
8974
- skipped: z3.boolean().optional()
9626
+ skipped: z4.boolean().optional()
8975
9627
  });
8976
- var SummarySchema = z3.object({
8977
- run_id: z3.string(),
8978
- git_sha: z3.string(),
9628
+ var SummarySchema = z4.object({
9629
+ run_id: z4.string(),
9630
+ git_sha: z4.string(),
8979
9631
  prompt_versions: PromptVersionsSchema,
8980
- results: z3.array(ResultSchema),
8981
- overall: z3.object({
8982
- passed: z3.number().int().nonnegative(),
8983
- failed: z3.number().int().nonnegative(),
8984
- total: z3.number().int().nonnegative(),
8985
- score: z3.number().min(0).max(1)
9632
+ results: z4.array(ResultSchema),
9633
+ overall: z4.object({
9634
+ passed: z4.number().int().nonnegative(),
9635
+ failed: z4.number().int().nonnegative(),
9636
+ total: z4.number().int().nonnegative(),
9637
+ score: z4.number().min(0).max(1)
8986
9638
  })
8987
9639
  });
8988
9640
 
8989
9641
  // src/bin-internal/eval-deterministic.ts
8990
9642
  function checkFeatureFromStory(actualFeaturePath) {
8991
- if (!existsSync10(actualFeaturePath)) {
9643
+ if (!existsSync11(actualFeaturePath)) {
8992
9644
  return { passed: false, checks: ["validate-feature"], error: "actual missing: test.feature" };
8993
9645
  }
8994
9646
  try {
8995
- const r = validateGherkin(readFileSync7(actualFeaturePath, "utf8"));
9647
+ const r = validateGherkin(readFileSync9(actualFeaturePath, "utf8"));
8996
9648
  if (r.ok)
8997
9649
  return { passed: true, checks: ["validate-feature"] };
8998
9650
  return {
@@ -9005,31 +9657,31 @@ function checkFeatureFromStory(actualFeaturePath) {
9005
9657
  }
9006
9658
  }
9007
9659
  function checkScriptFromFeature(actualTicketDir) {
9008
- const specPath = join11(actualTicketDir, "spec.ts");
9009
- if (!existsSync10(specPath)) {
9660
+ const specPath = join14(actualTicketDir, "spec.ts");
9661
+ if (!existsSync11(specPath)) {
9010
9662
  return { passed: false, checks: ["file-presence"], error: "actual missing: spec.ts" };
9011
9663
  }
9012
9664
  return { passed: true, checks: ["file-presence"] };
9013
9665
  }
9014
9666
  function checkDiagnoseFailure(inputsTicketDir, actualTicketDir) {
9015
- const inputPath = join11(inputsTicketDir, "classifier-input.json");
9016
- const actualPath = join11(actualTicketDir, "classification.json");
9017
- if (!existsSync10(actualPath)) {
9667
+ const inputPath = join14(inputsTicketDir, "classifier-input.json");
9668
+ const actualPath = join14(actualTicketDir, "classification.json");
9669
+ if (!existsSync11(actualPath)) {
9018
9670
  return {
9019
9671
  passed: false,
9020
9672
  checks: ["bucket-match"],
9021
9673
  error: "actual missing: classification.json"
9022
9674
  };
9023
9675
  }
9024
- if (!existsSync10(inputPath)) {
9676
+ if (!existsSync11(inputPath)) {
9025
9677
  return {
9026
9678
  passed: false,
9027
9679
  checks: ["bucket-match"],
9028
9680
  error: "inputs missing: classifier-input.json"
9029
9681
  };
9030
9682
  }
9031
- const golden = JSON.parse(readFileSync7(inputPath, "utf8"));
9032
- const actual = JSON.parse(readFileSync7(actualPath, "utf8"));
9683
+ const golden = JSON.parse(readFileSync9(inputPath, "utf8"));
9684
+ const actual = JSON.parse(readFileSync9(actualPath, "utf8"));
9033
9685
  const goldScens = golden.scenarios ?? [];
9034
9686
  const actScens = actual.scenarios ?? [];
9035
9687
  const mismatches = [];
@@ -9059,11 +9711,11 @@ async function evalDeterministicCmd(argv, opts = {}) {
9059
9711
  return 1;
9060
9712
  }
9061
9713
  const paths = resolveEvalPaths(cwd, runId);
9062
- if (!existsSync10(paths.manifest)) {
9714
+ if (!existsSync11(paths.manifest)) {
9063
9715
  console.error(`[xera:eval-deterministic] missing manifest.json at ${paths.manifest}`);
9064
9716
  return 1;
9065
9717
  }
9066
- const manifest = ManifestSchema.parse(JSON.parse(readFileSync7(paths.manifest, "utf8")));
9718
+ const manifest = ManifestSchema.parse(JSON.parse(readFileSync9(paths.manifest, "utf8")));
9067
9719
  const entries = [];
9068
9720
  for (const [ticket, ticketStages] of Object.entries(manifest.ticket_stages)) {
9069
9721
  for (const stage of ticketStages) {
@@ -9071,7 +9723,7 @@ async function evalDeterministicCmd(argv, opts = {}) {
9071
9723
  const actualDir = paths.ticketActualDir(ticket);
9072
9724
  let result;
9073
9725
  if (stage === "feature-from-story") {
9074
- result = checkFeatureFromStory(join11(actualDir, "test.feature"));
9726
+ result = checkFeatureFromStory(join14(actualDir, "test.feature"));
9075
9727
  } else if (stage === "script-from-feature") {
9076
9728
  result = checkScriptFromFeature(actualDir);
9077
9729
  } else {
@@ -9090,7 +9742,7 @@ async function evalDeterministicCmd(argv, opts = {}) {
9090
9742
  }
9091
9743
  const scores = { run_id: runId, entries };
9092
9744
  DeterministicScoresSchema.parse(scores);
9093
- writeFileSync2(paths.deterministicScores, JSON.stringify(scores, null, 2));
9745
+ writeFileSync4(paths.deterministicScores, JSON.stringify(scores, null, 2));
9094
9746
  console.log(`[xera:eval-deterministic] wrote ${entries.length} entries`);
9095
9747
  return 0;
9096
9748
  }
@@ -9098,13 +9750,13 @@ async function evalDeterministicCmd(argv, opts = {}) {
9098
9750
  // src/bin-internal/eval-prepare.ts
9099
9751
  import {
9100
9752
  copyFileSync,
9101
- existsSync as existsSync12,
9102
- mkdirSync as mkdirSync4,
9753
+ existsSync as existsSync13,
9754
+ mkdirSync as mkdirSync6,
9103
9755
  readdirSync as readdirSync5,
9104
- readFileSync as readFileSync9,
9105
- writeFileSync as writeFileSync4
9756
+ readFileSync as readFileSync11,
9757
+ writeFileSync as writeFileSync6
9106
9758
  } from "fs";
9107
- import { join as join12 } from "path";
9759
+ import { join as join15 } from "path";
9108
9760
 
9109
9761
  // src/eval/run-id.ts
9110
9762
  import { execSync } from "child_process";
@@ -9115,27 +9767,27 @@ function defaultGetGitSha() {
9115
9767
  return null;
9116
9768
  }
9117
9769
  }
9118
- function pad(n) {
9770
+ function pad3(n) {
9119
9771
  return n.toString().padStart(2, "0");
9120
9772
  }
9121
9773
  function generateRunId2(opts = {}) {
9122
9774
  const getGitSha = opts.getGitSha ?? defaultGetGitSha;
9123
9775
  const now = (opts.now ?? (() => new Date))();
9124
- const date = `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}`;
9125
- const time = `${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`;
9776
+ const date = `${now.getUTCFullYear()}${pad3(now.getUTCMonth() + 1)}${pad3(now.getUTCDate())}`;
9777
+ const time = `${pad3(now.getUTCHours())}${pad3(now.getUTCMinutes())}${pad3(now.getUTCSeconds())}`;
9126
9778
  const sha = getGitSha();
9127
9779
  const short = sha ? sha.slice(0, 7) : "nogit";
9128
9780
  return `${date}-${time}-${short}`;
9129
9781
  }
9130
9782
 
9131
9783
  // src/lock/file-lock.ts
9132
- import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
9784
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
9133
9785
  import { hostname } from "os";
9134
9786
  import { dirname as dirname2 } from "path";
9135
9787
  function acquireLock(path, runId) {
9136
- if (existsSync11(path))
9788
+ if (existsSync12(path))
9137
9789
  return false;
9138
- mkdirSync3(dirname2(path), { recursive: true });
9790
+ mkdirSync5(dirname2(path), { recursive: true });
9139
9791
  const data = {
9140
9792
  pid: process.pid,
9141
9793
  hostname: hostname(),
@@ -9143,20 +9795,20 @@ function acquireLock(path, runId) {
9143
9795
  run_id: runId
9144
9796
  };
9145
9797
  try {
9146
- writeFileSync3(path, JSON.stringify(data), { flag: "wx" });
9798
+ writeFileSync5(path, JSON.stringify(data), { flag: "wx" });
9147
9799
  return true;
9148
9800
  } catch {
9149
9801
  return false;
9150
9802
  }
9151
9803
  }
9152
9804
  function releaseLock(path) {
9153
- if (existsSync11(path))
9805
+ if (existsSync12(path))
9154
9806
  unlinkSync(path);
9155
9807
  }
9156
9808
  function readLock(path) {
9157
- if (!existsSync11(path))
9809
+ if (!existsSync12(path))
9158
9810
  return null;
9159
- return JSON.parse(readFileSync8(path, "utf8"));
9811
+ return JSON.parse(readFileSync10(path, "utf8"));
9160
9812
  }
9161
9813
  function isLockStale(path) {
9162
9814
  const lock = readLock(path);
@@ -9197,16 +9849,16 @@ function parseFlags2(argv) {
9197
9849
  return flags;
9198
9850
  }
9199
9851
  function readPromptVersion(repoRoot, name) {
9200
- const path = join12(repoRoot, "packages/prompts", `${name}.md`);
9201
- if (!existsSync12(path))
9852
+ const path = join15(repoRoot, "packages/prompts", `${name}.md`);
9853
+ if (!existsSync13(path))
9202
9854
  return "0.0.0";
9203
- const text = readFileSync9(path, "utf8");
9855
+ const text = readFileSync11(path, "utf8");
9204
9856
  const m = /^version:\s*(\S+)\s*$/m.exec(text);
9205
9857
  return m?.[1] ?? "0.0.0";
9206
9858
  }
9207
9859
  function discoverEvalTickets(repoRoot) {
9208
- const root = join12(repoRoot, "fixtures/golden-eval");
9209
- if (!existsSync12(root))
9860
+ const root = join15(repoRoot, "fixtures/golden-eval");
9861
+ if (!existsSync13(root))
9210
9862
  return [];
9211
9863
  const out = [];
9212
9864
  for (const entry of readdirSync5(root, { withFileTypes: true })) {
@@ -9214,25 +9866,25 @@ function discoverEvalTickets(repoRoot) {
9214
9866
  continue;
9215
9867
  if (entry.name === "README.md" || entry.name.startsWith("."))
9216
9868
  continue;
9217
- const dir = join12(root, entry.name);
9218
- const metaPath = join12(dir, "meta.json");
9219
- if (!existsSync12(metaPath))
9869
+ const dir = join15(root, entry.name);
9870
+ const metaPath = join15(dir, "meta.json");
9871
+ if (!existsSync13(metaPath))
9220
9872
  continue;
9221
- const meta = JSON.parse(readFileSync9(metaPath, "utf8"));
9873
+ const meta = JSON.parse(readFileSync11(metaPath, "utf8"));
9222
9874
  out.push({ id: meta.id, dir, stages: meta.stages });
9223
9875
  }
9224
9876
  return out.sort((a, b) => a.id.localeCompare(b.id));
9225
9877
  }
9226
9878
  function discoverClassifierTickets(repoRoot) {
9227
- const root = join12(repoRoot, "fixtures/golden-tickets");
9228
- if (!existsSync12(root))
9879
+ const root = join15(repoRoot, "fixtures/golden-tickets");
9880
+ if (!existsSync13(root))
9229
9881
  return [];
9230
9882
  const out = [];
9231
9883
  for (const entry of readdirSync5(root, { withFileTypes: true })) {
9232
9884
  if (!entry.isFile() || !entry.name.endsWith(".json"))
9233
9885
  continue;
9234
- const path = join12(root, entry.name);
9235
- const data = JSON.parse(readFileSync9(path, "utf8"));
9886
+ const path = join15(root, entry.name);
9887
+ const data = JSON.parse(readFileSync11(path, "utf8"));
9236
9888
  if (typeof data.ticket === "string")
9237
9889
  out.push({ id: data.ticket, path });
9238
9890
  }
@@ -9291,25 +9943,25 @@ async function evalPrepareCmd(argv, opts = {}) {
9291
9943
  ...opts.getGitSha ? { getGitSha: opts.getGitSha } : {}
9292
9944
  });
9293
9945
  const paths = resolveEvalPaths(repoRoot, runId);
9294
- if (existsSync12(paths.root) && !flags.force) {
9946
+ if (existsSync13(paths.root) && !flags.force) {
9295
9947
  console.error(`[xera:eval-prepare] run dir already exists: ${paths.root}. Pass --force to re-run.`);
9296
9948
  return 1;
9297
9949
  }
9298
- mkdirSync4(paths.inputsDir, { recursive: true });
9299
- mkdirSync4(paths.actualDir, { recursive: true });
9950
+ mkdirSync6(paths.inputsDir, { recursive: true });
9951
+ mkdirSync6(paths.actualDir, { recursive: true });
9300
9952
  for (const ticket of selectedTickets) {
9301
9953
  const ticketInputs = paths.ticketInputsDir(ticket);
9302
- mkdirSync4(ticketInputs, { recursive: true });
9954
+ mkdirSync6(ticketInputs, { recursive: true });
9303
9955
  const evalT = evalTickets.find((t) => t.id === ticket);
9304
9956
  const classT = classifierTickets.find((t) => t.id === ticket);
9305
9957
  if (evalT) {
9306
- copyFileSync(join12(evalT.dir, "story.md"), join12(ticketInputs, "story.md"));
9307
- const featurePath = join12(evalT.dir, "golden/test.feature");
9308
- if (existsSync12(featurePath))
9309
- copyFileSync(featurePath, join12(ticketInputs, "test.feature"));
9958
+ copyFileSync(join15(evalT.dir, "story.md"), join15(ticketInputs, "story.md"));
9959
+ const featurePath = join15(evalT.dir, "golden/test.feature");
9960
+ if (existsSync13(featurePath))
9961
+ copyFileSync(featurePath, join15(ticketInputs, "test.feature"));
9310
9962
  }
9311
9963
  if (classT) {
9312
- copyFileSync(classT.path, join12(ticketInputs, "classifier-input.json"));
9964
+ copyFileSync(classT.path, join15(ticketInputs, "classifier-input.json"));
9313
9965
  }
9314
9966
  }
9315
9967
  const now = (opts.now ?? (() => new Date))();
@@ -9334,7 +9986,7 @@ async function evalPrepareCmd(argv, opts = {}) {
9334
9986
  }
9335
9987
  };
9336
9988
  ManifestSchema.parse(manifest);
9337
- writeFileSync4(paths.manifest, JSON.stringify(manifest, null, 2));
9989
+ writeFileSync6(paths.manifest, JSON.stringify(manifest, null, 2));
9338
9990
  if (!acquireLock(paths.lock, runId)) {
9339
9991
  console.error(`[xera:eval-prepare] failed to acquire lock at ${paths.lock}`);
9340
9992
  return 4;
@@ -9345,7 +9997,7 @@ async function evalPrepareCmd(argv, opts = {}) {
9345
9997
  }
9346
9998
 
9347
9999
  // src/bin-internal/eval-report.ts
9348
- import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
10000
+ import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
9349
10001
  function scoreJudgment(j) {
9350
10002
  const nonNa = j.dimensions.filter((d) => d.verdict !== "NA");
9351
10003
  if (nonNa.length === 0)
@@ -9400,22 +10052,22 @@ async function evalReportCmd(argv, opts = {}) {
9400
10052
  return 1;
9401
10053
  }
9402
10054
  const paths = resolveEvalPaths(cwd, runId);
9403
- if (!existsSync13(paths.manifest)) {
10055
+ if (!existsSync14(paths.manifest)) {
9404
10056
  console.error(`[xera:eval-report] missing manifest.json at ${paths.manifest}`);
9405
10057
  return 1;
9406
10058
  }
9407
- const manifest = ManifestSchema.parse(JSON.parse(readFileSync10(paths.manifest, "utf8")));
10059
+ const manifest = ManifestSchema.parse(JSON.parse(readFileSync12(paths.manifest, "utf8")));
9408
10060
  try {
9409
10061
  let det;
9410
10062
  let judge;
9411
10063
  try {
9412
- det = DeterministicScoresSchema.parse(JSON.parse(readFileSync10(paths.deterministicScores, "utf8")));
10064
+ det = DeterministicScoresSchema.parse(JSON.parse(readFileSync12(paths.deterministicScores, "utf8")));
9413
10065
  } catch (err) {
9414
10066
  console.error(`[xera:eval-report] invalid deterministic-scores.json: ${err.message}`);
9415
10067
  return 2;
9416
10068
  }
9417
10069
  try {
9418
- judge = JudgeScoresSchema.parse(JSON.parse(readFileSync10(paths.judgeScores, "utf8")));
10070
+ judge = JudgeScoresSchema.parse(JSON.parse(readFileSync12(paths.judgeScores, "utf8")));
9419
10071
  } catch (err) {
9420
10072
  console.error(`[xera:eval-report] invalid judge-scores.json: ${err.message}`);
9421
10073
  return 2;
@@ -9477,8 +10129,8 @@ async function evalReportCmd(argv, opts = {}) {
9477
10129
  overall: { passed: passedCount, failed: failedCount, total: counted.length, score: avgScore }
9478
10130
  };
9479
10131
  SummarySchema.parse(summary);
9480
- writeFileSync5(paths.summary, JSON.stringify(summary, null, 2));
9481
- writeFileSync5(paths.report, renderReport(summary));
10132
+ writeFileSync7(paths.summary, JSON.stringify(summary, null, 2));
10133
+ writeFileSync7(paths.report, renderReport(summary));
9482
10134
  console.log(`[xera:eval-report] ${passedCount}/${counted.length} PASS (avg ${(avgScore * 100).toFixed(0)}%)`);
9483
10135
  return 0;
9484
10136
  } finally {
@@ -9487,37 +10139,37 @@ async function evalReportCmd(argv, opts = {}) {
9487
10139
  }
9488
10140
 
9489
10141
  // src/bin-internal/exec.ts
9490
- import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
9491
- import { join as join14 } from "path";
10142
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10 } from "fs";
10143
+ import { join as join17 } from "path";
9492
10144
  import { chromium } from "@playwright/test";
9493
10145
  import { runAuthSetup, runPlaywright, stagePlaywrightState } from "@xera-ai/web";
9494
10146
 
9495
10147
  // src/artifact/meta.ts
9496
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
10148
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
9497
10149
  import { dirname as dirname3 } from "path";
9498
- import { z as z4 } from "zod";
9499
- var MetaJsonSchema = z4.object({
9500
- ticket: z4.string(),
9501
- adapter: z4.string(),
9502
- xera_version: z4.string(),
9503
- prompts_version: z4.string(),
9504
- fetched_at: z4.string().optional(),
9505
- story_hash: z4.string().optional(),
9506
- feature_generated_at: z4.string().optional(),
9507
- feature_generated_from_story_hash: z4.string().optional(),
9508
- feature_hash: z4.string().optional(),
9509
- script_generated_at: z4.string().optional(),
9510
- script_generated_from_feature_hash: z4.string().optional(),
9511
- script_warnings: z4.array(z4.string()).optional()
10150
+ import { z as z5 } from "zod";
10151
+ var MetaJsonSchema = z5.object({
10152
+ ticket: z5.string(),
10153
+ adapter: z5.string(),
10154
+ xera_version: z5.string(),
10155
+ prompts_version: z5.string(),
10156
+ fetched_at: z5.string().optional(),
10157
+ story_hash: z5.string().optional(),
10158
+ feature_generated_at: z5.string().optional(),
10159
+ feature_generated_from_story_hash: z5.string().optional(),
10160
+ feature_hash: z5.string().optional(),
10161
+ script_generated_at: z5.string().optional(),
10162
+ script_generated_from_feature_hash: z5.string().optional(),
10163
+ script_warnings: z5.array(z5.string()).optional()
9512
10164
  });
9513
10165
  function readMeta(path) {
9514
- if (!existsSync14(path))
10166
+ if (!existsSync15(path))
9515
10167
  return null;
9516
- return MetaJsonSchema.parse(JSON.parse(readFileSync11(path, "utf8")));
10168
+ return MetaJsonSchema.parse(JSON.parse(readFileSync13(path, "utf8")));
9517
10169
  }
9518
10170
  function writeMeta(path, meta) {
9519
- mkdirSync5(dirname3(path), { recursive: true });
9520
- writeFileSync6(path, JSON.stringify(meta, null, 2));
10171
+ mkdirSync7(dirname3(path), { recursive: true });
10172
+ writeFileSync8(path, JSON.stringify(meta, null, 2));
9521
10173
  }
9522
10174
  function updateMeta(path, patch) {
9523
10175
  const existing = readMeta(path);
@@ -9561,9 +10213,9 @@ function needsRefresh(entry, policy, now = new Date) {
9561
10213
  }
9562
10214
 
9563
10215
  // src/auth/state.ts
9564
- import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
9565
- import { join as join13 } from "path";
9566
- import { z as z5 } from "zod";
10216
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
10217
+ import { join as join16 } from "path";
10218
+ import { z as z6 } from "zod";
9567
10219
 
9568
10220
  // src/auth/encrypt.ts
9569
10221
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
@@ -9620,39 +10272,39 @@ function resolveAuthKey() {
9620
10272
  }
9621
10273
 
9622
10274
  // src/auth/state.ts
9623
- var AuthStateEntrySchema = z5.object({
9624
- role: z5.string(),
9625
- strategy: z5.enum(["storageState", "apiToken"]),
9626
- created_at: z5.string(),
9627
- expires_at: z5.string(),
9628
- payload: z5.record(z5.string(), z5.unknown())
10275
+ var AuthStateEntrySchema = z6.object({
10276
+ role: z6.string(),
10277
+ strategy: z6.enum(["storageState", "apiToken"]),
10278
+ created_at: z6.string(),
10279
+ expires_at: z6.string(),
10280
+ payload: z6.record(z6.string(), z6.unknown())
9629
10281
  });
9630
10282
  function pathFor(authDir, role) {
9631
- return join13(authDir, `${role}.json`);
10283
+ return join16(authDir, `${role}.json`);
9632
10284
  }
9633
10285
  function writeAuthState(authDir, entry) {
9634
- mkdirSync6(authDir, { recursive: true });
10286
+ mkdirSync8(authDir, { recursive: true });
9635
10287
  const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
9636
- writeFileSync7(pathFor(authDir, entry.role), ct);
10288
+ writeFileSync9(pathFor(authDir, entry.role), ct);
9637
10289
  }
9638
10290
  function readAuthState(authDir, role) {
9639
10291
  const p = pathFor(authDir, role);
9640
- if (!existsSync15(p))
10292
+ if (!existsSync16(p))
9641
10293
  return null;
9642
- const txt = readFileSync12(p, "utf8");
10294
+ const txt = readFileSync14(p, "utf8");
9643
10295
  const plain = decrypt(txt, resolveAuthKey());
9644
10296
  return AuthStateEntrySchema.parse(JSON.parse(plain));
9645
10297
  }
9646
10298
 
9647
10299
  // src/logging/ndjson-logger.ts
9648
- import { appendFileSync as appendFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync13 } from "fs";
10300
+ import { appendFileSync as appendFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync15 } from "fs";
9649
10301
  import { dirname as dirname4 } from "path";
9650
10302
 
9651
10303
  class NdjsonLogger {
9652
10304
  path;
9653
10305
  constructor(path) {
9654
10306
  this.path = path;
9655
- mkdirSync7(dirname4(path), { recursive: true });
10307
+ mkdirSync9(dirname4(path), { recursive: true });
9656
10308
  }
9657
10309
  log(payload) {
9658
10310
  const entry = { ts: new Date().toISOString(), ...payload };
@@ -9660,9 +10312,9 @@ class NdjsonLogger {
9660
10312
  `);
9661
10313
  }
9662
10314
  static readAll(path) {
9663
- if (!existsSync16(path))
10315
+ if (!existsSync17(path))
9664
10316
  return [];
9665
- const txt = readFileSync13(path, "utf8").trim();
10317
+ const txt = readFileSync15(path, "utf8").trim();
9666
10318
  if (!txt)
9667
10319
  return [];
9668
10320
  return txt.split(`
@@ -9742,7 +10394,7 @@ async function execCmd(argv) {
9742
10394
  await runAuthSetup({
9743
10395
  role: roleName,
9744
10396
  creds: { email, password },
9745
- setupScriptPath: join14(cwd, webConfig.auth.setupScript),
10397
+ setupScriptPath: join17(cwd, webConfig.auth.setupScript),
9746
10398
  authDir: paths.authDir,
9747
10399
  browser
9748
10400
  });
@@ -9760,16 +10412,16 @@ async function execCmd(argv) {
9760
10412
  }
9761
10413
  }
9762
10414
  }
9763
- const cfgPath = join14(cwd, "playwright.config.ts");
9764
- if (!existsSync17(cfgPath)) {
10415
+ const cfgPath = join17(cwd, "playwright.config.ts");
10416
+ if (!existsSync18(cfgPath)) {
9765
10417
  console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
9766
10418
  return 1;
9767
10419
  }
9768
10420
  const runDir = paths.runPath(runId).runDir;
9769
- mkdirSync8(runDir, { recursive: true });
10421
+ mkdirSync10(runDir, { recursive: true });
9770
10422
  const envName = process.env.XERA_ENV ?? webConfig.defaultEnv;
9771
10423
  const baseURL = webConfig.baseUrl[envName] ?? webConfig.baseUrl[webConfig.defaultEnv];
9772
- const reportJsonPath = join14(runDir, "report.json");
10424
+ const reportJsonPath = join17(runDir, "report.json");
9773
10425
  log.log({ step: "exec.start", runId, env: envName, baseURL });
9774
10426
  const r = await runPlaywright({
9775
10427
  specPath: paths.specPath,
@@ -9791,20 +10443,20 @@ async function execCmd(argv) {
9791
10443
  }
9792
10444
 
9793
10445
  // src/bin-internal/fetch.ts
9794
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
10446
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
9795
10447
  import { dirname as dirname5 } from "path";
9796
10448
 
9797
10449
  // src/artifact/hash.ts
9798
10450
  import { createHash as createHash4 } from "crypto";
9799
- import { existsSync as existsSync18, readFileSync as readFileSync14 } from "fs";
10451
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
9800
10452
  function hashString(s) {
9801
10453
  return `sha256:${createHash4("sha256").update(s).digest("hex")}`;
9802
10454
  }
9803
10455
  function hashFile(path) {
9804
- return hashString(readFileSync14(path, "utf8"));
10456
+ return hashString(readFileSync16(path, "utf8"));
9805
10457
  }
9806
10458
  function hashFileIfExists(path) {
9807
- if (!existsSync18(path))
10459
+ if (!existsSync19(path))
9808
10460
  return null;
9809
10461
  return hashFile(path);
9810
10462
  }
@@ -9813,33 +10465,33 @@ function hashFileIfExists(path) {
9813
10465
  init_paths2();
9814
10466
 
9815
10467
  // src/jira/mcp-backend.ts
9816
- import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
10468
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
9817
10469
  import { tmpdir } from "os";
9818
- import { join as join15 } from "path";
10470
+ import { join as join18 } from "path";
9819
10471
  var MCP_ENV = "XERA_MCP_JIRA";
9820
10472
  async function createMcpBackend(_baseUrl) {
9821
10473
  if (process.env[MCP_ENV] !== "1")
9822
10474
  return null;
9823
- const tmpDir = join15(tmpdir(), "xera-mcp");
9824
- mkdirSync9(tmpDir, { recursive: true });
10475
+ const tmpDir = join18(tmpdir(), "xera-mcp");
10476
+ mkdirSync11(tmpDir, { recursive: true });
9825
10477
  return {
9826
10478
  backend: "mcp",
9827
10479
  async fetchTicket(key, _fields) {
9828
- const cachePath = join15(tmpDir, `${key}.json`);
9829
- if (!existsSync19(cachePath)) {
10480
+ const cachePath = join18(tmpDir, `${key}.json`);
10481
+ if (!existsSync20(cachePath)) {
9830
10482
  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.`);
9831
10483
  }
9832
- const parsed = JSON.parse(readFileSync15(cachePath, "utf8"));
10484
+ const parsed = JSON.parse(readFileSync17(cachePath, "utf8"));
9833
10485
  return parsed;
9834
10486
  },
9835
10487
  async postComment(key, body) {
9836
- const outPath = join15(tmpDir, `${key}.comment.json`);
9837
- writeFileSync8(outPath, JSON.stringify({ key, body }));
10488
+ const outPath = join18(tmpDir, `${key}.comment.json`);
10489
+ writeFileSync10(outPath, JSON.stringify({ key, body }));
9838
10490
  return { id: "mcp-pending" };
9839
10491
  },
9840
10492
  async transitionStatus(key, statusName) {
9841
- const outPath = join15(tmpDir, `${key}.transition.json`);
9842
- writeFileSync8(outPath, JSON.stringify({ key, statusName }));
10493
+ const outPath = join18(tmpDir, `${key}.transition.json`);
10494
+ writeFileSync10(outPath, JSON.stringify({ key, statusName }));
9843
10495
  },
9844
10496
  async listFields(_sampleKey) {
9845
10497
  throw new Error("listFields is REST-only; init flow uses REST for field discovery.");
@@ -9992,8 +10644,8 @@ async function fetchCmd(argv, opts = {}) {
9992
10644
  const storyHash = hashString(body);
9993
10645
  const acLines = parseAcLines(t.acceptanceCriteria);
9994
10646
  const full = renderStory(t.key, t.summary, storyHash, acLines, body);
9995
- mkdirSync10(dirname5(paths.storyPath), { recursive: true });
9996
- writeFileSync9(paths.storyPath, full);
10647
+ mkdirSync12(dirname5(paths.storyPath), { recursive: true });
10648
+ writeFileSync11(paths.storyPath, full);
9997
10649
  const existing = readMeta(paths.metaPath);
9998
10650
  writeMeta(paths.metaPath, {
9999
10651
  ticket,
@@ -10053,24 +10705,223 @@ function renderStory(key, summary, storyHash, acLines, body) {
10053
10705
  `) + body;
10054
10706
  }
10055
10707
 
10708
+ // src/bin-internal/fill-gap-finalize.ts
10709
+ import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
10710
+ import { join as join19 } from "path";
10711
+ import { z as z7 } from "zod";
10712
+ var ProposalsSchema = z7.object({
10713
+ proposals: z7.array(z7.object({
10714
+ id: z7.string().min(1),
10715
+ ticketId: z7.string().min(1),
10716
+ title: z7.string().min(1),
10717
+ rationale: z7.string().min(1),
10718
+ gherkin: z7.string().min(1),
10719
+ satisfiesAcs: z7.array(z7.number().int().nonnegative())
10720
+ }))
10721
+ });
10722
+ function parseArgs4(argv) {
10723
+ let accept;
10724
+ let ticket;
10725
+ let source;
10726
+ let force = false;
10727
+ for (let i = 0;i < argv.length; i++) {
10728
+ const a = argv[i];
10729
+ if (a === "--accept") {
10730
+ const v = argv[++i];
10731
+ if (v !== undefined)
10732
+ accept = v;
10733
+ } else if (a === "--ticket") {
10734
+ const v = argv[++i];
10735
+ if (v !== undefined)
10736
+ ticket = v;
10737
+ } else if (a === "--source") {
10738
+ const v = argv[++i];
10739
+ if (v !== undefined)
10740
+ source = v;
10741
+ } else if (a === "--force") {
10742
+ force = true;
10743
+ } else if (a === "--help-stub") {} else {
10744
+ return { error: `unknown flag: ${a}` };
10745
+ }
10746
+ }
10747
+ if (!accept || !ticket)
10748
+ return { error: "required: --accept <proposal-id> --ticket <TICKET>" };
10749
+ const out = { accept, ticket, force };
10750
+ if (source !== undefined)
10751
+ out.source = source;
10752
+ return out;
10753
+ }
10754
+ function formatDraft(ticketId, proposal) {
10755
+ const lines = [
10756
+ `# Draft scenario for ${ticketId}`,
10757
+ "",
10758
+ `> ${proposal.rationale}`,
10759
+ "",
10760
+ proposal.gherkin,
10761
+ ""
10762
+ ];
10763
+ if (proposal.satisfiesAcs.length > 0) {
10764
+ lines.push(`<!-- satisfiesAcs: [${proposal.satisfiesAcs.join(", ")}] -->`);
10765
+ lines.push("");
10766
+ }
10767
+ return lines.join(`
10768
+ `);
10769
+ }
10770
+ async function fillGapFinalizeCmd(argv) {
10771
+ if (argv.includes("--help-stub")) {}
10772
+ const parsed = parseArgs4(argv);
10773
+ if ("error" in parsed) {
10774
+ console.error(`[fill-gap-finalize] ${parsed.error}`);
10775
+ return 1;
10776
+ }
10777
+ const cwd = process.cwd();
10778
+ const sourcePath = parsed.source ?? join19(cwd, ".xera/coverage/proposals.json");
10779
+ if (!existsSync21(sourcePath)) {
10780
+ console.error(`[fill-gap-finalize] source not found: ${sourcePath}`);
10781
+ return 2;
10782
+ }
10783
+ let proposals;
10784
+ try {
10785
+ const raw = JSON.parse(readFileSync18(sourcePath, "utf8"));
10786
+ proposals = ProposalsSchema.parse(raw);
10787
+ } catch (e) {
10788
+ console.error(`[fill-gap-finalize] invalid proposals: ${e.message}`);
10789
+ return 2;
10790
+ }
10791
+ const proposal = proposals.proposals.find((p) => p.id === parsed.accept);
10792
+ if (!proposal) {
10793
+ console.error(`[fill-gap-finalize] proposal id "${parsed.accept}" not in source`);
10794
+ return 2;
10795
+ }
10796
+ const ticketDir = join19(cwd, ".xera", parsed.ticket);
10797
+ mkdirSync13(ticketDir, { recursive: true });
10798
+ const draftPath = join19(ticketDir, "feature.draft.md");
10799
+ if (existsSync21(draftPath) && !parsed.force) {
10800
+ console.error(`[fill-gap-finalize] ${draftPath} exists; pass --force to overwrite`);
10801
+ return 3;
10802
+ }
10803
+ writeFileSync12(draftPath, formatDraft(parsed.ticket, proposal));
10804
+ return 0;
10805
+ }
10806
+
10807
+ // src/bin-internal/fill-gap-prepare.ts
10808
+ init_store();
10809
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
10810
+ import { join as join20 } from "path";
10811
+ function parseArgs5(argv) {
10812
+ const args = {};
10813
+ for (let i = 0;i < argv.length; i++) {
10814
+ const a = argv[i];
10815
+ if (a === "--area") {
10816
+ const v = argv[++i];
10817
+ if (v !== undefined)
10818
+ args.area = v;
10819
+ } else if (a === "--ticket") {
10820
+ const v = argv[++i];
10821
+ if (v !== undefined)
10822
+ args.ticket = v;
10823
+ } else if (a === "--output-dir") {
10824
+ const v = argv[++i];
10825
+ if (v !== undefined)
10826
+ args.outputDir = v;
10827
+ } else if (a === "--help-stub") {} else {
10828
+ return { error: `unknown flag: ${a}` };
10829
+ }
10830
+ }
10831
+ if (!args.area && !args.ticket)
10832
+ return { error: "one of --area or --ticket required" };
10833
+ if (args.area && args.ticket)
10834
+ return { error: "--area and --ticket are mutually exclusive" };
10835
+ return args;
10836
+ }
10837
+ function buildAreaContext(snap, area) {
10838
+ const edgeTicketIds = new Set(snap.edges.filter((e) => e.kind === "modifies" && e.to === area).map((e) => e.from));
10839
+ const ticketsForArea = Object.values(snap.tickets).filter((t) => t.modifiesAreas.includes(area) || edgeTicketIds.has(t.id));
10840
+ if (ticketsForArea.length === 0)
10841
+ return null;
10842
+ const scenariosFromOtherAreas = Object.values(snap.scenarios).filter((s) => {
10843
+ const t = snap.tickets[s.ticketId];
10844
+ if (!t)
10845
+ return false;
10846
+ return !t.modifiesAreas.includes(area) && t.modifiesAreas.length > 0;
10847
+ }).slice(0, 3).map((s) => {
10848
+ const ownerTicket = snap.tickets[s.ticketId];
10849
+ const areaSlug = ownerTicket?.modifiesAreas[0] ?? "unknown";
10850
+ return { areaSlug, gherkin: s.gherkin };
10851
+ });
10852
+ return {
10853
+ mode: "area",
10854
+ area,
10855
+ tickets: ticketsForArea.map((t) => ({ id: t.id, summary: t.summary, ac: t.ac })),
10856
+ existingScenarios: scenariosFromOtherAreas
10857
+ };
10858
+ }
10859
+ function buildTicketContext(snap, ticketId) {
10860
+ const ticket = snap.tickets[ticketId];
10861
+ if (!ticket)
10862
+ return null;
10863
+ const acNodesForTicket = Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticketId).sort((a, b) => a.index - b.index);
10864
+ const satisfiedAcIds = new Set(snap.edges.filter((e) => e.kind === "satisfies" && acNodesForTicket.some((ac) => ac.id === e.to)).map((e) => e.to));
10865
+ const unsatisfiedAcs = acNodesForTicket.filter((ac) => !satisfiedAcIds.has(ac.id)).map((ac) => ({ index: ac.index, text: ac.text }));
10866
+ if (unsatisfiedAcs.length === 0)
10867
+ return null;
10868
+ const existingScenarios = Object.values(snap.scenarios).filter((s) => s.ticketId === ticketId).map((s) => ({ scenarioId: s.id, name: s.name, gherkin: s.gherkin }));
10869
+ return {
10870
+ mode: "ticket",
10871
+ ticket: { id: ticket.id, summary: ticket.summary, ac: ticket.ac },
10872
+ unsatisfiedAcs,
10873
+ existingScenarios
10874
+ };
10875
+ }
10876
+ async function fillGapPrepareCmd(argv) {
10877
+ const parsed = parseArgs5(argv);
10878
+ if ("error" in parsed) {
10879
+ console.error(`[fill-gap-prepare] ${parsed.error}`);
10880
+ return 1;
10881
+ }
10882
+ const cwd = process.cwd();
10883
+ const snap = deriveSnapshot(loadAllEvents(cwd));
10884
+ let context;
10885
+ let scope;
10886
+ if (parsed.area) {
10887
+ context = buildAreaContext(snap, parsed.area);
10888
+ scope = parsed.area;
10889
+ if (!context) {
10890
+ console.error(`[fill-gap-prepare] area "${parsed.area}" has no tickets modifying it; cannot fill`);
10891
+ return 2;
10892
+ }
10893
+ } else {
10894
+ context = buildTicketContext(snap, parsed.ticket);
10895
+ scope = parsed.ticket;
10896
+ if (!context) {
10897
+ console.error(`[fill-gap-prepare] ticket "${parsed.ticket}" not found or has no unsatisfied ACs`);
10898
+ return 2;
10899
+ }
10900
+ }
10901
+ const outDir = parsed.outputDir ?? join20(cwd, ".xera/coverage", scope);
10902
+ mkdirSync14(outDir, { recursive: true });
10903
+ writeFileSync13(join20(outDir, "context.json"), JSON.stringify(context, null, 2));
10904
+ return 0;
10905
+ }
10906
+
10056
10907
  // src/bin-internal/index.ts
10057
10908
  init_graph_backfill();
10058
10909
 
10059
10910
  // src/graph/enrich.ts
10060
10911
  init_store();
10061
10912
  init_ulid();
10062
- import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
10063
- import { join as join16 } from "path";
10064
- import { z as z6 } from "zod";
10913
+ import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
10914
+ import { join as join21 } from "path";
10915
+ import { z as z8 } from "zod";
10065
10916
  var MAX_SIMILAR_EDGES = 10;
10066
10917
  var MIN_CONFIDENCE = 0.7;
10067
- var SimilarEntrySchema = z6.object({
10068
- ticketId: z6.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
10069
- confidence: z6.number(),
10070
- reason: z6.string()
10918
+ var SimilarEntrySchema = z8.object({
10919
+ ticketId: z8.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
10920
+ confidence: z8.number(),
10921
+ reason: z8.string()
10071
10922
  });
10072
- var EnrichmentInputSchema = z6.object({
10073
- similar: z6.array(SimilarEntrySchema)
10923
+ var EnrichmentInputSchema = z8.object({
10924
+ similar: z8.array(SimilarEntrySchema)
10074
10925
  });
10075
10926
  var nowIso3 = () => new Date().toISOString();
10076
10927
  var mk2 = (actor, type, payload) => ({
@@ -10082,11 +10933,11 @@ var mk2 = (actor, type, payload) => ({
10082
10933
  payload
10083
10934
  });
10084
10935
  async function enrichTicket(repoRoot, ticketId, opts) {
10085
- const inputPath = join16(repoRoot, ".xera", ticketId, "enrichment-input.json");
10086
- if (!existsSync20(inputPath)) {
10936
+ const inputPath = join21(repoRoot, ".xera", ticketId, "enrichment-input.json");
10937
+ if (!existsSync22(inputPath)) {
10087
10938
  throw new Error(`enrichment-input.json not found at ${inputPath}`);
10088
10939
  }
10089
- const raw = JSON.parse(readFileSync16(inputPath, "utf8"));
10940
+ const raw = JSON.parse(readFileSync19(inputPath, "utf8"));
10090
10941
  const parsed = EnrichmentInputSchema.safeParse(raw);
10091
10942
  if (!parsed.success) {
10092
10943
  throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
@@ -10195,12 +11046,12 @@ async function graphQueryCmd(argv) {
10195
11046
  init_graph_record();
10196
11047
 
10197
11048
  // src/bin-internal/graph-render.ts
10198
- import { mkdirSync as mkdirSync11, renameSync as renameSync2, writeFileSync as writeFileSync10 } from "fs";
10199
- import { dirname as dirname7, join as join18 } from "path";
11049
+ import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync21, renameSync as renameSync2, writeFileSync as writeFileSync14 } from "fs";
11050
+ import { dirname as dirname7, join as join23 } from "path";
10200
11051
 
10201
11052
  // src/graph/render.ts
10202
- import { readFileSync as readFileSync17 } from "fs";
10203
- import { dirname as dirname6, join as join17 } from "path";
11053
+ import { readFileSync as readFileSync20 } from "fs";
11054
+ import { dirname as dirname6, join as join22 } from "path";
10204
11055
  import { fileURLToPath } from "url";
10205
11056
  var COLORS = {
10206
11057
  ticket: "#3B82F6",
@@ -10419,9 +11270,9 @@ function transformForVisNetwork(snap, opts) {
10419
11270
  }
10420
11271
  var __filename2 = fileURLToPath(import.meta.url);
10421
11272
  var __dirname2 = dirname6(__filename2);
10422
- var TEMPLATES_DIR = join17(__dirname2, "templates");
11273
+ var TEMPLATES_DIR = join22(__dirname2, "templates");
10423
11274
  function loadTemplate(name) {
10424
- return readFileSync17(join17(TEMPLATES_DIR, name), "utf8");
11275
+ return readFileSync20(join22(TEMPLATES_DIR, name), "utf8");
10425
11276
  }
10426
11277
  function statsToHuman(s) {
10427
11278
  return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
@@ -10433,7 +11284,10 @@ function renderHtml(input) {
10433
11284
  const visNetwork = loadTemplate("vis-network.min.js");
10434
11285
  const graphJson = JSON.stringify(input.data);
10435
11286
  const statsHuman = statsToHuman(input.stats);
10436
- 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);
11287
+ const coverageTabButton = input.coverage ? '<button data-tab="coverage">Coverage</button>' : "";
11288
+ const coverageTabPanel = input.coverage ? loadTemplate("coverage-panel.html.fragment") : "";
11289
+ const coverageJson = input.coverage ? JSON.stringify(input.coverage) : "null";
11290
+ 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).replace("{{COVERAGE_TAB_BUTTON}}", () => coverageTabButton).replace("{{COVERAGE_TAB_PANEL}}", () => coverageTabPanel).replace("{{COVERAGE_DATA}}", () => coverageJson);
10437
11291
  }
10438
11292
 
10439
11293
  // src/bin-internal/graph-render.ts
@@ -10456,6 +11310,7 @@ async function graphRenderCmd(argv) {
10456
11310
  let ticketId;
10457
11311
  let since;
10458
11312
  let depth = 2;
11313
+ let includeCoverage = false;
10459
11314
  for (let i = 0;i < argv.length; i++) {
10460
11315
  if (argv[i] === "--out")
10461
11316
  outPath = argv[++i];
@@ -10465,16 +11320,19 @@ async function graphRenderCmd(argv) {
10465
11320
  since = argv[++i];
10466
11321
  else if (argv[i] === "--depth")
10467
11322
  depth = parseDepth(argv[++i]);
11323
+ else if (argv[i] === "--include-coverage")
11324
+ includeCoverage = true;
10468
11325
  }
10469
11326
  const repoRoot = process.cwd();
10470
- const finalPath = outPath ?? join18(repoRoot, ".xera/graph.html");
10471
- const snap = deriveSnapshot(loadAllEvents(repoRoot));
11327
+ const finalPath = outPath ?? join23(repoRoot, ".xera/graph.html");
11328
+ const events = loadAllEvents(repoRoot);
11329
+ const snap = deriveSnapshot(events);
10472
11330
  const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
10473
11331
  const performanceMode = decidePerformanceMode(totalNodeCount);
10474
11332
  if (performanceMode === "text-fallback") {
10475
11333
  const txtPath = finalPath.replace(/\.html$/, ".txt");
10476
- mkdirSync11(dirname7(txtPath), { recursive: true });
10477
- writeFileSync10(txtPath, `Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.
11334
+ mkdirSync15(dirname7(txtPath), { recursive: true });
11335
+ writeFileSync14(txtPath, `Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.
10478
11336
  `);
10479
11337
  console.log(`[graph-render] graph too large (${totalNodeCount} nodes); wrote ${txtPath}`);
10480
11338
  return 0;
@@ -10484,11 +11342,29 @@ async function graphRenderCmd(argv) {
10484
11342
  opts.ticketId = ticketId;
10485
11343
  if (since)
10486
11344
  opts.since = since;
11345
+ let coverage;
11346
+ if (includeCoverage) {
11347
+ const reportPath = join23(repoRoot, ".xera/coverage/report.json");
11348
+ if (existsSync23(reportPath)) {
11349
+ const report = JSON.parse(readFileSync21(reportPath, "utf8"));
11350
+ const snapshots = events.filter((e) => e.type === "coverage.snapshot").map((e) => e.payload);
11351
+ coverage = { report, snapshots };
11352
+ } else {
11353
+ console.warn("[graph-render] --include-coverage: report.json not found; run /xera-coverage first");
11354
+ }
11355
+ }
10487
11356
  const data = transformForVisNetwork(snap, opts);
10488
- const html = renderHtml({ data, stats: data.stats, generatedAt: new Date().toISOString() });
10489
- mkdirSync11(dirname7(finalPath), { recursive: true });
11357
+ const renderInput = {
11358
+ data,
11359
+ stats: data.stats,
11360
+ generatedAt: new Date().toISOString()
11361
+ };
11362
+ if (coverage)
11363
+ renderInput.coverage = coverage;
11364
+ const html = renderHtml(renderInput);
11365
+ mkdirSync15(dirname7(finalPath), { recursive: true });
10490
11366
  const tmpPath = `${finalPath}.tmp`;
10491
- writeFileSync10(tmpPath, html);
11367
+ writeFileSync14(tmpPath, html);
10492
11368
  renameSync2(tmpPath, finalPath);
10493
11369
  console.log(`[graph-render] wrote ${finalPath} (${data.stats.tickets} tickets \xB7 ${data.stats.scenarios} scenarios \xB7 ${html.length} bytes)`);
10494
11370
  return 0;
@@ -10519,8 +11395,8 @@ async function graphSnapshotCmd(argv) {
10519
11395
  }
10520
11396
 
10521
11397
  // src/bin-internal/heal-prepare.ts
10522
- import { existsSync as existsSync21, readdirSync as readdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
10523
- import { join as join19 } from "path";
11398
+ import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
11399
+ import { join as join24 } from "path";
10524
11400
  import { scrubFreeText } from "@xera-ai/web";
10525
11401
 
10526
11402
  // ../../node_modules/fflate/esm/index.mjs
@@ -10867,15 +11743,15 @@ function strFromU8(dat, latin1) {
10867
11743
  var slzh = function(d, b) {
10868
11744
  return b + 30 + b2(d, b + 26) + b2(d, b + 28);
10869
11745
  };
10870
- var zh = function(d, b, z7) {
11746
+ var zh = function(d, b, z9) {
10871
11747
  var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
10872
- var _a2 = z64hs(d, es, efl, z7, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
11748
+ var _a2 = z64hs(d, es, efl, z9, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
10873
11749
  return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
10874
11750
  };
10875
- var z64hs = function(d, b, l, z7, sc, su, off) {
11751
+ var z64hs = function(d, b, l, z9, sc, su, off) {
10876
11752
  var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
10877
11753
  var nf = nsc + nsu + noff;
10878
- if (z7 && nf) {
11754
+ if (z9 && nf) {
10879
11755
  for (;b + 4 < e; b += 4 + b2(d, b + 2)) {
10880
11756
  if (b2(d, b) == 1) {
10881
11757
  return [
@@ -10886,7 +11762,7 @@ var z64hs = function(d, b, l, z7, sc, su, off) {
10886
11762
  ];
10887
11763
  }
10888
11764
  }
10889
- if (z7 < 2)
11765
+ if (z9 < 2)
10890
11766
  err(13);
10891
11767
  }
10892
11768
  return [sc, su, off, 0];
@@ -10902,18 +11778,18 @@ function unzipSync(data, opts) {
10902
11778
  if (!c)
10903
11779
  return {};
10904
11780
  var o = b4(data, e + 16);
10905
- var z7 = b4(data, e - 20) == 117853008;
10906
- if (z7) {
11781
+ var z9 = b4(data, e - 20) == 117853008;
11782
+ if (z9) {
10907
11783
  var ze = b4(data, e - 12);
10908
- z7 = b4(data, ze) == 101075792;
10909
- if (z7) {
11784
+ z9 = b4(data, ze) == 101075792;
11785
+ if (z9) {
10910
11786
  c = b4(data, ze + 32);
10911
11787
  o = b4(data, ze + 48);
10912
11788
  }
10913
11789
  }
10914
11790
  var fltr = opts && opts.filter;
10915
11791
  for (var i2 = 0;i2 < c; ++i2) {
10916
- var _a2 = zh(data, o, z7), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
11792
+ var _a2 = zh(data, o, z9), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
10917
11793
  o = no;
10918
11794
  if (!fltr || fltr({
10919
11795
  name: fn,
@@ -10949,9 +11825,9 @@ function classifyKind(raw) {
10949
11825
  return "other";
10950
11826
  }
10951
11827
  function extractDomSnapshot(tracePath) {
10952
- if (!existsSync21(tracePath))
11828
+ if (!existsSync24(tracePath))
10953
11829
  return "";
10954
- const buf = readFileSync18(tracePath);
11830
+ const buf = readFileSync22(tracePath);
10955
11831
  const entries = unzipSync(buf);
10956
11832
  const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
10957
11833
  let chosenKey = null;
@@ -10999,16 +11875,16 @@ function extractDomSnapshot(tracePath) {
10999
11875
  return scrubFreeText(html);
11000
11876
  }
11001
11877
  function findPomLine(ticketDir, rawLocator) {
11002
- const pomDir = join19(ticketDir, "page-objects");
11878
+ const pomDir = join24(ticketDir, "page-objects");
11003
11879
  const candidates = [];
11004
- if (existsSync21(pomDir)) {
11880
+ if (existsSync24(pomDir)) {
11005
11881
  for (const name of readdirSync6(pomDir)) {
11006
11882
  if (name.endsWith(".ts"))
11007
- candidates.push(join19(pomDir, name));
11883
+ candidates.push(join24(pomDir, name));
11008
11884
  }
11009
11885
  }
11010
11886
  for (const file of candidates) {
11011
- const text = readFileSync18(file, "utf8");
11887
+ const text = readFileSync22(file, "utf8");
11012
11888
  const lines = text.split(`
11013
11889
  `);
11014
11890
  for (let i2 = 0;i2 < lines.length; i2++) {
@@ -11046,13 +11922,13 @@ function findGherkinStep(featureText, rawLocator) {
11046
11922
  }
11047
11923
  function healPrepare(repoRoot, ticket, runId, scenarioName) {
11048
11924
  const paths = resolveArtifactPaths(repoRoot, ticket);
11049
- const classifierPath = join19(paths.ticketDir, "classifier-input.json");
11050
- const classifier = JSON.parse(readFileSync18(classifierPath, "utf8"));
11925
+ const classifierPath = join24(paths.ticketDir, "classifier-input.json");
11926
+ const classifier = JSON.parse(readFileSync22(classifierPath, "utf8"));
11051
11927
  const cls = classifier.scenarios.find((s) => s.name === scenarioName);
11052
11928
  if (!cls)
11053
11929
  throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
11054
- const runDir = join19(paths.runsDir, runId);
11055
- const normalized = JSON.parse(readFileSync18(join19(runDir, "normalized.json"), "utf8"));
11930
+ const runDir = join24(paths.runsDir, runId);
11931
+ const normalized = JSON.parse(readFileSync22(join24(runDir, "normalized.json"), "utf8"));
11056
11932
  const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
11057
11933
  if (!normSc?.failure)
11058
11934
  throw new Error(`no failure recorded for scenario "${scenarioName}"`);
@@ -11063,9 +11939,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
11063
11939
  const raw = m[1].trim();
11064
11940
  const kind = classifyKind(raw);
11065
11941
  const pomLoc = findPomLine(paths.ticketDir, raw);
11066
- const featureText = readFileSync18(paths.featurePath, "utf8");
11942
+ const featureText = readFileSync22(paths.featurePath, "utf8");
11067
11943
  const gherkinStep = findGherkinStep(featureText, raw);
11068
- const domSnapshotAtFailure = extractDomSnapshot(join19(runDir, "trace.zip"));
11944
+ const domSnapshotAtFailure = extractDomSnapshot(join24(runDir, "trace.zip"));
11069
11945
  return {
11070
11946
  ticket,
11071
11947
  runId,
@@ -11085,8 +11961,8 @@ async function healPrepareCmd(argv) {
11085
11961
  try {
11086
11962
  const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
11087
11963
  const paths = resolveArtifactPaths(process.cwd(), ticket);
11088
- const outPath = join19(paths.runsDir, runId, "heal-input.json");
11089
- writeFileSync11(outPath, JSON.stringify(result, null, 2));
11964
+ const outPath = join24(paths.runsDir, runId, "heal-input.json");
11965
+ writeFileSync15(outPath, JSON.stringify(result, null, 2));
11090
11966
  console.log(`[xera:heal-prepare] wrote ${outPath}`);
11091
11967
  return 0;
11092
11968
  } catch (err2) {
@@ -11096,8 +11972,8 @@ async function healPrepareCmd(argv) {
11096
11972
  }
11097
11973
 
11098
11974
  // src/bin-internal/impact-prepare.ts
11099
- import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
11100
- import { join as join20 } from "path";
11975
+ import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync16 } from "fs";
11976
+ import { join as join25 } from "path";
11101
11977
 
11102
11978
  // src/graph/impact.ts
11103
11979
  var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
@@ -11347,11 +12223,11 @@ async function impactPrepareCmd(argv) {
11347
12223
  scenarios,
11348
12224
  generatedAt: new Date().toISOString()
11349
12225
  };
11350
- const impactDir = join20(repoRoot, ".xera/impact");
11351
- mkdirSync12(impactDir, { recursive: true });
11352
- writeFileSync12(join20(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
12226
+ const impactDir = join25(repoRoot, ".xera/impact");
12227
+ mkdirSync16(impactDir, { recursive: true });
12228
+ writeFileSync16(join25(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
11353
12229
  if (!quiet) {
11354
- writeFileSync12(join20(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
12230
+ writeFileSync16(join25(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
11355
12231
  }
11356
12232
  return 0;
11357
12233
  }
@@ -11377,8 +12253,8 @@ async function lintCmd(argv) {
11377
12253
  }
11378
12254
 
11379
12255
  // src/bin-internal/normalize.ts
11380
- import { existsSync as existsSync22, readdirSync as readdirSync7 } from "fs";
11381
- import { join as join21 } from "path";
12256
+ import { existsSync as existsSync25, readdirSync as readdirSync7 } from "fs";
12257
+ import { join as join26 } from "path";
11382
12258
  init_paths2();
11383
12259
  async function normalizeCmd(argv) {
11384
12260
  const ticket = argv[0];
@@ -11393,8 +12269,8 @@ async function normalizeCmd(argv) {
11393
12269
  console.error("[xera:normalize] no run found");
11394
12270
  return 1;
11395
12271
  }
11396
- const runDir = join21(paths.runsDir, runId);
11397
- if (!existsSync22(runDir)) {
12272
+ const runDir = join26(paths.runsDir, runId);
12273
+ if (!existsSync25(runDir)) {
11398
12274
  console.error(`[xera:normalize] runs/${runId} missing`);
11399
12275
  return 1;
11400
12276
  }
@@ -11414,14 +12290,14 @@ async function normalizeCmd(argv) {
11414
12290
 
11415
12291
  // src/bin-internal/post.ts
11416
12292
  init_paths2();
11417
- import { existsSync as existsSync24, readFileSync as readFileSync20 } from "fs";
11418
- import { join as join22 } from "path";
12293
+ import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
12294
+ import { join as join27 } from "path";
11419
12295
 
11420
12296
  // src/artifact/status.ts
11421
- import { existsSync as existsSync23, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
12297
+ import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync17 } from "fs";
11422
12298
  import { dirname as dirname8 } from "path";
11423
- import { z as z7 } from "zod";
11424
- var ClassificationEnum = z7.enum([
12299
+ import { z as z9 } from "zod";
12300
+ var ClassificationEnum = z9.enum([
11425
12301
  "PASS",
11426
12302
  "REAL_BUG",
11427
12303
  "SELECTOR_DRIFT",
@@ -11432,37 +12308,37 @@ var ClassificationEnum = z7.enum([
11432
12308
  "RATE_LIMITED",
11433
12309
  "AUTH_EXPIRED"
11434
12310
  ]);
11435
- var ResultEnum = z7.enum(["PASS", "FAIL"]);
11436
- var ConfidenceEnum = z7.enum(["low", "medium", "high"]);
11437
- var HistoryEntrySchema = z7.object({
11438
- ts: z7.string(),
12311
+ var ResultEnum = z9.enum(["PASS", "FAIL"]);
12312
+ var ConfidenceEnum = z9.enum(["low", "medium", "high"]);
12313
+ var HistoryEntrySchema = z9.object({
12314
+ ts: z9.string(),
11439
12315
  result: ResultEnum,
11440
12316
  class: ClassificationEnum
11441
12317
  });
11442
- var StatusJsonSchema = z7.object({
11443
- ticket: z7.string(),
11444
- lastRun: z7.string(),
12318
+ var StatusJsonSchema = z9.object({
12319
+ ticket: z9.string(),
12320
+ lastRun: z9.string(),
11445
12321
  result: ResultEnum,
11446
12322
  classification: ClassificationEnum,
11447
12323
  confidence: ConfidenceEnum,
11448
- scenarios: z7.object({
11449
- total: z7.number().int().nonnegative(),
11450
- passed: z7.number().int().nonnegative(),
11451
- failed: z7.number().int().nonnegative(),
11452
- skipped: z7.number().int().nonnegative()
12324
+ scenarios: z9.object({
12325
+ total: z9.number().int().nonnegative(),
12326
+ passed: z9.number().int().nonnegative(),
12327
+ failed: z9.number().int().nonnegative(),
12328
+ skipped: z9.number().int().nonnegative()
11453
12329
  }),
11454
- history: z7.array(HistoryEntrySchema).default([]),
11455
- last_jira_comment_id: z7.string().optional()
12330
+ history: z9.array(HistoryEntrySchema).default([]),
12331
+ last_jira_comment_id: z9.string().optional()
11456
12332
  });
11457
12333
  var HISTORY_CAP = 20;
11458
12334
  function readStatus(path) {
11459
- if (!existsSync23(path))
12335
+ if (!existsSync26(path))
11460
12336
  return null;
11461
- return StatusJsonSchema.parse(JSON.parse(readFileSync19(path, "utf8")));
12337
+ return StatusJsonSchema.parse(JSON.parse(readFileSync23(path, "utf8")));
11462
12338
  }
11463
12339
  function writeStatus(path, status) {
11464
- mkdirSync13(dirname8(path), { recursive: true });
11465
- writeFileSync13(path, JSON.stringify(status, null, 2));
12340
+ mkdirSync17(dirname8(path), { recursive: true });
12341
+ writeFileSync17(path, JSON.stringify(status, null, 2));
11466
12342
  }
11467
12343
  function appendHistory(path, entry) {
11468
12344
  const s = readStatus(path);
@@ -11488,12 +12364,12 @@ async function postCmd(argv) {
11488
12364
  return 0;
11489
12365
  }
11490
12366
  const paths = resolveArtifactPaths(cwd, ticket);
11491
- const draftPath = join22(paths.ticketDir, "jira-comment.draft.md");
11492
- if (!existsSync24(draftPath)) {
12367
+ const draftPath = join27(paths.ticketDir, "jira-comment.draft.md");
12368
+ if (!existsSync27(draftPath)) {
11493
12369
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
11494
12370
  return 1;
11495
12371
  }
11496
- const body = readFileSync20(draftPath, "utf8");
12372
+ const body = readFileSync24(draftPath, "utf8");
11497
12373
  const client = await createJiraClient({
11498
12374
  baseUrl: config.jira.baseUrl,
11499
12375
  preferMcp: true,
@@ -11521,8 +12397,8 @@ async function promoteCmd(argv) {
11521
12397
  }
11522
12398
 
11523
12399
  // src/bin-internal/report.ts
11524
- import { existsSync as existsSync26, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
11525
- import { join as join23 } from "path";
12400
+ import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync18 } from "fs";
12401
+ import { join as join28 } from "path";
11526
12402
  init_paths2();
11527
12403
 
11528
12404
  // src/classifier/aggregate.ts
@@ -11789,11 +12665,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
11789
12665
  }
11790
12666
 
11791
12667
  // src/reporter/status-writer.ts
11792
- import { existsSync as existsSync25 } from "fs";
12668
+ import { existsSync as existsSync28 } from "fs";
11793
12669
  function writeStatusFromClassification(path, input) {
11794
12670
  const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
11795
12671
  const entry = { ts: input.runTs, result, class: input.classification.overall };
11796
- if (!existsSync25(path)) {
12672
+ if (!existsSync28(path)) {
11797
12673
  writeStatus(path, {
11798
12674
  ticket: input.ticket,
11799
12675
  lastRun: input.runTs,
@@ -11827,22 +12703,22 @@ async function reportCmd(argv) {
11827
12703
  }
11828
12704
  const cwd = process.cwd();
11829
12705
  const paths = resolveArtifactPaths(cwd, ticket);
11830
- const input = JSON.parse(readFileSync21(inputArg.slice("--input=".length), "utf8"));
12706
+ const input = JSON.parse(readFileSync25(inputArg.slice("--input=".length), "utf8"));
11831
12707
  let httpRuleOverride = null;
11832
12708
  const meta = readMeta(paths.metaPath);
11833
12709
  if (meta?.adapter === "http") {
11834
12710
  const config = await loadConfig(cwd);
11835
12711
  if (config.http) {
11836
- const normalizedPath = join23(paths.ticketDir, "runs", input.runId, "normalized.json");
11837
- if (existsSync26(normalizedPath)) {
11838
- const norm = JSON.parse(readFileSync21(normalizedPath, "utf8"));
12712
+ const normalizedPath = join28(paths.ticketDir, "runs", input.runId, "normalized.json");
12713
+ if (existsSync29(normalizedPath)) {
12714
+ const norm = JSON.parse(readFileSync25(normalizedPath, "utf8"));
11839
12715
  const calls = norm.http?.calls ?? [];
11840
12716
  const rate = classifyRateLimited({ calls });
11841
12717
  if (rate)
11842
12718
  httpRuleOverride = rate;
11843
12719
  if (!httpRuleOverride) {
11844
12720
  const authFiles = {};
11845
- const httpAuthDir = join23(cwd, ".xera", ".auth", "http");
12721
+ const httpAuthDir = join28(cwd, ".xera", ".auth", "http");
11846
12722
  for (const role of Object.keys(config.http.auth.roles)) {
11847
12723
  const entry = readAuthState(httpAuthDir, role);
11848
12724
  if (entry) {
@@ -11887,8 +12763,8 @@ async function reportCmd(argv) {
11887
12763
  confidence: "high"
11888
12764
  } : s) : input.scenarios;
11889
12765
  const aggregated = aggregateScenarios(scenariosForAggregation);
11890
- const decisionsPath = join23(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
11891
- const decisions = existsSync26(decisionsPath) ? JSON.parse(readFileSync21(decisionsPath, "utf8")) : {};
12766
+ const decisionsPath = join28(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
12767
+ const decisions = existsSync29(decisionsPath) ? JSON.parse(readFileSync25(decisionsPath, "utf8")) : {};
11892
12768
  const graph = deriveSnapshot(loadAllEvents(process.cwd()));
11893
12769
  const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
11894
12770
  const scenarioIdByName = {};
@@ -11936,8 +12812,8 @@ async function reportCmd(argv) {
11936
12812
  xeraVersion: XERA_VERSION,
11937
12813
  promptsVersion: PROMPTS_VERSION
11938
12814
  });
11939
- const draftPath = join23(paths.ticketDir, "jira-comment.draft.md");
11940
- writeFileSync14(draftPath, md);
12815
+ const draftPath = join28(paths.ticketDir, "jira-comment.draft.md");
12816
+ writeFileSync18(draftPath, md);
11941
12817
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
11942
12818
  return 0;
11943
12819
  }
@@ -12008,7 +12884,7 @@ async function unlockCmd(argv) {
12008
12884
 
12009
12885
  // src/bin-internal/validate-feature.ts
12010
12886
  init_paths2();
12011
- import { existsSync as existsSync27, readFileSync as readFileSync22 } from "fs";
12887
+ import { existsSync as existsSync30, readFileSync as readFileSync26 } from "fs";
12012
12888
  import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
12013
12889
  async function validateFeatureCmd(argv) {
12014
12890
  const ticket = argv[0];
@@ -12017,11 +12893,11 @@ async function validateFeatureCmd(argv) {
12017
12893
  return 1;
12018
12894
  }
12019
12895
  const paths = resolveArtifactPaths(process.cwd(), ticket);
12020
- if (!existsSync27(paths.featurePath)) {
12896
+ if (!existsSync30(paths.featurePath)) {
12021
12897
  console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
12022
12898
  return 1;
12023
12899
  }
12024
- const r = validateGherkin2(readFileSync22(paths.featurePath, "utf8"));
12900
+ const r = validateGherkin2(readFileSync26(paths.featurePath, "utf8"));
12025
12901
  if (r.ok) {
12026
12902
  console.log("[xera:validate-feature] ok");
12027
12903
  return 0;
@@ -12033,13 +12909,18 @@ async function validateFeatureCmd(argv) {
12033
12909
 
12034
12910
  // src/bin-internal/index.ts
12035
12911
  var COMMANDS = {
12912
+ "ac-coverage-backfill-finalize": acCoverageBackfillFinalizeCmd,
12913
+ "ac-coverage-backfill-prepare": acCoverageBackfillPrepareCmd,
12036
12914
  "auth-setup": authSetupCmd,
12915
+ "coverage-prepare": coveragePrepareCmd,
12037
12916
  disputes: disputesCmd,
12038
12917
  doctor: doctorCmd,
12039
12918
  "eval-deterministic": evalDeterministicCmd,
12040
12919
  "eval-prepare": evalPrepareCmd,
12041
12920
  "eval-report": evalReportCmd,
12042
12921
  exec: execCmd,
12922
+ "fill-gap-finalize": fillGapFinalizeCmd,
12923
+ "fill-gap-prepare": fillGapPrepareCmd,
12043
12924
  fetch: fetchCmd,
12044
12925
  "graph-backfill": graphBackfillCmd,
12045
12926
  "graph-enrich": graphEnrichCmd,