@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-render.d.ts","sourceRoot":"","sources":["../../src/bin-internal/graph-render.ts"],"names":[],"mappings":"AAkBA,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmDpE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impact-prepare.d.ts","sourceRoot":"","sources":["../../src/bin-internal/impact-prepare.ts"],"names":[],"mappings":"AAkBA,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CtE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin-internal/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin-internal/index.ts"],"names":[],"mappings":"AAoDA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAczD"}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ export declare const XeraConfigSchema: z.ZodObject<{
|
|
|
60
60
|
local: "local";
|
|
61
61
|
}>>;
|
|
62
62
|
}, z.core.$strip>>;
|
|
63
|
+
run: z.ZodPrefault<z.ZodPrefault<z.ZodObject<{
|
|
64
|
+
autoImpact: z.ZodPrefault<z.ZodObject<{
|
|
65
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
66
|
+
threshold: z.ZodDefault<z.ZodNumber>;
|
|
67
|
+
}, z.core.$strip>>;
|
|
68
|
+
}, z.core.$strip>>>;
|
|
63
69
|
adapters: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
64
70
|
}, z.core.$strip>;
|
|
65
71
|
export type XeraConfig = z.infer<typeof XeraConfigSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkFxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EdgeKind, Priority, Snapshot, TicketNode } from './types';
|
|
2
|
+
export interface ImpactEdge {
|
|
3
|
+
kind: EdgeKind;
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
confidence?: number;
|
|
7
|
+
source?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ImpactScenario {
|
|
10
|
+
scenarioId: string;
|
|
11
|
+
ticketId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
priority: Priority;
|
|
14
|
+
edgePath: ImpactEdge[];
|
|
15
|
+
riskScore: number;
|
|
16
|
+
lastPassedAt?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ImpactOpts {
|
|
19
|
+
depth: 1 | 2 | 3;
|
|
20
|
+
minPriority?: Priority;
|
|
21
|
+
}
|
|
22
|
+
export interface ImpactReport {
|
|
23
|
+
targetTicket: string;
|
|
24
|
+
modifiedAreas: string[];
|
|
25
|
+
scenarios: ImpactScenario[];
|
|
26
|
+
generatedAt: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function riskScore(scenario: ImpactScenario, daysSinceLastPass: number): number;
|
|
29
|
+
export declare function walkImpact(graph: Snapshot, target: TicketNode, opts: ImpactOpts): ImpactScenario[];
|
|
30
|
+
export declare function renderImpactMarkdown(report: ImpactReport): string;
|
|
31
|
+
//# sourceMappingURL=impact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impact.d.ts","sourceRoot":"","sources":["../../src/graph/impact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAExE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjB,WAAW,CAAC,EAAE,QAAQ,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB;AA4BD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAOrF;AAUD,wBAAgB,UAAU,CACxB,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,GACf,cAAc,EAAE,CAkHlB;AAeD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAqDjE"}
|
package/dist/graph/index.d.ts
CHANGED
|
@@ -4,7 +4,11 @@ export type { CostSummary, LlmCallLog } from './cost';
|
|
|
4
4
|
export { logLlmCall, summarizeCost } from './cost';
|
|
5
5
|
export type { EnrichOptions, EnrichResult } from './enrich';
|
|
6
6
|
export { enrichTicket } from './enrich';
|
|
7
|
+
export type { ImpactEdge, ImpactOpts, ImpactReport, ImpactScenario, } from './impact';
|
|
8
|
+
export { renderImpactMarkdown, riskScore, walkImpact, } from './impact';
|
|
7
9
|
export { currentYyyyMm, graphPaths } from './paths';
|
|
10
|
+
export type { GraphStats, RenderHtmlInput, RenderOpts, VisEdge, VisNode } from './render';
|
|
11
|
+
export { renderHtml, transformForVisNetwork } from './render';
|
|
8
12
|
export { EventSchema, safeParseEvent } from './schema';
|
|
9
13
|
export { buildSimilarityPrompt } from './similarity';
|
|
10
14
|
export { appendEvents, computeEventsHash, deriveSnapshot, isSnapshotStale, loadAllEvents, loadSnapshot, writeSnapshot, } from './store';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/graph/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/graph/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,oBAAoB,EACpB,SAAS,EACT,UAAU,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Snapshot } from './types';
|
|
2
|
+
export interface VisNode {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
group: 'Ticket' | 'Scenario' | 'POM' | 'SUTArea' | 'Failure';
|
|
6
|
+
color?: string;
|
|
7
|
+
shape?: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
title?: string;
|
|
10
|
+
borderWidth?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface VisEdge {
|
|
13
|
+
id?: string;
|
|
14
|
+
from: string;
|
|
15
|
+
to: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
color?: string;
|
|
18
|
+
dashes?: boolean;
|
|
19
|
+
width?: number;
|
|
20
|
+
arrows?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface GraphStats {
|
|
23
|
+
tickets: number;
|
|
24
|
+
scenarios: number;
|
|
25
|
+
poms: number;
|
|
26
|
+
areas: number;
|
|
27
|
+
failures: number;
|
|
28
|
+
edges: number;
|
|
29
|
+
}
|
|
30
|
+
export interface RenderOpts {
|
|
31
|
+
since?: string;
|
|
32
|
+
ticketId?: string;
|
|
33
|
+
depth?: 1 | 2 | 3;
|
|
34
|
+
performanceMode?: 'full' | 'ticket-only' | 'text-fallback';
|
|
35
|
+
}
|
|
36
|
+
export declare function transformForVisNetwork(snap: Snapshot, opts: RenderOpts): {
|
|
37
|
+
nodes: VisNode[];
|
|
38
|
+
edges: VisEdge[];
|
|
39
|
+
stats: GraphStats;
|
|
40
|
+
};
|
|
41
|
+
export interface RenderHtmlInput {
|
|
42
|
+
data: {
|
|
43
|
+
nodes: VisNode[];
|
|
44
|
+
edges: VisEdge[];
|
|
45
|
+
};
|
|
46
|
+
stats: GraphStats;
|
|
47
|
+
generatedAt: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function renderHtml(input: RenderHtmlInput): string;
|
|
50
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/graph/render.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAc,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,QAAQ,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,GAAG,aAAa,GAAG,eAAe,CAAC;CAC5D;AAmKD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,UAAU,GACf;IACD,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,EAAE,UAAU,CAAC;CACnB,CA4EA;AAcD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAAC,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;IAC7C,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAgBzD"}
|
package/dist/src/index.js
CHANGED
|
@@ -324,11 +324,18 @@ var ReportingSchema = z4.object({
|
|
|
324
324
|
}).prefault({}),
|
|
325
325
|
artifactLinks: z4.enum(["git", "local"]).default("git")
|
|
326
326
|
}).prefault({});
|
|
327
|
+
var RunSchema = z4.object({
|
|
328
|
+
autoImpact: z4.object({
|
|
329
|
+
enabled: z4.boolean().default(true),
|
|
330
|
+
threshold: z4.number().nonnegative().default(6)
|
|
331
|
+
}).prefault({})
|
|
332
|
+
}).prefault({});
|
|
327
333
|
var XeraConfigSchema = z4.object({
|
|
328
334
|
jira: JiraSchema,
|
|
329
335
|
web: WebSchema,
|
|
330
336
|
ai: AISchema,
|
|
331
337
|
reporting: ReportingSchema,
|
|
338
|
+
run: RunSchema.prefault({}),
|
|
332
339
|
adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
|
|
333
340
|
});
|
|
334
341
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import type { RenderOpts } from '../graph/render';
|
|
4
|
+
import { renderHtml, transformForVisNetwork } from '../graph/render';
|
|
5
|
+
import { deriveSnapshot, loadAllEvents } from '../graph/store';
|
|
6
|
+
|
|
7
|
+
function parseDepth(s: string | undefined): 1 | 2 | 3 {
|
|
8
|
+
const n = s ? Number.parseInt(s, 10) : 2;
|
|
9
|
+
if (n === 1 || n === 3) return n;
|
|
10
|
+
return 2;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decidePerformanceMode(nodeCount: number): 'full' | 'ticket-only' | 'text-fallback' {
|
|
14
|
+
if (nodeCount > 2000) return 'text-fallback';
|
|
15
|
+
if (nodeCount > 500) return 'ticket-only';
|
|
16
|
+
return 'full';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function graphRenderCmd(argv: string[]): Promise<number> {
|
|
20
|
+
let outPath: string | undefined;
|
|
21
|
+
let ticketId: string | undefined;
|
|
22
|
+
let since: string | undefined;
|
|
23
|
+
let depth: 1 | 2 | 3 = 2;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
if (argv[i] === '--out') outPath = argv[++i];
|
|
27
|
+
else if (argv[i] === '--ticket') ticketId = argv[++i];
|
|
28
|
+
else if (argv[i] === '--since') since = argv[++i];
|
|
29
|
+
else if (argv[i] === '--depth') depth = parseDepth(argv[++i]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const repoRoot = process.cwd();
|
|
33
|
+
const finalPath = outPath ?? join(repoRoot, '.xera/graph.html');
|
|
34
|
+
|
|
35
|
+
const snap = deriveSnapshot(loadAllEvents(repoRoot));
|
|
36
|
+
const totalNodeCount =
|
|
37
|
+
Object.keys(snap.tickets).length +
|
|
38
|
+
Object.keys(snap.scenarios).length +
|
|
39
|
+
Object.keys(snap.poms).length +
|
|
40
|
+
Object.keys(snap.areas).length;
|
|
41
|
+
const performanceMode = decidePerformanceMode(totalNodeCount);
|
|
42
|
+
|
|
43
|
+
if (performanceMode === 'text-fallback') {
|
|
44
|
+
const txtPath = finalPath.replace(/\.html$/, '.txt');
|
|
45
|
+
mkdirSync(dirname(txtPath), { recursive: true });
|
|
46
|
+
writeFileSync(
|
|
47
|
+
txtPath,
|
|
48
|
+
`Graph too large for HTML viewer (${totalNodeCount} nodes). Use 'xera:graph-query --format text' instead.\n`,
|
|
49
|
+
);
|
|
50
|
+
console.log(`[graph-render] graph too large (${totalNodeCount} nodes); wrote ${txtPath}`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const opts: RenderOpts = { depth, performanceMode };
|
|
55
|
+
if (ticketId) opts.ticketId = ticketId;
|
|
56
|
+
if (since) opts.since = since;
|
|
57
|
+
|
|
58
|
+
const data = transformForVisNetwork(snap, opts);
|
|
59
|
+
const html = renderHtml({ data, stats: data.stats, generatedAt: new Date().toISOString() });
|
|
60
|
+
|
|
61
|
+
mkdirSync(dirname(finalPath), { recursive: true });
|
|
62
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
63
|
+
writeFileSync(tmpPath, html);
|
|
64
|
+
renameSync(tmpPath, finalPath);
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`[graph-render] wrote ${finalPath} (${data.stats.tickets} tickets · ${data.stats.scenarios} scenarios · ${html.length} bytes)`,
|
|
68
|
+
);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ImpactOpts, ImpactReport } from '../graph/impact';
|
|
4
|
+
import { renderImpactMarkdown, walkImpact } from '../graph/impact';
|
|
5
|
+
import { deriveSnapshot, loadAllEvents } from '../graph/store';
|
|
6
|
+
import type { Priority } from '../graph/types';
|
|
7
|
+
|
|
8
|
+
function parseDepth(s: string | undefined): 1 | 2 | 3 {
|
|
9
|
+
const n = s ? Number.parseInt(s, 10) : 2;
|
|
10
|
+
if (n === 1 || n === 3) return n;
|
|
11
|
+
return 2;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseMinPriority(s: string | undefined): Priority | undefined {
|
|
15
|
+
if (s === 'p0' || s === 'p1' || s === 'p2') return s;
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function impactPrepareCmd(argv: string[]): Promise<number> {
|
|
20
|
+
const ticket = argv[0];
|
|
21
|
+
if (!ticket || ticket.startsWith('--')) {
|
|
22
|
+
console.error(
|
|
23
|
+
'[impact-prepare] usage: impact-prepare <TICKET> [--depth 1|2|3] [--min-priority p0|p1|p2] [--quiet]',
|
|
24
|
+
);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let depth: 1 | 2 | 3 = 2;
|
|
29
|
+
let minPriority: Priority | undefined;
|
|
30
|
+
let quiet = false;
|
|
31
|
+
for (let i = 1; i < argv.length; i++) {
|
|
32
|
+
if (argv[i] === '--depth') depth = parseDepth(argv[++i]);
|
|
33
|
+
else if (argv[i] === '--min-priority') minPriority = parseMinPriority(argv[++i]);
|
|
34
|
+
else if (argv[i] === '--quiet') quiet = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const repoRoot = process.cwd();
|
|
38
|
+
const graph = deriveSnapshot(loadAllEvents(repoRoot));
|
|
39
|
+
const target = graph.tickets[ticket];
|
|
40
|
+
if (!target) {
|
|
41
|
+
console.error(`[impact-prepare] ticket ${ticket} not in graph; run /xera-fetch first`);
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const opts: ImpactOpts = { depth };
|
|
46
|
+
if (minPriority) opts.minPriority = minPriority;
|
|
47
|
+
|
|
48
|
+
const scenarios = walkImpact(graph, target, opts);
|
|
49
|
+
|
|
50
|
+
const report: ImpactReport = {
|
|
51
|
+
targetTicket: ticket,
|
|
52
|
+
modifiedAreas: target.modifiesAreas,
|
|
53
|
+
scenarios,
|
|
54
|
+
generatedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const impactDir = join(repoRoot, '.xera/impact');
|
|
58
|
+
mkdirSync(impactDir, { recursive: true });
|
|
59
|
+
writeFileSync(join(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
|
|
60
|
+
if (!quiet) {
|
|
61
|
+
writeFileSync(join(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
@@ -8,8 +8,10 @@ import { graphBackfillCmd } from './graph-backfill';
|
|
|
8
8
|
import { graphEnrichCmd } from './graph-enrich';
|
|
9
9
|
import { graphQueryCmd } from './graph-query';
|
|
10
10
|
import { graphRecordCmd } from './graph-record';
|
|
11
|
+
import { graphRenderCmd } from './graph-render';
|
|
11
12
|
import { graphSnapshotCmd } from './graph-snapshot';
|
|
12
13
|
import { healPrepareCmd } from './heal-prepare';
|
|
14
|
+
import { impactPrepareCmd } from './impact-prepare';
|
|
13
15
|
import { lintCmd } from './lint';
|
|
14
16
|
import { normalizeCmd } from './normalize';
|
|
15
17
|
import { postCmd } from './post';
|
|
@@ -30,10 +32,12 @@ const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
|
|
|
30
32
|
fetch: fetchCmd,
|
|
31
33
|
'graph-backfill': graphBackfillCmd,
|
|
32
34
|
'graph-enrich': graphEnrichCmd,
|
|
35
|
+
'graph-render': graphRenderCmd,
|
|
33
36
|
'graph-query': graphQueryCmd,
|
|
34
37
|
'graph-record': graphRecordCmd,
|
|
35
38
|
'graph-snapshot': graphSnapshotCmd,
|
|
36
39
|
'heal-prepare': healPrepareCmd,
|
|
40
|
+
'impact-prepare': impactPrepareCmd,
|
|
37
41
|
lint: lintCmd,
|
|
38
42
|
normalize: normalizeCmd,
|
|
39
43
|
post: postCmd,
|
package/src/config/schema.ts
CHANGED
|
@@ -69,11 +69,23 @@ const ReportingSchema = z
|
|
|
69
69
|
})
|
|
70
70
|
.prefault({});
|
|
71
71
|
|
|
72
|
+
const RunSchema = z
|
|
73
|
+
.object({
|
|
74
|
+
autoImpact: z
|
|
75
|
+
.object({
|
|
76
|
+
enabled: z.boolean().default(true),
|
|
77
|
+
threshold: z.number().nonnegative().default(6.0),
|
|
78
|
+
})
|
|
79
|
+
.prefault({}),
|
|
80
|
+
})
|
|
81
|
+
.prefault({});
|
|
82
|
+
|
|
72
83
|
export const XeraConfigSchema = z.object({
|
|
73
84
|
jira: JiraSchema,
|
|
74
85
|
web: WebSchema,
|
|
75
86
|
ai: AISchema,
|
|
76
87
|
reporting: ReportingSchema,
|
|
88
|
+
run: RunSchema.prefault({}),
|
|
77
89
|
adapters: z.array(z.string().min(1)).min(1).default(['web']),
|
|
78
90
|
});
|
|
79
91
|
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { EdgeKind, Priority, Snapshot, TicketNode } from './types';
|
|
2
|
+
|
|
3
|
+
export interface ImpactEdge {
|
|
4
|
+
kind: EdgeKind;
|
|
5
|
+
from: string;
|
|
6
|
+
to: string;
|
|
7
|
+
confidence?: number;
|
|
8
|
+
source?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ImpactScenario {
|
|
12
|
+
scenarioId: string;
|
|
13
|
+
ticketId: string; // owner of the scenario (NOT the impact target)
|
|
14
|
+
name: string;
|
|
15
|
+
priority: Priority;
|
|
16
|
+
edgePath: ImpactEdge[];
|
|
17
|
+
riskScore: number;
|
|
18
|
+
lastPassedAt?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ImpactOpts {
|
|
22
|
+
depth: 1 | 2 | 3;
|
|
23
|
+
minPriority?: Priority;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ImpactReport {
|
|
27
|
+
targetTicket: string;
|
|
28
|
+
modifiedAreas: string[];
|
|
29
|
+
scenarios: ImpactScenario[];
|
|
30
|
+
generatedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PRIORITY_WEIGHT: Record<Priority, number> = { p0: 3, p1: 2, p2: 1 };
|
|
34
|
+
|
|
35
|
+
const EDGE_WEIGHT_FIXED: Partial<Record<EdgeKind, number>> = {
|
|
36
|
+
modifies: 5, // direct collision via SUT area
|
|
37
|
+
uses: 4, // shared POM
|
|
38
|
+
covers: 4, // shared POM (alt path)
|
|
39
|
+
// 'jira-linked' weight is dynamic — see jiraRelationWeight
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function jiraRelationWeight(source?: string): number {
|
|
43
|
+
if (!source) return 0;
|
|
44
|
+
if (source.endsWith('blocks')) return 4;
|
|
45
|
+
if (source.endsWith('duplicates')) return 3;
|
|
46
|
+
if (source.endsWith('relates')) return 2;
|
|
47
|
+
if (source.endsWith('supersedes')) return 3;
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function edgeWeight(edge: ImpactEdge): number {
|
|
52
|
+
if (edge.kind === 'modifies') return EDGE_WEIGHT_FIXED.modifies ?? 0;
|
|
53
|
+
if (edge.kind === 'uses' || edge.kind === 'covers') return EDGE_WEIGHT_FIXED.uses ?? 0;
|
|
54
|
+
if (edge.kind === 'jira-linked') return jiraRelationWeight(edge.source);
|
|
55
|
+
if (edge.kind === 'similar') return 1 * (edge.confidence ?? 0);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function riskScore(scenario: ImpactScenario, daysSinceLastPass: number): number {
|
|
60
|
+
const pri = PRIORITY_WEIGHT[scenario.priority] * 3;
|
|
61
|
+
const firstEdge = scenario.edgePath[0];
|
|
62
|
+
const edgeW = firstEdge ? edgeWeight(firstEdge) : 0;
|
|
63
|
+
const confW = firstEdge?.confidence !== undefined ? firstEdge.confidence * 2 : 0;
|
|
64
|
+
const decay = daysSinceLastPass * 0.1;
|
|
65
|
+
return pri + edgeW + confW - decay;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const PRIORITY_RANK: Record<Priority, number> = { p0: 3, p1: 2, p2: 1 };
|
|
69
|
+
|
|
70
|
+
function daysSince(ts: string | undefined): number {
|
|
71
|
+
if (!ts) return 0;
|
|
72
|
+
const ms = Date.now() - Date.parse(ts);
|
|
73
|
+
return ms < 0 ? 0 : ms / (86400 * 1000);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function walkImpact(
|
|
77
|
+
graph: Snapshot,
|
|
78
|
+
target: TicketNode,
|
|
79
|
+
opts: ImpactOpts,
|
|
80
|
+
): ImpactScenario[] {
|
|
81
|
+
const result: ImpactScenario[] = [];
|
|
82
|
+
const seen = new Set<string>();
|
|
83
|
+
|
|
84
|
+
// Areas the target modifies
|
|
85
|
+
const targetAreas = new Set(target.modifiesAreas);
|
|
86
|
+
|
|
87
|
+
// POMs covering any of those areas
|
|
88
|
+
const pomIds = graph.edges
|
|
89
|
+
.filter((e) => e.kind === 'covers' && targetAreas.has(e.to))
|
|
90
|
+
.map((e) => e.from);
|
|
91
|
+
|
|
92
|
+
// Scenarios using any of those POMs (depth 1 — direct collision)
|
|
93
|
+
const directScenarios = graph.edges
|
|
94
|
+
.filter((e) => e.kind === 'uses' && pomIds.includes(e.to))
|
|
95
|
+
.map((e) => e.from);
|
|
96
|
+
|
|
97
|
+
for (const scenarioId of directScenarios) {
|
|
98
|
+
if (seen.has(scenarioId)) continue;
|
|
99
|
+
const scenario = graph.scenarios[scenarioId];
|
|
100
|
+
if (!scenario) continue;
|
|
101
|
+
if (scenario.ticketId === target.id) continue; // exclude own scenarios
|
|
102
|
+
|
|
103
|
+
const usingPom = graph.edges.find((e) => e.kind === 'uses' && e.from === scenarioId);
|
|
104
|
+
const modifyEdge = graph.edges.find(
|
|
105
|
+
(e) => e.kind === 'modifies' && e.from === target.id && targetAreas.has(e.to),
|
|
106
|
+
);
|
|
107
|
+
const edgePath: ImpactEdge[] = [];
|
|
108
|
+
if (modifyEdge) edgePath.push({ kind: 'modifies', from: modifyEdge.from, to: modifyEdge.to });
|
|
109
|
+
if (usingPom) edgePath.push({ kind: 'uses', from: usingPom.from, to: usingPom.to });
|
|
110
|
+
|
|
111
|
+
seen.add(scenarioId);
|
|
112
|
+
const impact: ImpactScenario = {
|
|
113
|
+
scenarioId,
|
|
114
|
+
ticketId: scenario.ticketId,
|
|
115
|
+
name: scenario.name,
|
|
116
|
+
priority: scenario.priority,
|
|
117
|
+
edgePath,
|
|
118
|
+
riskScore: 0,
|
|
119
|
+
};
|
|
120
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId]?.ts));
|
|
121
|
+
result.push(impact);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Depth >= 2: jira-linked tickets contribute their scenarios
|
|
125
|
+
if (opts.depth >= 2) {
|
|
126
|
+
const linked = graph.edges
|
|
127
|
+
.filter((e) => e.kind === 'jira-linked' && e.from === target.id)
|
|
128
|
+
.map((e) => ({ to: e.to, source: e.source }));
|
|
129
|
+
for (const link of linked) {
|
|
130
|
+
const sceneIds = graph.edges
|
|
131
|
+
.filter((e) => e.kind === 'tests' && e.from === link.to)
|
|
132
|
+
.map((e) => e.to);
|
|
133
|
+
for (const scenarioId of sceneIds) {
|
|
134
|
+
if (seen.has(scenarioId)) continue;
|
|
135
|
+
const scenario = graph.scenarios[scenarioId];
|
|
136
|
+
if (!scenario || scenario.ticketId === target.id) continue;
|
|
137
|
+
seen.add(scenarioId);
|
|
138
|
+
const edge: ImpactEdge = { kind: 'jira-linked', from: target.id, to: link.to };
|
|
139
|
+
if (link.source !== undefined) edge.source = link.source;
|
|
140
|
+
const impact: ImpactScenario = {
|
|
141
|
+
scenarioId,
|
|
142
|
+
ticketId: scenario.ticketId,
|
|
143
|
+
name: scenario.name,
|
|
144
|
+
priority: scenario.priority,
|
|
145
|
+
edgePath: [edge],
|
|
146
|
+
riskScore: 0,
|
|
147
|
+
};
|
|
148
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId]?.ts));
|
|
149
|
+
result.push(impact);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Depth >= 3: similar tickets contribute their scenarios
|
|
155
|
+
if (opts.depth >= 3) {
|
|
156
|
+
const similar = graph.edges
|
|
157
|
+
.filter((e) => e.kind === 'similar' && e.from === target.id)
|
|
158
|
+
.map((e) => ({ to: e.to, confidence: e.confidence }));
|
|
159
|
+
for (const link of similar) {
|
|
160
|
+
const sceneIds = graph.edges
|
|
161
|
+
.filter((e) => e.kind === 'tests' && e.from === link.to)
|
|
162
|
+
.map((e) => e.to);
|
|
163
|
+
for (const scenarioId of sceneIds) {
|
|
164
|
+
if (seen.has(scenarioId)) continue;
|
|
165
|
+
const scenario = graph.scenarios[scenarioId];
|
|
166
|
+
if (!scenario || scenario.ticketId === target.id) continue;
|
|
167
|
+
seen.add(scenarioId);
|
|
168
|
+
const edge: ImpactEdge = { kind: 'similar', from: target.id, to: link.to };
|
|
169
|
+
if (link.confidence !== undefined) edge.confidence = link.confidence;
|
|
170
|
+
const impact: ImpactScenario = {
|
|
171
|
+
scenarioId,
|
|
172
|
+
ticketId: scenario.ticketId,
|
|
173
|
+
name: scenario.name,
|
|
174
|
+
priority: scenario.priority,
|
|
175
|
+
edgePath: [edge],
|
|
176
|
+
riskScore: 0,
|
|
177
|
+
};
|
|
178
|
+
impact.riskScore = riskScore(impact, daysSince(graph.latest_failures[scenarioId]?.ts));
|
|
179
|
+
result.push(impact);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Filter by min-priority
|
|
185
|
+
let filtered = result;
|
|
186
|
+
if (opts.minPriority) {
|
|
187
|
+
const min = PRIORITY_RANK[opts.minPriority];
|
|
188
|
+
filtered = filtered.filter((s) => PRIORITY_RANK[s.priority] >= min);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Sort by riskScore descending
|
|
192
|
+
filtered.sort((a, b) => b.riskScore - a.riskScore);
|
|
193
|
+
return filtered;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const HIGH_THRESHOLD = 7.0;
|
|
197
|
+
const MEDIUM_THRESHOLD = 4.0;
|
|
198
|
+
|
|
199
|
+
function bucket(score: number): 'high' | 'medium' | 'low' {
|
|
200
|
+
if (score >= HIGH_THRESHOLD) return 'high';
|
|
201
|
+
if (score >= MEDIUM_THRESHOLD) return 'medium';
|
|
202
|
+
return 'low';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function fmtEdgePath(path: ImpactEdge[]): string {
|
|
206
|
+
return path.map((e) => `${e.from} →[${e.kind}]→ ${e.to}`).join(' · ');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function renderImpactMarkdown(report: ImpactReport): string {
|
|
210
|
+
const lines: string[] = [];
|
|
211
|
+
lines.push(`# Impact Analysis — ${report.targetTicket}`);
|
|
212
|
+
lines.push('');
|
|
213
|
+
lines.push(`**Modified areas:** ${report.modifiedAreas.join(', ') || '(none)'}`);
|
|
214
|
+
lines.push(`**Generated:** ${report.generatedAt}`);
|
|
215
|
+
lines.push('');
|
|
216
|
+
|
|
217
|
+
if (report.scenarios.length === 0) {
|
|
218
|
+
lines.push('No prior scenarios in the modified areas. This may be a new feature area.');
|
|
219
|
+
lines.push('');
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const bySeverity = {
|
|
224
|
+
high: [] as ImpactScenario[],
|
|
225
|
+
medium: [] as ImpactScenario[],
|
|
226
|
+
low: [] as ImpactScenario[],
|
|
227
|
+
};
|
|
228
|
+
for (const s of report.scenarios) bySeverity[bucket(s.riskScore)].push(s);
|
|
229
|
+
|
|
230
|
+
lines.push(
|
|
231
|
+
`**Total impacted:** ${report.scenarios.length} scenarios (${bySeverity.high.length} high · ${bySeverity.medium.length} medium · ${bySeverity.low.length} low)`,
|
|
232
|
+
);
|
|
233
|
+
lines.push('');
|
|
234
|
+
|
|
235
|
+
for (const [name, scenarios] of [
|
|
236
|
+
['High-risk', bySeverity.high],
|
|
237
|
+
['Medium-risk', bySeverity.medium],
|
|
238
|
+
['Low-risk', bySeverity.low],
|
|
239
|
+
] as const) {
|
|
240
|
+
if (scenarios.length === 0) continue;
|
|
241
|
+
lines.push(`## ${name}`);
|
|
242
|
+
lines.push('');
|
|
243
|
+
for (const s of scenarios) {
|
|
244
|
+
lines.push(
|
|
245
|
+
`### ${s.ticketId} / "${s.name}" [${s.priority.toUpperCase()}] score ${s.riskScore.toFixed(1)}`,
|
|
246
|
+
);
|
|
247
|
+
lines.push(`- Edge: ${fmtEdgePath(s.edgePath)}`);
|
|
248
|
+
if (s.lastPassedAt) lines.push(`- Last passed: ${s.lastPassedAt}`);
|
|
249
|
+
lines.push('');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
lines.push('## Re-run commands');
|
|
254
|
+
lines.push(`- All: \`bun run xera:exec --from-impact ${report.targetTicket}\``);
|
|
255
|
+
lines.push(
|
|
256
|
+
`- P0 only: \`bun run xera:exec --from-impact ${report.targetTicket} --min-priority p0\``,
|
|
257
|
+
);
|
|
258
|
+
lines.push(`- Select: \`bun run xera:exec --from-impact ${report.targetTicket} --select\``);
|
|
259
|
+
lines.push('');
|
|
260
|
+
|
|
261
|
+
return lines.join('\n');
|
|
262
|
+
}
|
package/src/graph/index.ts
CHANGED
|
@@ -14,7 +14,20 @@ export type { CostSummary, LlmCallLog } from './cost';
|
|
|
14
14
|
export { logLlmCall, summarizeCost } from './cost';
|
|
15
15
|
export type { EnrichOptions, EnrichResult } from './enrich';
|
|
16
16
|
export { enrichTicket } from './enrich';
|
|
17
|
+
export type {
|
|
18
|
+
ImpactEdge,
|
|
19
|
+
ImpactOpts,
|
|
20
|
+
ImpactReport,
|
|
21
|
+
ImpactScenario,
|
|
22
|
+
} from './impact';
|
|
23
|
+
export {
|
|
24
|
+
renderImpactMarkdown,
|
|
25
|
+
riskScore,
|
|
26
|
+
walkImpact,
|
|
27
|
+
} from './impact';
|
|
17
28
|
export { currentYyyyMm, graphPaths } from './paths';
|
|
29
|
+
export type { GraphStats, RenderHtmlInput, RenderOpts, VisEdge, VisNode } from './render';
|
|
30
|
+
export { renderHtml, transformForVisNetwork } from './render';
|
|
18
31
|
export { EventSchema, safeParseEvent } from './schema';
|
|
19
32
|
export { buildSimilarityPrompt } from './similarity';
|
|
20
33
|
export {
|