@xera-ai/core 0.11.5 → 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.
@@ -8853,11 +8853,14 @@ async function authSetupCmd(argv) {
8853
8853
  const mod = await import(pathToFileURL2(authSetupScript).href);
8854
8854
  let exitCode = 0;
8855
8855
  if ((opts.shape === "all" || opts.shape === "web") && config.web && typeof mod.web === "function") {
8856
+ const webConfig = config.web;
8857
+ const envName = process.env.XERA_ENV ?? webConfig.defaultEnv;
8858
+ const baseURL = process.env.XERA_BASE_URL ?? webConfig.baseUrl[envName] ?? webConfig.baseUrl[webConfig.defaultEnv];
8856
8859
  const { runAuthSetup } = await import("@xera-ai/web");
8857
8860
  const { chromium } = await import("@playwright/test");
8858
8861
  const browser = await chromium.launch();
8859
8862
  try {
8860
- for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
8863
+ for (const [roleName, roleCreds] of Object.entries(webConfig.auth.roles)) {
8861
8864
  if (opts.role && roleName !== opts.role)
8862
8865
  continue;
8863
8866
  const email = process.env[roleCreds.envEmail];
@@ -8873,7 +8876,8 @@ async function authSetupCmd(argv) {
8873
8876
  creds: { email, password },
8874
8877
  setupScriptPath: authSetupScript,
8875
8878
  authDir: join5(cwd, ".xera", ".auth"),
8876
- browser
8879
+ browser,
8880
+ ...baseURL ? { baseURL } : {}
8877
8881
  });
8878
8882
  console.log(`[xera:auth-setup] \u2713 ${roleName}.json (web)`);
8879
8883
  } catch (e) {
@@ -10540,21 +10544,270 @@ async function execCmd(argv) {
10540
10544
  }
10541
10545
  }
10542
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
+
10543
10796
  // src/bin-internal/fetch.ts
10544
- import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
10797
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "fs";
10545
10798
  import { dirname as dirname5 } from "path";
10546
10799
 
10547
10800
  // src/artifact/hash.ts
10548
10801
  import { createHash as createHash4 } from "crypto";
10549
- import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
10802
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
10550
10803
  function hashString(s) {
10551
10804
  return `sha256:${createHash4("sha256").update(s).digest("hex")}`;
10552
10805
  }
10553
10806
  function hashFile(path) {
10554
- return hashString(readFileSync16(path, "utf8"));
10807
+ return hashString(readFileSync18(path, "utf8"));
10555
10808
  }
10556
10809
  function hashFileIfExists(path) {
10557
- if (!existsSync19(path))
10810
+ if (!existsSync21(path))
10558
10811
  return null;
10559
10812
  return hashFile(path);
10560
10813
  }
@@ -10563,33 +10816,33 @@ function hashFileIfExists(path) {
10563
10816
  init_paths2();
10564
10817
 
10565
10818
  // src/jira/mcp-backend.ts
10566
- 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";
10567
10820
  import { tmpdir } from "os";
10568
- import { join as join18 } from "path";
10821
+ import { join as join20 } from "path";
10569
10822
  var MCP_ENV = "XERA_MCP_JIRA";
10570
10823
  async function createMcpBackend(_baseUrl) {
10571
10824
  if (process.env[MCP_ENV] !== "1")
10572
10825
  return null;
10573
- const tmpDir = join18(tmpdir(), "xera-mcp");
10826
+ const tmpDir = join20(tmpdir(), "xera-mcp");
10574
10827
  mkdirSync11(tmpDir, { recursive: true });
10575
10828
  return {
10576
10829
  backend: "mcp",
10577
10830
  async fetchTicket(key, _fields) {
10578
- const cachePath = join18(tmpDir, `${key}.json`);
10579
- if (!existsSync20(cachePath)) {
10831
+ const cachePath = join20(tmpDir, `${key}.json`);
10832
+ if (!existsSync22(cachePath)) {
10580
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.`);
10581
10834
  }
10582
- const parsed = JSON.parse(readFileSync17(cachePath, "utf8"));
10835
+ const parsed = JSON.parse(readFileSync19(cachePath, "utf8"));
10583
10836
  return parsed;
10584
10837
  },
10585
10838
  async postComment(key, body) {
10586
- const outPath = join18(tmpDir, `${key}.comment.json`);
10587
- writeFileSync10(outPath, JSON.stringify({ key, body }));
10839
+ const outPath = join20(tmpDir, `${key}.comment.json`);
10840
+ writeFileSync12(outPath, JSON.stringify({ key, body }));
10588
10841
  return { id: "mcp-pending" };
10589
10842
  },
10590
10843
  async transitionStatus(key, statusName) {
10591
- const outPath = join18(tmpDir, `${key}.transition.json`);
10592
- writeFileSync10(outPath, JSON.stringify({ key, statusName }));
10844
+ const outPath = join20(tmpDir, `${key}.transition.json`);
10845
+ writeFileSync12(outPath, JSON.stringify({ key, statusName }));
10593
10846
  },
10594
10847
  async listFields(_sampleKey) {
10595
10848
  throw new Error("listFields is REST-only; init flow uses REST for field discovery.");
@@ -10743,7 +10996,7 @@ async function fetchCmd(argv, opts = {}) {
10743
10996
  const acLines = parseAcLines(t.acceptanceCriteria);
10744
10997
  const full = renderStory(t.key, t.summary, storyHash, acLines, body);
10745
10998
  mkdirSync12(dirname5(paths.storyPath), { recursive: true });
10746
- writeFileSync11(paths.storyPath, full);
10999
+ writeFileSync13(paths.storyPath, full);
10747
11000
  const existing = readMeta(paths.metaPath);
10748
11001
  writeMeta(paths.metaPath, {
10749
11002
  ticket,
@@ -10804,20 +11057,20 @@ function renderStory(key, summary, storyHash, acLines, body) {
10804
11057
  }
10805
11058
 
10806
11059
  // src/bin-internal/fill-gap-finalize.ts
10807
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
10808
- import { join as join19 } from "path";
10809
- import { z as z7 } from "zod";
10810
- var ProposalsSchema = z7.object({
10811
- proposals: z7.array(z7.object({
10812
- id: z7.string().min(1),
10813
- ticketId: z7.string().min(1),
10814
- title: z7.string().min(1),
10815
- rationale: z7.string().min(1),
10816
- gherkin: z7.string().min(1),
10817
- 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())
10818
11071
  }))
10819
11072
  });
10820
- function parseArgs4(argv) {
11073
+ function parseArgs6(argv) {
10821
11074
  let accept;
10822
11075
  let ticket;
10823
11076
  let source;
@@ -10867,21 +11120,21 @@ function formatDraft(ticketId, proposal) {
10867
11120
  }
10868
11121
  async function fillGapFinalizeCmd(argv) {
10869
11122
  if (argv.includes("--help-stub")) {}
10870
- const parsed = parseArgs4(argv);
11123
+ const parsed = parseArgs6(argv);
10871
11124
  if ("error" in parsed) {
10872
11125
  console.error(`[fill-gap-finalize] ${parsed.error}`);
10873
11126
  return 1;
10874
11127
  }
10875
11128
  const cwd = process.cwd();
10876
- const sourcePath = parsed.source ?? join19(cwd, ".xera/coverage/proposals.json");
10877
- if (!existsSync21(sourcePath)) {
11129
+ const sourcePath = parsed.source ?? join21(cwd, ".xera/coverage/proposals.json");
11130
+ if (!existsSync23(sourcePath)) {
10878
11131
  console.error(`[fill-gap-finalize] source not found: ${sourcePath}`);
10879
11132
  return 2;
10880
11133
  }
10881
11134
  let proposals;
10882
11135
  try {
10883
- const raw = JSON.parse(readFileSync18(sourcePath, "utf8"));
10884
- proposals = ProposalsSchema.parse(raw);
11136
+ const raw = JSON.parse(readFileSync20(sourcePath, "utf8"));
11137
+ proposals = ProposalsSchema2.parse(raw);
10885
11138
  } catch (e) {
10886
11139
  console.error(`[fill-gap-finalize] invalid proposals: ${e.message}`);
10887
11140
  return 2;
@@ -10891,22 +11144,22 @@ async function fillGapFinalizeCmd(argv) {
10891
11144
  console.error(`[fill-gap-finalize] proposal id "${parsed.accept}" not in source`);
10892
11145
  return 2;
10893
11146
  }
10894
- const ticketDir = join19(cwd, ".xera", parsed.ticket);
11147
+ const ticketDir = join21(cwd, ".xera", parsed.ticket);
10895
11148
  mkdirSync13(ticketDir, { recursive: true });
10896
- const draftPath = join19(ticketDir, "feature.draft.md");
10897
- if (existsSync21(draftPath) && !parsed.force) {
11149
+ const draftPath = join21(ticketDir, "feature.draft.md");
11150
+ if (existsSync23(draftPath) && !parsed.force) {
10898
11151
  console.error(`[fill-gap-finalize] ${draftPath} exists; pass --force to overwrite`);
10899
11152
  return 3;
10900
11153
  }
10901
- writeFileSync12(draftPath, formatDraft(parsed.ticket, proposal));
11154
+ writeFileSync14(draftPath, formatDraft(parsed.ticket, proposal));
10902
11155
  return 0;
10903
11156
  }
10904
11157
 
10905
11158
  // src/bin-internal/fill-gap-prepare.ts
10906
11159
  init_store();
10907
- import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
10908
- import { join as join20 } from "path";
10909
- function parseArgs5(argv) {
11160
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync15 } from "fs";
11161
+ import { join as join22 } from "path";
11162
+ function parseArgs7(argv) {
10910
11163
  const args = {};
10911
11164
  for (let i = 0;i < argv.length; i++) {
10912
11165
  const a = argv[i];
@@ -10972,7 +11225,7 @@ function buildTicketContext(snap, ticketId) {
10972
11225
  };
10973
11226
  }
10974
11227
  async function fillGapPrepareCmd(argv) {
10975
- const parsed = parseArgs5(argv);
11228
+ const parsed = parseArgs7(argv);
10976
11229
  if ("error" in parsed) {
10977
11230
  console.error(`[fill-gap-prepare] ${parsed.error}`);
10978
11231
  return 1;
@@ -10996,9 +11249,9 @@ async function fillGapPrepareCmd(argv) {
10996
11249
  return 2;
10997
11250
  }
10998
11251
  }
10999
- const outDir = parsed.outputDir ?? join20(cwd, ".xera/coverage", scope);
11252
+ const outDir = parsed.outputDir ?? join22(cwd, ".xera/coverage", scope);
11000
11253
  mkdirSync14(outDir, { recursive: true });
11001
- writeFileSync13(join20(outDir, "context.json"), JSON.stringify(context, null, 2));
11254
+ writeFileSync15(join22(outDir, "context.json"), JSON.stringify(context, null, 2));
11002
11255
  return 0;
11003
11256
  }
11004
11257
 
@@ -11008,18 +11261,18 @@ init_graph_backfill();
11008
11261
  // src/graph/enrich.ts
11009
11262
  init_store();
11010
11263
  init_ulid();
11011
- import { existsSync as existsSync22, readFileSync as readFileSync19, unlinkSync as unlinkSync2 } from "fs";
11012
- import { join as join21 } from "path";
11013
- 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";
11014
11267
  var MAX_SIMILAR_EDGES = 10;
11015
11268
  var MIN_CONFIDENCE = 0.7;
11016
- var SimilarEntrySchema = z8.object({
11017
- ticketId: z8.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
11018
- confidence: z8.number(),
11019
- 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()
11020
11273
  });
11021
- var EnrichmentInputSchema = z8.object({
11022
- similar: z8.array(SimilarEntrySchema)
11274
+ var EnrichmentInputSchema = z9.object({
11275
+ similar: z9.array(SimilarEntrySchema)
11023
11276
  });
11024
11277
  var nowIso3 = () => new Date().toISOString();
11025
11278
  var mk2 = (actor, type, payload) => ({
@@ -11038,11 +11291,11 @@ async function enrichTicket(repoRoot, ticketId, opts) {
11038
11291
  if (snapshot.tickets[ticketId].enrichedAt && !opts.force) {
11039
11292
  return { ticketId, similarCount: 0, enrichedAt: snapshot.tickets[ticketId].enrichedAt };
11040
11293
  }
11041
- const inputPath = join21(repoRoot, ".xera", ticketId, "enrichment-input.json");
11042
- if (!existsSync22(inputPath)) {
11294
+ const inputPath = join23(repoRoot, ".xera", ticketId, "enrichment-input.json");
11295
+ if (!existsSync24(inputPath)) {
11043
11296
  throw new Error(`enrichment-input.json not found at ${inputPath}`);
11044
11297
  }
11045
- const raw = JSON.parse(readFileSync19(inputPath, "utf8"));
11298
+ const raw = JSON.parse(readFileSync21(inputPath, "utf8"));
11046
11299
  const parsed = EnrichmentInputSchema.safeParse(raw);
11047
11300
  if (!parsed.success) {
11048
11301
  throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
@@ -11147,12 +11400,12 @@ async function graphQueryCmd(argv) {
11147
11400
  init_graph_record();
11148
11401
 
11149
11402
  // src/bin-internal/graph-render.ts
11150
- import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync21, renameSync as renameSync2, writeFileSync as writeFileSync14 } from "fs";
11151
- 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";
11152
11405
 
11153
11406
  // src/graph/render.ts
11154
- import { readFileSync as readFileSync20 } from "fs";
11155
- 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";
11156
11409
  import { fileURLToPath } from "url";
11157
11410
  var COLORS = {
11158
11411
  ticket: "#3B82F6",
@@ -11371,9 +11624,9 @@ function transformForVisNetwork(snap, opts) {
11371
11624
  }
11372
11625
  var __filename2 = fileURLToPath(import.meta.url);
11373
11626
  var __dirname2 = dirname6(__filename2);
11374
- var TEMPLATES_DIR = join22(__dirname2, "templates");
11627
+ var TEMPLATES_DIR = join24(__dirname2, "templates");
11375
11628
  function loadTemplate(name) {
11376
- return readFileSync20(join22(TEMPLATES_DIR, name), "utf8");
11629
+ return readFileSync22(join24(TEMPLATES_DIR, name), "utf8");
11377
11630
  }
11378
11631
  function statsToHuman(s) {
11379
11632
  return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
@@ -11425,7 +11678,7 @@ async function graphRenderCmd(argv) {
11425
11678
  includeCoverage = true;
11426
11679
  }
11427
11680
  const repoRoot = process.cwd();
11428
- const finalPath = outPath ?? join23(repoRoot, ".xera/graph.html");
11681
+ const finalPath = outPath ?? join25(repoRoot, ".xera/graph.html");
11429
11682
  const events = loadAllEvents(repoRoot);
11430
11683
  const snap = deriveSnapshot(events);
11431
11684
  const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
@@ -11433,7 +11686,7 @@ async function graphRenderCmd(argv) {
11433
11686
  if (performanceMode === "text-fallback") {
11434
11687
  const txtPath = finalPath.replace(/\.html$/, ".txt");
11435
11688
  mkdirSync15(dirname7(txtPath), { recursive: true });
11436
- 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.
11437
11690
  `);
11438
11691
  console.log(`[graph-render] graph too large (${totalNodeCount} nodes); wrote ${txtPath}`);
11439
11692
  return 0;
@@ -11445,9 +11698,9 @@ async function graphRenderCmd(argv) {
11445
11698
  opts.since = since;
11446
11699
  let coverage;
11447
11700
  if (includeCoverage) {
11448
- const reportPath = join23(repoRoot, ".xera/coverage/report.json");
11449
- if (existsSync23(reportPath)) {
11450
- 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"));
11451
11704
  const snapshots = events.filter((e) => e.type === "coverage.snapshot").map((e) => e.payload);
11452
11705
  coverage = { report, snapshots };
11453
11706
  } else {
@@ -11465,7 +11718,7 @@ async function graphRenderCmd(argv) {
11465
11718
  const html = renderHtml(renderInput);
11466
11719
  mkdirSync15(dirname7(finalPath), { recursive: true });
11467
11720
  const tmpPath = `${finalPath}.tmp`;
11468
- writeFileSync14(tmpPath, html);
11721
+ writeFileSync16(tmpPath, html);
11469
11722
  renameSync2(tmpPath, finalPath);
11470
11723
  console.log(`[graph-render] wrote ${finalPath} (${data.stats.tickets} tickets \xB7 ${data.stats.scenarios} scenarios \xB7 ${html.length} bytes)`);
11471
11724
  return 0;
@@ -11496,8 +11749,8 @@ async function graphSnapshotCmd(argv) {
11496
11749
  }
11497
11750
 
11498
11751
  // src/bin-internal/heal-prepare.ts
11499
- import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
11500
- 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";
11501
11754
  import { scrubFreeText } from "@xera-ai/web";
11502
11755
 
11503
11756
  // ../../node_modules/fflate/esm/index.mjs
@@ -11844,15 +12097,15 @@ function strFromU8(dat, latin1) {
11844
12097
  var slzh = function(d, b) {
11845
12098
  return b + 30 + b2(d, b + 26) + b2(d, b + 28);
11846
12099
  };
11847
- var zh = function(d, b, z9) {
12100
+ var zh = function(d, b, z10) {
11848
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;
11849
- 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];
11850
12103
  return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
11851
12104
  };
11852
- var z64hs = function(d, b, l, z9, sc, su, off) {
12105
+ var z64hs = function(d, b, l, z10, sc, su, off) {
11853
12106
  var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
11854
12107
  var nf = nsc + nsu + noff;
11855
- if (z9 && nf) {
12108
+ if (z10 && nf) {
11856
12109
  for (;b + 4 < e; b += 4 + b2(d, b + 2)) {
11857
12110
  if (b2(d, b) == 1) {
11858
12111
  return [
@@ -11863,7 +12116,7 @@ var z64hs = function(d, b, l, z9, sc, su, off) {
11863
12116
  ];
11864
12117
  }
11865
12118
  }
11866
- if (z9 < 2)
12119
+ if (z10 < 2)
11867
12120
  err(13);
11868
12121
  }
11869
12122
  return [sc, su, off, 0];
@@ -11879,18 +12132,18 @@ function unzipSync(data, opts) {
11879
12132
  if (!c)
11880
12133
  return {};
11881
12134
  var o = b4(data, e + 16);
11882
- var z9 = b4(data, e - 20) == 117853008;
11883
- if (z9) {
12135
+ var z10 = b4(data, e - 20) == 117853008;
12136
+ if (z10) {
11884
12137
  var ze = b4(data, e - 12);
11885
- z9 = b4(data, ze) == 101075792;
11886
- if (z9) {
12138
+ z10 = b4(data, ze) == 101075792;
12139
+ if (z10) {
11887
12140
  c = b4(data, ze + 32);
11888
12141
  o = b4(data, ze + 48);
11889
12142
  }
11890
12143
  }
11891
12144
  var fltr = opts && opts.filter;
11892
12145
  for (var i2 = 0;i2 < c; ++i2) {
11893
- 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);
11894
12147
  o = no;
11895
12148
  if (!fltr || fltr({
11896
12149
  name: fn,
@@ -11926,9 +12179,9 @@ function classifyKind(raw) {
11926
12179
  return "other";
11927
12180
  }
11928
12181
  function extractDomSnapshot(tracePath) {
11929
- if (!existsSync24(tracePath))
12182
+ if (!existsSync26(tracePath))
11930
12183
  return "";
11931
- const buf = readFileSync22(tracePath);
12184
+ const buf = readFileSync24(tracePath);
11932
12185
  const entries = unzipSync(buf);
11933
12186
  const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
11934
12187
  let chosenKey = null;
@@ -11976,16 +12229,16 @@ function extractDomSnapshot(tracePath) {
11976
12229
  return scrubFreeText(html);
11977
12230
  }
11978
12231
  function findPomLine(ticketDir, rawLocator) {
11979
- const pomDir = join24(ticketDir, "page-objects");
12232
+ const pomDir = join26(ticketDir, "page-objects");
11980
12233
  const candidates = [];
11981
- if (existsSync24(pomDir)) {
12234
+ if (existsSync26(pomDir)) {
11982
12235
  for (const name of readdirSync6(pomDir)) {
11983
12236
  if (name.endsWith(".ts"))
11984
- candidates.push(join24(pomDir, name));
12237
+ candidates.push(join26(pomDir, name));
11985
12238
  }
11986
12239
  }
11987
12240
  for (const file of candidates) {
11988
- const text = readFileSync22(file, "utf8");
12241
+ const text = readFileSync24(file, "utf8");
11989
12242
  const lines = text.split(`
11990
12243
  `);
11991
12244
  for (let i2 = 0;i2 < lines.length; i2++) {
@@ -12023,13 +12276,13 @@ function findGherkinStep(featureText, rawLocator) {
12023
12276
  }
12024
12277
  function healPrepare(repoRoot, ticket, runId, scenarioName) {
12025
12278
  const paths = resolveArtifactPaths(repoRoot, ticket);
12026
- const classifierPath = join24(paths.ticketDir, "classifier-input.json");
12027
- const classifier = JSON.parse(readFileSync22(classifierPath, "utf8"));
12279
+ const classifierPath = join26(paths.ticketDir, "classifier-input.json");
12280
+ const classifier = JSON.parse(readFileSync24(classifierPath, "utf8"));
12028
12281
  const cls = classifier.scenarios.find((s) => s.name === scenarioName);
12029
12282
  if (!cls)
12030
12283
  throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
12031
- const runDir = join24(paths.runsDir, runId);
12032
- 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"));
12033
12286
  const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
12034
12287
  if (!normSc?.failure)
12035
12288
  throw new Error(`no failure recorded for scenario "${scenarioName}"`);
@@ -12040,9 +12293,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
12040
12293
  const raw = m[1].trim();
12041
12294
  const kind = classifyKind(raw);
12042
12295
  const pomLoc = findPomLine(paths.ticketDir, raw);
12043
- const featureText = readFileSync22(paths.featurePath, "utf8");
12296
+ const featureText = readFileSync24(paths.featurePath, "utf8");
12044
12297
  const gherkinStep = findGherkinStep(featureText, raw);
12045
- const domSnapshotAtFailure = extractDomSnapshot(join24(runDir, "trace.zip"));
12298
+ const domSnapshotAtFailure = extractDomSnapshot(join26(runDir, "trace.zip"));
12046
12299
  return {
12047
12300
  ticket,
12048
12301
  runId,
@@ -12062,8 +12315,8 @@ async function healPrepareCmd(argv) {
12062
12315
  try {
12063
12316
  const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
12064
12317
  const paths = resolveArtifactPaths(process.cwd(), ticket);
12065
- const outPath = join24(paths.runsDir, runId, "heal-input.json");
12066
- 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));
12067
12320
  console.log(`[xera:heal-prepare] wrote ${outPath}`);
12068
12321
  return 0;
12069
12322
  } catch (err2) {
@@ -12073,8 +12326,8 @@ async function healPrepareCmd(argv) {
12073
12326
  }
12074
12327
 
12075
12328
  // src/bin-internal/impact-prepare.ts
12076
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync16 } from "fs";
12077
- import { join as join25 } from "path";
12329
+ import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync18 } from "fs";
12330
+ import { join as join27 } from "path";
12078
12331
 
12079
12332
  // src/graph/impact.ts
12080
12333
  var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
@@ -12324,11 +12577,11 @@ async function impactPrepareCmd(argv) {
12324
12577
  scenarios,
12325
12578
  generatedAt: new Date().toISOString()
12326
12579
  };
12327
- const impactDir = join25(repoRoot, ".xera/impact");
12580
+ const impactDir = join27(repoRoot, ".xera/impact");
12328
12581
  mkdirSync16(impactDir, { recursive: true });
12329
- writeFileSync16(join25(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
12582
+ writeFileSync18(join27(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
12330
12583
  if (!quiet) {
12331
- writeFileSync16(join25(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
12584
+ writeFileSync18(join27(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
12332
12585
  }
12333
12586
  return 0;
12334
12587
  }
@@ -12354,8 +12607,8 @@ async function lintCmd(argv) {
12354
12607
  }
12355
12608
 
12356
12609
  // src/bin-internal/normalize.ts
12357
- import { existsSync as existsSync25, readdirSync as readdirSync7 } from "fs";
12358
- import { join as join26 } from "path";
12610
+ import { existsSync as existsSync27, readdirSync as readdirSync7 } from "fs";
12611
+ import { join as join28 } from "path";
12359
12612
  init_paths2();
12360
12613
  async function normalizeCmd(argv) {
12361
12614
  const ticket = argv[0];
@@ -12370,8 +12623,8 @@ async function normalizeCmd(argv) {
12370
12623
  console.error("[xera:normalize] no run found");
12371
12624
  return 1;
12372
12625
  }
12373
- const runDir = join26(paths.runsDir, runId);
12374
- if (!existsSync25(runDir)) {
12626
+ const runDir = join28(paths.runsDir, runId);
12627
+ if (!existsSync27(runDir)) {
12375
12628
  console.error(`[xera:normalize] runs/${runId} missing`);
12376
12629
  return 1;
12377
12630
  }
@@ -12391,14 +12644,14 @@ async function normalizeCmd(argv) {
12391
12644
 
12392
12645
  // src/bin-internal/post.ts
12393
12646
  init_paths2();
12394
- import { existsSync as existsSync27, readFileSync as readFileSync24 } from "fs";
12395
- import { join as join27 } from "path";
12647
+ import { existsSync as existsSync29, readFileSync as readFileSync26 } from "fs";
12648
+ import { join as join29 } from "path";
12396
12649
 
12397
12650
  // src/artifact/status.ts
12398
- 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";
12399
12652
  import { dirname as dirname8 } from "path";
12400
- import { z as z9 } from "zod";
12401
- var ClassificationEnum = z9.enum([
12653
+ import { z as z10 } from "zod";
12654
+ var ClassificationEnum = z10.enum([
12402
12655
  "PASS",
12403
12656
  "REAL_BUG",
12404
12657
  "SELECTOR_DRIFT",
@@ -12409,37 +12662,37 @@ var ClassificationEnum = z9.enum([
12409
12662
  "RATE_LIMITED",
12410
12663
  "AUTH_EXPIRED"
12411
12664
  ]);
12412
- var ResultEnum = z9.enum(["PASS", "FAIL"]);
12413
- var ConfidenceEnum = z9.enum(["low", "medium", "high"]);
12414
- var HistoryEntrySchema = z9.object({
12415
- 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(),
12416
12669
  result: ResultEnum,
12417
12670
  class: ClassificationEnum
12418
12671
  });
12419
- var StatusJsonSchema = z9.object({
12420
- ticket: z9.string(),
12421
- lastRun: z9.string(),
12672
+ var StatusJsonSchema = z10.object({
12673
+ ticket: z10.string(),
12674
+ lastRun: z10.string(),
12422
12675
  result: ResultEnum,
12423
12676
  classification: ClassificationEnum,
12424
12677
  confidence: ConfidenceEnum,
12425
- scenarios: z9.object({
12426
- total: z9.number().int().nonnegative(),
12427
- passed: z9.number().int().nonnegative(),
12428
- failed: z9.number().int().nonnegative(),
12429
- 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()
12430
12683
  }),
12431
- history: z9.array(HistoryEntrySchema).default([]),
12432
- last_jira_comment_id: z9.string().optional()
12684
+ history: z10.array(HistoryEntrySchema).default([]),
12685
+ last_jira_comment_id: z10.string().optional()
12433
12686
  });
12434
12687
  var HISTORY_CAP = 20;
12435
12688
  function readStatus(path) {
12436
- if (!existsSync26(path))
12689
+ if (!existsSync28(path))
12437
12690
  return null;
12438
- return StatusJsonSchema.parse(JSON.parse(readFileSync23(path, "utf8")));
12691
+ return StatusJsonSchema.parse(JSON.parse(readFileSync25(path, "utf8")));
12439
12692
  }
12440
12693
  function writeStatus(path, status) {
12441
12694
  mkdirSync17(dirname8(path), { recursive: true });
12442
- writeFileSync17(path, JSON.stringify(status, null, 2));
12695
+ writeFileSync19(path, JSON.stringify(status, null, 2));
12443
12696
  }
12444
12697
  function appendHistory(path, entry) {
12445
12698
  const s = readStatus(path);
@@ -12465,12 +12718,12 @@ async function postCmd(argv) {
12465
12718
  return 0;
12466
12719
  }
12467
12720
  const paths = resolveArtifactPaths(cwd, ticket);
12468
- const draftPath = join27(paths.ticketDir, "jira-comment.draft.md");
12469
- if (!existsSync27(draftPath)) {
12721
+ const draftPath = join29(paths.ticketDir, "jira-comment.draft.md");
12722
+ if (!existsSync29(draftPath)) {
12470
12723
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
12471
12724
  return 1;
12472
12725
  }
12473
- const body = readFileSync24(draftPath, "utf8");
12726
+ const body = readFileSync26(draftPath, "utf8");
12474
12727
  const client = await createJiraClient({
12475
12728
  baseUrl: config.jira.baseUrl,
12476
12729
  preferMcp: true,
@@ -12498,8 +12751,8 @@ async function promoteCmd(argv) {
12498
12751
  }
12499
12752
 
12500
12753
  // src/bin-internal/report.ts
12501
- import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync18 } from "fs";
12502
- 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";
12503
12756
  init_paths2();
12504
12757
 
12505
12758
  // src/classifier/aggregate.ts
@@ -12766,11 +13019,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
12766
13019
  }
12767
13020
 
12768
13021
  // src/reporter/status-writer.ts
12769
- import { existsSync as existsSync28 } from "fs";
13022
+ import { existsSync as existsSync30 } from "fs";
12770
13023
  function writeStatusFromClassification(path, input) {
12771
13024
  const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
12772
13025
  const entry = { ts: input.runTs, result, class: input.classification.overall };
12773
- if (!existsSync28(path)) {
13026
+ if (!existsSync30(path)) {
12774
13027
  writeStatus(path, {
12775
13028
  ticket: input.ticket,
12776
13029
  lastRun: input.runTs,
@@ -12804,22 +13057,22 @@ async function reportCmd(argv) {
12804
13057
  }
12805
13058
  const cwd = process.cwd();
12806
13059
  const paths = resolveArtifactPaths(cwd, ticket);
12807
- const input = JSON.parse(readFileSync25(inputArg.slice("--input=".length), "utf8"));
13060
+ const input = JSON.parse(readFileSync27(inputArg.slice("--input=".length), "utf8"));
12808
13061
  let httpRuleOverride = null;
12809
13062
  const meta = readMeta(paths.metaPath);
12810
13063
  if (meta?.adapter === "http") {
12811
13064
  const config = await loadConfig(cwd);
12812
13065
  if (config.http) {
12813
- const normalizedPath = join28(paths.ticketDir, "runs", input.runId, "normalized.json");
12814
- if (existsSync29(normalizedPath)) {
12815
- 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"));
12816
13069
  const calls = norm.http?.calls ?? [];
12817
13070
  const rate = classifyRateLimited({ calls });
12818
13071
  if (rate)
12819
13072
  httpRuleOverride = rate;
12820
13073
  if (!httpRuleOverride) {
12821
13074
  const authFiles = {};
12822
- const httpAuthDir = join28(cwd, ".xera", ".auth", "http");
13075
+ const httpAuthDir = join30(cwd, ".xera", ".auth", "http");
12823
13076
  for (const role of Object.keys(config.http.auth.roles)) {
12824
13077
  const entry = readAuthState(httpAuthDir, role);
12825
13078
  if (entry) {
@@ -12864,8 +13117,8 @@ async function reportCmd(argv) {
12864
13117
  confidence: "high"
12865
13118
  } : s) : input.scenarios;
12866
13119
  const aggregated = aggregateScenarios(scenariosForAggregation);
12867
- const decisionsPath = join28(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
12868
- 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")) : {};
12869
13122
  const graph = deriveSnapshot(loadAllEvents(process.cwd()));
12870
13123
  const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
12871
13124
  const scenarioIdByName = {};
@@ -12913,8 +13166,8 @@ async function reportCmd(argv) {
12913
13166
  xeraVersion: XERA_VERSION,
12914
13167
  promptsVersion: PROMPTS_VERSION
12915
13168
  });
12916
- const draftPath = join28(paths.ticketDir, "jira-comment.draft.md");
12917
- writeFileSync18(draftPath, md);
13169
+ const draftPath = join30(paths.ticketDir, "jira-comment.draft.md");
13170
+ writeFileSync20(draftPath, md);
12918
13171
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
12919
13172
  return 0;
12920
13173
  }
@@ -12985,7 +13238,7 @@ async function unlockCmd(argv) {
12985
13238
 
12986
13239
  // src/bin-internal/validate-feature.ts
12987
13240
  init_paths2();
12988
- import { existsSync as existsSync30, readFileSync as readFileSync26 } from "fs";
13241
+ import { existsSync as existsSync32, readFileSync as readFileSync28 } from "fs";
12989
13242
  import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
12990
13243
  async function validateFeatureCmd(argv) {
12991
13244
  const ticket = argv[0];
@@ -12994,11 +13247,11 @@ async function validateFeatureCmd(argv) {
12994
13247
  return 1;
12995
13248
  }
12996
13249
  const paths = resolveArtifactPaths(process.cwd(), ticket);
12997
- if (!existsSync30(paths.featurePath)) {
13250
+ if (!existsSync32(paths.featurePath)) {
12998
13251
  console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
12999
13252
  return 1;
13000
13253
  }
13001
- const r = validateGherkin2(readFileSync26(paths.featurePath, "utf8"));
13254
+ const r = validateGherkin2(readFileSync28(paths.featurePath, "utf8"));
13002
13255
  if (r.ok) {
13003
13256
  console.log("[xera:validate-feature] ok");
13004
13257
  return 0;
@@ -13020,6 +13273,8 @@ var COMMANDS = {
13020
13273
  "eval-prepare": evalPrepareCmd,
13021
13274
  "eval-report": evalReportCmd,
13022
13275
  exec: execCmd,
13276
+ "explore-finalize": exploreFinalizeCmd,
13277
+ "explore-prepare": explorePrepareCmd,
13023
13278
  "fill-gap-finalize": fillGapFinalizeCmd,
13024
13279
  "fill-gap-prepare": fillGapPrepareCmd,
13025
13280
  fetch: fetchCmd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/core",
3
- "version": "0.11.5",
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.5",
35
- "@xera-ai/http": "^0.11.5",
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",
@@ -50,11 +50,17 @@ export async function authSetupCmd(argv: string[]): Promise<number> {
50
50
  config.web &&
51
51
  typeof mod.web === 'function'
52
52
  ) {
53
+ const webConfig = config.web;
54
+ const envName = process.env.XERA_ENV ?? webConfig.defaultEnv;
55
+ const baseURL =
56
+ process.env.XERA_BASE_URL ??
57
+ webConfig.baseUrl[envName] ??
58
+ webConfig.baseUrl[webConfig.defaultEnv];
53
59
  const { runAuthSetup } = await import('@xera-ai/web');
54
60
  const { chromium } = await import('@playwright/test');
55
61
  const browser = await chromium.launch();
56
62
  try {
57
- for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
63
+ for (const [roleName, roleCreds] of Object.entries(webConfig.auth.roles)) {
58
64
  if (opts.role && roleName !== opts.role) continue;
59
65
  const email = process.env[roleCreds.envEmail];
60
66
  const password = process.env[roleCreds.envPassword];
@@ -72,6 +78,7 @@ export async function authSetupCmd(argv: string[]): Promise<number> {
72
78
  setupScriptPath: authSetupScript,
73
79
  authDir: join(cwd, '.xera', '.auth'),
74
80
  browser,
81
+ ...(baseURL ? { baseURL } : {}),
75
82
  });
76
83
  console.log(`[xera:auth-setup] ✓ ${roleName}.json (web)`);
77
84
  } catch (e) {
@@ -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,