@xera-ai/core 0.4.0 → 0.4.2
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/artifact/status.d.ts +4 -0
- package/dist/artifact/status.d.ts.map +1 -1
- package/dist/bin/internal.js +593 -72
- package/dist/bin-internal/graph-enrich.d.ts +2 -0
- package/dist/bin-internal/graph-enrich.d.ts.map +1 -0
- package/dist/bin-internal/graph-record.d.ts.map +1 -1
- 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/bin-internal/report.d.ts.map +1 -1
- package/dist/bin-internal/verify-prompts.d.ts.map +1 -1
- package/dist/classifier/aggregate.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/classify.d.ts +42 -0
- package/dist/graph/classify.d.ts.map +1 -0
- package/dist/graph/enrich.d.ts +10 -0
- package/dist/graph/enrich.d.ts.map +1 -0
- package/dist/graph/impact.d.ts +31 -0
- package/dist/graph/impact.d.ts.map +1 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/schema.d.ts +3 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/similarity.d.ts +3 -0
- package/dist/graph/similarity.d.ts.map +1 -0
- package/dist/graph/types.d.ts +1 -1
- package/dist/graph/types.d.ts.map +1 -1
- package/dist/src/index.js +15 -1
- package/package.json +1 -1
- package/src/artifact/status.ts +8 -1
- package/src/bin-internal/graph-enrich.ts +28 -0
- package/src/bin-internal/graph-record.ts +45 -1
- package/src/bin-internal/impact-prepare.ts +64 -0
- package/src/bin-internal/index.ts +4 -0
- package/src/bin-internal/report.ts +63 -5
- package/src/bin-internal/verify-prompts.ts +2 -0
- package/src/classifier/aggregate.ts +1 -0
- package/src/config/schema.ts +12 -0
- package/src/graph/classify.ts +126 -0
- package/src/graph/enrich.ts +103 -0
- package/src/graph/impact.ts +262 -0
- package/src/graph/index.ts +26 -0
- package/src/graph/schema.ts +8 -1
- package/src/graph/similarity.ts +43 -0
- package/src/graph/types.ts +7 -2
package/dist/bin/internal.js
CHANGED
|
@@ -98,7 +98,14 @@ var init_schema = __esm(() => {
|
|
|
98
98
|
traceId: z.string().optional(),
|
|
99
99
|
runtime: z.number().nonnegative()
|
|
100
100
|
}).passthrough();
|
|
101
|
-
classification = z.enum([
|
|
101
|
+
classification = z.enum([
|
|
102
|
+
"REAL_BUG",
|
|
103
|
+
"TEST_BUG",
|
|
104
|
+
"SELECTOR_DRIFT",
|
|
105
|
+
"FLAKY",
|
|
106
|
+
"PASS",
|
|
107
|
+
"TEST_OUTDATED"
|
|
108
|
+
]);
|
|
102
109
|
runClassified = z.object({
|
|
103
110
|
scenarioId: z.string(),
|
|
104
111
|
runId: z.string(),
|
|
@@ -7730,7 +7737,7 @@ function parseFlags2(args) {
|
|
|
7730
7737
|
async function graphRecordCmd(argv) {
|
|
7731
7738
|
const [action, ...rest] = argv;
|
|
7732
7739
|
if (!action) {
|
|
7733
|
-
console.error(`Usage: xera-internal graph-record <fetch|script|exec|classify|promote> [args]`);
|
|
7740
|
+
console.error(`Usage: xera-internal graph-record <fetch|script|exec|classify|promote|dispute> [args]`);
|
|
7734
7741
|
return 1;
|
|
7735
7742
|
}
|
|
7736
7743
|
const repoRoot = process.cwd();
|
|
@@ -7774,6 +7781,43 @@ async function graphRecordCmd(argv) {
|
|
|
7774
7781
|
case "promote": {
|
|
7775
7782
|
return recordPromote(repoRoot, parseFlags2(rest));
|
|
7776
7783
|
}
|
|
7784
|
+
case "dispute": {
|
|
7785
|
+
const flags = parseFlags2(rest);
|
|
7786
|
+
const runId = flags.get("--run-id");
|
|
7787
|
+
const scenarioIdArg = flags.get("--scenario-id");
|
|
7788
|
+
const from = flags.get("--from");
|
|
7789
|
+
const to = flags.get("--to");
|
|
7790
|
+
const actor = flags.get("--actor");
|
|
7791
|
+
const reason = flags.get("--reason");
|
|
7792
|
+
if (!runId || !scenarioIdArg || !from || !to || !actor) {
|
|
7793
|
+
console.error("[graph-record dispute] required: --run-id --scenario-id --from --to --actor [--reason]");
|
|
7794
|
+
return 1;
|
|
7795
|
+
}
|
|
7796
|
+
const validClass = [
|
|
7797
|
+
"REAL_BUG",
|
|
7798
|
+
"TEST_BUG",
|
|
7799
|
+
"SELECTOR_DRIFT",
|
|
7800
|
+
"FLAKY",
|
|
7801
|
+
"PASS",
|
|
7802
|
+
"TEST_OUTDATED"
|
|
7803
|
+
];
|
|
7804
|
+
if (!validClass.includes(from) || !validClass.includes(to)) {
|
|
7805
|
+
console.error(`[graph-record dispute] --from and --to must be one of: ${validClass.join(", ")}`);
|
|
7806
|
+
return 1;
|
|
7807
|
+
}
|
|
7808
|
+
const payload = {
|
|
7809
|
+
runId,
|
|
7810
|
+
scenarioId: scenarioIdArg,
|
|
7811
|
+
originalClassification: from,
|
|
7812
|
+
disputedTo: to,
|
|
7813
|
+
qaActor: actor
|
|
7814
|
+
};
|
|
7815
|
+
if (reason)
|
|
7816
|
+
payload.qaReason = reason;
|
|
7817
|
+
const e = makeEvent("xera-report", "classification.disputed", payload);
|
|
7818
|
+
appendEvents(repoRoot, [e], { skill: "xera-report", ticketId: scenarioIdArg.slice(0, 12) });
|
|
7819
|
+
return 0;
|
|
7820
|
+
}
|
|
7777
7821
|
default:
|
|
7778
7822
|
console.error(`Unknown action: ${action}`);
|
|
7779
7823
|
return 1;
|
|
@@ -7831,7 +7875,9 @@ var IN_SCOPE_PROMPTS = [
|
|
|
7831
7875
|
"feature-from-story.md",
|
|
7832
7876
|
"script-from-feature.md",
|
|
7833
7877
|
"heal-locator.md",
|
|
7834
|
-
"extract-areas.md"
|
|
7878
|
+
"extract-areas.md",
|
|
7879
|
+
"similarity-match.md",
|
|
7880
|
+
"classify-outdated.md"
|
|
7835
7881
|
];
|
|
7836
7882
|
var REQUIRED_SECTION_HEADING = "## Handling untrusted input";
|
|
7837
7883
|
var REQUIRED_KEYWORDS = ["UNTRUSTED", "injection-follow", "<XR_"];
|
|
@@ -8836,11 +8882,18 @@ var ReportingSchema = z4.object({
|
|
|
8836
8882
|
}).prefault({}),
|
|
8837
8883
|
artifactLinks: z4.enum(["git", "local"]).default("git")
|
|
8838
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({});
|
|
8839
8891
|
var XeraConfigSchema = z4.object({
|
|
8840
8892
|
jira: JiraSchema,
|
|
8841
8893
|
web: WebSchema,
|
|
8842
8894
|
ai: AISchema,
|
|
8843
8895
|
reporting: ReportingSchema,
|
|
8896
|
+
run: RunSchema.prefault({}),
|
|
8844
8897
|
adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
|
|
8845
8898
|
});
|
|
8846
8899
|
|
|
@@ -9266,6 +9319,96 @@ async function graphBackfillCmd(argv) {
|
|
|
9266
9319
|
return 0;
|
|
9267
9320
|
}
|
|
9268
9321
|
|
|
9322
|
+
// src/graph/enrich.ts
|
|
9323
|
+
init_store();
|
|
9324
|
+
init_ulid();
|
|
9325
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
|
|
9326
|
+
import { join as join15 } from "path";
|
|
9327
|
+
import { z as z6 } from "zod";
|
|
9328
|
+
var MAX_SIMILAR_EDGES = 10;
|
|
9329
|
+
var MIN_CONFIDENCE = 0.7;
|
|
9330
|
+
var SimilarEntrySchema = z6.object({
|
|
9331
|
+
ticketId: z6.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
9332
|
+
confidence: z6.number(),
|
|
9333
|
+
reason: z6.string()
|
|
9334
|
+
});
|
|
9335
|
+
var EnrichmentInputSchema = z6.object({
|
|
9336
|
+
similar: z6.array(SimilarEntrySchema)
|
|
9337
|
+
});
|
|
9338
|
+
var nowIso3 = () => new Date().toISOString();
|
|
9339
|
+
var mk2 = (actor, type, payload) => ({
|
|
9340
|
+
event_id: ulid(),
|
|
9341
|
+
schema_version: SCHEMA_VERSION,
|
|
9342
|
+
ts: nowIso3(),
|
|
9343
|
+
actor,
|
|
9344
|
+
type,
|
|
9345
|
+
payload
|
|
9346
|
+
});
|
|
9347
|
+
async function enrichTicket(repoRoot, ticketId, opts) {
|
|
9348
|
+
const inputPath = join15(repoRoot, ".xera", ticketId, "enrichment-input.json");
|
|
9349
|
+
if (!existsSync19(inputPath)) {
|
|
9350
|
+
throw new Error(`enrichment-input.json not found at ${inputPath}`);
|
|
9351
|
+
}
|
|
9352
|
+
const raw = JSON.parse(readFileSync16(inputPath, "utf8"));
|
|
9353
|
+
const parsed = EnrichmentInputSchema.safeParse(raw);
|
|
9354
|
+
if (!parsed.success) {
|
|
9355
|
+
throw new Error(`invalid enrichment-input.json: ${parsed.error.message}`);
|
|
9356
|
+
}
|
|
9357
|
+
const snapshot = deriveSnapshot(loadAllEvents(repoRoot));
|
|
9358
|
+
if (!snapshot.tickets[ticketId]) {
|
|
9359
|
+
throw new Error(`ticket ${ticketId} not in graph; run /xera-fetch first`);
|
|
9360
|
+
}
|
|
9361
|
+
if (snapshot.tickets[ticketId].enrichedAt && !opts.force) {
|
|
9362
|
+
return { ticketId, similarCount: 0, enrichedAt: snapshot.tickets[ticketId].enrichedAt };
|
|
9363
|
+
}
|
|
9364
|
+
const validated = parsed.data.similar.map((s) => ({ ...s, confidence: Math.max(0, Math.min(1, s.confidence)) })).filter((s) => s.confidence >= MIN_CONFIDENCE).filter((s) => snapshot.tickets[s.ticketId] !== undefined).filter((s) => s.ticketId !== ticketId).slice(0, MAX_SIMILAR_EDGES);
|
|
9365
|
+
const events = [];
|
|
9366
|
+
for (const s of validated) {
|
|
9367
|
+
const payload = {
|
|
9368
|
+
kind: "similar",
|
|
9369
|
+
from: ticketId,
|
|
9370
|
+
to: s.ticketId,
|
|
9371
|
+
confidence: s.confidence,
|
|
9372
|
+
source: `llm-similarity:${s.reason.slice(0, 80)}`
|
|
9373
|
+
};
|
|
9374
|
+
events.push(mk2("graph-enrich", "edge.discovered", payload));
|
|
9375
|
+
}
|
|
9376
|
+
const enrichedAt = nowIso3();
|
|
9377
|
+
const enrichedPayload = {
|
|
9378
|
+
ticketId,
|
|
9379
|
+
enrichedAt,
|
|
9380
|
+
similarCount: validated.length
|
|
9381
|
+
};
|
|
9382
|
+
events.push(mk2("graph-enrich", "ticket.enriched", enrichedPayload));
|
|
9383
|
+
appendEvents(repoRoot, events, { skill: "graph-enrich", ticketId });
|
|
9384
|
+
return { ticketId, similarCount: validated.length, enrichedAt };
|
|
9385
|
+
}
|
|
9386
|
+
|
|
9387
|
+
// src/bin-internal/graph-enrich.ts
|
|
9388
|
+
async function graphEnrichCmd(argv) {
|
|
9389
|
+
let ticket;
|
|
9390
|
+
let force = false;
|
|
9391
|
+
for (let i = 0;i < argv.length; i++) {
|
|
9392
|
+
if (argv[i] === "--ticket")
|
|
9393
|
+
ticket = argv[++i];
|
|
9394
|
+
else if (argv[i] === "--force")
|
|
9395
|
+
force = true;
|
|
9396
|
+
}
|
|
9397
|
+
const repoRoot = process.cwd();
|
|
9398
|
+
if (!ticket) {
|
|
9399
|
+
console.error("[graph-enrich] usage: graph-enrich --ticket <id> [--force]");
|
|
9400
|
+
return 1;
|
|
9401
|
+
}
|
|
9402
|
+
try {
|
|
9403
|
+
const result = await enrichTicket(repoRoot, ticket, { force });
|
|
9404
|
+
console.log(`[graph-enrich] ${ticket} enriched (${result.similarCount} similar edges, at ${result.enrichedAt})`);
|
|
9405
|
+
return 0;
|
|
9406
|
+
} catch (e) {
|
|
9407
|
+
console.error(`[graph-enrich] ${ticket} failed: ${e.message}`);
|
|
9408
|
+
return 1;
|
|
9409
|
+
}
|
|
9410
|
+
}
|
|
9411
|
+
|
|
9269
9412
|
// src/bin-internal/graph-query.ts
|
|
9270
9413
|
init_store();
|
|
9271
9414
|
function filterByTicket(snap, ticket) {
|
|
@@ -9339,8 +9482,8 @@ async function graphSnapshotCmd(argv) {
|
|
|
9339
9482
|
}
|
|
9340
9483
|
|
|
9341
9484
|
// src/bin-internal/heal-prepare.ts
|
|
9342
|
-
import { existsSync as
|
|
9343
|
-
import { join as
|
|
9485
|
+
import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
|
|
9486
|
+
import { join as join16 } from "path";
|
|
9344
9487
|
import { scrubFreeText } from "@xera-ai/web";
|
|
9345
9488
|
|
|
9346
9489
|
// ../../node_modules/.bun/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
|
@@ -9687,15 +9830,15 @@ function strFromU8(dat, latin1) {
|
|
|
9687
9830
|
var slzh = function(d, b) {
|
|
9688
9831
|
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
9689
9832
|
};
|
|
9690
|
-
var zh = function(d, b,
|
|
9833
|
+
var zh = function(d, b, z7) {
|
|
9691
9834
|
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;
|
|
9692
|
-
var _a2 = z64hs(d, es, efl,
|
|
9835
|
+
var _a2 = z64hs(d, es, efl, z7, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
|
|
9693
9836
|
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
9694
9837
|
};
|
|
9695
|
-
var z64hs = function(d, b, l,
|
|
9838
|
+
var z64hs = function(d, b, l, z7, sc, su, off) {
|
|
9696
9839
|
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
9697
9840
|
var nf = nsc + nsu + noff;
|
|
9698
|
-
if (
|
|
9841
|
+
if (z7 && nf) {
|
|
9699
9842
|
for (;b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
9700
9843
|
if (b2(d, b) == 1) {
|
|
9701
9844
|
return [
|
|
@@ -9706,7 +9849,7 @@ var z64hs = function(d, b, l, z6, sc, su, off) {
|
|
|
9706
9849
|
];
|
|
9707
9850
|
}
|
|
9708
9851
|
}
|
|
9709
|
-
if (
|
|
9852
|
+
if (z7 < 2)
|
|
9710
9853
|
err(13);
|
|
9711
9854
|
}
|
|
9712
9855
|
return [sc, su, off, 0];
|
|
@@ -9722,18 +9865,18 @@ function unzipSync(data, opts) {
|
|
|
9722
9865
|
if (!c)
|
|
9723
9866
|
return {};
|
|
9724
9867
|
var o = b4(data, e + 16);
|
|
9725
|
-
var
|
|
9726
|
-
if (
|
|
9868
|
+
var z7 = b4(data, e - 20) == 117853008;
|
|
9869
|
+
if (z7) {
|
|
9727
9870
|
var ze = b4(data, e - 12);
|
|
9728
|
-
|
|
9729
|
-
if (
|
|
9871
|
+
z7 = b4(data, ze) == 101075792;
|
|
9872
|
+
if (z7) {
|
|
9730
9873
|
c = b4(data, ze + 32);
|
|
9731
9874
|
o = b4(data, ze + 48);
|
|
9732
9875
|
}
|
|
9733
9876
|
}
|
|
9734
9877
|
var fltr = opts && opts.filter;
|
|
9735
9878
|
for (var i2 = 0;i2 < c; ++i2) {
|
|
9736
|
-
var _a2 = zh(data, o,
|
|
9879
|
+
var _a2 = zh(data, o, z7), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
|
|
9737
9880
|
o = no;
|
|
9738
9881
|
if (!fltr || fltr({
|
|
9739
9882
|
name: fn,
|
|
@@ -9768,9 +9911,9 @@ function classifyKind(raw) {
|
|
|
9768
9911
|
return "other";
|
|
9769
9912
|
}
|
|
9770
9913
|
function extractDomSnapshot(tracePath) {
|
|
9771
|
-
if (!
|
|
9914
|
+
if (!existsSync20(tracePath))
|
|
9772
9915
|
return "";
|
|
9773
|
-
const buf =
|
|
9916
|
+
const buf = readFileSync17(tracePath);
|
|
9774
9917
|
const entries = unzipSync(buf);
|
|
9775
9918
|
const traceKey = Object.keys(entries).find((name) => name.endsWith(".trace"));
|
|
9776
9919
|
let chosenKey = null;
|
|
@@ -9818,16 +9961,16 @@ function extractDomSnapshot(tracePath) {
|
|
|
9818
9961
|
return scrubFreeText(html);
|
|
9819
9962
|
}
|
|
9820
9963
|
function findPomLine(ticketDir, rawLocator) {
|
|
9821
|
-
const pomDir =
|
|
9964
|
+
const pomDir = join16(ticketDir, "page-objects");
|
|
9822
9965
|
const candidates = [];
|
|
9823
|
-
if (
|
|
9966
|
+
if (existsSync20(pomDir)) {
|
|
9824
9967
|
for (const name of readdirSync6(pomDir)) {
|
|
9825
9968
|
if (name.endsWith(".ts"))
|
|
9826
|
-
candidates.push(
|
|
9969
|
+
candidates.push(join16(pomDir, name));
|
|
9827
9970
|
}
|
|
9828
9971
|
}
|
|
9829
9972
|
for (const file of candidates) {
|
|
9830
|
-
const text =
|
|
9973
|
+
const text = readFileSync17(file, "utf8");
|
|
9831
9974
|
const lines = text.split(`
|
|
9832
9975
|
`);
|
|
9833
9976
|
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
@@ -9865,13 +10008,13 @@ function findGherkinStep(featureText, rawLocator) {
|
|
|
9865
10008
|
}
|
|
9866
10009
|
function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
9867
10010
|
const paths = resolveArtifactPaths(repoRoot, ticket);
|
|
9868
|
-
const classifierPath =
|
|
9869
|
-
const classifier = JSON.parse(
|
|
10011
|
+
const classifierPath = join16(paths.ticketDir, "classifier-input.json");
|
|
10012
|
+
const classifier = JSON.parse(readFileSync17(classifierPath, "utf8"));
|
|
9870
10013
|
const cls = classifier.scenarios.find((s) => s.name === scenarioName);
|
|
9871
10014
|
if (!cls)
|
|
9872
10015
|
throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
|
|
9873
|
-
const runDir =
|
|
9874
|
-
const normalized = JSON.parse(
|
|
10016
|
+
const runDir = join16(paths.runsDir, runId);
|
|
10017
|
+
const normalized = JSON.parse(readFileSync17(join16(runDir, "normalized.json"), "utf8"));
|
|
9875
10018
|
const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
|
|
9876
10019
|
if (!normSc?.failure)
|
|
9877
10020
|
throw new Error(`no failure recorded for scenario "${scenarioName}"`);
|
|
@@ -9882,9 +10025,9 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
|
9882
10025
|
const raw = m[1].trim();
|
|
9883
10026
|
const kind = classifyKind(raw);
|
|
9884
10027
|
const pomLoc = findPomLine(paths.ticketDir, raw);
|
|
9885
|
-
const featureText =
|
|
10028
|
+
const featureText = readFileSync17(paths.featurePath, "utf8");
|
|
9886
10029
|
const gherkinStep = findGherkinStep(featureText, raw);
|
|
9887
|
-
const domSnapshotAtFailure = extractDomSnapshot(
|
|
10030
|
+
const domSnapshotAtFailure = extractDomSnapshot(join16(runDir, "trace.zip"));
|
|
9888
10031
|
return {
|
|
9889
10032
|
ticket,
|
|
9890
10033
|
runId,
|
|
@@ -9904,7 +10047,7 @@ async function healPrepareCmd(argv) {
|
|
|
9904
10047
|
try {
|
|
9905
10048
|
const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
|
|
9906
10049
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
9907
|
-
const outPath =
|
|
10050
|
+
const outPath = join16(paths.runsDir, runId, "heal-input.json");
|
|
9908
10051
|
writeFileSync10(outPath, JSON.stringify(result, null, 2));
|
|
9909
10052
|
console.log(`[xera:heal-prepare] wrote ${outPath}`);
|
|
9910
10053
|
return 0;
|
|
@@ -9914,6 +10057,267 @@ async function healPrepareCmd(argv) {
|
|
|
9914
10057
|
}
|
|
9915
10058
|
}
|
|
9916
10059
|
|
|
10060
|
+
// src/bin-internal/impact-prepare.ts
|
|
10061
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
10062
|
+
import { join as join17 } from "path";
|
|
10063
|
+
|
|
10064
|
+
// src/graph/impact.ts
|
|
10065
|
+
var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
|
|
10066
|
+
var EDGE_WEIGHT_FIXED = {
|
|
10067
|
+
modifies: 5,
|
|
10068
|
+
uses: 4,
|
|
10069
|
+
covers: 4
|
|
10070
|
+
};
|
|
10071
|
+
function jiraRelationWeight(source) {
|
|
10072
|
+
if (!source)
|
|
10073
|
+
return 0;
|
|
10074
|
+
if (source.endsWith("blocks"))
|
|
10075
|
+
return 4;
|
|
10076
|
+
if (source.endsWith("duplicates"))
|
|
10077
|
+
return 3;
|
|
10078
|
+
if (source.endsWith("relates"))
|
|
10079
|
+
return 2;
|
|
10080
|
+
if (source.endsWith("supersedes"))
|
|
10081
|
+
return 3;
|
|
10082
|
+
return 1;
|
|
10083
|
+
}
|
|
10084
|
+
function edgeWeight(edge) {
|
|
10085
|
+
if (edge.kind === "modifies")
|
|
10086
|
+
return EDGE_WEIGHT_FIXED.modifies ?? 0;
|
|
10087
|
+
if (edge.kind === "uses" || edge.kind === "covers")
|
|
10088
|
+
return EDGE_WEIGHT_FIXED.uses ?? 0;
|
|
10089
|
+
if (edge.kind === "jira-linked")
|
|
10090
|
+
return jiraRelationWeight(edge.source);
|
|
10091
|
+
if (edge.kind === "similar")
|
|
10092
|
+
return 1 * (edge.confidence ?? 0);
|
|
10093
|
+
return 0;
|
|
10094
|
+
}
|
|
10095
|
+
function riskScore(scenario, daysSinceLastPass) {
|
|
10096
|
+
const pri = PRIORITY_WEIGHT[scenario.priority] * 3;
|
|
10097
|
+
const firstEdge = scenario.edgePath[0];
|
|
10098
|
+
const edgeW = firstEdge ? edgeWeight(firstEdge) : 0;
|
|
10099
|
+
const confW = firstEdge?.confidence !== undefined ? firstEdge.confidence * 2 : 0;
|
|
10100
|
+
const decay = daysSinceLastPass * 0.1;
|
|
10101
|
+
return pri + edgeW + confW - decay;
|
|
10102
|
+
}
|
|
10103
|
+
var PRIORITY_RANK = { p0: 3, p1: 2, p2: 1 };
|
|
10104
|
+
function daysSince(ts) {
|
|
10105
|
+
if (!ts)
|
|
10106
|
+
return 0;
|
|
10107
|
+
const ms = Date.now() - Date.parse(ts);
|
|
10108
|
+
return ms < 0 ? 0 : ms / (86400 * 1000);
|
|
10109
|
+
}
|
|
10110
|
+
function walkImpact(graph, target, opts) {
|
|
10111
|
+
const result = [];
|
|
10112
|
+
const seen = new Set;
|
|
10113
|
+
const targetAreas = new Set(target.modifiesAreas);
|
|
10114
|
+
const pomIds = graph.edges.filter((e) => e.kind === "covers" && targetAreas.has(e.to)).map((e) => e.from);
|
|
10115
|
+
const directScenarios = graph.edges.filter((e) => e.kind === "uses" && pomIds.includes(e.to)).map((e) => e.from);
|
|
10116
|
+
for (const scenarioId2 of directScenarios) {
|
|
10117
|
+
if (seen.has(scenarioId2))
|
|
10118
|
+
continue;
|
|
10119
|
+
const scenario = graph.scenarios[scenarioId2];
|
|
10120
|
+
if (!scenario)
|
|
10121
|
+
continue;
|
|
10122
|
+
if (scenario.ticketId === target.id)
|
|
10123
|
+
continue;
|
|
10124
|
+
const usingPom = graph.edges.find((e) => e.kind === "uses" && e.from === scenarioId2);
|
|
10125
|
+
const modifyEdge = graph.edges.find((e) => e.kind === "modifies" && e.from === target.id && targetAreas.has(e.to));
|
|
10126
|
+
const edgePath = [];
|
|
10127
|
+
if (modifyEdge)
|
|
10128
|
+
edgePath.push({ kind: "modifies", from: modifyEdge.from, to: modifyEdge.to });
|
|
10129
|
+
if (usingPom)
|
|
10130
|
+
edgePath.push({ kind: "uses", from: usingPom.from, to: usingPom.to });
|
|
10131
|
+
seen.add(scenarioId2);
|
|
10132
|
+
const impact = {
|
|
10133
|
+
scenarioId: scenarioId2,
|
|
10134
|
+
ticketId: scenario.ticketId,
|
|
10135
|
+
name: scenario.name,
|
|
10136
|
+
priority: scenario.priority,
|
|
10137
|
+
edgePath,
|
|
10138
|
+
riskScore: 0
|
|
10139
|
+
};
|
|
10140
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
|
|
10141
|
+
result.push(impact);
|
|
10142
|
+
}
|
|
10143
|
+
if (opts.depth >= 2) {
|
|
10144
|
+
const linked = graph.edges.filter((e) => e.kind === "jira-linked" && e.from === target.id).map((e) => ({ to: e.to, source: e.source }));
|
|
10145
|
+
for (const link of linked) {
|
|
10146
|
+
const sceneIds = graph.edges.filter((e) => e.kind === "tests" && e.from === link.to).map((e) => e.to);
|
|
10147
|
+
for (const scenarioId2 of sceneIds) {
|
|
10148
|
+
if (seen.has(scenarioId2))
|
|
10149
|
+
continue;
|
|
10150
|
+
const scenario = graph.scenarios[scenarioId2];
|
|
10151
|
+
if (!scenario || scenario.ticketId === target.id)
|
|
10152
|
+
continue;
|
|
10153
|
+
seen.add(scenarioId2);
|
|
10154
|
+
const edge = { kind: "jira-linked", from: target.id, to: link.to };
|
|
10155
|
+
if (link.source !== undefined)
|
|
10156
|
+
edge.source = link.source;
|
|
10157
|
+
const impact = {
|
|
10158
|
+
scenarioId: scenarioId2,
|
|
10159
|
+
ticketId: scenario.ticketId,
|
|
10160
|
+
name: scenario.name,
|
|
10161
|
+
priority: scenario.priority,
|
|
10162
|
+
edgePath: [edge],
|
|
10163
|
+
riskScore: 0
|
|
10164
|
+
};
|
|
10165
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
|
|
10166
|
+
result.push(impact);
|
|
10167
|
+
}
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
if (opts.depth >= 3) {
|
|
10171
|
+
const similar = graph.edges.filter((e) => e.kind === "similar" && e.from === target.id).map((e) => ({ to: e.to, confidence: e.confidence }));
|
|
10172
|
+
for (const link of similar) {
|
|
10173
|
+
const sceneIds = graph.edges.filter((e) => e.kind === "tests" && e.from === link.to).map((e) => e.to);
|
|
10174
|
+
for (const scenarioId2 of sceneIds) {
|
|
10175
|
+
if (seen.has(scenarioId2))
|
|
10176
|
+
continue;
|
|
10177
|
+
const scenario = graph.scenarios[scenarioId2];
|
|
10178
|
+
if (!scenario || scenario.ticketId === target.id)
|
|
10179
|
+
continue;
|
|
10180
|
+
seen.add(scenarioId2);
|
|
10181
|
+
const edge = { kind: "similar", from: target.id, to: link.to };
|
|
10182
|
+
if (link.confidence !== undefined)
|
|
10183
|
+
edge.confidence = link.confidence;
|
|
10184
|
+
const impact = {
|
|
10185
|
+
scenarioId: scenarioId2,
|
|
10186
|
+
ticketId: scenario.ticketId,
|
|
10187
|
+
name: scenario.name,
|
|
10188
|
+
priority: scenario.priority,
|
|
10189
|
+
edgePath: [edge],
|
|
10190
|
+
riskScore: 0
|
|
10191
|
+
};
|
|
10192
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId2]?.ts));
|
|
10193
|
+
result.push(impact);
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
10196
|
+
}
|
|
10197
|
+
let filtered = result;
|
|
10198
|
+
if (opts.minPriority) {
|
|
10199
|
+
const min = PRIORITY_RANK[opts.minPriority];
|
|
10200
|
+
filtered = filtered.filter((s) => PRIORITY_RANK[s.priority] >= min);
|
|
10201
|
+
}
|
|
10202
|
+
filtered.sort((a, b) => b.riskScore - a.riskScore);
|
|
10203
|
+
return filtered;
|
|
10204
|
+
}
|
|
10205
|
+
var HIGH_THRESHOLD = 7;
|
|
10206
|
+
var MEDIUM_THRESHOLD = 4;
|
|
10207
|
+
function bucket(score) {
|
|
10208
|
+
if (score >= HIGH_THRESHOLD)
|
|
10209
|
+
return "high";
|
|
10210
|
+
if (score >= MEDIUM_THRESHOLD)
|
|
10211
|
+
return "medium";
|
|
10212
|
+
return "low";
|
|
10213
|
+
}
|
|
10214
|
+
function fmtEdgePath(path) {
|
|
10215
|
+
return path.map((e) => `${e.from} \u2192[${e.kind}]\u2192 ${e.to}`).join(" \xB7 ");
|
|
10216
|
+
}
|
|
10217
|
+
function renderImpactMarkdown(report) {
|
|
10218
|
+
const lines = [];
|
|
10219
|
+
lines.push(`# Impact Analysis \u2014 ${report.targetTicket}`);
|
|
10220
|
+
lines.push("");
|
|
10221
|
+
lines.push(`**Modified areas:** ${report.modifiedAreas.join(", ") || "(none)"}`);
|
|
10222
|
+
lines.push(`**Generated:** ${report.generatedAt}`);
|
|
10223
|
+
lines.push("");
|
|
10224
|
+
if (report.scenarios.length === 0) {
|
|
10225
|
+
lines.push("No prior scenarios in the modified areas. This may be a new feature area.");
|
|
10226
|
+
lines.push("");
|
|
10227
|
+
return lines.join(`
|
|
10228
|
+
`);
|
|
10229
|
+
}
|
|
10230
|
+
const bySeverity = {
|
|
10231
|
+
high: [],
|
|
10232
|
+
medium: [],
|
|
10233
|
+
low: []
|
|
10234
|
+
};
|
|
10235
|
+
for (const s of report.scenarios)
|
|
10236
|
+
bySeverity[bucket(s.riskScore)].push(s);
|
|
10237
|
+
lines.push(`**Total impacted:** ${report.scenarios.length} scenarios (${bySeverity.high.length} high \xB7 ${bySeverity.medium.length} medium \xB7 ${bySeverity.low.length} low)`);
|
|
10238
|
+
lines.push("");
|
|
10239
|
+
for (const [name, scenarios] of [
|
|
10240
|
+
["High-risk", bySeverity.high],
|
|
10241
|
+
["Medium-risk", bySeverity.medium],
|
|
10242
|
+
["Low-risk", bySeverity.low]
|
|
10243
|
+
]) {
|
|
10244
|
+
if (scenarios.length === 0)
|
|
10245
|
+
continue;
|
|
10246
|
+
lines.push(`## ${name}`);
|
|
10247
|
+
lines.push("");
|
|
10248
|
+
for (const s of scenarios) {
|
|
10249
|
+
lines.push(`### ${s.ticketId} / "${s.name}" [${s.priority.toUpperCase()}] score ${s.riskScore.toFixed(1)}`);
|
|
10250
|
+
lines.push(`- Edge: ${fmtEdgePath(s.edgePath)}`);
|
|
10251
|
+
if (s.lastPassedAt)
|
|
10252
|
+
lines.push(`- Last passed: ${s.lastPassedAt}`);
|
|
10253
|
+
lines.push("");
|
|
10254
|
+
}
|
|
10255
|
+
}
|
|
10256
|
+
lines.push("## Re-run commands");
|
|
10257
|
+
lines.push(`- All: \`bun run xera:exec --from-impact ${report.targetTicket}\``);
|
|
10258
|
+
lines.push(`- P0 only: \`bun run xera:exec --from-impact ${report.targetTicket} --min-priority p0\``);
|
|
10259
|
+
lines.push(`- Select: \`bun run xera:exec --from-impact ${report.targetTicket} --select\``);
|
|
10260
|
+
lines.push("");
|
|
10261
|
+
return lines.join(`
|
|
10262
|
+
`);
|
|
10263
|
+
}
|
|
10264
|
+
|
|
10265
|
+
// src/bin-internal/impact-prepare.ts
|
|
10266
|
+
init_store();
|
|
10267
|
+
function parseDepth(s) {
|
|
10268
|
+
const n = s ? Number.parseInt(s, 10) : 2;
|
|
10269
|
+
if (n === 1 || n === 3)
|
|
10270
|
+
return n;
|
|
10271
|
+
return 2;
|
|
10272
|
+
}
|
|
10273
|
+
function parseMinPriority(s) {
|
|
10274
|
+
if (s === "p0" || s === "p1" || s === "p2")
|
|
10275
|
+
return s;
|
|
10276
|
+
return;
|
|
10277
|
+
}
|
|
10278
|
+
async function impactPrepareCmd(argv) {
|
|
10279
|
+
const ticket = argv[0];
|
|
10280
|
+
if (!ticket || ticket.startsWith("--")) {
|
|
10281
|
+
console.error("[impact-prepare] usage: impact-prepare <TICKET> [--depth 1|2|3] [--min-priority p0|p1|p2] [--quiet]");
|
|
10282
|
+
return 1;
|
|
10283
|
+
}
|
|
10284
|
+
let depth = 2;
|
|
10285
|
+
let minPriority;
|
|
10286
|
+
let quiet = false;
|
|
10287
|
+
for (let i2 = 1;i2 < argv.length; i2++) {
|
|
10288
|
+
if (argv[i2] === "--depth")
|
|
10289
|
+
depth = parseDepth(argv[++i2]);
|
|
10290
|
+
else if (argv[i2] === "--min-priority")
|
|
10291
|
+
minPriority = parseMinPriority(argv[++i2]);
|
|
10292
|
+
else if (argv[i2] === "--quiet")
|
|
10293
|
+
quiet = true;
|
|
10294
|
+
}
|
|
10295
|
+
const repoRoot = process.cwd();
|
|
10296
|
+
const graph = deriveSnapshot(loadAllEvents(repoRoot));
|
|
10297
|
+
const target = graph.tickets[ticket];
|
|
10298
|
+
if (!target) {
|
|
10299
|
+
console.error(`[impact-prepare] ticket ${ticket} not in graph; run /xera-fetch first`);
|
|
10300
|
+
return 2;
|
|
10301
|
+
}
|
|
10302
|
+
const opts = { depth };
|
|
10303
|
+
if (minPriority)
|
|
10304
|
+
opts.minPriority = minPriority;
|
|
10305
|
+
const scenarios = walkImpact(graph, target, opts);
|
|
10306
|
+
const report = {
|
|
10307
|
+
targetTicket: ticket,
|
|
10308
|
+
modifiedAreas: target.modifiesAreas,
|
|
10309
|
+
scenarios,
|
|
10310
|
+
generatedAt: new Date().toISOString()
|
|
10311
|
+
};
|
|
10312
|
+
const impactDir = join17(repoRoot, ".xera/impact");
|
|
10313
|
+
mkdirSync11(impactDir, { recursive: true });
|
|
10314
|
+
writeFileSync11(join17(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
|
|
10315
|
+
if (!quiet) {
|
|
10316
|
+
writeFileSync11(join17(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
|
|
10317
|
+
}
|
|
10318
|
+
return 0;
|
|
10319
|
+
}
|
|
10320
|
+
|
|
9917
10321
|
// src/bin-internal/lint.ts
|
|
9918
10322
|
import { lintTicket } from "@xera-ai/web";
|
|
9919
10323
|
async function lintCmd(argv) {
|
|
@@ -9934,8 +10338,8 @@ async function lintCmd(argv) {
|
|
|
9934
10338
|
}
|
|
9935
10339
|
|
|
9936
10340
|
// src/bin-internal/normalize.ts
|
|
9937
|
-
import { existsSync as
|
|
9938
|
-
import { join as
|
|
10341
|
+
import { existsSync as existsSync21, readdirSync as readdirSync7 } from "fs";
|
|
10342
|
+
import { join as join18 } from "path";
|
|
9939
10343
|
import { normalizeRun } from "@xera-ai/web";
|
|
9940
10344
|
async function normalizeCmd(argv) {
|
|
9941
10345
|
const ticket = argv[0];
|
|
@@ -9950,8 +10354,8 @@ async function normalizeCmd(argv) {
|
|
|
9950
10354
|
console.error("[xera:normalize] no run found");
|
|
9951
10355
|
return 1;
|
|
9952
10356
|
}
|
|
9953
|
-
const runDir =
|
|
9954
|
-
if (!
|
|
10357
|
+
const runDir = join18(paths.runsDir, runId);
|
|
10358
|
+
if (!existsSync21(runDir)) {
|
|
9955
10359
|
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
9956
10360
|
return 1;
|
|
9957
10361
|
}
|
|
@@ -9961,45 +10365,52 @@ async function normalizeCmd(argv) {
|
|
|
9961
10365
|
}
|
|
9962
10366
|
|
|
9963
10367
|
// src/bin-internal/post.ts
|
|
9964
|
-
import { existsSync as
|
|
9965
|
-
import { join as
|
|
10368
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
|
|
10369
|
+
import { join as join19 } from "path";
|
|
9966
10370
|
|
|
9967
10371
|
// src/artifact/status.ts
|
|
9968
|
-
import { existsSync as
|
|
10372
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
|
|
9969
10373
|
import { dirname as dirname6 } from "path";
|
|
9970
|
-
import { z as
|
|
9971
|
-
var ClassificationEnum =
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
10374
|
+
import { z as z7 } from "zod";
|
|
10375
|
+
var ClassificationEnum = z7.enum([
|
|
10376
|
+
"PASS",
|
|
10377
|
+
"REAL_BUG",
|
|
10378
|
+
"SELECTOR_DRIFT",
|
|
10379
|
+
"FLAKY",
|
|
10380
|
+
"TEST_BUG",
|
|
10381
|
+
"TEST_OUTDATED"
|
|
10382
|
+
]);
|
|
10383
|
+
var ResultEnum = z7.enum(["PASS", "FAIL"]);
|
|
10384
|
+
var ConfidenceEnum = z7.enum(["low", "medium", "high"]);
|
|
10385
|
+
var HistoryEntrySchema = z7.object({
|
|
10386
|
+
ts: z7.string(),
|
|
9976
10387
|
result: ResultEnum,
|
|
9977
10388
|
class: ClassificationEnum
|
|
9978
10389
|
});
|
|
9979
|
-
var StatusJsonSchema =
|
|
9980
|
-
ticket:
|
|
9981
|
-
lastRun:
|
|
10390
|
+
var StatusJsonSchema = z7.object({
|
|
10391
|
+
ticket: z7.string(),
|
|
10392
|
+
lastRun: z7.string(),
|
|
9982
10393
|
result: ResultEnum,
|
|
9983
10394
|
classification: ClassificationEnum,
|
|
9984
10395
|
confidence: ConfidenceEnum,
|
|
9985
|
-
scenarios:
|
|
9986
|
-
total:
|
|
9987
|
-
passed:
|
|
9988
|
-
failed:
|
|
9989
|
-
skipped:
|
|
10396
|
+
scenarios: z7.object({
|
|
10397
|
+
total: z7.number().int().nonnegative(),
|
|
10398
|
+
passed: z7.number().int().nonnegative(),
|
|
10399
|
+
failed: z7.number().int().nonnegative(),
|
|
10400
|
+
skipped: z7.number().int().nonnegative()
|
|
9990
10401
|
}),
|
|
9991
|
-
history:
|
|
9992
|
-
last_jira_comment_id:
|
|
10402
|
+
history: z7.array(HistoryEntrySchema).default([]),
|
|
10403
|
+
last_jira_comment_id: z7.string().optional()
|
|
9993
10404
|
});
|
|
9994
10405
|
var HISTORY_CAP = 20;
|
|
9995
10406
|
function readStatus(path) {
|
|
9996
|
-
if (!
|
|
10407
|
+
if (!existsSync22(path))
|
|
9997
10408
|
return null;
|
|
9998
|
-
return StatusJsonSchema.parse(JSON.parse(
|
|
10409
|
+
return StatusJsonSchema.parse(JSON.parse(readFileSync18(path, "utf8")));
|
|
9999
10410
|
}
|
|
10000
10411
|
function writeStatus(path, status) {
|
|
10001
|
-
|
|
10002
|
-
|
|
10412
|
+
mkdirSync12(dirname6(path), { recursive: true });
|
|
10413
|
+
writeFileSync12(path, JSON.stringify(status, null, 2));
|
|
10003
10414
|
}
|
|
10004
10415
|
function appendHistory(path, entry) {
|
|
10005
10416
|
const s = readStatus(path);
|
|
@@ -10025,12 +10436,12 @@ async function postCmd(argv) {
|
|
|
10025
10436
|
return 0;
|
|
10026
10437
|
}
|
|
10027
10438
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
10028
|
-
const draftPath =
|
|
10029
|
-
if (!
|
|
10439
|
+
const draftPath = join19(paths.ticketDir, "jira-comment.draft.md");
|
|
10440
|
+
if (!existsSync23(draftPath)) {
|
|
10030
10441
|
console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
|
|
10031
10442
|
return 1;
|
|
10032
10443
|
}
|
|
10033
|
-
const body =
|
|
10444
|
+
const body = readFileSync19(draftPath, "utf8");
|
|
10034
10445
|
const client = await createJiraClient({
|
|
10035
10446
|
baseUrl: config.jira.baseUrl,
|
|
10036
10447
|
preferMcp: true,
|
|
@@ -10058,12 +10469,13 @@ async function promoteCmd(argv) {
|
|
|
10058
10469
|
}
|
|
10059
10470
|
|
|
10060
10471
|
// src/bin-internal/report.ts
|
|
10061
|
-
import { readFileSync as
|
|
10062
|
-
import { join as
|
|
10472
|
+
import { existsSync as existsSync25, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "fs";
|
|
10473
|
+
import { join as join20 } from "path";
|
|
10063
10474
|
|
|
10064
10475
|
// src/classifier/aggregate.ts
|
|
10065
10476
|
var CLASS_PRIORITY = [
|
|
10066
10477
|
"REAL_BUG",
|
|
10478
|
+
"TEST_OUTDATED",
|
|
10067
10479
|
"TEST_BUG",
|
|
10068
10480
|
"SELECTOR_DRIFT",
|
|
10069
10481
|
"FLAKY",
|
|
@@ -10089,6 +10501,80 @@ function aggregateScenarios(scenarios) {
|
|
|
10089
10501
|
return { overall: chosen, overallConfidence: minConf, scenarios };
|
|
10090
10502
|
}
|
|
10091
10503
|
|
|
10504
|
+
// src/graph/classify.ts
|
|
10505
|
+
var DEFAULT_THRESHOLD = 0.7;
|
|
10506
|
+
var SHORT_CIRCUIT = ["FLAKY", "PASS"];
|
|
10507
|
+
function findCandidateTickets(graph, scenario) {
|
|
10508
|
+
const poms = graph.edges.filter((e) => e.kind === "uses" && e.from === scenario.id).map((e) => e.to);
|
|
10509
|
+
if (poms.length === 0)
|
|
10510
|
+
return [];
|
|
10511
|
+
const areas = graph.edges.filter((e) => e.kind === "covers" && poms.includes(e.from)).map((e) => e.to);
|
|
10512
|
+
if (areas.length === 0)
|
|
10513
|
+
return [];
|
|
10514
|
+
const ticketIds = graph.edges.filter((e) => e.kind === "modifies" && areas.includes(e.to)).map((e) => e.from);
|
|
10515
|
+
const seen = new Set;
|
|
10516
|
+
const out = [];
|
|
10517
|
+
for (const id of ticketIds) {
|
|
10518
|
+
if (seen.has(id))
|
|
10519
|
+
continue;
|
|
10520
|
+
seen.add(id);
|
|
10521
|
+
if (id === scenario.ticketId)
|
|
10522
|
+
continue;
|
|
10523
|
+
const t = graph.tickets[id];
|
|
10524
|
+
if (!t)
|
|
10525
|
+
continue;
|
|
10526
|
+
if (t.fetchedAt <= scenario.generatedAt)
|
|
10527
|
+
continue;
|
|
10528
|
+
out.push(t);
|
|
10529
|
+
}
|
|
10530
|
+
return out;
|
|
10531
|
+
}
|
|
10532
|
+
async function enhanceClassification(input, graph, decideOutdated, options = {}) {
|
|
10533
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
10534
|
+
if (SHORT_CIRCUIT.includes(input.traceClassification)) {
|
|
10535
|
+
return { classification: input.traceClassification, confidence: 1 };
|
|
10536
|
+
}
|
|
10537
|
+
const scenario = graph.scenarios[input.scenarioId];
|
|
10538
|
+
if (!scenario)
|
|
10539
|
+
return { classification: input.traceClassification, confidence: 1 };
|
|
10540
|
+
const candidates = findCandidateTickets(graph, scenario);
|
|
10541
|
+
if (candidates.length === 0) {
|
|
10542
|
+
return { classification: input.traceClassification, confidence: 1 };
|
|
10543
|
+
}
|
|
10544
|
+
const candidateEvidence = candidates.map((t) => {
|
|
10545
|
+
const area = graph.edges.find((e) => e.kind === "modifies" && e.from === t.id)?.to ?? "";
|
|
10546
|
+
const ev = { ticketId: t.id, summary: t.summary, modifiedArea: area };
|
|
10547
|
+
if (t.ac[0])
|
|
10548
|
+
ev.relevantAcRef = t.ac[0];
|
|
10549
|
+
return ev;
|
|
10550
|
+
});
|
|
10551
|
+
const decision = await decideOutdated({ scenario, candidates });
|
|
10552
|
+
if (decision.classification === "TEST_OUTDATED" && decision.confidence >= threshold) {
|
|
10553
|
+
const evidence = {
|
|
10554
|
+
candidateTickets: candidateEvidence,
|
|
10555
|
+
reasoning: decision.evidence.reasoning,
|
|
10556
|
+
proposedAction: "regenerate-scenario"
|
|
10557
|
+
};
|
|
10558
|
+
if (decision.evidence.expectedByTest)
|
|
10559
|
+
evidence.expectedByTest = decision.evidence.expectedByTest;
|
|
10560
|
+
if (decision.evidence.actualInApp)
|
|
10561
|
+
evidence.actualInApp = decision.evidence.actualInApp;
|
|
10562
|
+
return {
|
|
10563
|
+
classification: "TEST_OUTDATED",
|
|
10564
|
+
confidence: decision.confidence,
|
|
10565
|
+
evidence
|
|
10566
|
+
};
|
|
10567
|
+
}
|
|
10568
|
+
return {
|
|
10569
|
+
classification: input.traceClassification,
|
|
10570
|
+
confidence: 1,
|
|
10571
|
+
evidence: { candidateTickets: candidateEvidence }
|
|
10572
|
+
};
|
|
10573
|
+
}
|
|
10574
|
+
|
|
10575
|
+
// src/bin-internal/report.ts
|
|
10576
|
+
init_store();
|
|
10577
|
+
|
|
10092
10578
|
// src/reporter/jira-comment.ts
|
|
10093
10579
|
function buildJiraComment(input) {
|
|
10094
10580
|
const passed = input.scenarios.filter((s) => s.outcome === "PASS").length;
|
|
@@ -10119,11 +10605,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
|
|
|
10119
10605
|
}
|
|
10120
10606
|
|
|
10121
10607
|
// src/reporter/status-writer.ts
|
|
10122
|
-
import { existsSync as
|
|
10608
|
+
import { existsSync as existsSync24 } from "fs";
|
|
10123
10609
|
function writeStatusFromClassification(path, input) {
|
|
10124
10610
|
const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
|
|
10125
10611
|
const entry = { ts: input.runTs, result, class: input.classification.overall };
|
|
10126
|
-
if (!
|
|
10612
|
+
if (!existsSync24(path)) {
|
|
10127
10613
|
writeStatus(path, {
|
|
10128
10614
|
ticket: input.ticket,
|
|
10129
10615
|
lastRun: input.runTs,
|
|
@@ -10156,26 +10642,59 @@ async function reportCmd(argv) {
|
|
|
10156
10642
|
return 1;
|
|
10157
10643
|
}
|
|
10158
10644
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
10159
|
-
const input = JSON.parse(
|
|
10645
|
+
const input = JSON.parse(readFileSync20(inputArg.slice("--input=".length), "utf8"));
|
|
10160
10646
|
const aggregated = aggregateScenarios(input.scenarios);
|
|
10647
|
+
const decisionsPath = join20(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
|
|
10648
|
+
const decisions = existsSync25(decisionsPath) ? JSON.parse(readFileSync20(decisionsPath, "utf8")) : {};
|
|
10649
|
+
const graph = deriveSnapshot(loadAllEvents(process.cwd()));
|
|
10650
|
+
const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
10651
|
+
const scenarioIdByName = {};
|
|
10652
|
+
for (const [id, node] of Object.entries(graph.scenarios)) {
|
|
10653
|
+
if (node.ticketId === ticket) {
|
|
10654
|
+
scenarioIdByName[normalizeScenarioName(node.name)] = id;
|
|
10655
|
+
}
|
|
10656
|
+
}
|
|
10657
|
+
const enhancedScenarios = await Promise.all(aggregated.scenarios.map(async (s) => {
|
|
10658
|
+
if (s.outcome !== "FAIL")
|
|
10659
|
+
return s;
|
|
10660
|
+
const scenarioId2 = scenarioIdByName[normalizeScenarioName(s.name)];
|
|
10661
|
+
if (!scenarioId2)
|
|
10662
|
+
return s;
|
|
10663
|
+
const decision = decisions[scenarioId2];
|
|
10664
|
+
const decideOutdated = async () => decision ?? {
|
|
10665
|
+
classification: "BUG",
|
|
10666
|
+
confidence: 0,
|
|
10667
|
+
evidence: { reasoning: "no LLM decision" }
|
|
10668
|
+
};
|
|
10669
|
+
const enhanced = await enhanceClassification({ scenarioId: scenarioId2, traceClassification: s.class }, graph, decideOutdated);
|
|
10670
|
+
if (enhanced.classification !== s.class) {
|
|
10671
|
+
return {
|
|
10672
|
+
...s,
|
|
10673
|
+
class: enhanced.classification,
|
|
10674
|
+
rationale: `${s.rationale} | TEST_OUTDATED override (conf ${enhanced.confidence})`
|
|
10675
|
+
};
|
|
10676
|
+
}
|
|
10677
|
+
return s;
|
|
10678
|
+
}));
|
|
10679
|
+
const reAggregated = aggregateScenarios(enhancedScenarios);
|
|
10161
10680
|
const ts = new Date().toISOString();
|
|
10162
10681
|
writeStatusFromClassification(paths.statusPath, {
|
|
10163
10682
|
ticket,
|
|
10164
10683
|
runTs: ts,
|
|
10165
|
-
classification:
|
|
10684
|
+
classification: reAggregated,
|
|
10166
10685
|
scenarioCounts: input.scenarioCounts
|
|
10167
10686
|
});
|
|
10168
10687
|
const md = buildJiraComment({
|
|
10169
10688
|
ticket,
|
|
10170
10689
|
runId: input.runId,
|
|
10171
|
-
overall:
|
|
10172
|
-
overallConfidence:
|
|
10173
|
-
scenarios:
|
|
10690
|
+
overall: reAggregated.overall,
|
|
10691
|
+
overallConfidence: reAggregated.overallConfidence,
|
|
10692
|
+
scenarios: reAggregated.scenarios,
|
|
10174
10693
|
xeraVersion: "0.1.0",
|
|
10175
10694
|
promptsVersion: "1.0.0"
|
|
10176
10695
|
});
|
|
10177
|
-
const draftPath =
|
|
10178
|
-
|
|
10696
|
+
const draftPath = join20(paths.ticketDir, "jira-comment.draft.md");
|
|
10697
|
+
writeFileSync13(draftPath, md);
|
|
10179
10698
|
console.log(`[xera:report] wrote status.json and ${draftPath}`);
|
|
10180
10699
|
return 0;
|
|
10181
10700
|
}
|
|
@@ -10242,7 +10761,7 @@ async function unlockCmd(argv) {
|
|
|
10242
10761
|
}
|
|
10243
10762
|
|
|
10244
10763
|
// src/bin-internal/validate-feature.ts
|
|
10245
|
-
import { existsSync as
|
|
10764
|
+
import { existsSync as existsSync26, readFileSync as readFileSync21 } from "fs";
|
|
10246
10765
|
import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
|
|
10247
10766
|
async function validateFeatureCmd(argv) {
|
|
10248
10767
|
const ticket = argv[0];
|
|
@@ -10251,11 +10770,11 @@ async function validateFeatureCmd(argv) {
|
|
|
10251
10770
|
return 1;
|
|
10252
10771
|
}
|
|
10253
10772
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
10254
|
-
if (!
|
|
10773
|
+
if (!existsSync26(paths.featurePath)) {
|
|
10255
10774
|
console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
|
|
10256
10775
|
return 1;
|
|
10257
10776
|
}
|
|
10258
|
-
const r = validateGherkin2(
|
|
10777
|
+
const r = validateGherkin2(readFileSync21(paths.featurePath, "utf8"));
|
|
10259
10778
|
if (r.ok) {
|
|
10260
10779
|
console.log("[xera:validate-feature] ok");
|
|
10261
10780
|
return 0;
|
|
@@ -10274,10 +10793,12 @@ var COMMANDS = {
|
|
|
10274
10793
|
exec: execCmd,
|
|
10275
10794
|
fetch: fetchCmd,
|
|
10276
10795
|
"graph-backfill": graphBackfillCmd,
|
|
10796
|
+
"graph-enrich": graphEnrichCmd,
|
|
10277
10797
|
"graph-query": graphQueryCmd,
|
|
10278
10798
|
"graph-record": graphRecordCmd,
|
|
10279
10799
|
"graph-snapshot": graphSnapshotCmd,
|
|
10280
10800
|
"heal-prepare": healPrepareCmd,
|
|
10801
|
+
"impact-prepare": impactPrepareCmd,
|
|
10281
10802
|
lint: lintCmd,
|
|
10282
10803
|
normalize: normalizeCmd,
|
|
10283
10804
|
post: postCmd,
|