@xera-ai/core 0.13.1 → 0.14.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 +31 -16
- package/dist/src/index.js +1 -0
- package/package.json +3 -3
- package/src/artifact/status.ts +1 -0
- package/src/bin-internal/ac-coverage-backfill-prepare.ts +15 -5
- package/src/bin-internal/graph-record.ts +2 -1
- package/src/bin-internal/normalize.ts +19 -7
- package/src/coverage/report.ts +15 -6
- package/src/graph/classify.ts +1 -1
- package/src/graph/schema.ts +1 -0
- package/src/graph/store.ts +7 -2
- package/src/graph/types.ts +1 -0
package/dist/bin/internal.js
CHANGED
|
@@ -490,6 +490,7 @@ var init_schema = __esm(() => {
|
|
|
490
490
|
"SELECTOR_DRIFT",
|
|
491
491
|
"FLAKY",
|
|
492
492
|
"PASS",
|
|
493
|
+
"SKIPPED",
|
|
493
494
|
"TEST_OUTDATED",
|
|
494
495
|
"CONTRACT_DRIFT",
|
|
495
496
|
"RATE_LIMITED",
|
|
@@ -828,9 +829,10 @@ function deriveSnapshot(events) {
|
|
|
828
829
|
}
|
|
829
830
|
case "ac-coverage.backfilled": {
|
|
830
831
|
const { ts, ticketId, mappings } = e.payload;
|
|
832
|
+
const touchedScenarios = new Set(mappings.map((m) => m.scenarioId));
|
|
831
833
|
for (let i = edges.length - 1;i >= 0; i--) {
|
|
832
834
|
const ed = edges[i];
|
|
833
|
-
if (ed.kind === "satisfies" && ed.source === "ac-coverage" && ed.to.startsWith(`${ticketId}#ac-`)) {
|
|
835
|
+
if (ed.kind === "satisfies" && ed.source === "ac-coverage" && ed.to.startsWith(`${ticketId}#ac-`) && touchedScenarios.has(ed.from)) {
|
|
834
836
|
edges.splice(i, 1);
|
|
835
837
|
}
|
|
836
838
|
}
|
|
@@ -8175,6 +8177,7 @@ var init_paths2 = __esm(() => {
|
|
|
8175
8177
|
var exports_graph_record = {};
|
|
8176
8178
|
__export(exports_graph_record, {
|
|
8177
8179
|
recordFetch: () => recordFetch,
|
|
8180
|
+
recordExec: () => recordExec,
|
|
8178
8181
|
graphRecordCmd: () => graphRecordCmd
|
|
8179
8182
|
});
|
|
8180
8183
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -8476,6 +8479,7 @@ async function graphRecordCmd(argv) {
|
|
|
8476
8479
|
"SELECTOR_DRIFT",
|
|
8477
8480
|
"FLAKY",
|
|
8478
8481
|
"PASS",
|
|
8482
|
+
"SKIPPED",
|
|
8479
8483
|
"TEST_OUTDATED",
|
|
8480
8484
|
"CONTRACT_DRIFT",
|
|
8481
8485
|
"RATE_LIMITED",
|
|
@@ -8657,15 +8661,15 @@ function findUnmapped(snap) {
|
|
|
8657
8661
|
const ticketScenarios = Object.values(snap.scenarios).filter((s) => s.ticketId === ticket.id);
|
|
8658
8662
|
if (ticketScenarios.length === 0)
|
|
8659
8663
|
continue;
|
|
8660
|
-
const
|
|
8661
|
-
const
|
|
8662
|
-
if (
|
|
8664
|
+
const acIdsForTicket = new Set(Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticket.id).map((ac) => ac.id));
|
|
8665
|
+
const unmappedScenarios = ticketScenarios.filter((s) => !snap.edges.some((e) => e.kind === "satisfies" && e.from === s.id && acIdsForTicket.has(e.to)));
|
|
8666
|
+
if (unmappedScenarios.length === 0)
|
|
8663
8667
|
continue;
|
|
8664
8668
|
out.push({
|
|
8665
8669
|
id: ticket.id,
|
|
8666
8670
|
summary: ticket.summary,
|
|
8667
8671
|
acs: ticket.ac,
|
|
8668
|
-
scenarios:
|
|
8672
|
+
scenarios: unmappedScenarios.map((s) => ({
|
|
8669
8673
|
id: s.id,
|
|
8670
8674
|
name: s.name,
|
|
8671
8675
|
gherkin: s.gherkin
|
|
@@ -9094,15 +9098,17 @@ function buildCoverageReport(snap, config, now) {
|
|
|
9094
9098
|
}
|
|
9095
9099
|
function needsBackfill(snap) {
|
|
9096
9100
|
for (const ticket of Object.values(snap.tickets)) {
|
|
9097
|
-
const
|
|
9098
|
-
if (
|
|
9101
|
+
const acIdsForTicket = new Set(Object.values(snap.acNodes).filter((ac) => ac.ticketId === ticket.id).map((ac) => ac.id));
|
|
9102
|
+
if (acIdsForTicket.size === 0)
|
|
9099
9103
|
continue;
|
|
9100
9104
|
const scenariosForTicket = Object.values(snap.scenarios).filter((s) => s.ticketId === ticket.id);
|
|
9101
9105
|
if (scenariosForTicket.length === 0)
|
|
9102
9106
|
continue;
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9107
|
+
for (const s of scenariosForTicket) {
|
|
9108
|
+
const mapped = snap.edges.some((e) => e.kind === "satisfies" && e.from === s.id && acIdsForTicket.has(e.to));
|
|
9109
|
+
if (!mapped)
|
|
9110
|
+
return true;
|
|
9111
|
+
}
|
|
9106
9112
|
}
|
|
9107
9113
|
return false;
|
|
9108
9114
|
}
|
|
@@ -12656,10 +12662,11 @@ async function lintCmd(argv) {
|
|
|
12656
12662
|
import { existsSync as existsSync27, readdirSync as readdirSync7 } from "fs";
|
|
12657
12663
|
import { join as join28 } from "path";
|
|
12658
12664
|
init_paths2();
|
|
12665
|
+
init_graph_record();
|
|
12659
12666
|
async function normalizeCmd(argv) {
|
|
12660
12667
|
const ticket = argv[0];
|
|
12661
12668
|
if (!ticket) {
|
|
12662
|
-
console.error("[xera:normalize] usage: normalize <TICKET> [--run=<runId>]");
|
|
12669
|
+
console.error("[xera:normalize] usage: normalize <TICKET> [--run=<runId>] [--no-graph-record]");
|
|
12663
12670
|
return 1;
|
|
12664
12671
|
}
|
|
12665
12672
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
@@ -12674,17 +12681,24 @@ async function normalizeCmd(argv) {
|
|
|
12674
12681
|
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
12675
12682
|
return 1;
|
|
12676
12683
|
}
|
|
12684
|
+
const skipGraphRecord = argv.includes("--no-graph-record");
|
|
12677
12685
|
const meta = readMeta(paths.metaPath);
|
|
12678
12686
|
const adapter = meta?.adapter ?? "web";
|
|
12679
12687
|
if (adapter === "http") {
|
|
12680
12688
|
const { normalizeHttpRun } = await import("@xera-ai/http");
|
|
12681
12689
|
await normalizeHttpRun({ runId, runDir });
|
|
12682
12690
|
console.log(`[xera:normalize] wrote normalized.json (http)`);
|
|
12683
|
-
|
|
12691
|
+
} else {
|
|
12692
|
+
const { normalizeRun } = await import("@xera-ai/web");
|
|
12693
|
+
const r = await normalizeRun({ runId, runDir });
|
|
12694
|
+
console.log(`[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`);
|
|
12695
|
+
}
|
|
12696
|
+
if (!skipGraphRecord) {
|
|
12697
|
+
const code = await recordExec(process.cwd(), ticket, runId);
|
|
12698
|
+
if (code !== 0) {
|
|
12699
|
+
console.warn(`[xera:normalize] graph-record exec exited ${code} \u2014 continuing (non-fatal)`);
|
|
12700
|
+
}
|
|
12684
12701
|
}
|
|
12685
|
-
const { normalizeRun } = await import("@xera-ai/web");
|
|
12686
|
-
const r = await normalizeRun({ runId, runDir });
|
|
12687
|
-
console.log(`[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`);
|
|
12688
12702
|
return 0;
|
|
12689
12703
|
}
|
|
12690
12704
|
|
|
@@ -12699,6 +12713,7 @@ import { dirname as dirname8 } from "path";
|
|
|
12699
12713
|
import { z as z10 } from "zod";
|
|
12700
12714
|
var ClassificationEnum = z10.enum([
|
|
12701
12715
|
"PASS",
|
|
12716
|
+
"SKIPPED",
|
|
12702
12717
|
"REAL_BUG",
|
|
12703
12718
|
"SELECTOR_DRIFT",
|
|
12704
12719
|
"FLAKY",
|
|
@@ -12963,7 +12978,7 @@ function classifyRateLimited(input) {
|
|
|
12963
12978
|
|
|
12964
12979
|
// src/graph/classify.ts
|
|
12965
12980
|
var DEFAULT_THRESHOLD = 0.7;
|
|
12966
|
-
var SHORT_CIRCUIT = ["FLAKY", "PASS"];
|
|
12981
|
+
var SHORT_CIRCUIT = ["FLAKY", "PASS", "SKIPPED"];
|
|
12967
12982
|
function findCandidateTickets(graph, scenario) {
|
|
12968
12983
|
const poms = graph.edges.filter((e) => e.kind === "uses" && e.from === scenario.id).map((e) => e.to);
|
|
12969
12984
|
if (poms.length === 0)
|
package/dist/src/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xera-ai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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.14.0",
|
|
35
|
+
"@xera-ai/http": "^0.14.0",
|
|
36
36
|
"@playwright/test": "1.60.0",
|
|
37
37
|
"dotenv": "^16.0.0",
|
|
38
38
|
"fflate": "0.8.3",
|
package/src/artifact/status.ts
CHANGED
|
@@ -18,16 +18,26 @@ function findUnmapped(snap: Snapshot): BackfillInput {
|
|
|
18
18
|
if (ticket.ac.length === 0) continue;
|
|
19
19
|
const ticketScenarios = Object.values(snap.scenarios).filter((s) => s.ticketId === ticket.id);
|
|
20
20
|
if (ticketScenarios.length === 0) continue;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const acIdsForTicket = new Set(
|
|
22
|
+
Object.values(snap.acNodes)
|
|
23
|
+
.filter((ac) => ac.ticketId === ticket.id)
|
|
24
|
+
.map((ac) => ac.id),
|
|
24
25
|
);
|
|
25
|
-
if
|
|
26
|
+
// Per-scenario: a scenario is "unmapped" if it has no `satisfies` edge to
|
|
27
|
+
// any of this ticket's ACs. Surface only those — finalize is now additive
|
|
28
|
+
// (#119) so partial mappings won't clobber prior edges.
|
|
29
|
+
const unmappedScenarios = ticketScenarios.filter(
|
|
30
|
+
(s) =>
|
|
31
|
+
!snap.edges.some(
|
|
32
|
+
(e) => e.kind === 'satisfies' && e.from === s.id && acIdsForTicket.has(e.to),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
if (unmappedScenarios.length === 0) continue;
|
|
26
36
|
out.push({
|
|
27
37
|
id: ticket.id,
|
|
28
38
|
summary: ticket.summary,
|
|
29
39
|
acs: ticket.ac,
|
|
30
|
-
scenarios:
|
|
40
|
+
scenarios: unmappedScenarios.map((s) => ({
|
|
31
41
|
id: s.id,
|
|
32
42
|
name: s.name,
|
|
33
43
|
gherkin: s.gherkin,
|
|
@@ -190,7 +190,7 @@ async function recordScript(repoRoot: string, ticket: string): Promise<number> {
|
|
|
190
190
|
return recordScriptImpl(repoRoot, ticket);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
async function recordExec(repoRoot: string, ticket: string, runId: string): Promise<number> {
|
|
193
|
+
export async function recordExec(repoRoot: string, ticket: string, runId: string): Promise<number> {
|
|
194
194
|
const { normalizedPath } = resolveArtifactPaths(repoRoot, ticket).runPath(runId);
|
|
195
195
|
if (!existsSync(normalizedPath)) {
|
|
196
196
|
console.error(`[graph-record exec] normalized.json missing`);
|
|
@@ -362,6 +362,7 @@ export async function graphRecordCmd(argv: string[]): Promise<number> {
|
|
|
362
362
|
'SELECTOR_DRIFT',
|
|
363
363
|
'FLAKY',
|
|
364
364
|
'PASS',
|
|
365
|
+
'SKIPPED',
|
|
365
366
|
'TEST_OUTDATED',
|
|
366
367
|
'CONTRACT_DRIFT',
|
|
367
368
|
'RATE_LIMITED',
|
|
@@ -2,11 +2,12 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readMeta } from '../artifact/meta';
|
|
4
4
|
import { resolveArtifactPaths } from '../artifact/paths';
|
|
5
|
+
import { recordExec } from './graph-record';
|
|
5
6
|
|
|
6
7
|
export async function normalizeCmd(argv: string[]): Promise<number> {
|
|
7
8
|
const ticket = argv[0];
|
|
8
9
|
if (!ticket) {
|
|
9
|
-
console.error('[xera:normalize] usage: normalize <TICKET> [--run=<runId>]');
|
|
10
|
+
console.error('[xera:normalize] usage: normalize <TICKET> [--run=<runId>] [--no-graph-record]');
|
|
10
11
|
return 1;
|
|
11
12
|
}
|
|
12
13
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
@@ -26,6 +27,7 @@ export async function normalizeCmd(argv: string[]): Promise<number> {
|
|
|
26
27
|
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
27
28
|
return 1;
|
|
28
29
|
}
|
|
30
|
+
const skipGraphRecord = argv.includes('--no-graph-record');
|
|
29
31
|
|
|
30
32
|
const meta = readMeta(paths.metaPath);
|
|
31
33
|
const adapter = meta?.adapter ?? 'web';
|
|
@@ -34,13 +36,23 @@ export async function normalizeCmd(argv: string[]): Promise<number> {
|
|
|
34
36
|
const { normalizeHttpRun } = await import('@xera-ai/http');
|
|
35
37
|
await normalizeHttpRun({ runId, runDir });
|
|
36
38
|
console.log(`[xera:normalize] wrote normalized.json (http)`);
|
|
37
|
-
|
|
39
|
+
} else {
|
|
40
|
+
const { normalizeRun } = await import('@xera-ai/web');
|
|
41
|
+
const r = await normalizeRun({ runId, runDir });
|
|
42
|
+
console.log(
|
|
43
|
+
`[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`,
|
|
44
|
+
);
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
47
|
+
// Emit run.completed events so the graph's latest_failures (and any
|
|
48
|
+
// recency-based risk scoring) reflect this run. Without this the
|
|
49
|
+
// /xera-run pipeline silently renders failed scenarios as green in
|
|
50
|
+
// graph.html — see #118.
|
|
51
|
+
if (!skipGraphRecord) {
|
|
52
|
+
const code = await recordExec(process.cwd(), ticket, runId);
|
|
53
|
+
if (code !== 0) {
|
|
54
|
+
console.warn(`[xera:normalize] graph-record exec exited ${code} — continuing (non-fatal)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
45
57
|
return 0;
|
|
46
58
|
}
|
package/src/coverage/report.ts
CHANGED
|
@@ -114,17 +114,26 @@ export function buildCoverageReport(
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function needsBackfill(snap: Snapshot): boolean {
|
|
117
|
+
// True when at least one scenario lacks a `satisfies` edge to its ticket's
|
|
118
|
+
// ACs. Mirrors `findUnmapped` in ac-coverage-backfill-prepare so the flag
|
|
119
|
+
// and the prepare output stay consistent for partially mapped tickets (#119).
|
|
117
120
|
for (const ticket of Object.values(snap.tickets)) {
|
|
118
|
-
const
|
|
119
|
-
|
|
121
|
+
const acIdsForTicket = new Set(
|
|
122
|
+
Object.values(snap.acNodes)
|
|
123
|
+
.filter((ac) => ac.ticketId === ticket.id)
|
|
124
|
+
.map((ac) => ac.id),
|
|
125
|
+
);
|
|
126
|
+
if (acIdsForTicket.size === 0) continue;
|
|
120
127
|
const scenariosForTicket = Object.values(snap.scenarios).filter(
|
|
121
128
|
(s) => s.ticketId === ticket.id,
|
|
122
129
|
);
|
|
123
130
|
if (scenariosForTicket.length === 0) continue;
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
for (const s of scenariosForTicket) {
|
|
132
|
+
const mapped = snap.edges.some(
|
|
133
|
+
(e) => e.kind === 'satisfies' && e.from === s.id && acIdsForTicket.has(e.to),
|
|
134
|
+
);
|
|
135
|
+
if (!mapped) return true;
|
|
136
|
+
}
|
|
128
137
|
}
|
|
129
138
|
return false;
|
|
130
139
|
}
|
package/src/graph/classify.ts
CHANGED
|
@@ -43,7 +43,7 @@ export type DecideOutdated = (args: {
|
|
|
43
43
|
}) => Promise<OutdatedDecision>;
|
|
44
44
|
|
|
45
45
|
const DEFAULT_THRESHOLD = 0.7;
|
|
46
|
-
const SHORT_CIRCUIT: Classification[] = ['FLAKY', 'PASS'];
|
|
46
|
+
const SHORT_CIRCUIT: Classification[] = ['FLAKY', 'PASS', 'SKIPPED'];
|
|
47
47
|
|
|
48
48
|
export function findCandidateTickets(graph: Snapshot, scenario: ScenarioNode): TicketNode[] {
|
|
49
49
|
const poms = graph.edges
|
package/src/graph/schema.ts
CHANGED
package/src/graph/store.ts
CHANGED
|
@@ -277,13 +277,18 @@ export function deriveSnapshot(events: Event[]): Snapshot {
|
|
|
277
277
|
}
|
|
278
278
|
case 'ac-coverage.backfilled': {
|
|
279
279
|
const { ts, ticketId, mappings } = e.payload;
|
|
280
|
-
//
|
|
280
|
+
// Upsert per scenarioId: drop only the prior backfill edges from the
|
|
281
|
+
// scenarios this event maps, then add the new edges. Scenarios not
|
|
282
|
+
// mentioned in the payload keep their prior edges so partial backfills
|
|
283
|
+
// are additive (matches every other handler — see #119).
|
|
284
|
+
const touchedScenarios = new Set(mappings.map((m) => m.scenarioId));
|
|
281
285
|
for (let i = edges.length - 1; i >= 0; i--) {
|
|
282
286
|
const ed = edges[i]!;
|
|
283
287
|
if (
|
|
284
288
|
ed.kind === 'satisfies' &&
|
|
285
289
|
ed.source === 'ac-coverage' &&
|
|
286
|
-
ed.to.startsWith(`${ticketId}#ac-`)
|
|
290
|
+
ed.to.startsWith(`${ticketId}#ac-`) &&
|
|
291
|
+
touchedScenarios.has(ed.from)
|
|
287
292
|
) {
|
|
288
293
|
edges.splice(i, 1);
|
|
289
294
|
}
|