@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.
package/dist/bin/internal.js
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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(
|
|
10807
|
+
return hashString(readFileSync18(path, "utf8"));
|
|
10555
10808
|
}
|
|
10556
10809
|
function hashFileIfExists(path) {
|
|
10557
|
-
if (!
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
10579
|
-
if (!
|
|
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(
|
|
10835
|
+
const parsed = JSON.parse(readFileSync19(cachePath, "utf8"));
|
|
10583
10836
|
return parsed;
|
|
10584
10837
|
},
|
|
10585
10838
|
async postComment(key, body) {
|
|
10586
|
-
const outPath =
|
|
10587
|
-
|
|
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 =
|
|
10592
|
-
|
|
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
|
-
|
|
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
|
|
10808
|
-
import { join as
|
|
10809
|
-
import { z as
|
|
10810
|
-
var
|
|
10811
|
-
proposals:
|
|
10812
|
-
id:
|
|
10813
|
-
ticketId:
|
|
10814
|
-
title:
|
|
10815
|
-
rationale:
|
|
10816
|
-
gherkin:
|
|
10817
|
-
satisfiesAcs:
|
|
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
|
|
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 =
|
|
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 ??
|
|
10877
|
-
if (!
|
|
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(
|
|
10884
|
-
proposals =
|
|
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 =
|
|
11147
|
+
const ticketDir = join21(cwd, ".xera", parsed.ticket);
|
|
10895
11148
|
mkdirSync13(ticketDir, { recursive: true });
|
|
10896
|
-
const draftPath =
|
|
10897
|
-
if (
|
|
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
|
-
|
|
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
|
|
10908
|
-
import { join as
|
|
10909
|
-
function
|
|
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 =
|
|
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 ??
|
|
11252
|
+
const outDir = parsed.outputDir ?? join22(cwd, ".xera/coverage", scope);
|
|
11000
11253
|
mkdirSync14(outDir, { recursive: true });
|
|
11001
|
-
|
|
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
|
|
11012
|
-
import { join as
|
|
11013
|
-
import { z as
|
|
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 =
|
|
11017
|
-
ticketId:
|
|
11018
|
-
confidence:
|
|
11019
|
-
reason:
|
|
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 =
|
|
11022
|
-
similar:
|
|
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 =
|
|
11042
|
-
if (!
|
|
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(
|
|
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
|
|
11151
|
-
import { dirname as dirname7, join as
|
|
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
|
|
11155
|
-
import { dirname as dirname6, join as
|
|
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 =
|
|
11627
|
+
var TEMPLATES_DIR = join24(__dirname2, "templates");
|
|
11375
11628
|
function loadTemplate(name) {
|
|
11376
|
-
return
|
|
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 ??
|
|
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
|
-
|
|
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 =
|
|
11449
|
-
if (
|
|
11450
|
-
const report = JSON.parse(
|
|
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
|
-
|
|
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
|
|
11500
|
-
import { join as
|
|
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,
|
|
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,
|
|
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,
|
|
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 (
|
|
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 (
|
|
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
|
|
11883
|
-
if (
|
|
12135
|
+
var z10 = b4(data, e - 20) == 117853008;
|
|
12136
|
+
if (z10) {
|
|
11884
12137
|
var ze = b4(data, e - 12);
|
|
11885
|
-
|
|
11886
|
-
if (
|
|
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,
|
|
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 (!
|
|
12182
|
+
if (!existsSync26(tracePath))
|
|
11930
12183
|
return "";
|
|
11931
|
-
const buf =
|
|
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 =
|
|
12232
|
+
const pomDir = join26(ticketDir, "page-objects");
|
|
11980
12233
|
const candidates = [];
|
|
11981
|
-
if (
|
|
12234
|
+
if (existsSync26(pomDir)) {
|
|
11982
12235
|
for (const name of readdirSync6(pomDir)) {
|
|
11983
12236
|
if (name.endsWith(".ts"))
|
|
11984
|
-
candidates.push(
|
|
12237
|
+
candidates.push(join26(pomDir, name));
|
|
11985
12238
|
}
|
|
11986
12239
|
}
|
|
11987
12240
|
for (const file of candidates) {
|
|
11988
|
-
const text =
|
|
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 =
|
|
12027
|
-
const classifier = JSON.parse(
|
|
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 =
|
|
12032
|
-
const normalized = JSON.parse(
|
|
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 =
|
|
12296
|
+
const featureText = readFileSync24(paths.featurePath, "utf8");
|
|
12044
12297
|
const gherkinStep = findGherkinStep(featureText, raw);
|
|
12045
|
-
const domSnapshotAtFailure = extractDomSnapshot(
|
|
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 =
|
|
12066
|
-
|
|
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
|
|
12077
|
-
import { join as
|
|
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 =
|
|
12580
|
+
const impactDir = join27(repoRoot, ".xera/impact");
|
|
12328
12581
|
mkdirSync16(impactDir, { recursive: true });
|
|
12329
|
-
|
|
12582
|
+
writeFileSync18(join27(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
|
|
12330
12583
|
if (!quiet) {
|
|
12331
|
-
|
|
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
|
|
12358
|
-
import { join as
|
|
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 =
|
|
12374
|
-
if (!
|
|
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
|
|
12395
|
-
import { join as
|
|
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
|
|
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
|
|
12401
|
-
var ClassificationEnum =
|
|
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 =
|
|
12413
|
-
var ConfidenceEnum =
|
|
12414
|
-
var HistoryEntrySchema =
|
|
12415
|
-
ts:
|
|
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 =
|
|
12420
|
-
ticket:
|
|
12421
|
-
lastRun:
|
|
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:
|
|
12426
|
-
total:
|
|
12427
|
-
passed:
|
|
12428
|
-
failed:
|
|
12429
|
-
skipped:
|
|
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:
|
|
12432
|
-
last_jira_comment_id:
|
|
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 (!
|
|
12689
|
+
if (!existsSync28(path))
|
|
12437
12690
|
return null;
|
|
12438
|
-
return StatusJsonSchema.parse(JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
12469
|
-
if (!
|
|
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 =
|
|
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
|
|
12502
|
-
import { join as
|
|
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
|
|
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 (!
|
|
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(
|
|
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 =
|
|
12814
|
-
if (
|
|
12815
|
-
const norm = JSON.parse(
|
|
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 =
|
|
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 =
|
|
12868
|
-
const decisions =
|
|
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 =
|
|
12917
|
-
|
|
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
|
|
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 (!
|
|
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(
|
|
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.
|
|
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.
|
|
35
|
-
"@xera-ai/http": "^0.
|
|
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(
|
|
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,
|