@xera-ai/core 0.1.7 → 0.4.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/bin/internal.ts +1 -0
- package/dist/adapter/types.d.ts +1 -1
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/artifact/meta.d.ts +2 -28
- package/dist/artifact/meta.d.ts.map +1 -1
- package/dist/artifact/status.d.ts +49 -74
- package/dist/artifact/status.d.ts.map +1 -1
- package/dist/auth/key.d.ts.map +1 -1
- package/dist/auth/refresh.d.ts.map +1 -1
- package/dist/auth/state.d.ts +5 -14
- package/dist/auth/state.d.ts.map +1 -1
- package/dist/bin/internal.js +10037 -746
- package/dist/bin-internal/doctor.d.ts +5 -0
- package/dist/bin-internal/doctor.d.ts.map +1 -0
- package/dist/bin-internal/eval-deterministic.d.ts +5 -0
- package/dist/bin-internal/eval-deterministic.d.ts.map +1 -0
- package/dist/bin-internal/eval-prepare.d.ts +7 -0
- package/dist/bin-internal/eval-prepare.d.ts.map +1 -0
- package/dist/bin-internal/eval-report.d.ts +5 -0
- package/dist/bin-internal/eval-report.d.ts.map +1 -0
- package/dist/bin-internal/exec.d.ts.map +1 -1
- package/dist/bin-internal/fetch.d.ts.map +1 -1
- package/dist/bin-internal/graph-backfill.d.ts +2 -0
- package/dist/bin-internal/graph-backfill.d.ts.map +1 -0
- package/dist/bin-internal/graph-query.d.ts +2 -0
- package/dist/bin-internal/graph-query.d.ts.map +1 -0
- package/dist/bin-internal/graph-record-script.d.ts +2 -0
- package/dist/bin-internal/graph-record-script.d.ts.map +1 -0
- package/dist/bin-internal/graph-record.d.ts +3 -0
- package/dist/bin-internal/graph-record.d.ts.map +1 -0
- package/dist/bin-internal/graph-snapshot.d.ts +2 -0
- package/dist/bin-internal/graph-snapshot.d.ts.map +1 -0
- package/dist/bin-internal/heal-prepare.d.ts +19 -0
- package/dist/bin-internal/heal-prepare.d.ts.map +1 -0
- package/dist/bin-internal/index.d.ts.map +1 -1
- package/dist/bin-internal/lint.d.ts.map +1 -1
- package/dist/bin-internal/normalize.d.ts.map +1 -1
- package/dist/bin-internal/post.d.ts.map +1 -1
- package/dist/bin-internal/status-cmd.d.ts.map +1 -1
- package/dist/bin-internal/typecheck.d.ts.map +1 -1
- package/dist/bin-internal/unlock.d.ts.map +1 -1
- package/dist/bin-internal/validate-feature.d.ts.map +1 -1
- package/dist/bin-internal/verify-prompts.d.ts +7 -0
- package/dist/bin-internal/verify-prompts.d.ts.map +1 -0
- package/dist/classifier/aggregate.d.ts.map +1 -1
- package/dist/config/define.d.ts.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/schema.d.ts +38 -298
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/eval/paths.d.ts +15 -0
- package/dist/eval/paths.d.ts.map +1 -0
- package/dist/eval/run-id.d.ts +6 -0
- package/dist/eval/run-id.d.ts.map +1 -0
- package/dist/eval/types.d.ts +203 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/graph/cost.d.ts +21 -0
- package/dist/graph/cost.d.ts.map +1 -0
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/paths.d.ts +10 -0
- package/dist/graph/paths.d.ts.map +1 -0
- package/dist/graph/schema.d.ts +177 -0
- package/dist/graph/schema.d.ts.map +1 -0
- package/dist/graph/store.d.ts +14 -0
- package/dist/graph/store.d.ts.map +1 -0
- package/dist/graph/types.d.ts +151 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/ulid.d.ts +2 -0
- package/dist/graph/ulid.d.ts.map +1 -0
- package/dist/index.d.ts +11 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/jira/client.d.ts.map +1 -1
- package/dist/jira/fields.d.ts.map +1 -1
- package/dist/jira/rest-backend.d.ts.map +1 -1
- package/dist/reporter/jira-comment.d.ts.map +1 -1
- package/dist/reporter/status-writer.d.ts.map +1 -1
- package/dist/src/index.js +349 -321
- package/package.json +19 -13
- package/src/adapter/types.ts +5 -2
- package/src/artifact/meta.ts +1 -1
- package/src/artifact/status.ts +1 -1
- package/src/auth/encrypt.ts +2 -2
- package/src/auth/key.ts +1 -2
- package/src/auth/refresh.ts +5 -1
- package/src/auth/state.ts +2 -2
- package/src/bin-internal/doctor.ts +169 -0
- package/src/bin-internal/eval-deterministic.ts +149 -0
- package/src/bin-internal/eval-prepare.ts +214 -0
- package/src/bin-internal/eval-report.ts +177 -0
- package/src/bin-internal/exec.ts +28 -15
- package/src/bin-internal/fetch.ts +21 -10
- package/src/bin-internal/graph-backfill.ts +43 -0
- package/src/bin-internal/graph-query.ts +43 -0
- package/src/bin-internal/graph-record-script.ts +191 -0
- package/src/bin-internal/graph-record.ts +243 -0
- package/src/bin-internal/graph-snapshot.ts +23 -0
- package/src/bin-internal/heal-prepare.ts +230 -0
- package/src/bin-internal/index.ts +33 -11
- package/src/bin-internal/lint.ts +11 -4
- package/src/bin-internal/normalize.ts +23 -9
- package/src/bin-internal/post.ts +10 -4
- package/src/bin-internal/report.ts +3 -3
- package/src/bin-internal/status-cmd.ts +11 -3
- package/src/bin-internal/typecheck.ts +9 -3
- package/src/bin-internal/unlock.ts +12 -4
- package/src/bin-internal/validate-feature.ts +14 -5
- package/src/bin-internal/verify-prompts.ts +60 -0
- package/src/classifier/aggregate.ts +13 -6
- package/src/config/define.ts +3 -1
- package/src/config/load.ts +1 -1
- package/src/config/schema.ts +43 -37
- package/src/eval/paths.ts +32 -0
- package/src/eval/run-id.ts +30 -0
- package/src/eval/types.ts +101 -0
- package/src/graph/cost.ts +59 -0
- package/src/graph/index.ts +15 -0
- package/src/graph/paths.ts +27 -0
- package/src/graph/schema.ts +135 -0
- package/src/graph/store.ts +231 -0
- package/src/graph/types.ts +174 -0
- package/src/graph/ulid.ts +58 -0
- package/src/index.ts +11 -11
- package/src/jira/client.ts +4 -2
- package/src/jira/fields.ts +4 -2
- package/src/jira/mcp-backend.ts +1 -1
- package/src/jira/rest-backend.ts +18 -6
- package/src/jira/retry.ts +2 -2
- package/src/lock/file-lock.ts +2 -2
- package/src/logging/ndjson-logger.ts +2 -2
- package/src/reporter/jira-comment.ts +13 -7
- package/src/reporter/status-writer.ts +2 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Event } from './types';
|
|
3
|
+
import { SCHEMA_VERSION } from './types';
|
|
4
|
+
|
|
5
|
+
const schemaV = z.literal(SCHEMA_VERSION);
|
|
6
|
+
const iso = z.string().datetime({ offset: false });
|
|
7
|
+
|
|
8
|
+
const ticketFetched = z
|
|
9
|
+
.object({
|
|
10
|
+
ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
11
|
+
summary: z.string(),
|
|
12
|
+
ac: z.array(z.string()),
|
|
13
|
+
jiraLinks: z.array(
|
|
14
|
+
z.object({
|
|
15
|
+
ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
16
|
+
relation: z.enum(['blocks', 'duplicates', 'relates', 'supersedes']),
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
storyHash: z.string(),
|
|
20
|
+
modifiesAreas: z.array(z.string().regex(/^[a-z0-9-]+$/)),
|
|
21
|
+
})
|
|
22
|
+
.passthrough();
|
|
23
|
+
|
|
24
|
+
const ticketEnriched = z
|
|
25
|
+
.object({
|
|
26
|
+
ticketId: z.string(),
|
|
27
|
+
enrichedAt: iso,
|
|
28
|
+
similarCount: z.number().int().nonnegative(),
|
|
29
|
+
})
|
|
30
|
+
.passthrough();
|
|
31
|
+
|
|
32
|
+
const scenarioGenerated = z
|
|
33
|
+
.object({
|
|
34
|
+
scenarioId: z.string(),
|
|
35
|
+
ticketId: z.string(),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
gherkin: z.string(),
|
|
38
|
+
priority: z.enum(['p0', 'p1', 'p2']),
|
|
39
|
+
featureHash: z.string(),
|
|
40
|
+
generatedAt: iso,
|
|
41
|
+
})
|
|
42
|
+
.passthrough();
|
|
43
|
+
|
|
44
|
+
const pomGenerated = z
|
|
45
|
+
.object({
|
|
46
|
+
pomId: z.string(),
|
|
47
|
+
ticketId: z.string(),
|
|
48
|
+
filePath: z.string(),
|
|
49
|
+
route: z.string(),
|
|
50
|
+
locators: z.array(z.string()),
|
|
51
|
+
scope: z.enum(['local', 'shared']),
|
|
52
|
+
})
|
|
53
|
+
.passthrough();
|
|
54
|
+
|
|
55
|
+
const pomPromoted = z
|
|
56
|
+
.object({
|
|
57
|
+
pomId: z.string(),
|
|
58
|
+
fromPath: z.string(),
|
|
59
|
+
toPath: z.string(),
|
|
60
|
+
})
|
|
61
|
+
.passthrough();
|
|
62
|
+
|
|
63
|
+
const runCompleted = z
|
|
64
|
+
.object({
|
|
65
|
+
scenarioId: z.string(),
|
|
66
|
+
ticketId: z.string(),
|
|
67
|
+
runId: z.string(),
|
|
68
|
+
status: z.enum(['pass', 'fail']),
|
|
69
|
+
traceId: z.string().optional(),
|
|
70
|
+
runtime: z.number().nonnegative(),
|
|
71
|
+
})
|
|
72
|
+
.passthrough();
|
|
73
|
+
|
|
74
|
+
const classification = z.enum(['REAL_BUG', 'TEST_BUG', 'SELECTOR_DRIFT', 'FLAKY', 'PASS']);
|
|
75
|
+
|
|
76
|
+
const runClassified = z
|
|
77
|
+
.object({
|
|
78
|
+
scenarioId: z.string(),
|
|
79
|
+
runId: z.string(),
|
|
80
|
+
classification,
|
|
81
|
+
confidence: z.enum(['low', 'medium', 'high']),
|
|
82
|
+
})
|
|
83
|
+
.passthrough();
|
|
84
|
+
|
|
85
|
+
const classificationDisputed = z
|
|
86
|
+
.object({
|
|
87
|
+
runId: z.string(),
|
|
88
|
+
scenarioId: z.string(),
|
|
89
|
+
originalClassification: classification,
|
|
90
|
+
disputedTo: classification,
|
|
91
|
+
qaActor: z.string(),
|
|
92
|
+
qaReason: z.string().optional(),
|
|
93
|
+
})
|
|
94
|
+
.passthrough();
|
|
95
|
+
|
|
96
|
+
const edgeDiscovered = z
|
|
97
|
+
.object({
|
|
98
|
+
kind: z.enum(['tests', 'uses', 'covers', 'modifies', 'jira-linked', 'similar', 'ran']),
|
|
99
|
+
from: z.string(),
|
|
100
|
+
to: z.string(),
|
|
101
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
102
|
+
source: z.string(),
|
|
103
|
+
})
|
|
104
|
+
.passthrough();
|
|
105
|
+
|
|
106
|
+
const base = {
|
|
107
|
+
event_id: z.string().min(20),
|
|
108
|
+
schema_version: schemaV,
|
|
109
|
+
ts: iso,
|
|
110
|
+
actor: z.string(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const EventSchema = z.discriminatedUnion('type', [
|
|
114
|
+
z.object({ ...base, type: z.literal('ticket.fetched'), payload: ticketFetched }),
|
|
115
|
+
z.object({ ...base, type: z.literal('ticket.enriched'), payload: ticketEnriched }),
|
|
116
|
+
z.object({ ...base, type: z.literal('scenario.generated'), payload: scenarioGenerated }),
|
|
117
|
+
z.object({ ...base, type: z.literal('pom.generated'), payload: pomGenerated }),
|
|
118
|
+
z.object({ ...base, type: z.literal('pom.promoted'), payload: pomPromoted }),
|
|
119
|
+
z.object({ ...base, type: z.literal('run.completed'), payload: runCompleted }),
|
|
120
|
+
z.object({ ...base, type: z.literal('run.classified'), payload: runClassified }),
|
|
121
|
+
z.object({
|
|
122
|
+
...base,
|
|
123
|
+
type: z.literal('classification.disputed'),
|
|
124
|
+
payload: classificationDisputed,
|
|
125
|
+
}),
|
|
126
|
+
z.object({ ...base, type: z.literal('edge.discovered'), payload: edgeDiscovered }),
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
export function safeParseEvent(
|
|
130
|
+
value: unknown,
|
|
131
|
+
): { success: true; data: Event } | { success: false; error: z.ZodError } {
|
|
132
|
+
const r = EventSchema.safeParse(value);
|
|
133
|
+
if (r.success) return { success: true, data: r.data as Event };
|
|
134
|
+
return { success: false, error: r.error };
|
|
135
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { dirname } from 'node:path';
|
|
11
|
+
import { currentYyyyMm, graphPaths } from './paths';
|
|
12
|
+
import { safeParseEvent } from './schema';
|
|
13
|
+
import type {
|
|
14
|
+
EdgeRecord,
|
|
15
|
+
Event,
|
|
16
|
+
FailureNode,
|
|
17
|
+
PomNode,
|
|
18
|
+
ScenarioNode,
|
|
19
|
+
Snapshot,
|
|
20
|
+
TicketNode,
|
|
21
|
+
} from './types';
|
|
22
|
+
import { SCHEMA_VERSION } from './types';
|
|
23
|
+
|
|
24
|
+
export interface AppendOptions {
|
|
25
|
+
skill: string;
|
|
26
|
+
ticketId: string;
|
|
27
|
+
now?: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function appendEvents(repoRoot: string, events: Event[], opts: AppendOptions): string {
|
|
31
|
+
if (events.length === 0) return '';
|
|
32
|
+
const paths = graphPaths(repoRoot);
|
|
33
|
+
const yyyyMm = currentYyyyMm(opts.now);
|
|
34
|
+
const monthDir = paths.eventsMonthDir(yyyyMm);
|
|
35
|
+
mkdirSync(monthDir, { recursive: true });
|
|
36
|
+
const ulid = events[0]!.event_id;
|
|
37
|
+
const finalPath = paths.eventFile(ulid, opts.skill, opts.ticketId, yyyyMm);
|
|
38
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
39
|
+
const body = `${events.map((e) => JSON.stringify(e)).join('\n')}\n`;
|
|
40
|
+
writeFileSync(tmpPath, body);
|
|
41
|
+
renameSync(tmpPath, finalPath);
|
|
42
|
+
return finalPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function loadAllEvents(repoRoot: string): Event[] {
|
|
46
|
+
const paths = graphPaths(repoRoot);
|
|
47
|
+
if (!existsSync(paths.eventsDir)) return [];
|
|
48
|
+
const files: string[] = [];
|
|
49
|
+
for (const monthDir of readdirSync(paths.eventsDir, { withFileTypes: true })) {
|
|
50
|
+
if (!monthDir.isDirectory()) continue;
|
|
51
|
+
const monthPath = paths.eventsMonthDir(monthDir.name);
|
|
52
|
+
for (const f of readdirSync(monthPath)) {
|
|
53
|
+
if (f.endsWith('.jsonl')) files.push(`${monthPath}/${f}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
files.sort((a, b) => {
|
|
57
|
+
const ua = a.split('/').pop()!.split('-')[0]!;
|
|
58
|
+
const ub = b.split('/').pop()!.split('-')[0]!;
|
|
59
|
+
return ua < ub ? -1 : ua > ub ? 1 : 0;
|
|
60
|
+
});
|
|
61
|
+
const events: Event[] = [];
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
try {
|
|
64
|
+
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
let parsed: unknown;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(line);
|
|
69
|
+
} catch {
|
|
70
|
+
console.warn(`[graph.store] skip-line bad-json ${file}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const r = safeParseEvent(parsed);
|
|
74
|
+
if (!r.success) {
|
|
75
|
+
console.warn(`[graph.store] skip-line invalid ${file}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
events.push(r.data);
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.warn(`[graph.store] skip-file ${file} ${(e as Error).message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
events.sort((a, b) => (a.event_id < b.event_id ? -1 : a.event_id > b.event_id ? 1 : 0));
|
|
85
|
+
return events;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function computeEventsHash(events: Event[]): string {
|
|
89
|
+
const h = createHash('sha256');
|
|
90
|
+
for (const e of events) h.update(e.event_id);
|
|
91
|
+
return `sha256:${h.digest('hex')}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function deriveSnapshot(events: Event[]): Snapshot {
|
|
95
|
+
const tickets: Record<string, TicketNode> = {};
|
|
96
|
+
const scenarios: Record<string, ScenarioNode> = {};
|
|
97
|
+
const poms: Record<string, PomNode> = {};
|
|
98
|
+
const areas: Record<string, { id: string }> = {};
|
|
99
|
+
const edges: EdgeRecord[] = [];
|
|
100
|
+
const latestFailures: Record<string, FailureNode> = {};
|
|
101
|
+
|
|
102
|
+
for (const e of events) {
|
|
103
|
+
switch (e.type) {
|
|
104
|
+
case 'ticket.fetched':
|
|
105
|
+
tickets[e.payload.ticketId] = {
|
|
106
|
+
id: e.payload.ticketId,
|
|
107
|
+
summary: e.payload.summary,
|
|
108
|
+
ac: e.payload.ac,
|
|
109
|
+
storyHash: e.payload.storyHash,
|
|
110
|
+
modifiesAreas: e.payload.modifiesAreas,
|
|
111
|
+
fetchedAt: e.ts,
|
|
112
|
+
};
|
|
113
|
+
for (const a of e.payload.modifiesAreas) areas[a] = { id: a };
|
|
114
|
+
for (const link of e.payload.jiraLinks) {
|
|
115
|
+
edges.push({
|
|
116
|
+
kind: 'jira-linked',
|
|
117
|
+
from: e.payload.ticketId,
|
|
118
|
+
to: link.ticketId,
|
|
119
|
+
source: `jira:${link.relation}`,
|
|
120
|
+
discoveredAt: e.ts,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
case 'ticket.enriched':
|
|
125
|
+
if (tickets[e.payload.ticketId])
|
|
126
|
+
tickets[e.payload.ticketId]!.enrichedAt = e.payload.enrichedAt;
|
|
127
|
+
break;
|
|
128
|
+
case 'scenario.generated':
|
|
129
|
+
scenarios[e.payload.scenarioId] = {
|
|
130
|
+
id: e.payload.scenarioId,
|
|
131
|
+
ticketId: e.payload.ticketId,
|
|
132
|
+
name: e.payload.name,
|
|
133
|
+
gherkin: e.payload.gherkin,
|
|
134
|
+
priority: e.payload.priority,
|
|
135
|
+
featureHash: e.payload.featureHash,
|
|
136
|
+
generatedAt: e.payload.generatedAt,
|
|
137
|
+
};
|
|
138
|
+
edges.push({
|
|
139
|
+
kind: 'tests',
|
|
140
|
+
from: e.payload.ticketId,
|
|
141
|
+
to: e.payload.scenarioId,
|
|
142
|
+
source: 'xera-script',
|
|
143
|
+
discoveredAt: e.ts,
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
case 'pom.generated':
|
|
147
|
+
poms[e.payload.pomId] = {
|
|
148
|
+
id: e.payload.pomId,
|
|
149
|
+
ticketId: e.payload.ticketId,
|
|
150
|
+
filePath: e.payload.filePath,
|
|
151
|
+
route: e.payload.route,
|
|
152
|
+
locators: e.payload.locators,
|
|
153
|
+
scope: e.payload.scope,
|
|
154
|
+
};
|
|
155
|
+
break;
|
|
156
|
+
case 'pom.promoted':
|
|
157
|
+
if (poms[e.payload.pomId]) {
|
|
158
|
+
poms[e.payload.pomId]!.filePath = e.payload.toPath;
|
|
159
|
+
poms[e.payload.pomId]!.scope = 'shared';
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case 'run.completed':
|
|
163
|
+
if (e.payload.status === 'fail') {
|
|
164
|
+
const fail: FailureNode = {
|
|
165
|
+
id: `${e.payload.runId}:${e.payload.scenarioId}`,
|
|
166
|
+
scenarioId: e.payload.scenarioId,
|
|
167
|
+
runId: e.payload.runId,
|
|
168
|
+
ts: e.ts,
|
|
169
|
+
};
|
|
170
|
+
if (e.payload.traceId) fail.traceId = e.payload.traceId;
|
|
171
|
+
latestFailures[e.payload.scenarioId] = fail;
|
|
172
|
+
} else {
|
|
173
|
+
delete latestFailures[e.payload.scenarioId];
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case 'edge.discovered': {
|
|
177
|
+
const ed: EdgeRecord = {
|
|
178
|
+
kind: e.payload.kind,
|
|
179
|
+
from: e.payload.from,
|
|
180
|
+
to: e.payload.to,
|
|
181
|
+
source: e.payload.source,
|
|
182
|
+
discoveredAt: e.ts,
|
|
183
|
+
};
|
|
184
|
+
if (e.payload.confidence !== undefined) ed.confidence = e.payload.confidence;
|
|
185
|
+
edges.push(ed);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
// run.classified and classification.disputed: not materialized in v0.6.0 snapshot
|
|
189
|
+
default:
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
schema_version: SCHEMA_VERSION,
|
|
196
|
+
generated_at: new Date().toISOString(),
|
|
197
|
+
event_count: events.length,
|
|
198
|
+
events_hash: computeEventsHash(events),
|
|
199
|
+
tickets,
|
|
200
|
+
scenarios,
|
|
201
|
+
poms,
|
|
202
|
+
areas,
|
|
203
|
+
edges,
|
|
204
|
+
latest_failures: latestFailures,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function writeSnapshot(repoRoot: string, snap: Snapshot): void {
|
|
209
|
+
const paths = graphPaths(repoRoot);
|
|
210
|
+
mkdirSync(dirname(paths.snapshotFile), { recursive: true });
|
|
211
|
+
const tmp = `${paths.snapshotFile}.tmp`;
|
|
212
|
+
writeFileSync(tmp, JSON.stringify(snap, null, 2));
|
|
213
|
+
renameSync(tmp, paths.snapshotFile);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function loadSnapshot(repoRoot: string): Snapshot | null {
|
|
217
|
+
const paths = graphPaths(repoRoot);
|
|
218
|
+
if (!existsSync(paths.snapshotFile)) return null;
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(readFileSync(paths.snapshotFile, 'utf8')) as Snapshot;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function isSnapshotStale(repoRoot: string): boolean {
|
|
227
|
+
const snap = loadSnapshot(repoRoot);
|
|
228
|
+
if (!snap) return true;
|
|
229
|
+
const liveHash = computeEventsHash(loadAllEvents(repoRoot));
|
|
230
|
+
return snap.events_hash !== liveHash;
|
|
231
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Schema v1 — see docs/superpowers/specs/2026-05-16-xera-v06-project-knowledge-graph-design.md §3
|
|
2
|
+
|
|
3
|
+
export const SCHEMA_VERSION = 1 as const;
|
|
4
|
+
|
|
5
|
+
export type Priority = 'p0' | 'p1' | 'p2';
|
|
6
|
+
export type ScenarioStatus = 'pass' | 'fail';
|
|
7
|
+
export type EdgeKind = 'tests' | 'uses' | 'covers' | 'modifies' | 'jira-linked' | 'similar' | 'ran';
|
|
8
|
+
|
|
9
|
+
export type Classification = 'REAL_BUG' | 'TEST_BUG' | 'SELECTOR_DRIFT' | 'FLAKY' | 'PASS';
|
|
10
|
+
// Note: TEST_OUTDATED is added in v0.6.1.
|
|
11
|
+
|
|
12
|
+
export interface TicketFetchedPayload {
|
|
13
|
+
ticketId: string;
|
|
14
|
+
summary: string;
|
|
15
|
+
ac: string[];
|
|
16
|
+
jiraLinks: Array<{
|
|
17
|
+
ticketId: string;
|
|
18
|
+
relation: 'blocks' | 'duplicates' | 'relates' | 'supersedes';
|
|
19
|
+
}>;
|
|
20
|
+
storyHash: string;
|
|
21
|
+
modifiesAreas: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TicketEnrichedPayload {
|
|
25
|
+
ticketId: string;
|
|
26
|
+
enrichedAt: string;
|
|
27
|
+
similarCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ScenarioGeneratedPayload {
|
|
31
|
+
scenarioId: string;
|
|
32
|
+
ticketId: string;
|
|
33
|
+
name: string;
|
|
34
|
+
gherkin: string;
|
|
35
|
+
priority: Priority;
|
|
36
|
+
featureHash: string;
|
|
37
|
+
generatedAt: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PomGeneratedPayload {
|
|
41
|
+
pomId: string;
|
|
42
|
+
ticketId: string;
|
|
43
|
+
filePath: string;
|
|
44
|
+
route: string;
|
|
45
|
+
locators: string[];
|
|
46
|
+
scope: 'local' | 'shared';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PomPromotedPayload {
|
|
50
|
+
pomId: string;
|
|
51
|
+
fromPath: string;
|
|
52
|
+
toPath: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RunCompletedPayload {
|
|
56
|
+
scenarioId: string;
|
|
57
|
+
ticketId: string;
|
|
58
|
+
runId: string;
|
|
59
|
+
status: ScenarioStatus;
|
|
60
|
+
traceId?: string;
|
|
61
|
+
runtime: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RunClassifiedPayload {
|
|
65
|
+
scenarioId: string;
|
|
66
|
+
runId: string;
|
|
67
|
+
classification: Classification;
|
|
68
|
+
confidence: 'low' | 'medium' | 'high';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ClassificationDisputedPayload {
|
|
72
|
+
runId: string;
|
|
73
|
+
scenarioId: string;
|
|
74
|
+
originalClassification: Classification;
|
|
75
|
+
disputedTo: Classification;
|
|
76
|
+
qaActor: string;
|
|
77
|
+
qaReason?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EdgeDiscoveredPayload {
|
|
81
|
+
kind: EdgeKind;
|
|
82
|
+
from: string;
|
|
83
|
+
to: string;
|
|
84
|
+
confidence?: number;
|
|
85
|
+
source: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type EventPayloadMap = {
|
|
89
|
+
'ticket.fetched': TicketFetchedPayload;
|
|
90
|
+
'ticket.enriched': TicketEnrichedPayload;
|
|
91
|
+
'scenario.generated': ScenarioGeneratedPayload;
|
|
92
|
+
'pom.generated': PomGeneratedPayload;
|
|
93
|
+
'pom.promoted': PomPromotedPayload;
|
|
94
|
+
'run.completed': RunCompletedPayload;
|
|
95
|
+
'run.classified': RunClassifiedPayload;
|
|
96
|
+
'classification.disputed': ClassificationDisputedPayload;
|
|
97
|
+
'edge.discovered': EdgeDiscoveredPayload;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type EventType = keyof EventPayloadMap;
|
|
101
|
+
|
|
102
|
+
export type Event = {
|
|
103
|
+
[K in EventType]: {
|
|
104
|
+
event_id: string;
|
|
105
|
+
schema_version: typeof SCHEMA_VERSION;
|
|
106
|
+
ts: string;
|
|
107
|
+
actor: string;
|
|
108
|
+
type: K;
|
|
109
|
+
payload: EventPayloadMap[K];
|
|
110
|
+
};
|
|
111
|
+
}[EventType];
|
|
112
|
+
|
|
113
|
+
export interface TicketNode {
|
|
114
|
+
id: string;
|
|
115
|
+
summary: string;
|
|
116
|
+
ac: string[];
|
|
117
|
+
storyHash: string;
|
|
118
|
+
modifiesAreas: string[];
|
|
119
|
+
fetchedAt: string;
|
|
120
|
+
enrichedAt?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ScenarioNode {
|
|
124
|
+
id: string;
|
|
125
|
+
ticketId: string;
|
|
126
|
+
name: string;
|
|
127
|
+
gherkin: string;
|
|
128
|
+
priority: Priority;
|
|
129
|
+
featureHash: string;
|
|
130
|
+
generatedAt: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface PomNode {
|
|
134
|
+
id: string;
|
|
135
|
+
ticketId: string;
|
|
136
|
+
filePath: string;
|
|
137
|
+
route: string;
|
|
138
|
+
locators: string[];
|
|
139
|
+
scope: 'local' | 'shared';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface AreaNode {
|
|
143
|
+
id: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface FailureNode {
|
|
147
|
+
id: string;
|
|
148
|
+
scenarioId: string;
|
|
149
|
+
runId: string;
|
|
150
|
+
traceId?: string;
|
|
151
|
+
ts: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface EdgeRecord {
|
|
155
|
+
kind: EdgeKind;
|
|
156
|
+
from: string;
|
|
157
|
+
to: string;
|
|
158
|
+
confidence?: number;
|
|
159
|
+
source: string;
|
|
160
|
+
discoveredAt: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface Snapshot {
|
|
164
|
+
schema_version: typeof SCHEMA_VERSION;
|
|
165
|
+
generated_at: string;
|
|
166
|
+
event_count: number;
|
|
167
|
+
events_hash: string;
|
|
168
|
+
tickets: Record<string, TicketNode>;
|
|
169
|
+
scenarios: Record<string, ScenarioNode>;
|
|
170
|
+
poms: Record<string, PomNode>;
|
|
171
|
+
areas: Record<string, AreaNode>;
|
|
172
|
+
edges: EdgeRecord[];
|
|
173
|
+
latest_failures: Record<string, FailureNode>;
|
|
174
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Crockford base32, monotonic-per-process. Spec-compliant 26-char output.
|
|
2
|
+
const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
3
|
+
let lastMs = 0;
|
|
4
|
+
let lastRand = new Uint8Array(10);
|
|
5
|
+
|
|
6
|
+
// Encodes 48-bit timestamp as 10 Crockford base32 chars (10 × 5 bits = 50 bits;
|
|
7
|
+
// the two leading bits are always zero for any ms ≤ 2^48-1, covering ~year 10000).
|
|
8
|
+
function timestampPart(ms: number): string {
|
|
9
|
+
let out = '';
|
|
10
|
+
for (let i = 9; i >= 0; i--) {
|
|
11
|
+
const shift = i * 5;
|
|
12
|
+
// Use bigint to safely shift 48-bit values without sign-extension issues
|
|
13
|
+
const part = Number((BigInt(ms) >> BigInt(shift)) & 0x1fn);
|
|
14
|
+
out += CROCKFORD[part];
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Encodes 10 random bytes (80 bits) as 16 Crockford base32 chars (16 × 5 = 80 bits exactly).
|
|
20
|
+
function encodeRandom(buf: Uint8Array): string {
|
|
21
|
+
let out = '';
|
|
22
|
+
let bitBuf = 0;
|
|
23
|
+
let bits = 0;
|
|
24
|
+
for (const b of buf) {
|
|
25
|
+
bitBuf = (bitBuf << 8) | b;
|
|
26
|
+
bits += 8;
|
|
27
|
+
while (bits >= 5) {
|
|
28
|
+
bits -= 5;
|
|
29
|
+
out += CROCKFORD[(bitBuf >> bits) & 0x1f];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function bumpRandom(prev: Uint8Array): Uint8Array {
|
|
36
|
+
const next = new Uint8Array(prev);
|
|
37
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
38
|
+
if (next[i]! === 0xff) {
|
|
39
|
+
next[i] = 0;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
next[i] = next[i]! + 1;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ulid(now: number = Date.now()): string {
|
|
49
|
+
let rand: Uint8Array;
|
|
50
|
+
if (now === lastMs) {
|
|
51
|
+
rand = bumpRandom(lastRand);
|
|
52
|
+
} else {
|
|
53
|
+
rand = new Uint8Array(crypto.getRandomValues(new Uint8Array(10)));
|
|
54
|
+
lastMs = now;
|
|
55
|
+
}
|
|
56
|
+
lastRand = new Uint8Array(rand);
|
|
57
|
+
return timestampPart(now) + encodeRandom(rand);
|
|
58
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
export const VERSION = '0.1.0';
|
|
2
2
|
export type * from './adapter/types';
|
|
3
|
-
export * from './config/schema';
|
|
4
|
-
export * from './config/define';
|
|
5
|
-
export * from './config/load';
|
|
6
|
-
export * from './artifact/paths';
|
|
7
3
|
export * from './artifact/hash';
|
|
8
4
|
export * from './artifact/meta';
|
|
5
|
+
export * from './artifact/paths';
|
|
9
6
|
export * from './artifact/status';
|
|
10
|
-
export * from './logging/ndjson-logger';
|
|
11
|
-
export * from './lock/file-lock';
|
|
12
|
-
export * from './jira/types';
|
|
13
|
-
export * from './jira/client';
|
|
14
|
-
export * from './jira/fields';
|
|
15
|
-
export * from './jira/retry';
|
|
16
7
|
export * from './auth/encrypt';
|
|
17
8
|
export * from './auth/key';
|
|
18
|
-
export * from './auth/state';
|
|
19
9
|
export * from './auth/refresh';
|
|
10
|
+
export * from './auth/state';
|
|
11
|
+
export * from './config/define';
|
|
12
|
+
export * from './config/load';
|
|
13
|
+
export * from './config/schema';
|
|
14
|
+
export * from './jira/client';
|
|
15
|
+
export * from './jira/fields';
|
|
16
|
+
export * from './jira/retry';
|
|
17
|
+
export * from './jira/types';
|
|
18
|
+
export * from './lock/file-lock';
|
|
19
|
+
export * from './logging/ndjson-logger';
|
package/src/jira/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { JiraClient } from './types';
|
|
2
1
|
import { createMcpBackend } from './mcp-backend';
|
|
3
2
|
import { createRestBackend } from './rest-backend';
|
|
3
|
+
import type { JiraClient } from './types';
|
|
4
4
|
|
|
5
5
|
export interface CreateJiraClientOptions {
|
|
6
6
|
baseUrl: string;
|
|
@@ -14,7 +14,9 @@ export async function createJiraClient(opts: CreateJiraClientOptions): Promise<J
|
|
|
14
14
|
if (mcp) return mcp;
|
|
15
15
|
}
|
|
16
16
|
if (!opts.rest) {
|
|
17
|
-
throw new Error(
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Atlassian MCP not connected and no REST credentials provided (JIRA_EMAIL + JIRA_API_TOKEN).',
|
|
19
|
+
);
|
|
18
20
|
}
|
|
19
21
|
return createRestBackend(opts.baseUrl, opts.rest);
|
|
20
22
|
}
|
package/src/jira/fields.ts
CHANGED
|
@@ -8,8 +8,10 @@ export interface JiraFieldInfo {
|
|
|
8
8
|
|
|
9
9
|
export function rankStoryCandidates(fields: JiraFieldInfo[]): JiraFieldInfo[] {
|
|
10
10
|
return fields
|
|
11
|
-
.filter(f => f.hasContent)
|
|
12
|
-
.filter(
|
|
11
|
+
.filter((f) => f.hasContent)
|
|
12
|
+
.filter(
|
|
13
|
+
(f) => !['attachment', 'comment', 'created', 'updated', 'reporter', 'creator'].includes(f.id),
|
|
14
|
+
)
|
|
13
15
|
.sort((a, b) => {
|
|
14
16
|
const ai = PREFERRED_STORY_IDS.indexOf(a.id);
|
|
15
17
|
const bi = PREFERRED_STORY_IDS.indexOf(b.id);
|
package/src/jira/mcp-backend.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import type { JiraClient, JiraTicket } from './types';
|