@xera-ai/core 0.11.6 → 0.12.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.
@@ -10544,21 +10544,270 @@ async function execCmd(argv) {
10544
10544
  }
10545
10545
  }
10546
10546
 
10547
+ // src/bin-internal/explore-finalize.ts
10548
+ import { appendFileSync as appendFileSync3, existsSync as existsSync19, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
10549
+ import { join as join18 } from "path";
10550
+ import { z as z7 } from "zod";
10551
+ var CATEGORY_ENUM = z7.enum([
10552
+ "negative",
10553
+ "boundary",
10554
+ "state-combination",
10555
+ "race",
10556
+ "error-recovery",
10557
+ "a11y",
10558
+ "security-smell",
10559
+ "non-functional"
10560
+ ]);
10561
+ var ProposalsSchema = z7.object({
10562
+ proposals: z7.array(z7.object({
10563
+ id: z7.string().min(1),
10564
+ ticketId: z7.string().min(1),
10565
+ category: CATEGORY_ENUM,
10566
+ severity: z7.enum(["low", "medium", "high"]),
10567
+ title: z7.string().min(1),
10568
+ rationale: z7.string().min(1),
10569
+ gherkin: z7.string().min(1)
10570
+ }))
10571
+ });
10572
+ function parseArgs4(argv) {
10573
+ let ticket;
10574
+ let accept;
10575
+ for (let i = 0;i < argv.length; i++) {
10576
+ const a = argv[i];
10577
+ if (a === "--accept") {
10578
+ const v = argv[++i];
10579
+ if (v !== undefined)
10580
+ accept = v;
10581
+ } else if (a === "--help-stub") {} else if (a && !a.startsWith("--") && ticket === undefined) {
10582
+ ticket = a;
10583
+ } else {
10584
+ return { error: `unknown flag: ${a}` };
10585
+ }
10586
+ }
10587
+ if (!ticket)
10588
+ return { error: "ticket key is required as a positional argument" };
10589
+ if (!accept)
10590
+ return { error: '--accept is required (comma-separated IDs, "all", or "high-only")' };
10591
+ return { ticket, accept };
10592
+ }
10593
+ function selectProposals(all, accept) {
10594
+ const trimmed = accept.trim();
10595
+ if (trimmed === "all")
10596
+ return all;
10597
+ if (trimmed === "high-only")
10598
+ return all.filter((p) => p.severity === "high");
10599
+ const ids = new Set(trimmed.split(",").map((s) => s.trim()).filter(Boolean));
10600
+ if (ids.size === 0)
10601
+ return { error: "no IDs supplied" };
10602
+ const picked = all.filter((p) => ids.has(p.id));
10603
+ const found = new Set(picked.map((p) => p.id));
10604
+ const missing = [...ids].filter((id) => !found.has(id));
10605
+ if (missing.length > 0)
10606
+ return { error: `unknown proposal IDs: ${missing.join(", ")}` };
10607
+ return picked;
10608
+ }
10609
+ function ensureFeatureHeader(ticketDir, ticket) {
10610
+ const explorePath = join18(ticketDir, "explore.feature");
10611
+ if (existsSync19(explorePath))
10612
+ return explorePath;
10613
+ const testFeaturePath = join18(ticketDir, "test.feature");
10614
+ let header;
10615
+ if (existsSync19(testFeaturePath)) {
10616
+ const testContent = readFileSync16(testFeaturePath, "utf8");
10617
+ const firstFeatureMatch = testContent.match(/^Feature:.*$/m);
10618
+ header = firstFeatureMatch ? `${firstFeatureMatch[0]} (adversarial)
10619
+ Adversarial scenarios beyond the acceptance criteria.
10620
+ Generated by /xera-explore \u2014 review before merging into test.feature.
10621
+
10622
+ ` : `Feature: ${ticket} adversarial
10623
+
10624
+ `;
10625
+ } else {
10626
+ header = `Feature: ${ticket} adversarial
10627
+ Adversarial scenarios beyond the acceptance criteria.
10628
+ Generated by /xera-explore \u2014 review before merging into test.feature.
10629
+
10630
+ `;
10631
+ }
10632
+ writeFileSync10(explorePath, header);
10633
+ return explorePath;
10634
+ }
10635
+ function formatScenario(p) {
10636
+ const tags = `@adversarial @adversarial-${p.category} @severity-${p.severity}`;
10637
+ const rationaleComment = ` # ${p.rationale}`;
10638
+ const indented = p.gherkin.split(`
10639
+ `).map((line) => line.startsWith("Scenario:") ? ` ${line}` : line ? ` ${line}` : line).join(`
10640
+ `);
10641
+ return `
10642
+ ${tags}
10643
+ ${rationaleComment}
10644
+ ${indented}
10645
+ `;
10646
+ }
10647
+ async function exploreFinalizeCmd(argv) {
10648
+ const parsed = parseArgs4(argv);
10649
+ if ("error" in parsed) {
10650
+ console.error(`[explore-finalize] ${parsed.error}`);
10651
+ return 1;
10652
+ }
10653
+ const cwd = process.cwd();
10654
+ const ticketDir = join18(cwd, ".xera", parsed.ticket);
10655
+ const proposalsPath = join18(ticketDir, "adversarial-proposals.json");
10656
+ if (!existsSync19(proposalsPath)) {
10657
+ console.error(`[explore-finalize] adversarial-proposals.json not found for ${parsed.ticket} \u2014 run /xera-explore Step 4 first`);
10658
+ return 2;
10659
+ }
10660
+ let proposals;
10661
+ try {
10662
+ const raw = JSON.parse(readFileSync16(proposalsPath, "utf8"));
10663
+ proposals = ProposalsSchema.parse(raw);
10664
+ } catch (e) {
10665
+ console.error(`[explore-finalize] invalid proposals JSON: ${e.message}`);
10666
+ return 1;
10667
+ }
10668
+ const picked = selectProposals(proposals.proposals, parsed.accept);
10669
+ if ("error" in picked) {
10670
+ console.error(`[explore-finalize] ${picked.error}`);
10671
+ return 1;
10672
+ }
10673
+ if (picked.length === 0) {
10674
+ console.error("[explore-finalize] no proposals matched the --accept filter");
10675
+ return 1;
10676
+ }
10677
+ const explorePath = ensureFeatureHeader(ticketDir, parsed.ticket);
10678
+ for (const p of picked) {
10679
+ appendFileSync3(explorePath, formatScenario(p));
10680
+ }
10681
+ console.log(`[explore-finalize] appended ${picked.length} scenario(s) to ${explorePath} (${picked.map((p) => p.id).join(", ")})`);
10682
+ return 0;
10683
+ }
10684
+
10685
+ // src/bin-internal/explore-prepare.ts
10686
+ import { existsSync as existsSync20, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
10687
+ import { join as join19 } from "path";
10688
+ var VALID_CATEGORIES = [
10689
+ "negative",
10690
+ "boundary",
10691
+ "state-combination",
10692
+ "race",
10693
+ "error-recovery",
10694
+ "a11y",
10695
+ "security-smell",
10696
+ "non-functional"
10697
+ ];
10698
+ function parseArgs5(argv) {
10699
+ let ticket;
10700
+ let categoriesRaw = "";
10701
+ let userHint = "";
10702
+ for (let i = 0;i < argv.length; i++) {
10703
+ const a = argv[i];
10704
+ if (a === "--categories") {
10705
+ const v = argv[++i];
10706
+ if (v !== undefined)
10707
+ categoriesRaw = v;
10708
+ } else if (a === "--user-hint") {
10709
+ const v = argv[++i];
10710
+ if (v !== undefined)
10711
+ userHint = v;
10712
+ } else if (a === "--help-stub") {} else if (a && !a.startsWith("--") && ticket === undefined) {
10713
+ ticket = a;
10714
+ } else {
10715
+ return { error: `unknown flag: ${a}` };
10716
+ }
10717
+ }
10718
+ if (!ticket)
10719
+ return { error: "ticket key is required as a positional argument" };
10720
+ const categoriesInclude = [];
10721
+ for (const slug of categoriesRaw.split(",").map((s) => s.trim()).filter(Boolean)) {
10722
+ if (!VALID_CATEGORIES.includes(slug)) {
10723
+ return { error: `invalid category: ${slug}` };
10724
+ }
10725
+ categoriesInclude.push(slug);
10726
+ }
10727
+ return { ticket, categoriesInclude, userHint };
10728
+ }
10729
+ function parseStoryMd(content) {
10730
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
10731
+ if (!fmMatch)
10732
+ return { summary: "", ac: [], body: content };
10733
+ const [, fm, body] = fmMatch;
10734
+ const summaryMatch = fm.match(/^summary:\s*(.+)$/m);
10735
+ const summary = summaryMatch?.[1]?.trim() ?? "";
10736
+ const ac = [];
10737
+ const acBlock = fm.match(/^ac:\s*\n((?:\s*-\s.+\n?)+)/m);
10738
+ if (acBlock) {
10739
+ for (const line of acBlock[1].split(`
10740
+ `)) {
10741
+ const m = line.match(/^\s*-\s*(.+)$/);
10742
+ if (m)
10743
+ ac.push(m[1].trim());
10744
+ }
10745
+ }
10746
+ return { summary, ac, body: body.trim() };
10747
+ }
10748
+ async function explorePrepareCmd(argv) {
10749
+ const parsed = parseArgs5(argv);
10750
+ if ("error" in parsed) {
10751
+ console.error(`[explore-prepare] ${parsed.error}`);
10752
+ return 1;
10753
+ }
10754
+ const cwd = process.cwd();
10755
+ const configPath = join19(cwd, "xera.config.ts");
10756
+ if (!existsSync20(configPath)) {
10757
+ console.error("[explore-prepare] xera.config.ts not found \u2014 run inside a xera project");
10758
+ return 2;
10759
+ }
10760
+ const ticketDir = join19(cwd, ".xera", parsed.ticket);
10761
+ const storyPath = join19(ticketDir, "story.md");
10762
+ if (!existsSync20(storyPath)) {
10763
+ console.error(`[explore-prepare] no story for ${parsed.ticket} \u2014 run /xera-fetch ${parsed.ticket} first`);
10764
+ return 2;
10765
+ }
10766
+ const story = readFileSync17(storyPath, "utf8");
10767
+ const { summary, ac, body } = parseStoryMd(story);
10768
+ let adapter = "web";
10769
+ const metaPath = join19(ticketDir, "meta.json");
10770
+ if (existsSync20(metaPath)) {
10771
+ try {
10772
+ const meta = JSON.parse(readFileSync17(metaPath, "utf8"));
10773
+ if (meta.adapter === "http")
10774
+ adapter = "http";
10775
+ } catch {}
10776
+ }
10777
+ const input = {
10778
+ ticket: { id: parsed.ticket, summary, story: body, ac },
10779
+ adapter,
10780
+ categoriesInclude: parsed.categoriesInclude
10781
+ };
10782
+ const featurePath = join19(ticketDir, "test.feature");
10783
+ if (existsSync20(featurePath))
10784
+ input.existingFeature = readFileSync17(featurePath, "utf8");
10785
+ const specPath = join19(ticketDir, "spec.ts");
10786
+ if (existsSync20(specPath))
10787
+ input.existingSpec = readFileSync17(specPath, "utf8");
10788
+ if (parsed.userHint)
10789
+ input.userHint = parsed.userHint;
10790
+ const outPath = join19(ticketDir, "adversarial-input.json");
10791
+ writeFileSync11(outPath, JSON.stringify(input, null, 2));
10792
+ console.log(`[explore-prepare] wrote ${outPath}`);
10793
+ return 0;
10794
+ }
10795
+
10547
10796
  // src/bin-internal/fetch.ts
10548
- import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
10797
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "fs";
10549
10798
  import { dirname as dirname5 } from "path";
10550
10799
 
10551
10800
  // src/artifact/hash.ts
10552
10801
  import { createHash as createHash4 } from "crypto";
10553
- import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
10802
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
10554
10803
  function hashString(s) {
10555
10804
  return `sha256:${createHash4("sha256").update(s).digest("hex")}`;
10556
10805
  }
10557
10806
  function hashFile(path) {
10558
- return hashString(readFileSync16(path, "utf8"));
10807
+ return hashString(readFileSync18(path, "utf8"));
10559
10808
  }
10560
10809
  function hashFileIfExists(path) {
10561
- if (!existsSync19(path))
10810
+ if (!existsSync21(path))
10562
10811
  return null;
10563
10812
  return hashFile(path);
10564
10813
  }
@@ -10567,33 +10816,33 @@ function hashFileIfExists(path) {
10567
10816
  init_paths2();
10568
10817
 
10569
10818
  // src/jira/mcp-backend.ts
10570
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
10819
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
10571
10820
  import { tmpdir } from "os";
10572
- import { join as join18 } from "path";
10821
+ import { join as join20 } from "path";
10573
10822
  var MCP_ENV = "XERA_MCP_JIRA";
10574
10823
  async function createMcpBackend(_baseUrl) {
10575
10824
  if (process.env[MCP_ENV] !== "1")
10576
10825
  return null;
10577
- const tmpDir = join18(tmpdir(), "xera-mcp");
10826
+ const tmpDir = join20(tmpdir(), "xera-mcp");
10578
10827
  mkdirSync11(tmpDir, { recursive: true });
10579
10828
  return {
10580
10829
  backend: "mcp",
10581
10830
  async fetchTicket(key, _fields) {
10582
- const cachePath = join18(tmpDir, `${key}.json`);
10583
- if (!existsSync20(cachePath)) {
10831
+ const cachePath = join20(tmpDir, `${key}.json`);
10832
+ if (!existsSync22(cachePath)) {
10584
10833
  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.`);
10585
10834
  }
10586
- const parsed = JSON.parse(readFileSync17(cachePath, "utf8"));
10835
+ const parsed = JSON.parse(readFileSync19(cachePath, "utf8"));
10587
10836
  return parsed;
10588
10837
  },
10589
10838
  async postComment(key, body) {
10590
- const outPath = join18(tmpDir, `${key}.comment.json`);
10591
- writeFileSync10(outPath, JSON.stringify({ key, body }));
10839
+ const outPath = join20(tmpDir, `${key}.comment.json`);
10840
+ writeFileSync12(outPath, JSON.stringify({ key, body }));
10592
10841
  return { id: "mcp-pending" };
10593
10842
  },
10594
10843
  async transitionStatus(key, statusName) {
10595
- const outPath = join18(tmpDir, `${key}.transition.json`);
10596
- writeFileSync10(outPath, JSON.stringify({ key, statusName }));
10844
+ const outPath = join20(tmpDir, `${key}.transition.json`);
10845
+ writeFileSync12(outPath, JSON.stringify({ key, statusName }));
10597
10846
  },
10598
10847
  async listFields(_sampleKey) {
10599
10848
  throw new Error("listFields is REST-only; init flow uses REST for field discovery.");
@@ -10747,7 +10996,7 @@ async function fetchCmd(argv, opts = {}) {
10747
10996
  const acLines = parseAcLines(t.acceptanceCriteria);
10748
10997
  const full = renderStory(t.key, t.summary, storyHash, acLines, body);
10749
10998
  mkdirSync12(dirname5(paths.storyPath), { recursive: true });
10750
- writeFileSync11(paths.storyPath, full);
10999
+ writeFileSync13(paths.storyPath, full);
10751
11000
  const existing = readMeta(paths.metaPath);
10752
11001
  writeMeta(paths.metaPath, {
10753
11002
  ticket,
@@ -10808,20 +11057,20 @@ function renderStory(key, summary, storyHash, acLines, body) {
10808
11057
  }
10809
11058
 
10810
11059
  // src/bin-internal/fill-gap-finalize.ts
10811
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
10812
- import { join as join19 } from "path";
10813
- import { z as z7 } from "zod";
10814
- var ProposalsSchema = z7.object({
10815
- proposals: z7.array(z7.object({
10816
- id: z7.string().min(1),
10817
- ticketId: z7.string().min(1),
10818
- title: z7.string().min(1),
10819
- rationale: z7.string().min(1),
10820
- gherkin: z7.string().min(1),
10821
- satisfiesAcs: z7.array(z7.number().int().nonnegative())
11060
+ import { existsSync as existsSync23, mkdirSync as mkdirSync13, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
11061
+ import { join as join21 } from "path";
11062
+ import { z as z8 } from "zod";
11063
+ var ProposalsSchema2 = z8.object({
11064
+ proposals: z8.array(z8.object({
11065
+ id: z8.string().min(1),
11066
+ ticketId: z8.string().min(1),
11067
+ title: z8.string().min(1),
11068
+ rationale: z8.string().min(1),
11069
+ gherkin: z8.string().min(1),
11070
+ satisfiesAcs: z8.array(z8.number().int().nonnegative())
10822
11071
  }))
10823
11072
  });
10824
- function parseArgs4(argv) {
11073
+ function parseArgs6(argv) {
10825
11074
  let accept;
10826
11075
  let ticket;
10827
11076
  let source;
@@ -10871,21 +11120,21 @@ function formatDraft(ticketId, proposal) {
10871
11120
  }
10872
11121
  async function fillGapFinalizeCmd(argv) {
10873
11122
  if (argv.includes("--help-stub")) {}
10874
- const parsed = parseArgs4(argv);
11123
+ const parsed = parseArgs6(argv);
10875
11124
  if ("error" in parsed) {
10876
11125
  console.error(`[fill-gap-finalize] ${parsed.error}`);
10877
11126
  return 1;
10878
11127
  }
10879
11128
  const cwd = process.cwd();
10880
- const sourcePath = parsed.source ?? join19(cwd, ".xera/coverage/proposals.json");
10881
- if (!existsSync21(sourcePath)) {
11129
+ const sourcePath = parsed.source ?? join21(cwd, ".xera/coverage/proposals.json");
11130
+ if (!existsSync23(sourcePath)) {
10882
11131
  console.error(`[fill-gap-finalize] source not found: ${sourcePath}`);
10883
11132
  return 2;
10884
11133
  }
10885
11134
  let proposals;
10886
11135
  try {
10887
- const raw = JSON.parse(readFileSync18(sourcePath, "utf8"));
10888
- proposals = ProposalsSchema.parse(raw);
11136
+ const raw = JSON.parse(readFileSync20(sourcePath, "utf8"));
11137
+ proposals = ProposalsSchema2.parse(raw);
10889
11138
  } catch (e) {
10890
11139
  console.error(`[fill-gap-finalize] invalid proposals: ${e.message}`);
10891
11140
  return 2;
@@ -10895,22 +11144,22 @@ async function fillGapFinalizeCmd(argv) {
10895
11144
  console.error(`[fill-gap-finalize] proposal id "${parsed.accept}" not in source`);
10896
11145
  return 2;
10897
11146
  }
10898
- const ticketDir = join19(cwd, ".xera", parsed.ticket);
11147
+ const ticketDir = join21(cwd, ".xera", parsed.ticket);
10899
11148
  mkdirSync13(ticketDir, { recursive: true });
10900
- const draftPath = join19(ticketDir, "feature.draft.md");
10901
- if (existsSync21(draftPath) && !parsed.force) {
11149
+ const draftPath = join21(ticketDir, "feature.draft.md");
11150
+ if (existsSync23(draftPath) && !parsed.force) {
10902
11151
  console.error(`[fill-gap-finalize] ${draftPath} exists; pass --force to overwrite`);
10903
11152
  return 3;
10904
11153
  }
10905
- writeFileSync12(draftPath, formatDraft(parsed.ticket, proposal));
11154
+ writeFileSync14(draftPath, formatDraft(parsed.ticket, proposal));
10906
11155
  return 0;
10907
11156
  }
10908
11157
 
10909
11158
  // src/bin-internal/fill-gap-prepare.ts
10910
11159
  init_store();
10911
- import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
10912
- import { join as join20 } from "path";
10913
- function parseArgs5(argv) {
11160
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync15 } from "fs";
11161
+ import { join as join22 } from "path";
11162
+ function parseArgs7(argv) {
10914
11163
  const args = {};
10915
11164
  for (let i = 0;i < argv.length; i++) {
10916
11165
  const a = argv[i];
@@ -10976,7 +11225,7 @@ function buildTicketContext(snap, ticketId) {
10976
11225
  };
10977
11226
  }
10978
11227
  async function fillGapPrepareCmd(argv) {
10979
- const parsed = parseArgs5(argv);
11228
+ const parsed = parseArgs7(argv);
10980
11229
  if ("error" in parsed) {
10981
11230
  console.error(`[fill-gap-prepare] ${parsed.error}`);
10982
11231
  return 1;
@@ -11000,9 +11249,9 @@ async function fillGapPrepareCmd(argv) {
11000
11249
  return 2;
11001
11250
  }
11002
11251
  }
11003
- const outDir = parsed.outputDir ?? join20(cwd, ".xera/coverage", scope);
11252
+ const outDir = parsed.outputDir ?? join22(cwd, ".xera/coverage", scope);
11004
11253
  mkdirSync14(outDir, { recursive: true });
11005
- writeFileSync13(join20(outDir, "context.json"), JSON.stringify(context, null, 2));
11254
+ writeFileSync15(join22(outDir, "context.json"), JSON.stringify(context, null, 2));
11006
11255
  return 0;
11007
11256
  }
11008
11257
 
@@ -11012,18 +11261,18 @@ init_graph_backfill();
11012
11261
  // src/graph/enrich.ts
11013
11262
  init_store();
11014
11263
  init_ulid();
11015
- import { existsSync as existsSync22, readFileSync as readFileSync19, unlinkSync as unlinkSync2 } from "fs";
11016
- import { join as join21 } from "path";
11017
- import { z as z8 } from "zod";
11264
+ import { existsSync as existsSync24, readFileSync as readFileSync21, unlinkSync as unlinkSync2 } from "fs";
11265
+ import { join as join23 } from "path";
11266
+ import { z as z9 } from "zod";
11018
11267
  var MAX_SIMILAR_EDGES = 10;
11019
11268
  var MIN_CONFIDENCE = 0.7;
11020
- var SimilarEntrySchema = z8.object({
11021
- ticketId: z8.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
11022
- confidence: z8.number(),
11023
- reason: z8.string()
11269
+ var SimilarEntrySchema = z9.object({
11270
+ ticketId: z9.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
11271
+ confidence: z9.number(),
11272
+ reason: z9.string()
11024
11273
  });
11025
- var EnrichmentInputSchema = z8.object({
11026
- similar: z8.array(SimilarEntrySchema)
11274
+ var EnrichmentInputSchema = z9.object({
11275
+ similar: z9.array(SimilarEntrySchema)
11027
11276
  });
11028
11277
  var nowIso3 = () => new Date().toISOString();
11029
11278
  var mk2 = (actor, type, payload) => ({
@@ -11042,11 +11291,11 @@ async function enrichTicket(repoRoot, ticketId, opts) {
11042
11291
  if (snapshot.tickets[ticketId].enrichedAt && !opts.force) {
11043
11292
  return { ticketId, similarCount: 0, enrichedAt: snapshot.tickets[ticketId].enrichedAt };
11044
11293
  }
11045
- const inputPath = join21(repoRoot, ".xera", ticketId, "enrichment-input.json");
11046
- if (!existsSync22(inputPath)) {
11294
+ const inputPath = join23(repoRoot, ".xera", ticketId, "enrichment-input.json");
11295
+ if (!existsSync24(inputPath)) {
11047
11296
  throw new Error(`enrichment-input.json not found at ${inputPath}`);
11048
11297
  }
11049
- const raw = JSON.parse(readFileSync19(inputPath, "utf8"));
11298
+ const raw = JSON.parse(readFileSync21(inputPath, "utf8"));
11050
11299
  const parsed = EnrichmentInputSchema.safeParse(raw);
11051
11300
  if (!parsed.success) {
11052
11301
  throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
@@ -11151,12 +11400,12 @@ async function graphQueryCmd(argv) {
11151
11400
  init_graph_record();
11152
11401
 
11153
11402
  // src/bin-internal/graph-render.ts
11154
- import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync21, renameSync as renameSync2, writeFileSync as writeFileSync14 } from "fs";
11155
- import { dirname as dirname7, join as join23 } from "path";
11403
+ import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync23, renameSync as renameSync2, writeFileSync as writeFileSync16 } from "fs";
11404
+ import { dirname as dirname7, join as join25 } from "path";
11156
11405
 
11157
11406
  // src/graph/render.ts
11158
- import { readFileSync as readFileSync20 } from "fs";
11159
- import { dirname as dirname6, join as join22 } from "path";
11407
+ import { readFileSync as readFileSync22 } from "fs";
11408
+ import { dirname as dirname6, join as join24 } from "path";
11160
11409
  import { fileURLToPath } from "url";
11161
11410
  var COLORS = {
11162
11411
  ticket: "#3B82F6",
@@ -11375,9 +11624,9 @@ function transformForVisNetwork(snap, opts) {
11375
11624
  }
11376
11625
  var __filename2 = fileURLToPath(import.meta.url);
11377
11626
  var __dirname2 = dirname6(__filename2);
11378
- var TEMPLATES_DIR = join22(__dirname2, "templates");
11627
+ var TEMPLATES_DIR = join24(__dirname2, "templates");
11379
11628
  function loadTemplate(name) {
11380
- return readFileSync20(join22(TEMPLATES_DIR, name), "utf8");
11629
+ return readFileSync22(join24(TEMPLATES_DIR, name), "utf8");
11381
11630
  }
11382
11631
  function statsToHuman(s) {
11383
11632
  return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
@@ -11429,7 +11678,7 @@ async function graphRenderCmd(argv) {
11429
11678
  includeCoverage = true;
11430
11679
  }
11431
11680
  const repoRoot = process.cwd();
11432
- const finalPath = outPath ?? join23(repoRoot, ".xera/graph.html");
11681
+ const finalPath = outPath ?? join25(repoRoot, ".xera/graph.html");
11433
11682
  const events = loadAllEvents(repoRoot);
11434
11683
  const snap = deriveSnapshot(events);
11435
11684
  const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
@@ -11437,7 +11686,7 @@ async function graphRenderCmd(argv) {
11437
11686
  if (performanceMode === "text-fallback") {
11438
11687
  const txtPath = finalPath.replace(/\.html$/, ".txt");
11439
11688
  mkdirSync15(dirname7(txtPath), { recursive: true });
11440
- writeFileSync14(txtPath, `Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.
11689
+ writeFileSync16(txtPath, `Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.
11441
11690
  `);
11442
11691
  console.log(`[graph-render] graph too large (${totalNodeCount} nodes); wrote ${txtPath}`);
11443
11692
  return 0;
@@ -11449,9 +11698,9 @@ async function graphRenderCmd(argv) {
11449
11698
  opts.since = since;
11450
11699
  let coverage;
11451
11700
  if (includeCoverage) {
11452
- const reportPath = join23(repoRoot, ".xera/coverage/report.json");
11453
- if (existsSync23(reportPath)) {
11454
- const report = JSON.parse(readFileSync21(reportPath, "utf8"));
11701
+ const reportPath = join25(repoRoot, ".xera/coverage/report.json");
11702
+ if (existsSync25(reportPath)) {
11703
+ const report = JSON.parse(readFileSync23(reportPath, "utf8"));
11455
11704
  const snapshots = events.filter((e) => e.type === "coverage.snapshot").map((e) => e.payload);
11456
11705
  coverage = { report, snapshots };
11457
11706
  } else {
@@ -11469,7 +11718,7 @@ async function graphRenderCmd(argv) {
11469
11718
  const html = renderHtml(renderInput);
11470
11719
  mkdirSync15(dirname7(finalPath), { recursive: true });
11471
11720
  const tmpPath = `${finalPath}.tmp`;
11472
- writeFileSync14(tmpPath, html);
11721
+ writeFileSync16(tmpPath, html);
11473
11722
  renameSync2(tmpPath, finalPath);
11474
11723
  console.log(`[graph-render] wrote ${finalPath} (${data.stats.tickets} tickets \xB7 ${data.stats.scenarios} scenarios \xB7 ${html.length} bytes)`);
11475
11724
  return 0;
@@ -11500,8 +11749,8 @@ async function graphSnapshotCmd(argv) {
11500
11749
  }
11501
11750
 
11502
11751
  // src/bin-internal/heal-prepare.ts
11503
- import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
11504
- import { join as join24 } from "path";
11752
+ import { existsSync as existsSync26, readdirSync as readdirSync6, readFileSync as readFileSync24, writeFileSync as writeFileSync17 } from "fs";
11753
+ import { join as join26 } from "path";
11505
11754
  import { scrubFreeText } from "@xera-ai/web";
11506
11755
 
11507
11756
  // ../../node_modules/fflate/esm/index.mjs
@@ -11848,15 +12097,15 @@ function strFromU8(dat, latin1) {
11848
12097
  var slzh = function(d, b) {
11849
12098
  return b + 30 + b2(d, b + 26) + b2(d, b + 28);
11850
12099
  };
11851
- var zh = function(d, b, z9) {
12100
+ var zh = function(d, b, z10) {
11852
12101
  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;
11853
- 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];
12102
+ var _a2 = z64hs(d, es, efl, z10, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
11854
12103
  return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
11855
12104
  };
11856
- var z64hs = function(d, b, l, z9, sc, su, off) {
12105
+ var z64hs = function(d, b, l, z10, sc, su, off) {
11857
12106
  var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
11858
12107
  var nf = nsc + nsu + noff;
11859
- if (z9 && nf) {
12108
+ if (z10 && nf) {
11860
12109
  for (;b + 4 < e; b += 4 + b2(d, b + 2)) {
11861
12110
  if (b2(d, b) == 1) {
11862
12111
  return [
@@ -11867,7 +12116,7 @@ var z64hs = function(d, b, l, z9, sc, su, off) {
11867
12116
  ];
11868
12117
  }
11869
12118
  }
11870
- if (z9 < 2)
12119
+ if (z10 < 2)
11871
12120
  err(13);
11872
12121
  }
11873
12122
  return [sc, su, off, 0];
@@ -11883,18 +12132,18 @@ function unzipSync(data, opts) {
11883
12132
  if (!c)
11884
12133
  return {};
11885
12134
  var o = b4(data, e + 16);
11886
- var z9 = b4(data, e - 20) == 117853008;
11887
- if (z9) {
12135
+ var z10 = b4(data, e - 20) == 117853008;
12136
+ if (z10) {
11888
12137
  var ze = b4(data, e - 12);
11889
- z9 = b4(data, ze) == 101075792;
11890
- if (z9) {
12138
+ z10 = b4(data, ze) == 101075792;
12139
+ if (z10) {
11891
12140
  c = b4(data, ze + 32);
11892
12141
  o = b4(data, ze + 48);
11893
12142
  }
11894
12143
  }
11895
12144
  var fltr = opts && opts.filter;
11896
12145
  for (var i2 = 0;i2 < c; ++i2) {
11897
- 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);
12146
+ var _a2 = zh(data, o, z10), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
11898
12147
  o = no;
11899
12148
  if (!fltr || fltr({
11900
12149
  name: fn,
@@ -11930,9 +12179,9 @@ function classifyKind(raw) {
11930
12179
  return "other";
11931
12180
  }
11932
12181
  function extractDomSnapshot(tracePath) {
11933
- if (!existsSync24(tracePath))
12182
+ if (!existsSync26(tracePath))
11934
12183
  return "";
11935
- const buf = readFileSync22(tracePath);
12184
+ const buf = readFileSync24(tracePath);
11936
12185
  const entries = unzipSync(buf);
11937
12186
  const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
11938
12187
  let chosenKey = null;
@@ -11980,16 +12229,16 @@ function extractDomSnapshot(tracePath) {
11980
12229
  return scrubFreeText(html);
11981
12230
  }
11982
12231
  function findPomLine(ticketDir, rawLocator) {
11983
- const pomDir = join24(ticketDir, "page-objects");
12232
+ const pomDir = join26(ticketDir, "page-objects");
11984
12233
  const candidates = [];
11985
- if (existsSync24(pomDir)) {
12234
+ if (existsSync26(pomDir)) {
11986
12235
  for (const name of readdirSync6(pomDir)) {
11987
12236
  if (name.endsWith(".ts"))
11988
- candidates.push(join24(pomDir, name));
12237
+ candidates.push(join26(pomDir, name));
11989
12238
  }
11990
12239
  }
11991
12240
  for (const file of candidates) {
11992
- const text = readFileSync22(file, "utf8");
12241
+ const text = readFileSync24(file, "utf8");
11993
12242
  const lines = text.split(`
11994
12243
  `);
11995
12244
  for (let i2 = 0;i2 < lines.length; i2++) {
@@ -12027,13 +12276,13 @@ function findGherkinStep(featureText, rawLocator) {
12027
12276
  }
12028
12277
  function healPrepare(repoRoot, ticket, runId, scenarioName) {
12029
12278
  const paths = resolveArtifactPaths(repoRoot, ticket);
12030
- const classifierPath = join24(paths.ticketDir, "classifier-input.json");
12031
- const classifier = JSON.parse(readFileSync22(classifierPath, "utf8"));
12279
+ const classifierPath = join26(paths.ticketDir, "classifier-input.json");
12280
+ const classifier = JSON.parse(readFileSync24(classifierPath, "utf8"));
12032
12281
  const cls = classifier.scenarios.find((s) => s.name === scenarioName);
12033
12282
  if (!cls)
12034
12283
  throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
12035
- const runDir = join24(paths.runsDir, runId);
12036
- const normalized = JSON.parse(readFileSync22(join24(runDir, "normalized.json"), "utf8"));
12284
+ const runDir = join26(paths.runsDir, runId);
12285
+ const normalized = JSON.parse(readFileSync24(join26(runDir, "normalized.json"), "utf8"));
12037
12286
  const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
12038
12287
  if (!normSc?.failure)
12039
12288
  throw new Error(`no failure recorded for scenario "${scenarioName}"`);
@@ -12044,9 +12293,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
12044
12293
  const raw = m[1].trim();
12045
12294
  const kind = classifyKind(raw);
12046
12295
  const pomLoc = findPomLine(paths.ticketDir, raw);
12047
- const featureText = readFileSync22(paths.featurePath, "utf8");
12296
+ const featureText = readFileSync24(paths.featurePath, "utf8");
12048
12297
  const gherkinStep = findGherkinStep(featureText, raw);
12049
- const domSnapshotAtFailure = extractDomSnapshot(join24(runDir, "trace.zip"));
12298
+ const domSnapshotAtFailure = extractDomSnapshot(join26(runDir, "trace.zip"));
12050
12299
  return {
12051
12300
  ticket,
12052
12301
  runId,
@@ -12066,8 +12315,8 @@ async function healPrepareCmd(argv) {
12066
12315
  try {
12067
12316
  const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
12068
12317
  const paths = resolveArtifactPaths(process.cwd(), ticket);
12069
- const outPath = join24(paths.runsDir, runId, "heal-input.json");
12070
- writeFileSync15(outPath, JSON.stringify(result, null, 2));
12318
+ const outPath = join26(paths.runsDir, runId, "heal-input.json");
12319
+ writeFileSync17(outPath, JSON.stringify(result, null, 2));
12071
12320
  console.log(`[xera:heal-prepare] wrote ${outPath}`);
12072
12321
  return 0;
12073
12322
  } catch (err2) {
@@ -12077,8 +12326,8 @@ async function healPrepareCmd(argv) {
12077
12326
  }
12078
12327
 
12079
12328
  // src/bin-internal/impact-prepare.ts
12080
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync16 } from "fs";
12081
- import { join as join25 } from "path";
12329
+ import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync18 } from "fs";
12330
+ import { join as join27 } from "path";
12082
12331
 
12083
12332
  // src/graph/impact.ts
12084
12333
  var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
@@ -12328,11 +12577,11 @@ async function impactPrepareCmd(argv) {
12328
12577
  scenarios,
12329
12578
  generatedAt: new Date().toISOString()
12330
12579
  };
12331
- const impactDir = join25(repoRoot, ".xera/impact");
12580
+ const impactDir = join27(repoRoot, ".xera/impact");
12332
12581
  mkdirSync16(impactDir, { recursive: true });
12333
- writeFileSync16(join25(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
12582
+ writeFileSync18(join27(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
12334
12583
  if (!quiet) {
12335
- writeFileSync16(join25(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
12584
+ writeFileSync18(join27(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
12336
12585
  }
12337
12586
  return 0;
12338
12587
  }
@@ -12358,8 +12607,8 @@ async function lintCmd(argv) {
12358
12607
  }
12359
12608
 
12360
12609
  // src/bin-internal/normalize.ts
12361
- import { existsSync as existsSync25, readdirSync as readdirSync7 } from "fs";
12362
- import { join as join26 } from "path";
12610
+ import { existsSync as existsSync27, readdirSync as readdirSync7 } from "fs";
12611
+ import { join as join28 } from "path";
12363
12612
  init_paths2();
12364
12613
  async function normalizeCmd(argv) {
12365
12614
  const ticket = argv[0];
@@ -12374,8 +12623,8 @@ async function normalizeCmd(argv) {
12374
12623
  console.error("[xera:normalize] no run found");
12375
12624
  return 1;
12376
12625
  }
12377
- const runDir = join26(paths.runsDir, runId);
12378
- if (!existsSync25(runDir)) {
12626
+ const runDir = join28(paths.runsDir, runId);
12627
+ if (!existsSync27(runDir)) {
12379
12628
  console.error(`[xera:normalize] runs/${runId} missing`);
12380
12629
  return 1;
12381
12630
  }
@@ -12395,14 +12644,14 @@ async function normalizeCmd(argv) {
12395
12644
 
12396
12645
  // src/bin-internal/post.ts
12397
12646
  init_paths2();
12398
- import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
12399
- import { join as join27 } from "path";
12647
+ import { existsSync as existsSync29, readFileSync as readFileSync26 } from "fs";
12648
+ import { join as join29 } from "path";
12400
12649
 
12401
12650
  // src/artifact/status.ts
12402
- import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync17 } from "fs";
12651
+ import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync25, writeFileSync as writeFileSync19 } from "fs";
12403
12652
  import { dirname as dirname8 } from "path";
12404
- import { z as z9 } from "zod";
12405
- var ClassificationEnum = z9.enum([
12653
+ import { z as z10 } from "zod";
12654
+ var ClassificationEnum = z10.enum([
12406
12655
  "PASS",
12407
12656
  "REAL_BUG",
12408
12657
  "SELECTOR_DRIFT",
@@ -12413,37 +12662,37 @@ var ClassificationEnum = z9.enum([
12413
12662
  "RATE_LIMITED",
12414
12663
  "AUTH_EXPIRED"
12415
12664
  ]);
12416
- var ResultEnum = z9.enum(["PASS", "FAIL"]);
12417
- var ConfidenceEnum = z9.enum(["low", "medium", "high"]);
12418
- var HistoryEntrySchema = z9.object({
12419
- ts: z9.string(),
12665
+ var ResultEnum = z10.enum(["PASS", "FAIL"]);
12666
+ var ConfidenceEnum = z10.enum(["low", "medium", "high"]);
12667
+ var HistoryEntrySchema = z10.object({
12668
+ ts: z10.string(),
12420
12669
  result: ResultEnum,
12421
12670
  class: ClassificationEnum
12422
12671
  });
12423
- var StatusJsonSchema = z9.object({
12424
- ticket: z9.string(),
12425
- lastRun: z9.string(),
12672
+ var StatusJsonSchema = z10.object({
12673
+ ticket: z10.string(),
12674
+ lastRun: z10.string(),
12426
12675
  result: ResultEnum,
12427
12676
  classification: ClassificationEnum,
12428
12677
  confidence: ConfidenceEnum,
12429
- scenarios: z9.object({
12430
- total: z9.number().int().nonnegative(),
12431
- passed: z9.number().int().nonnegative(),
12432
- failed: z9.number().int().nonnegative(),
12433
- skipped: z9.number().int().nonnegative()
12678
+ scenarios: z10.object({
12679
+ total: z10.number().int().nonnegative(),
12680
+ passed: z10.number().int().nonnegative(),
12681
+ failed: z10.number().int().nonnegative(),
12682
+ skipped: z10.number().int().nonnegative()
12434
12683
  }),
12435
- history: z9.array(HistoryEntrySchema).default([]),
12436
- last_jira_comment_id: z9.string().optional()
12684
+ history: z10.array(HistoryEntrySchema).default([]),
12685
+ last_jira_comment_id: z10.string().optional()
12437
12686
  });
12438
12687
  var HISTORY_CAP = 20;
12439
12688
  function readStatus(path) {
12440
- if (!existsSync26(path))
12689
+ if (!existsSync28(path))
12441
12690
  return null;
12442
- return StatusJsonSchema.parse(JSON.parse(readFileSync23(path, "utf8")));
12691
+ return StatusJsonSchema.parse(JSON.parse(readFileSync25(path, "utf8")));
12443
12692
  }
12444
12693
  function writeStatus(path, status) {
12445
12694
  mkdirSync17(dirname8(path), { recursive: true });
12446
- writeFileSync17(path, JSON.stringify(status, null, 2));
12695
+ writeFileSync19(path, JSON.stringify(status, null, 2));
12447
12696
  }
12448
12697
  function appendHistory(path, entry) {
12449
12698
  const s = readStatus(path);
@@ -12469,12 +12718,12 @@ async function postCmd(argv) {
12469
12718
  return 0;
12470
12719
  }
12471
12720
  const paths = resolveArtifactPaths(cwd, ticket);
12472
- const draftPath = join27(paths.ticketDir, "jira-comment.draft.md");
12473
- if (!existsSync27(draftPath)) {
12721
+ const draftPath = join29(paths.ticketDir, "jira-comment.draft.md");
12722
+ if (!existsSync29(draftPath)) {
12474
12723
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
12475
12724
  return 1;
12476
12725
  }
12477
- const body = readFileSync24(draftPath, "utf8");
12726
+ const body = readFileSync26(draftPath, "utf8");
12478
12727
  const client = await createJiraClient({
12479
12728
  baseUrl: config.jira.baseUrl,
12480
12729
  preferMcp: true,
@@ -12502,8 +12751,8 @@ async function promoteCmd(argv) {
12502
12751
  }
12503
12752
 
12504
12753
  // src/bin-internal/report.ts
12505
- import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync18 } from "fs";
12506
- import { join as join28 } from "path";
12754
+ import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync20 } from "fs";
12755
+ import { join as join30 } from "path";
12507
12756
  init_paths2();
12508
12757
 
12509
12758
  // src/classifier/aggregate.ts
@@ -12770,11 +13019,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
12770
13019
  }
12771
13020
 
12772
13021
  // src/reporter/status-writer.ts
12773
- import { existsSync as existsSync28 } from "fs";
13022
+ import { existsSync as existsSync30 } from "fs";
12774
13023
  function writeStatusFromClassification(path, input) {
12775
13024
  const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
12776
13025
  const entry = { ts: input.runTs, result, class: input.classification.overall };
12777
- if (!existsSync28(path)) {
13026
+ if (!existsSync30(path)) {
12778
13027
  writeStatus(path, {
12779
13028
  ticket: input.ticket,
12780
13029
  lastRun: input.runTs,
@@ -12808,22 +13057,22 @@ async function reportCmd(argv) {
12808
13057
  }
12809
13058
  const cwd = process.cwd();
12810
13059
  const paths = resolveArtifactPaths(cwd, ticket);
12811
- const input = JSON.parse(readFileSync25(inputArg.slice("--input=".length), "utf8"));
13060
+ const input = JSON.parse(readFileSync27(inputArg.slice("--input=".length), "utf8"));
12812
13061
  let httpRuleOverride = null;
12813
13062
  const meta = readMeta(paths.metaPath);
12814
13063
  if (meta?.adapter === "http") {
12815
13064
  const config = await loadConfig(cwd);
12816
13065
  if (config.http) {
12817
- const normalizedPath = join28(paths.ticketDir, "runs", input.runId, "normalized.json");
12818
- if (existsSync29(normalizedPath)) {
12819
- const norm = JSON.parse(readFileSync25(normalizedPath, "utf8"));
13066
+ const normalizedPath = join30(paths.ticketDir, "runs", input.runId, "normalized.json");
13067
+ if (existsSync31(normalizedPath)) {
13068
+ const norm = JSON.parse(readFileSync27(normalizedPath, "utf8"));
12820
13069
  const calls = norm.http?.calls ?? [];
12821
13070
  const rate = classifyRateLimited({ calls });
12822
13071
  if (rate)
12823
13072
  httpRuleOverride = rate;
12824
13073
  if (!httpRuleOverride) {
12825
13074
  const authFiles = {};
12826
- const httpAuthDir = join28(cwd, ".xera", ".auth", "http");
13075
+ const httpAuthDir = join30(cwd, ".xera", ".auth", "http");
12827
13076
  for (const role of Object.keys(config.http.auth.roles)) {
12828
13077
  const entry = readAuthState(httpAuthDir, role);
12829
13078
  if (entry) {
@@ -12868,8 +13117,8 @@ async function reportCmd(argv) {
12868
13117
  confidence: "high"
12869
13118
  } : s) : input.scenarios;
12870
13119
  const aggregated = aggregateScenarios(scenariosForAggregation);
12871
- const decisionsPath = join28(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
12872
- const decisions = existsSync29(decisionsPath) ? JSON.parse(readFileSync25(decisionsPath, "utf8")) : {};
13120
+ const decisionsPath = join30(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
13121
+ const decisions = existsSync31(decisionsPath) ? JSON.parse(readFileSync27(decisionsPath, "utf8")) : {};
12873
13122
  const graph = deriveSnapshot(loadAllEvents(process.cwd()));
12874
13123
  const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
12875
13124
  const scenarioIdByName = {};
@@ -12917,8 +13166,8 @@ async function reportCmd(argv) {
12917
13166
  xeraVersion: XERA_VERSION,
12918
13167
  promptsVersion: PROMPTS_VERSION
12919
13168
  });
12920
- const draftPath = join28(paths.ticketDir, "jira-comment.draft.md");
12921
- writeFileSync18(draftPath, md);
13169
+ const draftPath = join30(paths.ticketDir, "jira-comment.draft.md");
13170
+ writeFileSync20(draftPath, md);
12922
13171
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
12923
13172
  return 0;
12924
13173
  }
@@ -12989,7 +13238,7 @@ async function unlockCmd(argv) {
12989
13238
 
12990
13239
  // src/bin-internal/validate-feature.ts
12991
13240
  init_paths2();
12992
- import { existsSync as existsSync30, readFileSync as readFileSync26 } from "fs";
13241
+ import { existsSync as existsSync32, readFileSync as readFileSync28 } from "fs";
12993
13242
  import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
12994
13243
  async function validateFeatureCmd(argv) {
12995
13244
  const ticket = argv[0];
@@ -12998,11 +13247,11 @@ async function validateFeatureCmd(argv) {
12998
13247
  return 1;
12999
13248
  }
13000
13249
  const paths = resolveArtifactPaths(process.cwd(), ticket);
13001
- if (!existsSync30(paths.featurePath)) {
13250
+ if (!existsSync32(paths.featurePath)) {
13002
13251
  console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
13003
13252
  return 1;
13004
13253
  }
13005
- const r = validateGherkin2(readFileSync26(paths.featurePath, "utf8"));
13254
+ const r = validateGherkin2(readFileSync28(paths.featurePath, "utf8"));
13006
13255
  if (r.ok) {
13007
13256
  console.log("[xera:validate-feature] ok");
13008
13257
  return 0;
@@ -13024,6 +13273,8 @@ var COMMANDS = {
13024
13273
  "eval-prepare": evalPrepareCmd,
13025
13274
  "eval-report": evalReportCmd,
13026
13275
  exec: execCmd,
13276
+ "explore-finalize": exploreFinalizeCmd,
13277
+ "explore-prepare": explorePrepareCmd,
13027
13278
  "fill-gap-finalize": fillGapFinalizeCmd,
13028
13279
  "fill-gap-prepare": fillGapPrepareCmd,
13029
13280
  fetch: fetchCmd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/core",
3
- "version": "0.11.6",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "zod": "4.4.3",
34
- "@xera-ai/web": "^0.11.6",
35
- "@xera-ai/http": "^0.11.6",
34
+ "@xera-ai/web": "^0.12.0",
35
+ "@xera-ai/http": "^0.12.0",
36
36
  "@playwright/test": "1.60.0",
37
37
  "dotenv": "^16.0.0",
38
38
  "fflate": "0.8.3",
@@ -0,0 +1,152 @@
1
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+
5
+ const CATEGORY_ENUM = z.enum([
6
+ 'negative',
7
+ 'boundary',
8
+ 'state-combination',
9
+ 'race',
10
+ 'error-recovery',
11
+ 'a11y',
12
+ 'security-smell',
13
+ 'non-functional',
14
+ ]);
15
+
16
+ const ProposalsSchema = z.object({
17
+ proposals: z.array(
18
+ z.object({
19
+ id: z.string().min(1),
20
+ ticketId: z.string().min(1),
21
+ category: CATEGORY_ENUM,
22
+ severity: z.enum(['low', 'medium', 'high']),
23
+ title: z.string().min(1),
24
+ rationale: z.string().min(1),
25
+ gherkin: z.string().min(1),
26
+ }),
27
+ ),
28
+ });
29
+
30
+ type Proposals = z.infer<typeof ProposalsSchema>;
31
+ type Proposal = Proposals['proposals'][number];
32
+
33
+ interface ParsedArgs {
34
+ ticket: string;
35
+ accept: string;
36
+ }
37
+
38
+ function parseArgs(argv: string[]): ParsedArgs | { error: string } {
39
+ let ticket: string | undefined;
40
+ let accept: string | undefined;
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const a = argv[i];
43
+ if (a === '--accept') {
44
+ const v = argv[++i];
45
+ if (v !== undefined) accept = v;
46
+ } else if (a === '--help-stub') {
47
+ /* no-op */
48
+ } else if (a && !a.startsWith('--') && ticket === undefined) {
49
+ ticket = a;
50
+ } else {
51
+ return { error: `unknown flag: ${a}` };
52
+ }
53
+ }
54
+ if (!ticket) return { error: 'ticket key is required as a positional argument' };
55
+ if (!accept)
56
+ return { error: '--accept is required (comma-separated IDs, "all", or "high-only")' };
57
+ return { ticket, accept };
58
+ }
59
+
60
+ function selectProposals(all: Proposal[], accept: string): Proposal[] | { error: string } {
61
+ const trimmed = accept.trim();
62
+ if (trimmed === 'all') return all;
63
+ if (trimmed === 'high-only') return all.filter((p) => p.severity === 'high');
64
+ const ids = new Set(
65
+ trimmed
66
+ .split(',')
67
+ .map((s) => s.trim())
68
+ .filter(Boolean),
69
+ );
70
+ if (ids.size === 0) return { error: 'no IDs supplied' };
71
+ const picked = all.filter((p) => ids.has(p.id));
72
+ const found = new Set(picked.map((p) => p.id));
73
+ const missing = [...ids].filter((id) => !found.has(id));
74
+ if (missing.length > 0) return { error: `unknown proposal IDs: ${missing.join(', ')}` };
75
+ return picked;
76
+ }
77
+
78
+ function ensureFeatureHeader(ticketDir: string, ticket: string): string {
79
+ const explorePath = join(ticketDir, 'explore.feature');
80
+ if (existsSync(explorePath)) return explorePath;
81
+
82
+ // Synthesize a Feature header. Prefer copying from test.feature if present.
83
+ const testFeaturePath = join(ticketDir, 'test.feature');
84
+ let header: string;
85
+ if (existsSync(testFeaturePath)) {
86
+ const testContent = readFileSync(testFeaturePath, 'utf8');
87
+ const firstFeatureMatch = testContent.match(/^Feature:.*$/m);
88
+ header = firstFeatureMatch
89
+ ? `${firstFeatureMatch[0]} (adversarial)\n Adversarial scenarios beyond the acceptance criteria.\n Generated by /xera-explore — review before merging into test.feature.\n\n`
90
+ : `Feature: ${ticket} adversarial\n\n`;
91
+ } else {
92
+ header = `Feature: ${ticket} adversarial\n Adversarial scenarios beyond the acceptance criteria.\n Generated by /xera-explore — review before merging into test.feature.\n\n`;
93
+ }
94
+ writeFileSync(explorePath, header);
95
+ return explorePath;
96
+ }
97
+
98
+ function formatScenario(p: Proposal): string {
99
+ const tags = `@adversarial @adversarial-${p.category} @severity-${p.severity}`;
100
+ const rationaleComment = ` # ${p.rationale}`;
101
+ const indented = p.gherkin
102
+ .split('\n')
103
+ .map((line) => (line.startsWith('Scenario:') ? ` ${line}` : line ? ` ${line}` : line))
104
+ .join('\n');
105
+ return `\n ${tags}\n${rationaleComment}\n${indented}\n`;
106
+ }
107
+
108
+ export async function exploreFinalizeCmd(argv: string[]): Promise<number> {
109
+ const parsed = parseArgs(argv);
110
+ if ('error' in parsed) {
111
+ console.error(`[explore-finalize] ${parsed.error}`);
112
+ return 1;
113
+ }
114
+
115
+ const cwd = process.cwd();
116
+ const ticketDir = join(cwd, '.xera', parsed.ticket);
117
+ const proposalsPath = join(ticketDir, 'adversarial-proposals.json');
118
+ if (!existsSync(proposalsPath)) {
119
+ console.error(
120
+ `[explore-finalize] adversarial-proposals.json not found for ${parsed.ticket} — run /xera-explore Step 4 first`,
121
+ );
122
+ return 2;
123
+ }
124
+
125
+ let proposals: Proposals;
126
+ try {
127
+ const raw = JSON.parse(readFileSync(proposalsPath, 'utf8'));
128
+ proposals = ProposalsSchema.parse(raw);
129
+ } catch (e) {
130
+ console.error(`[explore-finalize] invalid proposals JSON: ${(e as Error).message}`);
131
+ return 1;
132
+ }
133
+
134
+ const picked = selectProposals(proposals.proposals, parsed.accept);
135
+ if ('error' in picked) {
136
+ console.error(`[explore-finalize] ${picked.error}`);
137
+ return 1;
138
+ }
139
+ if (picked.length === 0) {
140
+ console.error('[explore-finalize] no proposals matched the --accept filter');
141
+ return 1;
142
+ }
143
+
144
+ const explorePath = ensureFeatureHeader(ticketDir, parsed.ticket);
145
+ for (const p of picked) {
146
+ appendFileSync(explorePath, formatScenario(p));
147
+ }
148
+ console.log(
149
+ `[explore-finalize] appended ${picked.length} scenario(s) to ${explorePath} (${picked.map((p) => p.id).join(', ')})`,
150
+ );
151
+ return 0;
152
+ }
@@ -0,0 +1,142 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const VALID_CATEGORIES = [
5
+ 'negative',
6
+ 'boundary',
7
+ 'state-combination',
8
+ 'race',
9
+ 'error-recovery',
10
+ 'a11y',
11
+ 'security-smell',
12
+ 'non-functional',
13
+ ] as const;
14
+
15
+ type Category = (typeof VALID_CATEGORIES)[number];
16
+
17
+ interface ParsedArgs {
18
+ ticket: string;
19
+ categoriesInclude: Category[];
20
+ userHint: string;
21
+ }
22
+
23
+ function parseArgs(argv: string[]): ParsedArgs | { error: string } {
24
+ let ticket: string | undefined;
25
+ let categoriesRaw = '';
26
+ let userHint = '';
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a === '--categories') {
30
+ const v = argv[++i];
31
+ if (v !== undefined) categoriesRaw = v;
32
+ } else if (a === '--user-hint') {
33
+ const v = argv[++i];
34
+ if (v !== undefined) userHint = v;
35
+ } else if (a === '--help-stub') {
36
+ /* no-op */
37
+ } else if (a && !a.startsWith('--') && ticket === undefined) {
38
+ ticket = a;
39
+ } else {
40
+ return { error: `unknown flag: ${a}` };
41
+ }
42
+ }
43
+ if (!ticket) return { error: 'ticket key is required as a positional argument' };
44
+
45
+ const categoriesInclude: Category[] = [];
46
+ for (const slug of categoriesRaw
47
+ .split(',')
48
+ .map((s) => s.trim())
49
+ .filter(Boolean)) {
50
+ if (!(VALID_CATEGORIES as readonly string[]).includes(slug)) {
51
+ return { error: `invalid category: ${slug}` };
52
+ }
53
+ categoriesInclude.push(slug as Category);
54
+ }
55
+
56
+ return { ticket, categoriesInclude, userHint };
57
+ }
58
+
59
+ interface AdversarialInput {
60
+ ticket: { id: string; summary: string; story: string; ac: string[] };
61
+ existingFeature?: string;
62
+ existingSpec?: string;
63
+ adapter: 'web' | 'http';
64
+ categoriesInclude: Category[];
65
+ userHint?: string;
66
+ }
67
+
68
+ function parseStoryMd(content: string): { summary: string; ac: string[]; body: string } {
69
+ // story.md format: optional frontmatter with `summary:` and `ac:` (yaml-ish list),
70
+ // followed by the body. We do a minimal parse — full YAML is overkill here.
71
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
72
+ if (!fmMatch) return { summary: '', ac: [], body: content };
73
+ const [, fm, body] = fmMatch;
74
+ const summaryMatch = fm!.match(/^summary:\s*(.+)$/m);
75
+ const summary = summaryMatch?.[1]?.trim() ?? '';
76
+ const ac: string[] = [];
77
+ const acBlock = fm!.match(/^ac:\s*\n((?:\s*-\s.+\n?)+)/m);
78
+ if (acBlock) {
79
+ for (const line of acBlock[1]!.split('\n')) {
80
+ const m = line.match(/^\s*-\s*(.+)$/);
81
+ if (m) ac.push(m[1]!.trim());
82
+ }
83
+ }
84
+ return { summary, ac, body: body!.trim() };
85
+ }
86
+
87
+ export async function explorePrepareCmd(argv: string[]): Promise<number> {
88
+ const parsed = parseArgs(argv);
89
+ if ('error' in parsed) {
90
+ console.error(`[explore-prepare] ${parsed.error}`);
91
+ return 1;
92
+ }
93
+
94
+ const cwd = process.cwd();
95
+ const configPath = join(cwd, 'xera.config.ts');
96
+ if (!existsSync(configPath)) {
97
+ console.error('[explore-prepare] xera.config.ts not found — run inside a xera project');
98
+ return 2;
99
+ }
100
+
101
+ const ticketDir = join(cwd, '.xera', parsed.ticket);
102
+ const storyPath = join(ticketDir, 'story.md');
103
+ if (!existsSync(storyPath)) {
104
+ console.error(
105
+ `[explore-prepare] no story for ${parsed.ticket} — run /xera-fetch ${parsed.ticket} first`,
106
+ );
107
+ return 2;
108
+ }
109
+
110
+ const story = readFileSync(storyPath, 'utf8');
111
+ const { summary, ac, body } = parseStoryMd(story);
112
+
113
+ let adapter: 'web' | 'http' = 'web';
114
+ const metaPath = join(ticketDir, 'meta.json');
115
+ if (existsSync(metaPath)) {
116
+ try {
117
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8')) as { adapter?: string };
118
+ if (meta.adapter === 'http') adapter = 'http';
119
+ } catch {
120
+ /* leave default web */
121
+ }
122
+ }
123
+
124
+ const input: AdversarialInput = {
125
+ ticket: { id: parsed.ticket, summary, story: body, ac },
126
+ adapter,
127
+ categoriesInclude: parsed.categoriesInclude,
128
+ };
129
+
130
+ const featurePath = join(ticketDir, 'test.feature');
131
+ if (existsSync(featurePath)) input.existingFeature = readFileSync(featurePath, 'utf8');
132
+
133
+ const specPath = join(ticketDir, 'spec.ts');
134
+ if (existsSync(specPath)) input.existingSpec = readFileSync(specPath, 'utf8');
135
+
136
+ if (parsed.userHint) input.userHint = parsed.userHint;
137
+
138
+ const outPath = join(ticketDir, 'adversarial-input.json');
139
+ writeFileSync(outPath, JSON.stringify(input, null, 2));
140
+ console.log(`[explore-prepare] wrote ${outPath}`);
141
+ return 0;
142
+ }
@@ -8,6 +8,8 @@ import { evalDeterministicCmd } from './eval-deterministic';
8
8
  import { evalPrepareCmd } from './eval-prepare';
9
9
  import { evalReportCmd } from './eval-report';
10
10
  import { execCmd } from './exec';
11
+ import { exploreFinalizeCmd } from './explore-finalize';
12
+ import { explorePrepareCmd } from './explore-prepare';
11
13
  import { fetchCmd } from './fetch';
12
14
  import { fillGapFinalizeCmd } from './fill-gap-finalize';
13
15
  import { fillGapPrepareCmd } from './fill-gap-prepare';
@@ -41,6 +43,8 @@ const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
41
43
  'eval-prepare': evalPrepareCmd,
42
44
  'eval-report': evalReportCmd,
43
45
  exec: execCmd,
46
+ 'explore-finalize': exploreFinalizeCmd,
47
+ 'explore-prepare': explorePrepareCmd,
44
48
  'fill-gap-finalize': fillGapFinalizeCmd,
45
49
  'fill-gap-prepare': fillGapPrepareCmd,
46
50
  fetch: fetchCmd,