@xera-ai/core 0.4.1 → 0.4.3
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 +604 -34
- package/dist/bin-internal/graph-render.d.ts +2 -0
- package/dist/bin-internal/graph-render.d.ts.map +1 -0
- package/dist/bin-internal/impact-prepare.d.ts +2 -0
- package/dist/bin-internal/impact-prepare.d.ts.map +1 -0
- package/dist/bin-internal/index.d.ts.map +1 -1
- package/dist/config/schema.d.ts +6 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/graph/impact.d.ts +31 -0
- package/dist/graph/impact.d.ts.map +1 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/render.d.ts +50 -0
- package/dist/graph/render.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/package.json +1 -1
- package/src/bin-internal/graph-render.ts +70 -0
- package/src/bin-internal/impact-prepare.ts +64 -0
- package/src/bin-internal/index.ts +4 -0
- package/src/config/schema.ts +12 -0
- package/src/graph/impact.ts +262 -0
- package/src/graph/index.ts +13 -0
- package/src/graph/render.ts +324 -0
- package/src/graph/templates/LICENSE-vis-network.txt +176 -0
- package/src/graph/templates/graph.css +88 -0
- package/src/graph/templates/graph.html.template +31 -0
- package/src/graph/templates/graph.js +101 -0
- package/src/graph/templates/vis-network.min.js +25000 -0
package/dist/bin/internal.js
CHANGED
|
@@ -8882,11 +8882,18 @@ var ReportingSchema = z4.object({
|
|
|
8882
8882
|
}).prefault({}),
|
|
8883
8883
|
artifactLinks: z4.enum(["git", "local"]).default("git")
|
|
8884
8884
|
}).prefault({});
|
|
8885
|
+
var RunSchema = z4.object({
|
|
8886
|
+
autoImpact: z4.object({
|
|
8887
|
+
enabled: z4.boolean().default(true),
|
|
8888
|
+
threshold: z4.number().nonnegative().default(6)
|
|
8889
|
+
}).prefault({})
|
|
8890
|
+
}).prefault({});
|
|
8885
8891
|
var XeraConfigSchema = z4.object({
|
|
8886
8892
|
jira: JiraSchema,
|
|
8887
8893
|
web: WebSchema,
|
|
8888
8894
|
ai: AISchema,
|
|
8889
8895
|
reporting: ReportingSchema,
|
|
8896
|
+
run: RunSchema.prefault({}),
|
|
8890
8897
|
adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
|
|
8891
8898
|
});
|
|
8892
8899
|
|
|
@@ -9450,6 +9457,306 @@ async function graphQueryCmd(argv) {
|
|
|
9450
9457
|
// src/bin-internal/index.ts
|
|
9451
9458
|
init_graph_record();
|
|
9452
9459
|
|
|
9460
|
+
// src/bin-internal/graph-render.ts
|
|
9461
|
+
import { mkdirSync as mkdirSync11, renameSync as renameSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
9462
|
+
import { dirname as dirname7, join as join17 } from "path";
|
|
9463
|
+
|
|
9464
|
+
// src/graph/render.ts
|
|
9465
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
9466
|
+
import { dirname as dirname6, join as join16 } from "path";
|
|
9467
|
+
import { fileURLToPath } from "url";
|
|
9468
|
+
var COLORS = {
|
|
9469
|
+
ticket: "#3B82F6",
|
|
9470
|
+
scenarioPass: "#10B981",
|
|
9471
|
+
scenarioFail: "#EF4444",
|
|
9472
|
+
pom: "#F59E0B",
|
|
9473
|
+
area: "#6B7280",
|
|
9474
|
+
failure: "#EF4444",
|
|
9475
|
+
edgeModifies: "#EF4444",
|
|
9476
|
+
edgeDefault: "#9CA3AF",
|
|
9477
|
+
edgeJira: "#3B82F6",
|
|
9478
|
+
edgeSimilar: "#A855F7"
|
|
9479
|
+
};
|
|
9480
|
+
function ticketsAfter(since, fetchedAt) {
|
|
9481
|
+
if (!since)
|
|
9482
|
+
return true;
|
|
9483
|
+
return Date.parse(fetchedAt) >= Date.parse(since);
|
|
9484
|
+
}
|
|
9485
|
+
function scenariosAfter(since, generatedAt) {
|
|
9486
|
+
if (!since)
|
|
9487
|
+
return true;
|
|
9488
|
+
return Date.parse(generatedAt) >= Date.parse(since);
|
|
9489
|
+
}
|
|
9490
|
+
function buildTicketNode(snap, ticketId) {
|
|
9491
|
+
const t = snap.tickets[ticketId];
|
|
9492
|
+
const usageCount = snap.edges.filter((e) => e.kind === "tests" && e.from === ticketId).length;
|
|
9493
|
+
const node = {
|
|
9494
|
+
id: t.id,
|
|
9495
|
+
label: t.id,
|
|
9496
|
+
group: "Ticket",
|
|
9497
|
+
color: COLORS.ticket,
|
|
9498
|
+
shape: "dot",
|
|
9499
|
+
size: 10 + Math.min(usageCount * 2, 20),
|
|
9500
|
+
title: `${t.id} \u2014 ${t.summary}`
|
|
9501
|
+
};
|
|
9502
|
+
return node;
|
|
9503
|
+
}
|
|
9504
|
+
function buildScenarioNode(snap, scenarioId2) {
|
|
9505
|
+
const s = snap.scenarios[scenarioId2];
|
|
9506
|
+
const failed = snap.latest_failures[scenarioId2];
|
|
9507
|
+
const sizeBase = s.priority === "p0" ? 14 : s.priority === "p1" ? 11 : 9;
|
|
9508
|
+
const node = {
|
|
9509
|
+
id: s.id,
|
|
9510
|
+
label: s.name,
|
|
9511
|
+
group: "Scenario",
|
|
9512
|
+
color: failed ? COLORS.scenarioFail : COLORS.scenarioPass,
|
|
9513
|
+
shape: "square",
|
|
9514
|
+
size: sizeBase,
|
|
9515
|
+
title: `${s.ticketId} / ${s.name} [${s.priority.toUpperCase()}]`
|
|
9516
|
+
};
|
|
9517
|
+
return node;
|
|
9518
|
+
}
|
|
9519
|
+
function buildPomNode(snap, pomId2) {
|
|
9520
|
+
const p = snap.poms[pomId2];
|
|
9521
|
+
const usageCount = snap.edges.filter((e) => e.kind === "uses" && e.to === pomId2).length;
|
|
9522
|
+
const node = {
|
|
9523
|
+
id: p.id,
|
|
9524
|
+
label: p.filePath.split("/").pop() ?? p.id,
|
|
9525
|
+
group: "POM",
|
|
9526
|
+
color: COLORS.pom,
|
|
9527
|
+
shape: "diamond",
|
|
9528
|
+
size: 8 + Math.min(usageCount * 2, 16),
|
|
9529
|
+
title: `${p.filePath} (${p.route || "no route"})`
|
|
9530
|
+
};
|
|
9531
|
+
return node;
|
|
9532
|
+
}
|
|
9533
|
+
function buildAreaNode(snap, areaId) {
|
|
9534
|
+
const a = snap.areas[areaId];
|
|
9535
|
+
const node = {
|
|
9536
|
+
id: a.id,
|
|
9537
|
+
label: a.id,
|
|
9538
|
+
group: "SUTArea",
|
|
9539
|
+
color: COLORS.area,
|
|
9540
|
+
shape: "hexagon",
|
|
9541
|
+
size: 12,
|
|
9542
|
+
title: `area: ${a.id}`
|
|
9543
|
+
};
|
|
9544
|
+
return node;
|
|
9545
|
+
}
|
|
9546
|
+
function buildFailureNode(_snap, failure) {
|
|
9547
|
+
const node = {
|
|
9548
|
+
id: failure.id,
|
|
9549
|
+
label: "fail",
|
|
9550
|
+
group: "Failure",
|
|
9551
|
+
color: COLORS.failure,
|
|
9552
|
+
shape: "triangle",
|
|
9553
|
+
size: 10,
|
|
9554
|
+
title: `failure on ${failure.scenarioId} @ ${failure.ts}`
|
|
9555
|
+
};
|
|
9556
|
+
return node;
|
|
9557
|
+
}
|
|
9558
|
+
function buildEdge(edge, idx) {
|
|
9559
|
+
const v = {
|
|
9560
|
+
id: `e-${idx}`,
|
|
9561
|
+
from: edge.from,
|
|
9562
|
+
to: edge.to,
|
|
9563
|
+
label: edge.kind,
|
|
9564
|
+
arrows: "to",
|
|
9565
|
+
width: 1
|
|
9566
|
+
};
|
|
9567
|
+
switch (edge.kind) {
|
|
9568
|
+
case "modifies":
|
|
9569
|
+
v.color = COLORS.edgeModifies;
|
|
9570
|
+
v.dashes = true;
|
|
9571
|
+
v.width = 2;
|
|
9572
|
+
break;
|
|
9573
|
+
case "jira-linked":
|
|
9574
|
+
v.color = COLORS.edgeJira;
|
|
9575
|
+
v.dashes = true;
|
|
9576
|
+
break;
|
|
9577
|
+
case "similar":
|
|
9578
|
+
v.color = COLORS.edgeSimilar;
|
|
9579
|
+
v.dashes = false;
|
|
9580
|
+
v.width = 1 + Math.round((edge.confidence ?? 0) * 3);
|
|
9581
|
+
break;
|
|
9582
|
+
default:
|
|
9583
|
+
v.color = COLORS.edgeDefault;
|
|
9584
|
+
break;
|
|
9585
|
+
}
|
|
9586
|
+
return v;
|
|
9587
|
+
}
|
|
9588
|
+
function bfsFromTicket(snap, ticketId, depth) {
|
|
9589
|
+
const nodeIds = new Set([ticketId]);
|
|
9590
|
+
const edgeIdxs = new Set;
|
|
9591
|
+
let frontier = new Set([ticketId]);
|
|
9592
|
+
for (let d = 0;d < depth; d++) {
|
|
9593
|
+
const next = new Set;
|
|
9594
|
+
snap.edges.forEach((e, i) => {
|
|
9595
|
+
if (frontier.has(e.from) && !nodeIds.has(e.to)) {
|
|
9596
|
+
nodeIds.add(e.to);
|
|
9597
|
+
next.add(e.to);
|
|
9598
|
+
edgeIdxs.add(i);
|
|
9599
|
+
} else if (frontier.has(e.to) && !nodeIds.has(e.from)) {
|
|
9600
|
+
nodeIds.add(e.from);
|
|
9601
|
+
next.add(e.from);
|
|
9602
|
+
edgeIdxs.add(i);
|
|
9603
|
+
} else if (frontier.has(e.from) && nodeIds.has(e.to)) {
|
|
9604
|
+
edgeIdxs.add(i);
|
|
9605
|
+
} else if (frontier.has(e.to) && nodeIds.has(e.from)) {
|
|
9606
|
+
edgeIdxs.add(i);
|
|
9607
|
+
}
|
|
9608
|
+
});
|
|
9609
|
+
frontier = next;
|
|
9610
|
+
if (frontier.size === 0)
|
|
9611
|
+
break;
|
|
9612
|
+
}
|
|
9613
|
+
return { nodeIds, edgeIdxs };
|
|
9614
|
+
}
|
|
9615
|
+
function transformForVisNetwork(snap, opts) {
|
|
9616
|
+
const mode = opts.performanceMode ?? "full";
|
|
9617
|
+
const nodes = [];
|
|
9618
|
+
const edges = [];
|
|
9619
|
+
let includeTickets = new Set;
|
|
9620
|
+
let includeScenarios = new Set;
|
|
9621
|
+
let includePoms = new Set;
|
|
9622
|
+
let includeAreas = new Set;
|
|
9623
|
+
let includeEdgeIdxs = new Set;
|
|
9624
|
+
if (opts.ticketId) {
|
|
9625
|
+
const result = bfsFromTicket(snap, opts.ticketId, opts.depth ?? 2);
|
|
9626
|
+
for (const id of result.nodeIds) {
|
|
9627
|
+
if (snap.tickets[id])
|
|
9628
|
+
includeTickets.add(id);
|
|
9629
|
+
else if (snap.scenarios[id])
|
|
9630
|
+
includeScenarios.add(id);
|
|
9631
|
+
else if (snap.poms[id])
|
|
9632
|
+
includePoms.add(id);
|
|
9633
|
+
else if (snap.areas[id])
|
|
9634
|
+
includeAreas.add(id);
|
|
9635
|
+
}
|
|
9636
|
+
includeEdgeIdxs = result.edgeIdxs;
|
|
9637
|
+
} else {
|
|
9638
|
+
includeTickets = new Set(Object.keys(snap.tickets).filter((id) => ticketsAfter(opts.since, snap.tickets[id].fetchedAt)));
|
|
9639
|
+
includeScenarios = new Set(Object.keys(snap.scenarios).filter((id) => scenariosAfter(opts.since, snap.scenarios[id].generatedAt)));
|
|
9640
|
+
includePoms = new Set(Object.keys(snap.poms));
|
|
9641
|
+
includeAreas = new Set(Object.keys(snap.areas));
|
|
9642
|
+
snap.edges.forEach((_, i) => {
|
|
9643
|
+
includeEdgeIdxs.add(i);
|
|
9644
|
+
});
|
|
9645
|
+
}
|
|
9646
|
+
if (mode === "ticket-only") {
|
|
9647
|
+
includeScenarios.clear();
|
|
9648
|
+
includePoms.clear();
|
|
9649
|
+
includeAreas.clear();
|
|
9650
|
+
}
|
|
9651
|
+
for (const id of includeTickets)
|
|
9652
|
+
nodes.push(buildTicketNode(snap, id));
|
|
9653
|
+
for (const id of includeScenarios)
|
|
9654
|
+
nodes.push(buildScenarioNode(snap, id));
|
|
9655
|
+
for (const id of includePoms)
|
|
9656
|
+
nodes.push(buildPomNode(snap, id));
|
|
9657
|
+
for (const id of includeAreas)
|
|
9658
|
+
nodes.push(buildAreaNode(snap, id));
|
|
9659
|
+
for (const failure of Object.values(snap.latest_failures)) {
|
|
9660
|
+
if (includeScenarios.has(failure.scenarioId)) {
|
|
9661
|
+
nodes.push(buildFailureNode(snap, failure));
|
|
9662
|
+
}
|
|
9663
|
+
}
|
|
9664
|
+
const visibleNodeIds = new Set(nodes.map((n) => n.id));
|
|
9665
|
+
for (const i of includeEdgeIdxs) {
|
|
9666
|
+
const e = snap.edges[i];
|
|
9667
|
+
if (!e)
|
|
9668
|
+
continue;
|
|
9669
|
+
if (!visibleNodeIds.has(e.from) || !visibleNodeIds.has(e.to))
|
|
9670
|
+
continue;
|
|
9671
|
+
edges.push(buildEdge(e, i));
|
|
9672
|
+
}
|
|
9673
|
+
const stats = {
|
|
9674
|
+
tickets: includeTickets.size,
|
|
9675
|
+
scenarios: includeScenarios.size,
|
|
9676
|
+
poms: includePoms.size,
|
|
9677
|
+
areas: includeAreas.size,
|
|
9678
|
+
failures: nodes.filter((n) => n.group === "Failure").length,
|
|
9679
|
+
edges: edges.length
|
|
9680
|
+
};
|
|
9681
|
+
return { nodes, edges, stats };
|
|
9682
|
+
}
|
|
9683
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
9684
|
+
var __dirname2 = dirname6(__filename2);
|
|
9685
|
+
var TEMPLATES_DIR = join16(__dirname2, "templates");
|
|
9686
|
+
function loadTemplate(name) {
|
|
9687
|
+
return readFileSync17(join16(TEMPLATES_DIR, name), "utf8");
|
|
9688
|
+
}
|
|
9689
|
+
function statsToHuman(s) {
|
|
9690
|
+
return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
|
|
9691
|
+
}
|
|
9692
|
+
function renderHtml(input) {
|
|
9693
|
+
const template = loadTemplate("graph.html.template");
|
|
9694
|
+
const css = loadTemplate("graph.css");
|
|
9695
|
+
const js = loadTemplate("graph.js");
|
|
9696
|
+
const visNetwork = loadTemplate("vis-network.min.js");
|
|
9697
|
+
const graphJson = JSON.stringify(input.data);
|
|
9698
|
+
const statsHuman = statsToHuman(input.stats);
|
|
9699
|
+
return template.replace("{{CSS}}", () => css).replace("{{STATS}}", () => statsHuman).replace("{{GENERATED_AT}}", () => input.generatedAt).replace("{{VIS_NETWORK_JS}}", () => visNetwork).replace("{{GRAPH_DATA}}", () => graphJson).replace("{{INTERACTION_JS}}", () => js);
|
|
9700
|
+
}
|
|
9701
|
+
|
|
9702
|
+
// src/bin-internal/graph-render.ts
|
|
9703
|
+
init_store();
|
|
9704
|
+
function parseDepth(s) {
|
|
9705
|
+
const n = s ? Number.parseInt(s, 10) : 2;
|
|
9706
|
+
if (n === 1 || n === 3)
|
|
9707
|
+
return n;
|
|
9708
|
+
return 2;
|
|
9709
|
+
}
|
|
9710
|
+
function decidePerformanceMode(nodeCount) {
|
|
9711
|
+
if (nodeCount > 2000)
|
|
9712
|
+
return "text-fallback";
|
|
9713
|
+
if (nodeCount > 500)
|
|
9714
|
+
return "ticket-only";
|
|
9715
|
+
return "full";
|
|
9716
|
+
}
|
|
9717
|
+
async function graphRenderCmd(argv) {
|
|
9718
|
+
let outPath;
|
|
9719
|
+
let ticketId;
|
|
9720
|
+
let since;
|
|
9721
|
+
let depth = 2;
|
|
9722
|
+
for (let i = 0;i < argv.length; i++) {
|
|
9723
|
+
if (argv[i] === "--out")
|
|
9724
|
+
outPath = argv[++i];
|
|
9725
|
+
else if (argv[i] === "--ticket")
|
|
9726
|
+
ticketId = argv[++i];
|
|
9727
|
+
else if (argv[i] === "--since")
|
|
9728
|
+
since = argv[++i];
|
|
9729
|
+
else if (argv[i] === "--depth")
|
|
9730
|
+
depth = parseDepth(argv[++i]);
|
|
9731
|
+
}
|
|
9732
|
+
const repoRoot = process.cwd();
|
|
9733
|
+
const finalPath = outPath ?? join17(repoRoot, ".xera/graph.html");
|
|
9734
|
+
const snap = deriveSnapshot(loadAllEvents(repoRoot));
|
|
9735
|
+
const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
|
|
9736
|
+
const performanceMode = decidePerformanceMode(totalNodeCount);
|
|
9737
|
+
if (performanceMode === "text-fallback") {
|
|
9738
|
+
const txtPath = finalPath.replace(/\.html$/, ".txt");
|
|
9739
|
+
mkdirSync11(dirname7(txtPath), { recursive: true });
|
|
9740
|
+
writeFileSync10(txtPath, `Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.
|
|
9741
|
+
`);
|
|
9742
|
+
console.log(`[graph-render] graph too large (${totalNodeCount} nodes); wrote ${txtPath}`);
|
|
9743
|
+
return 0;
|
|
9744
|
+
}
|
|
9745
|
+
const opts = { depth, performanceMode };
|
|
9746
|
+
if (ticketId)
|
|
9747
|
+
opts.ticketId = ticketId;
|
|
9748
|
+
if (since)
|
|
9749
|
+
opts.since = since;
|
|
9750
|
+
const data = transformForVisNetwork(snap, opts);
|
|
9751
|
+
const html = renderHtml({ data, stats: data.stats, generatedAt: new Date().toISOString() });
|
|
9752
|
+
mkdirSync11(dirname7(finalPath), { recursive: true });
|
|
9753
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
9754
|
+
writeFileSync10(tmpPath, html);
|
|
9755
|
+
renameSync2(tmpPath, finalPath);
|
|
9756
|
+
console.log(`[graph-render] wrote ${finalPath} (${data.stats.tickets} tickets \xB7 ${data.stats.scenarios} scenarios \xB7 ${html.length} bytes)`);
|
|
9757
|
+
return 0;
|
|
9758
|
+
}
|
|
9759
|
+
|
|
9453
9760
|
// src/bin-internal/graph-snapshot.ts
|
|
9454
9761
|
init_store();
|
|
9455
9762
|
async function graphSnapshotCmd(argv) {
|
|
@@ -9475,8 +9782,8 @@ async function graphSnapshotCmd(argv) {
|
|
|
9475
9782
|
}
|
|
9476
9783
|
|
|
9477
9784
|
// src/bin-internal/heal-prepare.ts
|
|
9478
|
-
import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as
|
|
9479
|
-
import { join as
|
|
9785
|
+
import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
|
|
9786
|
+
import { join as join18 } from "path";
|
|
9480
9787
|
import { scrubFreeText } from "@xera-ai/web";
|
|
9481
9788
|
|
|
9482
9789
|
// ../../node_modules/.bun/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
|
@@ -9906,7 +10213,7 @@ function classifyKind(raw) {
|
|
|
9906
10213
|
function extractDomSnapshot(tracePath) {
|
|
9907
10214
|
if (!existsSync20(tracePath))
|
|
9908
10215
|
return "";
|
|
9909
|
-
const buf =
|
|
10216
|
+
const buf = readFileSync18(tracePath);
|
|
9910
10217
|
const entries = unzipSync(buf);
|
|
9911
10218
|
const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
|
|
9912
10219
|
let chosenKey = null;
|
|
@@ -9954,16 +10261,16 @@ function extractDomSnapshot(tracePath) {
|
|
|
9954
10261
|
return scrubFreeText(html);
|
|
9955
10262
|
}
|
|
9956
10263
|
function findPomLine(ticketDir, rawLocator) {
|
|
9957
|
-
const pomDir =
|
|
10264
|
+
const pomDir = join18(ticketDir, "page-objects");
|
|
9958
10265
|
const candidates = [];
|
|
9959
10266
|
if (existsSync20(pomDir)) {
|
|
9960
10267
|
for (const name of readdirSync6(pomDir)) {
|
|
9961
10268
|
if (name.endsWith(".ts"))
|
|
9962
|
-
candidates.push(
|
|
10269
|
+
candidates.push(join18(pomDir, name));
|
|
9963
10270
|
}
|
|
9964
10271
|
}
|
|
9965
10272
|
for (const file of candidates) {
|
|
9966
|
-
const text =
|
|
10273
|
+
const text = readFileSync18(file, "utf8");
|
|
9967
10274
|
const lines = text.split(`
|
|
9968
10275
|
`);
|
|
9969
10276
|
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
@@ -10001,13 +10308,13 @@ function findGherkinStep(featureText, rawLocator) {
|
|
|
10001
10308
|
}
|
|
10002
10309
|
function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
10003
10310
|
const paths = resolveArtifactPaths(repoRoot, ticket);
|
|
10004
|
-
const classifierPath =
|
|
10005
|
-
const classifier = JSON.parse(
|
|
10311
|
+
const classifierPath = join18(paths.ticketDir, "classifier-input.json");
|
|
10312
|
+
const classifier = JSON.parse(readFileSync18(classifierPath, "utf8"));
|
|
10006
10313
|
const cls = classifier.scenarios.find((s) => s.name === scenarioName);
|
|
10007
10314
|
if (!cls)
|
|
10008
10315
|
throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
|
|
10009
|
-
const runDir =
|
|
10010
|
-
const normalized = JSON.parse(
|
|
10316
|
+
const runDir = join18(paths.runsDir, runId);
|
|
10317
|
+
const normalized = JSON.parse(readFileSync18(join18(runDir, "normalized.json"), "utf8"));
|
|
10011
10318
|
const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
|
|
10012
10319
|
if (!normSc?.failure)
|
|
10013
10320
|
throw new Error(`no failure recorded for scenario "${scenarioName}"`);
|
|
@@ -10018,9 +10325,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
|
10018
10325
|
const raw = m[1].trim();
|
|
10019
10326
|
const kind = classifyKind(raw);
|
|
10020
10327
|
const pomLoc = findPomLine(paths.ticketDir, raw);
|
|
10021
|
-
const featureText =
|
|
10328
|
+
const featureText = readFileSync18(paths.featurePath, "utf8");
|
|
10022
10329
|
const gherkinStep = findGherkinStep(featureText, raw);
|
|
10023
|
-
const domSnapshotAtFailure = extractDomSnapshot(
|
|
10330
|
+
const domSnapshotAtFailure = extractDomSnapshot(join18(runDir, "trace.zip"));
|
|
10024
10331
|
return {
|
|
10025
10332
|
ticket,
|
|
10026
10333
|
runId,
|
|
@@ -10040,8 +10347,8 @@ async function healPrepareCmd(argv) {
|
|
|
10040
10347
|
try {
|
|
10041
10348
|
const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
|
|
10042
10349
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
10043
|
-
const outPath =
|
|
10044
|
-
|
|
10350
|
+
const outPath = join18(paths.runsDir, runId, "heal-input.json");
|
|
10351
|
+
writeFileSync11(outPath, JSON.stringify(result, null, 2));
|
|
10045
10352
|
console.log(`[xera:heal-prepare] wrote ${outPath}`);
|
|
10046
10353
|
return 0;
|
|
10047
10354
|
} catch (err2) {
|
|
@@ -10050,6 +10357,267 @@ async function healPrepareCmd(argv) {
|
|
|
10050
10357
|
}
|
|
10051
10358
|
}
|
|
10052
10359
|
|
|
10360
|
+
// src/bin-internal/impact-prepare.ts
|
|
10361
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
|
|
10362
|
+
import { join as join19 } from "path";
|
|
10363
|
+
|
|
10364
|
+
// src/graph/impact.ts
|
|
10365
|
+
var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
|
|
10366
|
+
var EDGE_WEIGHT_FIXED = {
|
|
10367
|
+
modifies: 5,
|
|
10368
|
+
uses: 4,
|
|
10369
|
+
covers: 4
|
|
10370
|
+
};
|
|
10371
|
+
function jiraRelationWeight(source) {
|
|
10372
|
+
if (!source)
|
|
10373
|
+
return 0;
|
|
10374
|
+
if (source.endsWith("blocks"))
|
|
10375
|
+
return 4;
|
|
10376
|
+
if (source.endsWith("duplicates"))
|
|
10377
|
+
return 3;
|
|
10378
|
+
if (source.endsWith("relates"))
|
|
10379
|
+
return 2;
|
|
10380
|
+
if (source.endsWith("supersedes"))
|
|
10381
|
+
return 3;
|
|
10382
|
+
return 1;
|
|
10383
|
+
}
|
|
10384
|
+
function edgeWeight(edge) {
|
|
10385
|
+
if (edge.kind === "modifies")
|
|
10386
|
+
return EDGE_WEIGHT_FIXED.modifies ?? 0;
|
|
10387
|
+
if (edge.kind === "uses" || edge.kind === "covers")
|
|
10388
|
+
return EDGE_WEIGHT_FIXED.uses ?? 0;
|
|
10389
|
+
if (edge.kind === "jira-linked")
|
|
10390
|
+
return jiraRelationWeight(edge.source);
|
|
10391
|
+
if (edge.kind === "similar")
|
|
10392
|
+
return 1 * (edge.confidence ?? 0);
|
|
10393
|
+
return 0;
|
|
10394
|
+
}
|
|
10395
|
+
function riskScore(scenario, daysSinceLastPass) {
|
|
10396
|
+
const pri = PRIORITY_WEIGHT[scenario.priority] * 3;
|
|
10397
|
+
const firstEdge = scenario.edgePath[0];
|
|
10398
|
+
const edgeW = firstEdge ? edgeWeight(firstEdge) : 0;
|
|
10399
|
+
const confW = firstEdge?.confidence !== undefined ? firstEdge.confidence * 2 : 0;
|
|
10400
|
+
const decay = daysSinceLastPass * 0.1;
|
|
10401
|
+
return pri + edgeW + confW - decay;
|
|
10402
|
+
}
|
|
10403
|
+
var PRIORITY_RANK = { p0: 3, p1: 2, p2: 1 };
|
|
10404
|
+
function daysSince(ts) {
|
|
10405
|
+
if (!ts)
|
|
10406
|
+
return 0;
|
|
10407
|
+
const ms = Date.now() - Date.parse(ts);
|
|
10408
|
+
return ms < 0 ? 0 : ms / (86400 * 1000);
|
|
10409
|
+
}
|
|
10410
|
+
function walkImpact(graph, target, opts) {
|
|
10411
|
+
const result = [];
|
|
10412
|
+
const seen = new Set;
|
|
10413
|
+
const targetAreas = new Set(target.modifiesAreas);
|
|
10414
|
+
const pomIds = graph.edges.filter((e) => e.kind === "covers" && targetAreas.has(e.to)).map((e) => e.from);
|
|
10415
|
+
const directScenarios = graph.edges.filter((e) => e.kind === "uses" && pomIds.includes(e.to)).map((e) => e.from);
|
|
10416
|
+
for (const scenarioId2 of directScenarios) {
|
|
10417
|
+
if (seen.has(scenarioId2))
|
|
10418
|
+
continue;
|
|
10419
|
+
const scenario = graph.scenarios[scenarioId2];
|
|
10420
|
+
if (!scenario)
|
|
10421
|
+
continue;
|
|
10422
|
+
if (scenario.ticketId === target.id)
|
|
10423
|
+
continue;
|
|
10424
|
+
const usingPom = graph.edges.find((e) => e.kind === "uses" && e.from === scenarioId2);
|
|
10425
|
+
const modifyEdge = graph.edges.find((e) => e.kind === "modifies" && e.from === target.id && targetAreas.has(e.to));
|
|
10426
|
+
const edgePath = [];
|
|
10427
|
+
if (modifyEdge)
|
|
10428
|
+
edgePath.push({ kind: "modifies", from: modifyEdge.from, to: modifyEdge.to });
|
|
10429
|
+
if (usingPom)
|
|
10430
|
+
edgePath.push({ kind: "uses", from: usingPom.from, to: usingPom.to });
|
|
10431
|
+
seen.add(scenarioId2);
|
|
10432
|
+
const impact = {
|
|
10433
|
+
scenarioId: scenarioId2,
|
|
10434
|
+
ticketId: scenario.ticketId,
|
|
10435
|
+
name: scenario.name,
|
|
10436
|
+
priority: scenario.priority,
|
|
10437
|
+
edgePath,
|
|
10438
|
+
riskScore: 0
|
|
10439
|
+
};
|
|
10440
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
|
|
10441
|
+
result.push(impact);
|
|
10442
|
+
}
|
|
10443
|
+
if (opts.depth >= 2) {
|
|
10444
|
+
const linked = graph.edges.filter((e) => e.kind === "jira-linked" && e.from === target.id).map((e) => ({ to: e.to, source: e.source }));
|
|
10445
|
+
for (const link of linked) {
|
|
10446
|
+
const sceneIds = graph.edges.filter((e) => e.kind === "tests" && e.from === link.to).map((e) => e.to);
|
|
10447
|
+
for (const scenarioId2 of sceneIds) {
|
|
10448
|
+
if (seen.has(scenarioId2))
|
|
10449
|
+
continue;
|
|
10450
|
+
const scenario = graph.scenarios[scenarioId2];
|
|
10451
|
+
if (!scenario || scenario.ticketId === target.id)
|
|
10452
|
+
continue;
|
|
10453
|
+
seen.add(scenarioId2);
|
|
10454
|
+
const edge = { kind: "jira-linked", from: target.id, to: link.to };
|
|
10455
|
+
if (link.source !== undefined)
|
|
10456
|
+
edge.source = link.source;
|
|
10457
|
+
const impact = {
|
|
10458
|
+
scenarioId: scenarioId2,
|
|
10459
|
+
ticketId: scenario.ticketId,
|
|
10460
|
+
name: scenario.name,
|
|
10461
|
+
priority: scenario.priority,
|
|
10462
|
+
edgePath: [edge],
|
|
10463
|
+
riskScore: 0
|
|
10464
|
+
};
|
|
10465
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
|
|
10466
|
+
result.push(impact);
|
|
10467
|
+
}
|
|
10468
|
+
}
|
|
10469
|
+
}
|
|
10470
|
+
if (opts.depth >= 3) {
|
|
10471
|
+
const similar = graph.edges.filter((e) => e.kind === "similar" && e.from === target.id).map((e) => ({ to: e.to, confidence: e.confidence }));
|
|
10472
|
+
for (const link of similar) {
|
|
10473
|
+
const sceneIds = graph.edges.filter((e) => e.kind === "tests" && e.from === link.to).map((e) => e.to);
|
|
10474
|
+
for (const scenarioId2 of sceneIds) {
|
|
10475
|
+
if (seen.has(scenarioId2))
|
|
10476
|
+
continue;
|
|
10477
|
+
const scenario = graph.scenarios[scenarioId2];
|
|
10478
|
+
if (!scenario || scenario.ticketId === target.id)
|
|
10479
|
+
continue;
|
|
10480
|
+
seen.add(scenarioId2);
|
|
10481
|
+
const edge = { kind: "similar", from: target.id, to: link.to };
|
|
10482
|
+
if (link.confidence !== undefined)
|
|
10483
|
+
edge.confidence = link.confidence;
|
|
10484
|
+
const impact = {
|
|
10485
|
+
scenarioId: scenarioId2,
|
|
10486
|
+
ticketId: scenario.ticketId,
|
|
10487
|
+
name: scenario.name,
|
|
10488
|
+
priority: scenario.priority,
|
|
10489
|
+
edgePath: [edge],
|
|
10490
|
+
riskScore: 0
|
|
10491
|
+
};
|
|
10492
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
|
|
10493
|
+
result.push(impact);
|
|
10494
|
+
}
|
|
10495
|
+
}
|
|
10496
|
+
}
|
|
10497
|
+
let filtered = result;
|
|
10498
|
+
if (opts.minPriority) {
|
|
10499
|
+
const min = PRIORITY_RANK[opts.minPriority];
|
|
10500
|
+
filtered = filtered.filter((s) => PRIORITY_RANK[s.priority] >= min);
|
|
10501
|
+
}
|
|
10502
|
+
filtered.sort((a, b) => b.riskScore - a.riskScore);
|
|
10503
|
+
return filtered;
|
|
10504
|
+
}
|
|
10505
|
+
var HIGH_THRESHOLD = 7;
|
|
10506
|
+
var MEDIUM_THRESHOLD = 4;
|
|
10507
|
+
function bucket(score) {
|
|
10508
|
+
if (score >= HIGH_THRESHOLD)
|
|
10509
|
+
return "high";
|
|
10510
|
+
if (score >= MEDIUM_THRESHOLD)
|
|
10511
|
+
return "medium";
|
|
10512
|
+
return "low";
|
|
10513
|
+
}
|
|
10514
|
+
function fmtEdgePath(path) {
|
|
10515
|
+
return path.map((e) => `${e.from} \u2192[${e.kind}]\u2192 ${e.to}`).join(" \xB7 ");
|
|
10516
|
+
}
|
|
10517
|
+
function renderImpactMarkdown(report) {
|
|
10518
|
+
const lines = [];
|
|
10519
|
+
lines.push(`# Impact Analysis \u2014 ${report.targetTicket}`);
|
|
10520
|
+
lines.push("");
|
|
10521
|
+
lines.push(`**Modified areas:** ${report.modifiedAreas.join(", ") || "(none)"}`);
|
|
10522
|
+
lines.push(`**Generated:** ${report.generatedAt}`);
|
|
10523
|
+
lines.push("");
|
|
10524
|
+
if (report.scenarios.length === 0) {
|
|
10525
|
+
lines.push("No prior scenarios in the modified areas. This may be a new feature area.");
|
|
10526
|
+
lines.push("");
|
|
10527
|
+
return lines.join(`
|
|
10528
|
+
`);
|
|
10529
|
+
}
|
|
10530
|
+
const bySeverity = {
|
|
10531
|
+
high: [],
|
|
10532
|
+
medium: [],
|
|
10533
|
+
low: []
|
|
10534
|
+
};
|
|
10535
|
+
for (const s of report.scenarios)
|
|
10536
|
+
bySeverity[bucket(s.riskScore)].push(s);
|
|
10537
|
+
lines.push(`**Total impacted:** ${report.scenarios.length} scenarios (${bySeverity.high.length} high \xB7 ${bySeverity.medium.length} medium \xB7 ${bySeverity.low.length} low)`);
|
|
10538
|
+
lines.push("");
|
|
10539
|
+
for (const [name, scenarios] of [
|
|
10540
|
+
["High-risk", bySeverity.high],
|
|
10541
|
+
["Medium-risk", bySeverity.medium],
|
|
10542
|
+
["Low-risk", bySeverity.low]
|
|
10543
|
+
]) {
|
|
10544
|
+
if (scenarios.length === 0)
|
|
10545
|
+
continue;
|
|
10546
|
+
lines.push(`## ${name}`);
|
|
10547
|
+
lines.push("");
|
|
10548
|
+
for (const s of scenarios) {
|
|
10549
|
+
lines.push(`### ${s.ticketId} / "${s.name}" [${s.priority.toUpperCase()}] score ${s.riskScore.toFixed(1)}`);
|
|
10550
|
+
lines.push(`- Edge: ${fmtEdgePath(s.edgePath)}`);
|
|
10551
|
+
if (s.lastPassedAt)
|
|
10552
|
+
lines.push(`- Last passed: ${s.lastPassedAt}`);
|
|
10553
|
+
lines.push("");
|
|
10554
|
+
}
|
|
10555
|
+
}
|
|
10556
|
+
lines.push("## Re-run commands");
|
|
10557
|
+
lines.push(`- All: \`bun run xera:exec --from-impact ${report.targetTicket}\``);
|
|
10558
|
+
lines.push(`- P0 only: \`bun run xera:exec --from-impact ${report.targetTicket} --min-priority p0\``);
|
|
10559
|
+
lines.push(`- Select: \`bun run xera:exec --from-impact ${report.targetTicket} --select\``);
|
|
10560
|
+
lines.push("");
|
|
10561
|
+
return lines.join(`
|
|
10562
|
+
`);
|
|
10563
|
+
}
|
|
10564
|
+
|
|
10565
|
+
// src/bin-internal/impact-prepare.ts
|
|
10566
|
+
init_store();
|
|
10567
|
+
function parseDepth2(s) {
|
|
10568
|
+
const n = s ? Number.parseInt(s, 10) : 2;
|
|
10569
|
+
if (n === 1 || n === 3)
|
|
10570
|
+
return n;
|
|
10571
|
+
return 2;
|
|
10572
|
+
}
|
|
10573
|
+
function parseMinPriority(s) {
|
|
10574
|
+
if (s === "p0" || s === "p1" || s === "p2")
|
|
10575
|
+
return s;
|
|
10576
|
+
return;
|
|
10577
|
+
}
|
|
10578
|
+
async function impactPrepareCmd(argv) {
|
|
10579
|
+
const ticket = argv[0];
|
|
10580
|
+
if (!ticket || ticket.startsWith("--")) {
|
|
10581
|
+
console.error("[impact-prepare] usage: impact-prepare <TICKET> [--depth 1|2|3] [--min-priority p0|p1|p2] [--quiet]");
|
|
10582
|
+
return 1;
|
|
10583
|
+
}
|
|
10584
|
+
let depth = 2;
|
|
10585
|
+
let minPriority;
|
|
10586
|
+
let quiet = false;
|
|
10587
|
+
for (let i2 = 1;i2 < argv.length; i2++) {
|
|
10588
|
+
if (argv[i2] === "--depth")
|
|
10589
|
+
depth = parseDepth2(argv[++i2]);
|
|
10590
|
+
else if (argv[i2] === "--min-priority")
|
|
10591
|
+
minPriority = parseMinPriority(argv[++i2]);
|
|
10592
|
+
else if (argv[i2] === "--quiet")
|
|
10593
|
+
quiet = true;
|
|
10594
|
+
}
|
|
10595
|
+
const repoRoot = process.cwd();
|
|
10596
|
+
const graph = deriveSnapshot(loadAllEvents(repoRoot));
|
|
10597
|
+
const target = graph.tickets[ticket];
|
|
10598
|
+
if (!target) {
|
|
10599
|
+
console.error(`[impact-prepare] ticket ${ticket} not in graph; run /xera-fetch first`);
|
|
10600
|
+
return 2;
|
|
10601
|
+
}
|
|
10602
|
+
const opts = { depth };
|
|
10603
|
+
if (minPriority)
|
|
10604
|
+
opts.minPriority = minPriority;
|
|
10605
|
+
const scenarios = walkImpact(graph, target, opts);
|
|
10606
|
+
const report = {
|
|
10607
|
+
targetTicket: ticket,
|
|
10608
|
+
modifiedAreas: target.modifiesAreas,
|
|
10609
|
+
scenarios,
|
|
10610
|
+
generatedAt: new Date().toISOString()
|
|
10611
|
+
};
|
|
10612
|
+
const impactDir = join19(repoRoot, ".xera/impact");
|
|
10613
|
+
mkdirSync12(impactDir, { recursive: true });
|
|
10614
|
+
writeFileSync12(join19(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
|
|
10615
|
+
if (!quiet) {
|
|
10616
|
+
writeFileSync12(join19(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
|
|
10617
|
+
}
|
|
10618
|
+
return 0;
|
|
10619
|
+
}
|
|
10620
|
+
|
|
10053
10621
|
// src/bin-internal/lint.ts
|
|
10054
10622
|
import { lintTicket } from "@xera-ai/web";
|
|
10055
10623
|
async function lintCmd(argv) {
|
|
@@ -10071,7 +10639,7 @@ async function lintCmd(argv) {
|
|
|
10071
10639
|
|
|
10072
10640
|
// src/bin-internal/normalize.ts
|
|
10073
10641
|
import { existsSync as existsSync21, readdirSync as readdirSync7 } from "fs";
|
|
10074
|
-
import { join as
|
|
10642
|
+
import { join as join20 } from "path";
|
|
10075
10643
|
import { normalizeRun } from "@xera-ai/web";
|
|
10076
10644
|
async function normalizeCmd(argv) {
|
|
10077
10645
|
const ticket = argv[0];
|
|
@@ -10086,7 +10654,7 @@ async function normalizeCmd(argv) {
|
|
|
10086
10654
|
console.error("[xera:normalize] no run found");
|
|
10087
10655
|
return 1;
|
|
10088
10656
|
}
|
|
10089
|
-
const runDir =
|
|
10657
|
+
const runDir = join20(paths.runsDir, runId);
|
|
10090
10658
|
if (!existsSync21(runDir)) {
|
|
10091
10659
|
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
10092
10660
|
return 1;
|
|
@@ -10097,12 +10665,12 @@ async function normalizeCmd(argv) {
|
|
|
10097
10665
|
}
|
|
10098
10666
|
|
|
10099
10667
|
// src/bin-internal/post.ts
|
|
10100
|
-
import { existsSync as existsSync23, readFileSync as
|
|
10101
|
-
import { join as
|
|
10668
|
+
import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
|
|
10669
|
+
import { join as join21 } from "path";
|
|
10102
10670
|
|
|
10103
10671
|
// src/artifact/status.ts
|
|
10104
|
-
import { existsSync as existsSync22, mkdirSync as
|
|
10105
|
-
import { dirname as
|
|
10672
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
|
|
10673
|
+
import { dirname as dirname8 } from "path";
|
|
10106
10674
|
import { z as z7 } from "zod";
|
|
10107
10675
|
var ClassificationEnum = z7.enum([
|
|
10108
10676
|
"PASS",
|
|
@@ -10138,11 +10706,11 @@ var HISTORY_CAP = 20;
|
|
|
10138
10706
|
function readStatus(path) {
|
|
10139
10707
|
if (!existsSync22(path))
|
|
10140
10708
|
return null;
|
|
10141
|
-
return StatusJsonSchema.parse(JSON.parse(
|
|
10709
|
+
return StatusJsonSchema.parse(JSON.parse(readFileSync19(path, "utf8")));
|
|
10142
10710
|
}
|
|
10143
10711
|
function writeStatus(path, status) {
|
|
10144
|
-
|
|
10145
|
-
|
|
10712
|
+
mkdirSync13(dirname8(path), { recursive: true });
|
|
10713
|
+
writeFileSync13(path, JSON.stringify(status, null, 2));
|
|
10146
10714
|
}
|
|
10147
10715
|
function appendHistory(path, entry) {
|
|
10148
10716
|
const s = readStatus(path);
|
|
@@ -10168,12 +10736,12 @@ async function postCmd(argv) {
|
|
|
10168
10736
|
return 0;
|
|
10169
10737
|
}
|
|
10170
10738
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
10171
|
-
const draftPath =
|
|
10739
|
+
const draftPath = join21(paths.ticketDir, "jira-comment.draft.md");
|
|
10172
10740
|
if (!existsSync23(draftPath)) {
|
|
10173
10741
|
console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
|
|
10174
10742
|
return 1;
|
|
10175
10743
|
}
|
|
10176
|
-
const body =
|
|
10744
|
+
const body = readFileSync20(draftPath, "utf8");
|
|
10177
10745
|
const client = await createJiraClient({
|
|
10178
10746
|
baseUrl: config.jira.baseUrl,
|
|
10179
10747
|
preferMcp: true,
|
|
@@ -10201,8 +10769,8 @@ async function promoteCmd(argv) {
|
|
|
10201
10769
|
}
|
|
10202
10770
|
|
|
10203
10771
|
// src/bin-internal/report.ts
|
|
10204
|
-
import { existsSync as existsSync25, readFileSync as
|
|
10205
|
-
import { join as
|
|
10772
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
|
|
10773
|
+
import { join as join22 } from "path";
|
|
10206
10774
|
|
|
10207
10775
|
// src/classifier/aggregate.ts
|
|
10208
10776
|
var CLASS_PRIORITY = [
|
|
@@ -10374,10 +10942,10 @@ async function reportCmd(argv) {
|
|
|
10374
10942
|
return 1;
|
|
10375
10943
|
}
|
|
10376
10944
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
10377
|
-
const input = JSON.parse(
|
|
10945
|
+
const input = JSON.parse(readFileSync21(inputArg.slice("--input=".length), "utf8"));
|
|
10378
10946
|
const aggregated = aggregateScenarios(input.scenarios);
|
|
10379
|
-
const decisionsPath =
|
|
10380
|
-
const decisions = existsSync25(decisionsPath) ? JSON.parse(
|
|
10947
|
+
const decisionsPath = join22(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
|
|
10948
|
+
const decisions = existsSync25(decisionsPath) ? JSON.parse(readFileSync21(decisionsPath, "utf8")) : {};
|
|
10381
10949
|
const graph = deriveSnapshot(loadAllEvents(process.cwd()));
|
|
10382
10950
|
const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
10383
10951
|
const scenarioIdByName = {};
|
|
@@ -10425,8 +10993,8 @@ async function reportCmd(argv) {
|
|
|
10425
10993
|
xeraVersion: "0.1.0",
|
|
10426
10994
|
promptsVersion: "1.0.0"
|
|
10427
10995
|
});
|
|
10428
|
-
const draftPath =
|
|
10429
|
-
|
|
10996
|
+
const draftPath = join22(paths.ticketDir, "jira-comment.draft.md");
|
|
10997
|
+
writeFileSync14(draftPath, md);
|
|
10430
10998
|
console.log(`[xera:report] wrote status.json and ${draftPath}`);
|
|
10431
10999
|
return 0;
|
|
10432
11000
|
}
|
|
@@ -10493,7 +11061,7 @@ async function unlockCmd(argv) {
|
|
|
10493
11061
|
}
|
|
10494
11062
|
|
|
10495
11063
|
// src/bin-internal/validate-feature.ts
|
|
10496
|
-
import { existsSync as existsSync26, readFileSync as
|
|
11064
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
|
|
10497
11065
|
import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
|
|
10498
11066
|
async function validateFeatureCmd(argv) {
|
|
10499
11067
|
const ticket = argv[0];
|
|
@@ -10506,7 +11074,7 @@ async function validateFeatureCmd(argv) {
|
|
|
10506
11074
|
console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
|
|
10507
11075
|
return 1;
|
|
10508
11076
|
}
|
|
10509
|
-
const r = validateGherkin2(
|
|
11077
|
+
const r = validateGherkin2(readFileSync22(paths.featurePath, "utf8"));
|
|
10510
11078
|
if (r.ok) {
|
|
10511
11079
|
console.log("[xera:validate-feature] ok");
|
|
10512
11080
|
return 0;
|
|
@@ -10526,10 +11094,12 @@ var COMMANDS = {
|
|
|
10526
11094
|
fetch: fetchCmd,
|
|
10527
11095
|
"graph-backfill": graphBackfillCmd,
|
|
10528
11096
|
"graph-enrich": graphEnrichCmd,
|
|
11097
|
+
"graph-render": graphRenderCmd,
|
|
10529
11098
|
"graph-query": graphQueryCmd,
|
|
10530
11099
|
"graph-record": graphRecordCmd,
|
|
10531
11100
|
"graph-snapshot": graphSnapshotCmd,
|
|
10532
11101
|
"heal-prepare": healPrepareCmd,
|
|
11102
|
+
"impact-prepare": impactPrepareCmd,
|
|
10533
11103
|
lint: lintCmd,
|
|
10534
11104
|
normalize: normalizeCmd,
|
|
10535
11105
|
post: postCmd,
|