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