@xera-ai/core 0.4.3 → 0.5.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/artifact/status.d.ts +12 -0
- package/dist/artifact/status.d.ts.map +1 -1
- package/dist/bin/internal.js +1059 -567
- package/dist/bin-internal/auth-setup.d.ts +2 -0
- package/dist/bin-internal/auth-setup.d.ts.map +1 -0
- package/dist/bin-internal/disputes.d.ts +2 -0
- package/dist/bin-internal/disputes.d.ts.map +1 -0
- package/dist/bin-internal/doctor.d.ts +1 -1
- package/dist/bin-internal/doctor.d.ts.map +1 -1
- package/dist/bin-internal/exec.d.ts.map +1 -1
- package/dist/bin-internal/graph-record-script.d.ts.map +1 -1
- package/dist/bin-internal/graph-record.d.ts.map +1 -1
- package/dist/bin-internal/index.d.ts.map +1 -1
- package/dist/bin-internal/normalize.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/classifier/auth-expired.d.ts +12 -0
- package/dist/classifier/auth-expired.d.ts.map +1 -0
- package/dist/classifier/contract-drift.d.ts +35 -0
- package/dist/classifier/contract-drift.d.ts.map +1 -0
- package/dist/classifier/rate-limited.d.ts +15 -0
- package/dist/classifier/rate-limited.d.ts.map +1 -0
- package/dist/config/schema.d.ts +32 -3
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/graph/schema.d.ts +9 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/store.d.ts.map +1 -1
- package/dist/graph/types.d.ts +2 -1
- package/dist/graph/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/scrub/index.d.ts +2 -0
- package/dist/scrub/index.d.ts.map +1 -0
- package/dist/scrub/rules.d.ts +12 -0
- package/dist/scrub/rules.d.ts.map +1 -0
- package/dist/src/index.js +110 -5
- package/package.json +4 -3
- package/src/artifact/status.ts +3 -0
- package/src/bin-internal/auth-setup.ts +116 -0
- package/src/bin-internal/disputes.ts +88 -0
- package/src/bin-internal/doctor.ts +13 -1
- package/src/bin-internal/exec.ts +45 -9
- package/src/bin-internal/graph-record-script.ts +37 -8
- package/src/bin-internal/graph-record.ts +3 -0
- package/src/bin-internal/index.ts +4 -0
- package/src/bin-internal/normalize.ts +13 -1
- package/src/bin-internal/report.ts +94 -2
- package/src/bin-internal/verify-prompts.ts +2 -1
- package/src/classifier/aggregate.ts +3 -0
- package/src/classifier/auth-expired.ts +44 -0
- package/src/classifier/contract-drift.ts +111 -0
- package/src/classifier/rate-limited.ts +25 -0
- package/src/config/schema.ts +52 -9
- package/src/graph/schema.ts +3 -0
- package/src/graph/store.ts +8 -1
- package/src/graph/types.ts +5 -1
- package/src/index.ts +2 -0
- package/src/scrub/index.ts +1 -0
- package/src/scrub/rules.ts +69 -0
package/dist/bin/internal.js
CHANGED
|
@@ -19,15 +19,15 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
19
19
|
var __require = import.meta.require;
|
|
20
20
|
|
|
21
21
|
// src/graph/paths.ts
|
|
22
|
-
import { join } from "path";
|
|
22
|
+
import { join as join3 } from "path";
|
|
23
23
|
function graphPaths(repoRoot) {
|
|
24
|
-
const eventsDir =
|
|
24
|
+
const eventsDir = join3(repoRoot, ".xera/graph/events");
|
|
25
25
|
return {
|
|
26
26
|
eventsDir,
|
|
27
|
-
snapshotFile:
|
|
28
|
-
costLog:
|
|
29
|
-
eventsMonthDir: (yyyyMm) =>
|
|
30
|
-
eventFile: (ulid, skill, ticketId, yyyyMm) =>
|
|
27
|
+
snapshotFile: join3(repoRoot, ".xera/graph/snapshot.json"),
|
|
28
|
+
costLog: join3(repoRoot, ".xera/cost-log.jsonl"),
|
|
29
|
+
eventsMonthDir: (yyyyMm) => join3(eventsDir, yyyyMm),
|
|
30
|
+
eventFile: (ulid, skill, ticketId, yyyyMm) => join3(eventsDir, yyyyMm, `${ulid}-${skill}-${ticketId}.jsonl`)
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
function currentYyyyMm(now = new Date) {
|
|
@@ -41,7 +41,7 @@ var init_paths = () => {};
|
|
|
41
41
|
var SCHEMA_VERSION = 1;
|
|
42
42
|
|
|
43
43
|
// src/graph/schema.ts
|
|
44
|
-
import { z } from "zod";
|
|
44
|
+
import { z as z2 } from "zod";
|
|
45
45
|
function safeParseEvent(value) {
|
|
46
46
|
const r = EventSchema.safeParse(value);
|
|
47
47
|
if (r.success)
|
|
@@ -50,113 +50,116 @@ function safeParseEvent(value) {
|
|
|
50
50
|
}
|
|
51
51
|
var schemaV, iso, ticketFetched, ticketEnriched, scenarioGenerated, pomGenerated, pomPromoted, runCompleted, classification, runClassified, classificationDisputed, edgeDiscovered, base, EventSchema;
|
|
52
52
|
var init_schema = __esm(() => {
|
|
53
|
-
schemaV =
|
|
54
|
-
iso =
|
|
55
|
-
ticketFetched =
|
|
56
|
-
ticketId:
|
|
57
|
-
summary:
|
|
58
|
-
ac:
|
|
59
|
-
jiraLinks:
|
|
60
|
-
ticketId:
|
|
61
|
-
relation:
|
|
53
|
+
schemaV = z2.literal(SCHEMA_VERSION);
|
|
54
|
+
iso = z2.string().datetime({ offset: false });
|
|
55
|
+
ticketFetched = z2.object({
|
|
56
|
+
ticketId: z2.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
57
|
+
summary: z2.string(),
|
|
58
|
+
ac: z2.array(z2.string()),
|
|
59
|
+
jiraLinks: z2.array(z2.object({
|
|
60
|
+
ticketId: z2.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
|
|
61
|
+
relation: z2.enum(["blocks", "duplicates", "relates", "supersedes"])
|
|
62
62
|
})),
|
|
63
|
-
storyHash:
|
|
64
|
-
modifiesAreas:
|
|
63
|
+
storyHash: z2.string(),
|
|
64
|
+
modifiesAreas: z2.array(z2.string().regex(/^[a-z0-9-]+$/))
|
|
65
65
|
}).passthrough();
|
|
66
|
-
ticketEnriched =
|
|
67
|
-
ticketId:
|
|
66
|
+
ticketEnriched = z2.object({
|
|
67
|
+
ticketId: z2.string(),
|
|
68
68
|
enrichedAt: iso,
|
|
69
|
-
similarCount:
|
|
69
|
+
similarCount: z2.number().int().nonnegative()
|
|
70
70
|
}).passthrough();
|
|
71
|
-
scenarioGenerated =
|
|
72
|
-
scenarioId:
|
|
73
|
-
ticketId:
|
|
74
|
-
name:
|
|
75
|
-
gherkin:
|
|
76
|
-
priority:
|
|
77
|
-
featureHash:
|
|
71
|
+
scenarioGenerated = z2.object({
|
|
72
|
+
scenarioId: z2.string(),
|
|
73
|
+
ticketId: z2.string(),
|
|
74
|
+
name: z2.string(),
|
|
75
|
+
gherkin: z2.string(),
|
|
76
|
+
priority: z2.enum(["p0", "p1", "p2"]),
|
|
77
|
+
featureHash: z2.string(),
|
|
78
78
|
generatedAt: iso
|
|
79
79
|
}).passthrough();
|
|
80
|
-
pomGenerated =
|
|
81
|
-
pomId:
|
|
82
|
-
ticketId:
|
|
83
|
-
filePath:
|
|
84
|
-
route:
|
|
85
|
-
locators:
|
|
86
|
-
scope:
|
|
80
|
+
pomGenerated = z2.object({
|
|
81
|
+
pomId: z2.string(),
|
|
82
|
+
ticketId: z2.string(),
|
|
83
|
+
filePath: z2.string(),
|
|
84
|
+
route: z2.string(),
|
|
85
|
+
locators: z2.array(z2.string()),
|
|
86
|
+
scope: z2.enum(["local", "shared"])
|
|
87
87
|
}).passthrough();
|
|
88
|
-
pomPromoted =
|
|
89
|
-
pomId:
|
|
90
|
-
fromPath:
|
|
91
|
-
toPath:
|
|
88
|
+
pomPromoted = z2.object({
|
|
89
|
+
pomId: z2.string(),
|
|
90
|
+
fromPath: z2.string(),
|
|
91
|
+
toPath: z2.string()
|
|
92
92
|
}).passthrough();
|
|
93
|
-
runCompleted =
|
|
94
|
-
scenarioId:
|
|
95
|
-
ticketId:
|
|
96
|
-
runId:
|
|
97
|
-
status:
|
|
98
|
-
traceId:
|
|
99
|
-
runtime:
|
|
93
|
+
runCompleted = z2.object({
|
|
94
|
+
scenarioId: z2.string(),
|
|
95
|
+
ticketId: z2.string(),
|
|
96
|
+
runId: z2.string(),
|
|
97
|
+
status: z2.enum(["pass", "fail"]),
|
|
98
|
+
traceId: z2.string().optional(),
|
|
99
|
+
runtime: z2.number().nonnegative()
|
|
100
100
|
}).passthrough();
|
|
101
|
-
classification =
|
|
101
|
+
classification = z2.enum([
|
|
102
102
|
"REAL_BUG",
|
|
103
103
|
"TEST_BUG",
|
|
104
104
|
"SELECTOR_DRIFT",
|
|
105
105
|
"FLAKY",
|
|
106
106
|
"PASS",
|
|
107
|
-
"TEST_OUTDATED"
|
|
107
|
+
"TEST_OUTDATED",
|
|
108
|
+
"CONTRACT_DRIFT",
|
|
109
|
+
"RATE_LIMITED",
|
|
110
|
+
"AUTH_EXPIRED"
|
|
108
111
|
]);
|
|
109
|
-
runClassified =
|
|
110
|
-
scenarioId:
|
|
111
|
-
runId:
|
|
112
|
+
runClassified = z2.object({
|
|
113
|
+
scenarioId: z2.string(),
|
|
114
|
+
runId: z2.string(),
|
|
112
115
|
classification,
|
|
113
|
-
confidence:
|
|
116
|
+
confidence: z2.enum(["low", "medium", "high"])
|
|
114
117
|
}).passthrough();
|
|
115
|
-
classificationDisputed =
|
|
116
|
-
runId:
|
|
117
|
-
scenarioId:
|
|
118
|
+
classificationDisputed = z2.object({
|
|
119
|
+
runId: z2.string(),
|
|
120
|
+
scenarioId: z2.string(),
|
|
118
121
|
originalClassification: classification,
|
|
119
122
|
disputedTo: classification,
|
|
120
|
-
qaActor:
|
|
121
|
-
qaReason:
|
|
123
|
+
qaActor: z2.string(),
|
|
124
|
+
qaReason: z2.string().optional()
|
|
122
125
|
}).passthrough();
|
|
123
|
-
edgeDiscovered =
|
|
124
|
-
kind:
|
|
125
|
-
from:
|
|
126
|
-
to:
|
|
127
|
-
confidence:
|
|
128
|
-
source:
|
|
126
|
+
edgeDiscovered = z2.object({
|
|
127
|
+
kind: z2.enum(["tests", "uses", "covers", "modifies", "jira-linked", "similar", "ran"]),
|
|
128
|
+
from: z2.string(),
|
|
129
|
+
to: z2.string(),
|
|
130
|
+
confidence: z2.number().min(0).max(1).optional(),
|
|
131
|
+
source: z2.string()
|
|
129
132
|
}).passthrough();
|
|
130
133
|
base = {
|
|
131
|
-
event_id:
|
|
134
|
+
event_id: z2.string().min(20),
|
|
132
135
|
schema_version: schemaV,
|
|
133
136
|
ts: iso,
|
|
134
|
-
actor:
|
|
137
|
+
actor: z2.string()
|
|
135
138
|
};
|
|
136
|
-
EventSchema =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
EventSchema = z2.discriminatedUnion("type", [
|
|
140
|
+
z2.object({ ...base, type: z2.literal("ticket.fetched"), payload: ticketFetched }),
|
|
141
|
+
z2.object({ ...base, type: z2.literal("ticket.enriched"), payload: ticketEnriched }),
|
|
142
|
+
z2.object({ ...base, type: z2.literal("scenario.generated"), payload: scenarioGenerated }),
|
|
143
|
+
z2.object({ ...base, type: z2.literal("pom.generated"), payload: pomGenerated }),
|
|
144
|
+
z2.object({ ...base, type: z2.literal("pom.promoted"), payload: pomPromoted }),
|
|
145
|
+
z2.object({ ...base, type: z2.literal("run.completed"), payload: runCompleted }),
|
|
146
|
+
z2.object({ ...base, type: z2.literal("run.classified"), payload: runClassified }),
|
|
147
|
+
z2.object({
|
|
145
148
|
...base,
|
|
146
|
-
type:
|
|
149
|
+
type: z2.literal("classification.disputed"),
|
|
147
150
|
payload: classificationDisputed
|
|
148
151
|
}),
|
|
149
|
-
|
|
152
|
+
z2.object({ ...base, type: z2.literal("edge.discovered"), payload: edgeDiscovered })
|
|
150
153
|
]);
|
|
151
154
|
});
|
|
152
155
|
|
|
153
156
|
// src/graph/store.ts
|
|
154
157
|
import { createHash } from "crypto";
|
|
155
158
|
import {
|
|
156
|
-
existsSync as
|
|
157
|
-
mkdirSync
|
|
159
|
+
existsSync as existsSync3,
|
|
160
|
+
mkdirSync,
|
|
158
161
|
readdirSync,
|
|
159
|
-
readFileSync
|
|
162
|
+
readFileSync,
|
|
160
163
|
renameSync,
|
|
161
164
|
writeFileSync
|
|
162
165
|
} from "fs";
|
|
@@ -167,7 +170,7 @@ function appendEvents(repoRoot, events, opts) {
|
|
|
167
170
|
const paths = graphPaths(repoRoot);
|
|
168
171
|
const yyyyMm = currentYyyyMm(opts.now);
|
|
169
172
|
const monthDir = paths.eventsMonthDir(yyyyMm);
|
|
170
|
-
|
|
173
|
+
mkdirSync(monthDir, { recursive: true });
|
|
171
174
|
const ulid = events[0].event_id;
|
|
172
175
|
const finalPath = paths.eventFile(ulid, opts.skill, opts.ticketId, yyyyMm);
|
|
173
176
|
const tmpPath = `${finalPath}.tmp`;
|
|
@@ -180,7 +183,7 @@ function appendEvents(repoRoot, events, opts) {
|
|
|
180
183
|
}
|
|
181
184
|
function loadAllEvents(repoRoot) {
|
|
182
185
|
const paths = graphPaths(repoRoot);
|
|
183
|
-
if (!
|
|
186
|
+
if (!existsSync3(paths.eventsDir))
|
|
184
187
|
return [];
|
|
185
188
|
const files = [];
|
|
186
189
|
for (const monthDir of readdirSync(paths.eventsDir, { withFileTypes: true })) {
|
|
@@ -200,7 +203,7 @@ function loadAllEvents(repoRoot) {
|
|
|
200
203
|
const events = [];
|
|
201
204
|
for (const file of files) {
|
|
202
205
|
try {
|
|
203
|
-
const lines =
|
|
206
|
+
const lines = readFileSync(file, "utf8").split(`
|
|
204
207
|
`).filter(Boolean);
|
|
205
208
|
for (const line of lines) {
|
|
206
209
|
let parsed;
|
|
@@ -326,6 +329,13 @@ function deriveSnapshot(events) {
|
|
|
326
329
|
edges.push(ed);
|
|
327
330
|
break;
|
|
328
331
|
}
|
|
332
|
+
case "classification.disputed": {
|
|
333
|
+
const existing = latestFailures[e.payload.scenarioId];
|
|
334
|
+
if (existing && existing.runId === e.payload.runId) {
|
|
335
|
+
existing.disputed = true;
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
329
339
|
default:
|
|
330
340
|
break;
|
|
331
341
|
}
|
|
@@ -345,17 +355,17 @@ function deriveSnapshot(events) {
|
|
|
345
355
|
}
|
|
346
356
|
function writeSnapshot(repoRoot, snap) {
|
|
347
357
|
const paths = graphPaths(repoRoot);
|
|
348
|
-
|
|
358
|
+
mkdirSync(dirname(paths.snapshotFile), { recursive: true });
|
|
349
359
|
const tmp = `${paths.snapshotFile}.tmp`;
|
|
350
360
|
writeFileSync(tmp, JSON.stringify(snap, null, 2));
|
|
351
361
|
renameSync(tmp, paths.snapshotFile);
|
|
352
362
|
}
|
|
353
363
|
function loadSnapshot(repoRoot) {
|
|
354
364
|
const paths = graphPaths(repoRoot);
|
|
355
|
-
if (!
|
|
365
|
+
if (!existsSync3(paths.snapshotFile))
|
|
356
366
|
return null;
|
|
357
367
|
try {
|
|
358
|
-
return JSON.parse(
|
|
368
|
+
return JSON.parse(readFileSync(paths.snapshotFile, "utf8"));
|
|
359
369
|
} catch {
|
|
360
370
|
return null;
|
|
361
371
|
}
|
|
@@ -429,21 +439,29 @@ var exports_graph_record_script = {};
|
|
|
429
439
|
__export(exports_graph_record_script, {
|
|
430
440
|
recordScriptImpl: () => recordScriptImpl
|
|
431
441
|
});
|
|
432
|
-
import { createHash as
|
|
433
|
-
import { existsSync as
|
|
434
|
-
import { basename, join as
|
|
442
|
+
import { createHash as createHash2 } from "crypto";
|
|
443
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
444
|
+
import { basename, join as join5 } from "path";
|
|
445
|
+
function inferPriority(name, gherkin) {
|
|
446
|
+
const haystack = `${name} ${gherkin}`.toLowerCase();
|
|
447
|
+
for (const kw of P0_KEYWORDS) {
|
|
448
|
+
if (haystack.includes(kw))
|
|
449
|
+
return "p0";
|
|
450
|
+
}
|
|
451
|
+
return "p1";
|
|
452
|
+
}
|
|
435
453
|
function parseFeature(text) {
|
|
436
454
|
const scenarios = [];
|
|
437
455
|
const lines = text.split(`
|
|
438
456
|
`);
|
|
439
|
-
let
|
|
457
|
+
let explicitTag = null;
|
|
440
458
|
let i = 0;
|
|
441
459
|
while (i < lines.length) {
|
|
442
460
|
const line = lines[i].trim();
|
|
443
461
|
if (line.startsWith("@")) {
|
|
444
462
|
const tag = line.slice(1).split(/\s+/)[0].toLowerCase();
|
|
445
463
|
if (tag === "p0" || tag === "p1" || tag === "p2")
|
|
446
|
-
|
|
464
|
+
explicitTag = tag;
|
|
447
465
|
i++;
|
|
448
466
|
continue;
|
|
449
467
|
}
|
|
@@ -453,13 +471,11 @@ function parseFeature(text) {
|
|
|
453
471
|
i++;
|
|
454
472
|
while (i < lines.length && !lines[i].trim().startsWith("Scenario") && !lines[i].trim().startsWith("@"))
|
|
455
473
|
i++;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
});
|
|
462
|
-
currentTagPriority = "p1";
|
|
474
|
+
const gherkin = lines.slice(start, i).join(`
|
|
475
|
+
`);
|
|
476
|
+
const priority = explicitTag !== null ? explicitTag : inferPriority(name, gherkin);
|
|
477
|
+
scenarios.push({ name, priority, gherkin });
|
|
478
|
+
explicitTag = null;
|
|
463
479
|
continue;
|
|
464
480
|
}
|
|
465
481
|
i++;
|
|
@@ -467,9 +483,9 @@ function parseFeature(text) {
|
|
|
467
483
|
return scenarios;
|
|
468
484
|
}
|
|
469
485
|
function listPomFiles(dir) {
|
|
470
|
-
if (!
|
|
486
|
+
if (!existsSync6(dir))
|
|
471
487
|
return [];
|
|
472
|
-
return
|
|
488
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".ts")).map((f) => join5(dir, f));
|
|
473
489
|
}
|
|
474
490
|
function extractRoute(pomContent) {
|
|
475
491
|
const m = pomContent.match(/goto\s*\(\s*['"]([^'"]+)['"]/);
|
|
@@ -496,15 +512,15 @@ function extractPomUsage(specContent) {
|
|
|
496
512
|
return [...names];
|
|
497
513
|
}
|
|
498
514
|
async function recordScriptImpl(repoRoot, ticket) {
|
|
499
|
-
const ticketDir =
|
|
500
|
-
const featurePath =
|
|
501
|
-
const specPath =
|
|
502
|
-
const pomDir =
|
|
503
|
-
if (!
|
|
515
|
+
const ticketDir = join5(repoRoot, ".xera", ticket);
|
|
516
|
+
const featurePath = join5(ticketDir, "feature", `${ticket}.feature`);
|
|
517
|
+
const specPath = join5(ticketDir, "tests", `${ticket}.spec.ts`);
|
|
518
|
+
const pomDir = join5(ticketDir, "poms");
|
|
519
|
+
if (!existsSync6(featurePath)) {
|
|
504
520
|
console.error(`[graph-record script] feature missing`);
|
|
505
521
|
return 1;
|
|
506
522
|
}
|
|
507
|
-
const featureText =
|
|
523
|
+
const featureText = readFileSync4(featurePath, "utf8");
|
|
508
524
|
const featureHash = sha1(featureText);
|
|
509
525
|
const scenarios = parseFeature(featureText);
|
|
510
526
|
const events = [];
|
|
@@ -524,7 +540,7 @@ async function recordScriptImpl(repoRoot, ticket) {
|
|
|
524
540
|
const pomFiles = listPomFiles(pomDir);
|
|
525
541
|
const pomNameToId = new Map;
|
|
526
542
|
for (const pomFile of pomFiles) {
|
|
527
|
-
const content =
|
|
543
|
+
const content = readFileSync4(pomFile, "utf8");
|
|
528
544
|
const id = pId(pomFile);
|
|
529
545
|
const className = content.match(/export\s+class\s+([A-Z][A-Za-z0-9]*Page)/)?.[1] ?? "";
|
|
530
546
|
pomNameToId.set(className, id);
|
|
@@ -538,8 +554,8 @@ async function recordScriptImpl(repoRoot, ticket) {
|
|
|
538
554
|
};
|
|
539
555
|
events.push(mk("xera-script", "pom.generated", pg));
|
|
540
556
|
}
|
|
541
|
-
if (
|
|
542
|
-
const specContent =
|
|
557
|
+
if (existsSync6(specPath)) {
|
|
558
|
+
const specContent = readFileSync4(specPath, "utf8");
|
|
543
559
|
const usedPoms = extractPomUsage(specContent);
|
|
544
560
|
for (const scenario of scenarios) {
|
|
545
561
|
const scId = sId(ticket, scenario.name);
|
|
@@ -571,17 +587,39 @@ async function recordScriptImpl(repoRoot, ticket) {
|
|
|
571
587
|
appendEvents(repoRoot, events, { skill: "xera-script", ticketId: ticket });
|
|
572
588
|
return 0;
|
|
573
589
|
}
|
|
574
|
-
var sha1 = (s) =>
|
|
590
|
+
var sha1 = (s) => createHash2("sha1").update(s).digest("hex"), sId = (ticket, name) => sha1(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, " ")}`), pId = (file) => sha1(basename(file)), nowIso = () => new Date().toISOString(), mk = (actor, type, payload) => ({
|
|
575
591
|
event_id: ulid(),
|
|
576
592
|
schema_version: SCHEMA_VERSION,
|
|
577
593
|
ts: nowIso(),
|
|
578
594
|
actor,
|
|
579
595
|
type,
|
|
580
596
|
payload
|
|
581
|
-
});
|
|
597
|
+
}), P0_KEYWORDS;
|
|
582
598
|
var init_graph_record_script = __esm(() => {
|
|
583
599
|
init_store();
|
|
584
600
|
init_ulid();
|
|
601
|
+
P0_KEYWORDS = [
|
|
602
|
+
"log in",
|
|
603
|
+
"login",
|
|
604
|
+
"sign in",
|
|
605
|
+
"signin",
|
|
606
|
+
"sign up",
|
|
607
|
+
"signup",
|
|
608
|
+
"auth",
|
|
609
|
+
"authentic",
|
|
610
|
+
"payment",
|
|
611
|
+
"pay ",
|
|
612
|
+
"checkout",
|
|
613
|
+
"purchase",
|
|
614
|
+
"charge",
|
|
615
|
+
"password",
|
|
616
|
+
"credential",
|
|
617
|
+
"admin",
|
|
618
|
+
"permission",
|
|
619
|
+
"role",
|
|
620
|
+
"must ",
|
|
621
|
+
"critical"
|
|
622
|
+
];
|
|
585
623
|
});
|
|
586
624
|
|
|
587
625
|
// ../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
|
|
@@ -7580,14 +7618,14 @@ __export(exports_graph_record, {
|
|
|
7580
7618
|
recordFetch: () => recordFetch,
|
|
7581
7619
|
graphRecordCmd: () => graphRecordCmd
|
|
7582
7620
|
});
|
|
7583
|
-
import { createHash as
|
|
7584
|
-
import { existsSync as
|
|
7585
|
-
import { basename as basename2, join as
|
|
7621
|
+
import { createHash as createHash3 } from "crypto";
|
|
7622
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
7623
|
+
import { basename as basename2, join as join6 } from "path";
|
|
7586
7624
|
function nowIso2() {
|
|
7587
7625
|
return new Date().toISOString();
|
|
7588
7626
|
}
|
|
7589
7627
|
function sha12(s) {
|
|
7590
|
-
return
|
|
7628
|
+
return createHash3("sha1").update(s).digest("hex");
|
|
7591
7629
|
}
|
|
7592
7630
|
function scenarioId(ticket, name) {
|
|
7593
7631
|
return sha12(`${ticket}:${name.trim().toLowerCase().replace(/\s+/g, " ")}`);
|
|
@@ -7606,21 +7644,21 @@ function makeEvent(actor, type, payload) {
|
|
|
7606
7644
|
};
|
|
7607
7645
|
}
|
|
7608
7646
|
function readStoryFrontmatter(repoRoot, ticket) {
|
|
7609
|
-
const path =
|
|
7610
|
-
if (!
|
|
7647
|
+
const path = join6(repoRoot, ".xera", ticket, "story.md");
|
|
7648
|
+
if (!existsSync7(path))
|
|
7611
7649
|
return null;
|
|
7612
|
-
const raw =
|
|
7650
|
+
const raw = readFileSync5(path, "utf8");
|
|
7613
7651
|
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
7614
7652
|
if (!m)
|
|
7615
7653
|
return null;
|
|
7616
7654
|
return $parse(m[1]);
|
|
7617
7655
|
}
|
|
7618
7656
|
function readGraphInput(repoRoot, ticket) {
|
|
7619
|
-
const path =
|
|
7620
|
-
if (!
|
|
7657
|
+
const path = join6(repoRoot, ".xera", ticket, "graph-input.json");
|
|
7658
|
+
if (!existsSync7(path))
|
|
7621
7659
|
return { modifiesAreas: [] };
|
|
7622
7660
|
try {
|
|
7623
|
-
return JSON.parse(
|
|
7661
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
7624
7662
|
} catch {
|
|
7625
7663
|
return { modifiesAreas: [] };
|
|
7626
7664
|
}
|
|
@@ -7668,12 +7706,12 @@ async function recordScript(repoRoot, ticket) {
|
|
|
7668
7706
|
return recordScriptImpl2(repoRoot, ticket);
|
|
7669
7707
|
}
|
|
7670
7708
|
async function recordExec(repoRoot, ticket, runId) {
|
|
7671
|
-
const reporterPath =
|
|
7672
|
-
if (!
|
|
7709
|
+
const reporterPath = join6(repoRoot, ".xera", ticket, "runs", runId, "reporter.json");
|
|
7710
|
+
if (!existsSync7(reporterPath)) {
|
|
7673
7711
|
console.error(`[graph-record exec] reporter.json missing`);
|
|
7674
7712
|
return 1;
|
|
7675
7713
|
}
|
|
7676
|
-
const data = JSON.parse(
|
|
7714
|
+
const data = JSON.parse(readFileSync5(reporterPath, "utf8"));
|
|
7677
7715
|
const events = [];
|
|
7678
7716
|
for (const s of data.scenarios) {
|
|
7679
7717
|
const p = {
|
|
@@ -7691,12 +7729,12 @@ async function recordExec(repoRoot, ticket, runId) {
|
|
|
7691
7729
|
return 0;
|
|
7692
7730
|
}
|
|
7693
7731
|
async function recordClassify(repoRoot, ticket, runId) {
|
|
7694
|
-
const classifyPath =
|
|
7695
|
-
if (!
|
|
7732
|
+
const classifyPath = join6(repoRoot, ".xera", ticket, "runs", runId, "classifier-output.json");
|
|
7733
|
+
if (!existsSync7(classifyPath)) {
|
|
7696
7734
|
console.error(`[graph-record classify] classifier-output.json missing`);
|
|
7697
7735
|
return 1;
|
|
7698
7736
|
}
|
|
7699
|
-
const data = JSON.parse(
|
|
7737
|
+
const data = JSON.parse(readFileSync5(classifyPath, "utf8"));
|
|
7700
7738
|
const events = [];
|
|
7701
7739
|
for (const s of data.scenarios) {
|
|
7702
7740
|
const p = {
|
|
@@ -7724,7 +7762,7 @@ async function recordPromote(repoRoot, args) {
|
|
|
7724
7762
|
appendEvents(repoRoot, [e], { skill: "xera-promote", ticketId: "shared" });
|
|
7725
7763
|
return 0;
|
|
7726
7764
|
}
|
|
7727
|
-
function
|
|
7765
|
+
function parseFlags(args) {
|
|
7728
7766
|
const m = new Map;
|
|
7729
7767
|
for (let i = 0;i < args.length; i++) {
|
|
7730
7768
|
if (args[i].startsWith("--")) {
|
|
@@ -7760,7 +7798,7 @@ async function graphRecordCmd(argv) {
|
|
|
7760
7798
|
}
|
|
7761
7799
|
case "exec": {
|
|
7762
7800
|
const ticket = rest[0];
|
|
7763
|
-
const flags =
|
|
7801
|
+
const flags = parseFlags(rest);
|
|
7764
7802
|
const runId = flags.get("--run-id");
|
|
7765
7803
|
if (!ticket || !runId) {
|
|
7766
7804
|
console.error("ticket + --run-id required");
|
|
@@ -7770,7 +7808,7 @@ async function graphRecordCmd(argv) {
|
|
|
7770
7808
|
}
|
|
7771
7809
|
case "classify": {
|
|
7772
7810
|
const ticket = rest[0];
|
|
7773
|
-
const flags =
|
|
7811
|
+
const flags = parseFlags(rest);
|
|
7774
7812
|
const runId = flags.get("--run-id");
|
|
7775
7813
|
if (!ticket || !runId) {
|
|
7776
7814
|
console.error("ticket + --run-id required");
|
|
@@ -7779,10 +7817,10 @@ async function graphRecordCmd(argv) {
|
|
|
7779
7817
|
return recordClassify(repoRoot, ticket, runId);
|
|
7780
7818
|
}
|
|
7781
7819
|
case "promote": {
|
|
7782
|
-
return recordPromote(repoRoot,
|
|
7820
|
+
return recordPromote(repoRoot, parseFlags(rest));
|
|
7783
7821
|
}
|
|
7784
7822
|
case "dispute": {
|
|
7785
|
-
const flags =
|
|
7823
|
+
const flags = parseFlags(rest);
|
|
7786
7824
|
const runId = flags.get("--run-id");
|
|
7787
7825
|
const scenarioIdArg = flags.get("--scenario-id");
|
|
7788
7826
|
const from = flags.get("--from");
|
|
@@ -7799,7 +7837,10 @@ async function graphRecordCmd(argv) {
|
|
|
7799
7837
|
"SELECTOR_DRIFT",
|
|
7800
7838
|
"FLAKY",
|
|
7801
7839
|
"PASS",
|
|
7802
|
-
"TEST_OUTDATED"
|
|
7840
|
+
"TEST_OUTDATED",
|
|
7841
|
+
"CONTRACT_DRIFT",
|
|
7842
|
+
"RATE_LIMITED",
|
|
7843
|
+
"AUTH_EXPIRED"
|
|
7803
7844
|
];
|
|
7804
7845
|
if (!validClass.includes(from) || !validClass.includes(to)) {
|
|
7805
7846
|
console.error(`[graph-record dispute] --from and --to must be one of: ${validClass.join(", ")}`);
|
|
@@ -7829,20 +7870,351 @@ var init_graph_record = __esm(() => {
|
|
|
7829
7870
|
init_ulid();
|
|
7830
7871
|
});
|
|
7831
7872
|
|
|
7873
|
+
// src/bin-internal/graph-backfill.ts
|
|
7874
|
+
var exports_graph_backfill = {};
|
|
7875
|
+
__export(exports_graph_backfill, {
|
|
7876
|
+
graphBackfillCmd: () => graphBackfillCmd
|
|
7877
|
+
});
|
|
7878
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
|
|
7879
|
+
import { join as join7 } from "path";
|
|
7880
|
+
async function backfillTicket(repoRoot, ticket, dryRun) {
|
|
7881
|
+
const storyPath = join7(repoRoot, ".xera", ticket, "story.md");
|
|
7882
|
+
if (!existsSync8(storyPath))
|
|
7883
|
+
return 0;
|
|
7884
|
+
const { recordFetch: recordFetch2 } = await Promise.resolve().then(() => (init_graph_record(), exports_graph_record));
|
|
7885
|
+
if (dryRun) {
|
|
7886
|
+
console.log(`[backfill dry-run] would backfill ${ticket}`);
|
|
7887
|
+
return 0;
|
|
7888
|
+
}
|
|
7889
|
+
await recordScriptImpl(repoRoot, ticket);
|
|
7890
|
+
await recordFetch2(repoRoot, ticket);
|
|
7891
|
+
return 0;
|
|
7892
|
+
}
|
|
7893
|
+
async function graphBackfillCmd(argv) {
|
|
7894
|
+
const dryRun = argv.includes("--dry-run");
|
|
7895
|
+
const repoRoot = process.cwd();
|
|
7896
|
+
const xeraDir = join7(repoRoot, ".xera");
|
|
7897
|
+
if (!existsSync8(xeraDir)) {
|
|
7898
|
+
console.log("[backfill] no .xera/ directory");
|
|
7899
|
+
return 0;
|
|
7900
|
+
}
|
|
7901
|
+
const tickets = [];
|
|
7902
|
+
for (const entry of readdirSync3(xeraDir, { withFileTypes: true })) {
|
|
7903
|
+
if (!entry.isDirectory())
|
|
7904
|
+
continue;
|
|
7905
|
+
if (entry.name === "graph")
|
|
7906
|
+
continue;
|
|
7907
|
+
if (entry.name.startsWith("."))
|
|
7908
|
+
continue;
|
|
7909
|
+
if (!/^[A-Z]+-\d+$/.test(entry.name))
|
|
7910
|
+
continue;
|
|
7911
|
+
tickets.push(entry.name);
|
|
7912
|
+
}
|
|
7913
|
+
console.log(`[backfill] found ${tickets.length} tickets`);
|
|
7914
|
+
for (const t of tickets)
|
|
7915
|
+
await backfillTicket(repoRoot, t, dryRun);
|
|
7916
|
+
console.log(`[backfill] done`);
|
|
7917
|
+
return 0;
|
|
7918
|
+
}
|
|
7919
|
+
var init_graph_backfill = __esm(() => {
|
|
7920
|
+
init_graph_record_script();
|
|
7921
|
+
});
|
|
7922
|
+
|
|
7923
|
+
// src/bin-internal/auth-setup.ts
|
|
7924
|
+
import { existsSync as existsSync2 } from "fs";
|
|
7925
|
+
import { join as join2 } from "path";
|
|
7926
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
7927
|
+
|
|
7928
|
+
// src/config/load.ts
|
|
7929
|
+
import { existsSync } from "fs";
|
|
7930
|
+
import { join } from "path";
|
|
7931
|
+
import { pathToFileURL } from "url";
|
|
7932
|
+
|
|
7933
|
+
// src/config/schema.ts
|
|
7934
|
+
import { z } from "zod";
|
|
7935
|
+
var AuthRoleSchema = z.object({
|
|
7936
|
+
envEmail: z.string().min(1),
|
|
7937
|
+
envPassword: z.string().min(1)
|
|
7938
|
+
});
|
|
7939
|
+
var AuthSchema = z.object({
|
|
7940
|
+
strategy: z.enum(["storageState", "apiToken", "none"]).default("none"),
|
|
7941
|
+
ttl: z.string().default("8h"),
|
|
7942
|
+
refreshBuffer: z.string().default("30m"),
|
|
7943
|
+
setupScript: z.string().optional(),
|
|
7944
|
+
roles: z.record(z.string(), AuthRoleSchema).default({})
|
|
7945
|
+
});
|
|
7946
|
+
var WebSchema = z.object({
|
|
7947
|
+
baseUrl: z.record(z.string(), z.string().url()).refine((m) => Object.keys(m).length > 0, {
|
|
7948
|
+
message: "baseUrl must have at least one environment"
|
|
7949
|
+
}),
|
|
7950
|
+
defaultEnv: z.string(),
|
|
7951
|
+
auth: AuthSchema.prefault({}),
|
|
7952
|
+
testData: z.object({
|
|
7953
|
+
users: z.record(z.string(), z.object({ fromAuth: z.string() })).default({})
|
|
7954
|
+
}).prefault({})
|
|
7955
|
+
}).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
|
|
7956
|
+
message: "defaultEnv must exist in baseUrl map",
|
|
7957
|
+
path: ["defaultEnv"]
|
|
7958
|
+
});
|
|
7959
|
+
var HttpAuthRoleSchema = z.object({
|
|
7960
|
+
tokenEnv: z.string().optional(),
|
|
7961
|
+
userEnv: z.string().optional(),
|
|
7962
|
+
passEnv: z.string().optional(),
|
|
7963
|
+
tokenUrl: z.string().url().optional(),
|
|
7964
|
+
clientIdEnv: z.string().optional(),
|
|
7965
|
+
clientSecretEnv: z.string().optional(),
|
|
7966
|
+
scope: z.string().optional()
|
|
7967
|
+
});
|
|
7968
|
+
var HttpAuthSchema = z.object({
|
|
7969
|
+
strategy: z.enum(["bearer", "apiKey", "basic", "oauth-cc", "custom", "none"]).default("none"),
|
|
7970
|
+
ttl: z.string().default("8h"),
|
|
7971
|
+
refreshBuffer: z.string().default("30m"),
|
|
7972
|
+
roles: z.record(z.string(), HttpAuthRoleSchema).default({})
|
|
7973
|
+
});
|
|
7974
|
+
var HttpSchema = z.object({
|
|
7975
|
+
baseUrl: z.record(z.string(), z.string().url()).refine((m) => Object.keys(m).length > 0, {
|
|
7976
|
+
message: "baseUrl must have at least one environment"
|
|
7977
|
+
}),
|
|
7978
|
+
defaultEnv: z.string(),
|
|
7979
|
+
spec: z.string().optional(),
|
|
7980
|
+
auth: HttpAuthSchema.prefault({})
|
|
7981
|
+
}).refine((h) => h.baseUrl[h.defaultEnv] !== undefined, {
|
|
7982
|
+
message: "defaultEnv must exist in baseUrl map",
|
|
7983
|
+
path: ["defaultEnv"]
|
|
7984
|
+
});
|
|
7985
|
+
var JiraSchema = z.object({
|
|
7986
|
+
baseUrl: z.string().url(),
|
|
7987
|
+
projectKeys: z.array(z.string().min(1)).min(1),
|
|
7988
|
+
fields: z.object({
|
|
7989
|
+
story: z.string().min(1),
|
|
7990
|
+
acceptanceCriteria: z.string().optional(),
|
|
7991
|
+
attachments: z.string().default("attachment")
|
|
7992
|
+
})
|
|
7993
|
+
});
|
|
7994
|
+
var AISchema = z.object({
|
|
7995
|
+
livePageSnapshot: z.boolean().default(true),
|
|
7996
|
+
confidenceThreshold: z.enum(["low", "medium", "high"]).default("medium"),
|
|
7997
|
+
maxRetries: z.object({
|
|
7998
|
+
typecheck: z.number().int().min(0).max(5).default(2),
|
|
7999
|
+
lint: z.number().int().min(0).max(5).default(2),
|
|
8000
|
+
validateFeature: z.number().int().min(0).max(5).default(2)
|
|
8001
|
+
}).prefault({})
|
|
8002
|
+
}).prefault({});
|
|
8003
|
+
var ReportingSchema = z.object({
|
|
8004
|
+
language: z.enum(["en", "vi"]).default("en"),
|
|
8005
|
+
postToJira: z.boolean().default(true),
|
|
8006
|
+
transition: z.object({
|
|
8007
|
+
onPass: z.string().nullable().default(null),
|
|
8008
|
+
onFail: z.string().nullable().default(null)
|
|
8009
|
+
}).prefault({}),
|
|
8010
|
+
artifactLinks: z.enum(["git", "local"]).default("git")
|
|
8011
|
+
}).prefault({});
|
|
8012
|
+
var RunSchema = z.object({
|
|
8013
|
+
autoImpact: z.object({
|
|
8014
|
+
enabled: z.boolean().default(true),
|
|
8015
|
+
threshold: z.number().nonnegative().default(8)
|
|
8016
|
+
}).prefault({})
|
|
8017
|
+
}).prefault({});
|
|
8018
|
+
var XeraConfigSchema = z.object({
|
|
8019
|
+
jira: JiraSchema,
|
|
8020
|
+
web: WebSchema.optional(),
|
|
8021
|
+
http: HttpSchema.optional(),
|
|
8022
|
+
ai: AISchema,
|
|
8023
|
+
reporting: ReportingSchema,
|
|
8024
|
+
run: RunSchema.prefault({}),
|
|
8025
|
+
adapters: z.array(z.enum(["web", "http"])).min(1).default(["web"])
|
|
8026
|
+
}).refine((c) => c.web !== undefined || c.http !== undefined, {
|
|
8027
|
+
message: "At least one of `web` or `http` must be configured"
|
|
8028
|
+
}).refine((c) => c.adapters.every((a) => (a === "web" ? c.web : c.http) !== undefined), {
|
|
8029
|
+
message: "Every adapter in `adapters` must have a corresponding config block",
|
|
8030
|
+
path: ["adapters"]
|
|
8031
|
+
});
|
|
8032
|
+
|
|
8033
|
+
// src/config/load.ts
|
|
8034
|
+
async function loadConfig(cwd) {
|
|
8035
|
+
const path = join(cwd, "xera.config.ts");
|
|
8036
|
+
if (!existsSync(path)) {
|
|
8037
|
+
throw new Error(`xera.config.ts not found in ${cwd}`);
|
|
8038
|
+
}
|
|
8039
|
+
const mod = await import(pathToFileURL(path).href);
|
|
8040
|
+
const raw = mod.default ?? mod;
|
|
8041
|
+
return XeraConfigSchema.parse(raw);
|
|
8042
|
+
}
|
|
8043
|
+
|
|
8044
|
+
// src/bin-internal/auth-setup.ts
|
|
8045
|
+
function parseOpts(argv) {
|
|
8046
|
+
const opts = { shape: "all" };
|
|
8047
|
+
for (let i = 0;i < argv.length; i++) {
|
|
8048
|
+
const a = argv[i];
|
|
8049
|
+
const next = argv[i + 1];
|
|
8050
|
+
if (a === "--role" && next) {
|
|
8051
|
+
opts.role = next;
|
|
8052
|
+
i++;
|
|
8053
|
+
} else if (a === "--shape" && next) {
|
|
8054
|
+
if (next === "web" || next === "http" || next === "all")
|
|
8055
|
+
opts.shape = next;
|
|
8056
|
+
i++;
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
return opts;
|
|
8060
|
+
}
|
|
8061
|
+
async function authSetupCmd(argv) {
|
|
8062
|
+
const opts = parseOpts(argv);
|
|
8063
|
+
const cwd = process.cwd();
|
|
8064
|
+
const config = await loadConfig(cwd);
|
|
8065
|
+
const authSetupScript = join2(cwd, "shared", "auth-setup.ts");
|
|
8066
|
+
if (!existsSync2(authSetupScript)) {
|
|
8067
|
+
console.error(`[xera:auth-setup] auth-setup.ts not found at ${authSetupScript}. Run 'bunx @xera-ai/cli init' first.`);
|
|
8068
|
+
return 1;
|
|
8069
|
+
}
|
|
8070
|
+
const mod = await import(pathToFileURL2(authSetupScript).href);
|
|
8071
|
+
let exitCode = 0;
|
|
8072
|
+
if ((opts.shape === "all" || opts.shape === "web") && config.web && typeof mod.web === "function") {
|
|
8073
|
+
const { runAuthSetup } = await import("@xera-ai/web");
|
|
8074
|
+
const { chromium } = await import("@playwright/test");
|
|
8075
|
+
const browser = await chromium.launch();
|
|
8076
|
+
try {
|
|
8077
|
+
for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
|
|
8078
|
+
if (opts.role && roleName !== opts.role)
|
|
8079
|
+
continue;
|
|
8080
|
+
const email = process.env[roleCreds.envEmail];
|
|
8081
|
+
const password = process.env[roleCreds.envPassword];
|
|
8082
|
+
if (!email || !password) {
|
|
8083
|
+
console.error(`[xera:auth-setup] missing env vars ${roleCreds.envEmail} / ${roleCreds.envPassword} for role '${roleName}'`);
|
|
8084
|
+
exitCode = 1;
|
|
8085
|
+
continue;
|
|
8086
|
+
}
|
|
8087
|
+
try {
|
|
8088
|
+
await runAuthSetup({
|
|
8089
|
+
role: roleName,
|
|
8090
|
+
creds: { email, password },
|
|
8091
|
+
setupScriptPath: authSetupScript,
|
|
8092
|
+
authDir: join2(cwd, ".xera", ".auth"),
|
|
8093
|
+
browser
|
|
8094
|
+
});
|
|
8095
|
+
console.log(`[xera:auth-setup] \u2713 ${roleName}.json (web)`);
|
|
8096
|
+
} catch (e) {
|
|
8097
|
+
console.error(`[xera:auth-setup] \u2717 web/${roleName}: ${e.message}`);
|
|
8098
|
+
exitCode = 1;
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
} finally {
|
|
8102
|
+
await browser.close();
|
|
8103
|
+
}
|
|
8104
|
+
}
|
|
8105
|
+
if ((opts.shape === "all" || opts.shape === "http") && config.http && typeof mod.http === "function") {
|
|
8106
|
+
globalThis.__XERA_HTTP_CONFIG__ = config.http;
|
|
8107
|
+
const { runHttpAuthSetup } = await import("@xera-ai/http");
|
|
8108
|
+
for (const roleName of Object.keys(config.http.auth.roles)) {
|
|
8109
|
+
if (opts.role && roleName !== opts.role)
|
|
8110
|
+
continue;
|
|
8111
|
+
try {
|
|
8112
|
+
await runHttpAuthSetup({
|
|
8113
|
+
authDir: join2(cwd, ".xera", ".auth"),
|
|
8114
|
+
role: roleName,
|
|
8115
|
+
config: config.http,
|
|
8116
|
+
setupFn: mod.http,
|
|
8117
|
+
creds: { email: "", password: "" }
|
|
8118
|
+
});
|
|
8119
|
+
console.log(`[xera:auth-setup] \u2713 http/${roleName}.json`);
|
|
8120
|
+
} catch (e) {
|
|
8121
|
+
console.error(`[xera:auth-setup] \u2717 http/${roleName}: ${e.message}`);
|
|
8122
|
+
exitCode = 1;
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
}
|
|
8126
|
+
return exitCode;
|
|
8127
|
+
}
|
|
8128
|
+
|
|
8129
|
+
// src/bin-internal/disputes.ts
|
|
8130
|
+
init_store();
|
|
8131
|
+
function parseDuration(s) {
|
|
8132
|
+
const match = s.match(/^(\d+)([dhm])$/);
|
|
8133
|
+
if (!match)
|
|
8134
|
+
return 0;
|
|
8135
|
+
const n = Number.parseInt(match[1], 10);
|
|
8136
|
+
const unit = match[2];
|
|
8137
|
+
if (unit === "d")
|
|
8138
|
+
return n * 86400 * 1000;
|
|
8139
|
+
if (unit === "h")
|
|
8140
|
+
return n * 3600 * 1000;
|
|
8141
|
+
if (unit === "m")
|
|
8142
|
+
return n * 60 * 1000;
|
|
8143
|
+
return 0;
|
|
8144
|
+
}
|
|
8145
|
+
function eventToRow(e) {
|
|
8146
|
+
const p = e.payload;
|
|
8147
|
+
const row = {
|
|
8148
|
+
ts: e.ts,
|
|
8149
|
+
runId: p.runId,
|
|
8150
|
+
scenarioId: p.scenarioId,
|
|
8151
|
+
originalClassification: p.originalClassification,
|
|
8152
|
+
disputedTo: p.disputedTo,
|
|
8153
|
+
qaActor: p.qaActor
|
|
8154
|
+
};
|
|
8155
|
+
if (p.qaReason)
|
|
8156
|
+
row.qaReason = p.qaReason;
|
|
8157
|
+
return row;
|
|
8158
|
+
}
|
|
8159
|
+
function renderText(rows) {
|
|
8160
|
+
if (rows.length === 0)
|
|
8161
|
+
return `No disputes recorded.
|
|
8162
|
+
`;
|
|
8163
|
+
const lines = [];
|
|
8164
|
+
lines.push(`${rows.length} dispute(s):`);
|
|
8165
|
+
for (const r of rows) {
|
|
8166
|
+
lines.push(` ${r.ts} | ${r.scenarioId} | ${r.originalClassification} \u2192 ${r.disputedTo} | ${r.qaActor}`);
|
|
8167
|
+
if (r.qaReason)
|
|
8168
|
+
lines.push(` reason: ${r.qaReason}`);
|
|
8169
|
+
}
|
|
8170
|
+
return `${lines.join(`
|
|
8171
|
+
`)}
|
|
8172
|
+
`;
|
|
8173
|
+
}
|
|
8174
|
+
async function disputesCmd(argv) {
|
|
8175
|
+
let since;
|
|
8176
|
+
let format = "text";
|
|
8177
|
+
for (let i = 0;i < argv.length; i++) {
|
|
8178
|
+
if (argv[i] === "--since") {
|
|
8179
|
+
since = argv[++i];
|
|
8180
|
+
} else if (argv[i] === "--format") {
|
|
8181
|
+
const v = argv[++i];
|
|
8182
|
+
if (v === "json" || v === "text")
|
|
8183
|
+
format = v;
|
|
8184
|
+
}
|
|
8185
|
+
}
|
|
8186
|
+
const repoRoot = process.cwd();
|
|
8187
|
+
const events = loadAllEvents(repoRoot);
|
|
8188
|
+
const disputes = events.filter((e) => e.type === "classification.disputed");
|
|
8189
|
+
let cutoffMs;
|
|
8190
|
+
if (since) {
|
|
8191
|
+
const sinceMs = parseDuration(since);
|
|
8192
|
+
if (sinceMs > 0)
|
|
8193
|
+
cutoffMs = Date.now() - sinceMs;
|
|
8194
|
+
}
|
|
8195
|
+
const rows = disputes.filter((e) => cutoffMs === undefined || Date.parse(e.ts) >= cutoffMs).map(eventToRow).sort((a, b) => a.ts < b.ts ? 1 : -1);
|
|
8196
|
+
if (format === "json") {
|
|
8197
|
+
process.stdout.write(JSON.stringify(rows, null, 2));
|
|
8198
|
+
} else {
|
|
8199
|
+
process.stdout.write(renderText(rows));
|
|
8200
|
+
}
|
|
8201
|
+
return 0;
|
|
8202
|
+
}
|
|
8203
|
+
|
|
7832
8204
|
// src/bin-internal/doctor.ts
|
|
7833
|
-
import { existsSync as
|
|
7834
|
-
import { join as
|
|
8205
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
|
|
8206
|
+
import { join as join8 } from "path";
|
|
7835
8207
|
|
|
7836
8208
|
// src/graph/cost.ts
|
|
7837
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
8209
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
7838
8210
|
init_paths();
|
|
7839
8211
|
function summarizeCost(repoRoot, daysBack) {
|
|
7840
8212
|
const paths = graphPaths(repoRoot);
|
|
7841
8213
|
const result = { totalCalls: 0, totalUsd: 0, bySkill: {}, windowDays: daysBack };
|
|
7842
|
-
if (!
|
|
8214
|
+
if (!existsSync4(paths.costLog))
|
|
7843
8215
|
return result;
|
|
7844
8216
|
const cutoff = Date.now() - daysBack * 86400 * 1000;
|
|
7845
|
-
for (const line of
|
|
8217
|
+
for (const line of readFileSync2(paths.costLog, "utf8").split(`
|
|
7846
8218
|
`)) {
|
|
7847
8219
|
if (!line.trim())
|
|
7848
8220
|
continue;
|
|
@@ -7869,11 +8241,12 @@ function summarizeCost(repoRoot, daysBack) {
|
|
|
7869
8241
|
init_store();
|
|
7870
8242
|
|
|
7871
8243
|
// src/bin-internal/verify-prompts.ts
|
|
7872
|
-
import { existsSync as
|
|
7873
|
-
import { join as
|
|
8244
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
8245
|
+
import { join as join4 } from "path";
|
|
7874
8246
|
var IN_SCOPE_PROMPTS = [
|
|
7875
8247
|
"feature-from-story.md",
|
|
7876
|
-
"script-from-feature.md",
|
|
8248
|
+
"script-from-feature-web.md",
|
|
8249
|
+
"script-from-feature-http.md",
|
|
7877
8250
|
"heal-locator.md",
|
|
7878
8251
|
"extract-areas.md",
|
|
7879
8252
|
"similarity-match.md",
|
|
@@ -7882,11 +8255,11 @@ var IN_SCOPE_PROMPTS = [
|
|
|
7882
8255
|
var REQUIRED_SECTION_HEADING = "## Handling untrusted input";
|
|
7883
8256
|
var REQUIRED_KEYWORDS = ["UNTRUSTED", "injection-follow", "<XR_"];
|
|
7884
8257
|
function verifyPrompts(repoRoot) {
|
|
7885
|
-
const promptsDir =
|
|
8258
|
+
const promptsDir = join4(repoRoot, "packages/prompts");
|
|
7886
8259
|
const results = [];
|
|
7887
8260
|
for (const filename of IN_SCOPE_PROMPTS) {
|
|
7888
|
-
const path =
|
|
7889
|
-
if (!
|
|
8261
|
+
const path = join4(promptsDir, filename);
|
|
8262
|
+
if (!existsSync5(path)) {
|
|
7890
8263
|
results.push({
|
|
7891
8264
|
ok: false,
|
|
7892
8265
|
message: `${filename}: file missing at packages/prompts/${filename}`
|
|
@@ -7941,10 +8314,10 @@ function frontmatterField(content, field) {
|
|
|
7941
8314
|
return m?.[1] ?? null;
|
|
7942
8315
|
}
|
|
7943
8316
|
function checkGoldenEvalDir(repoRoot) {
|
|
7944
|
-
const root =
|
|
7945
|
-
if (!
|
|
8317
|
+
const root = join8(repoRoot, "fixtures/golden-eval");
|
|
8318
|
+
if (!existsSync9(root))
|
|
7946
8319
|
return [{ ok: false, message: "fixtures/golden-eval/ does not exist" }];
|
|
7947
|
-
const dirs =
|
|
8320
|
+
const dirs = readdirSync4(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
7948
8321
|
const results = [];
|
|
7949
8322
|
if (dirs.length < 3) {
|
|
7950
8323
|
results.push({
|
|
@@ -7953,15 +8326,15 @@ function checkGoldenEvalDir(repoRoot) {
|
|
|
7953
8326
|
});
|
|
7954
8327
|
}
|
|
7955
8328
|
for (const entry of dirs) {
|
|
7956
|
-
const dir =
|
|
7957
|
-
const metaPath =
|
|
7958
|
-
if (!
|
|
8329
|
+
const dir = join8(root, entry.name);
|
|
8330
|
+
const metaPath = join8(dir, "meta.json");
|
|
8331
|
+
if (!existsSync9(metaPath)) {
|
|
7959
8332
|
results.push({ ok: false, message: `${entry.name}: meta.json missing` });
|
|
7960
8333
|
continue;
|
|
7961
8334
|
}
|
|
7962
8335
|
let meta;
|
|
7963
8336
|
try {
|
|
7964
|
-
meta = JSON.parse(
|
|
8337
|
+
meta = JSON.parse(readFileSync6(metaPath, "utf8"));
|
|
7965
8338
|
} catch (err) {
|
|
7966
8339
|
results.push({
|
|
7967
8340
|
ok: false,
|
|
@@ -7972,12 +8345,12 @@ function checkGoldenEvalDir(repoRoot) {
|
|
|
7972
8345
|
const stages = Array.isArray(meta.stages) ? meta.stages : [];
|
|
7973
8346
|
if (stages.length === 0)
|
|
7974
8347
|
results.push({ ok: false, message: `${entry.name}: meta.stages is empty` });
|
|
7975
|
-
if (!
|
|
8348
|
+
if (!existsSync9(join8(dir, "story.md")))
|
|
7976
8349
|
results.push({ ok: false, message: `${entry.name}: story.md missing` });
|
|
7977
8350
|
for (const stage of stages) {
|
|
7978
8351
|
const required = REQUIRED_FILES_PER_STAGE[stage] ?? [];
|
|
7979
8352
|
for (const rel of required) {
|
|
7980
|
-
if (!
|
|
8353
|
+
if (!existsSync9(join8(dir, rel))) {
|
|
7981
8354
|
results.push({
|
|
7982
8355
|
ok: false,
|
|
7983
8356
|
message: `${meta.id ?? entry.name}: stage "${stage}" declared but ${rel} missing`
|
|
@@ -7989,10 +8362,10 @@ function checkGoldenEvalDir(repoRoot) {
|
|
|
7989
8362
|
return results;
|
|
7990
8363
|
}
|
|
7991
8364
|
function checkRubricPrompt(repoRoot) {
|
|
7992
|
-
const path =
|
|
7993
|
-
if (!
|
|
8365
|
+
const path = join8(repoRoot, "packages/prompts/eval-rubric.md");
|
|
8366
|
+
if (!existsSync9(path))
|
|
7994
8367
|
return [{ ok: false, message: "packages/prompts/eval-rubric.md missing" }];
|
|
7995
|
-
const text =
|
|
8368
|
+
const text = readFileSync6(path, "utf8");
|
|
7996
8369
|
const id = frontmatterField(text, "id");
|
|
7997
8370
|
const version = frontmatterField(text, "version");
|
|
7998
8371
|
if (id !== "eval-rubric")
|
|
@@ -8002,10 +8375,10 @@ function checkRubricPrompt(repoRoot) {
|
|
|
8002
8375
|
return [];
|
|
8003
8376
|
}
|
|
8004
8377
|
function checkEvalSkill(repoRoot) {
|
|
8005
|
-
const path =
|
|
8006
|
-
if (!
|
|
8378
|
+
const path = join8(repoRoot, "packages/skills/xera-eval.md");
|
|
8379
|
+
if (!existsSync9(path))
|
|
8007
8380
|
return [{ ok: false, message: "packages/skills/xera-eval.md missing" }];
|
|
8008
|
-
const text =
|
|
8381
|
+
const text = readFileSync6(path, "utf8");
|
|
8009
8382
|
if (!frontmatterField(text, "name"))
|
|
8010
8383
|
return [{ ok: false, message: 'xera-eval.md frontmatter "name" missing' }];
|
|
8011
8384
|
return [];
|
|
@@ -8014,16 +8387,17 @@ function checkPromptInjectionPreamble(repoRoot) {
|
|
|
8014
8387
|
return verifyPrompts(repoRoot);
|
|
8015
8388
|
}
|
|
8016
8389
|
function checkRootScripts(repoRoot) {
|
|
8017
|
-
const path =
|
|
8018
|
-
if (!
|
|
8390
|
+
const path = join8(repoRoot, "package.json");
|
|
8391
|
+
if (!existsSync9(path))
|
|
8019
8392
|
return [{ ok: false, message: "root package.json missing" }];
|
|
8020
|
-
const pkg = JSON.parse(
|
|
8393
|
+
const pkg = JSON.parse(readFileSync6(path, "utf8"));
|
|
8021
8394
|
const scripts = pkg.scripts ?? {};
|
|
8022
8395
|
const missing = REQUIRED_SCRIPTS.filter((s) => typeof scripts[s] !== "string");
|
|
8023
8396
|
return missing.map((s) => ({ ok: false, message: `root package.json missing script: ${s}` }));
|
|
8024
8397
|
}
|
|
8025
|
-
async function doctorCmd(
|
|
8398
|
+
async function doctorCmd(argv, opts = {}) {
|
|
8026
8399
|
const repoRoot = opts.cwd ?? process.cwd();
|
|
8400
|
+
const autoEnrich = argv.includes("--auto-enrich");
|
|
8027
8401
|
const results = [
|
|
8028
8402
|
...checkGoldenEvalDir(repoRoot),
|
|
8029
8403
|
...checkRubricPrompt(repoRoot),
|
|
@@ -8041,9 +8415,9 @@ async function doctorCmd(_argv, opts = {}) {
|
|
|
8041
8415
|
if (top)
|
|
8042
8416
|
console.log(` Top skill: ${top[0]} (${top[1].calls} calls, $${top[1].usd.toFixed(2)})`);
|
|
8043
8417
|
}
|
|
8044
|
-
const xeraDir =
|
|
8045
|
-
if (
|
|
8046
|
-
const ticketDirs =
|
|
8418
|
+
const xeraDir = join8(repoRoot, ".xera");
|
|
8419
|
+
if (existsSync9(xeraDir)) {
|
|
8420
|
+
const ticketDirs = readdirSync4(xeraDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name));
|
|
8047
8421
|
if (ticketDirs.length > 0) {
|
|
8048
8422
|
const events = loadAllEvents(repoRoot);
|
|
8049
8423
|
const fetchedTickets = new Set(events.filter((e) => e.type === "ticket.fetched").map((e) => e.payload.ticketId));
|
|
@@ -8054,6 +8428,16 @@ async function doctorCmd(_argv, opts = {}) {
|
|
|
8054
8428
|
console.log(` These won't participate in v0.6.1+ features (TEST_OUTDATED, /xera-impact).`);
|
|
8055
8429
|
console.log(` Run: bun run xera:graph-backfill`);
|
|
8056
8430
|
console.log(` (Use --dry-run to preview.)`);
|
|
8431
|
+
if (autoEnrich) {
|
|
8432
|
+
console.log("[doctor] --auto-enrich: running backfill for unbackfilled tickets...");
|
|
8433
|
+
const { graphBackfillCmd: graphBackfillCmd2 } = await Promise.resolve().then(() => (init_graph_backfill(), exports_graph_backfill));
|
|
8434
|
+
const exitCode = await graphBackfillCmd2([]);
|
|
8435
|
+
if (exitCode === 0) {
|
|
8436
|
+
console.log(`[doctor] auto-enrich: backfilled ${unbackfilled.length} tickets`);
|
|
8437
|
+
} else {
|
|
8438
|
+
console.error("[doctor] auto-enrich: backfill failed");
|
|
8439
|
+
}
|
|
8440
|
+
}
|
|
8057
8441
|
}
|
|
8058
8442
|
}
|
|
8059
8443
|
}
|
|
@@ -8067,115 +8451,115 @@ async function doctorCmd(_argv, opts = {}) {
|
|
|
8067
8451
|
}
|
|
8068
8452
|
|
|
8069
8453
|
// src/bin-internal/eval-deterministic.ts
|
|
8070
|
-
import { existsSync as
|
|
8071
|
-
import { join as
|
|
8454
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
8455
|
+
import { join as join10 } from "path";
|
|
8072
8456
|
import { validateGherkin } from "@xera-ai/web";
|
|
8073
8457
|
|
|
8074
8458
|
// src/eval/paths.ts
|
|
8075
|
-
import { join as
|
|
8459
|
+
import { join as join9 } from "path";
|
|
8076
8460
|
function resolveEvalPaths(cwd, runId) {
|
|
8077
|
-
const root =
|
|
8461
|
+
const root = join9(cwd, ".xera", "eval", runId);
|
|
8078
8462
|
return {
|
|
8079
8463
|
root,
|
|
8080
|
-
manifest:
|
|
8081
|
-
lock:
|
|
8082
|
-
deterministicScores:
|
|
8083
|
-
judgeScores:
|
|
8084
|
-
report:
|
|
8085
|
-
summary:
|
|
8086
|
-
inputsDir:
|
|
8087
|
-
actualDir:
|
|
8088
|
-
ticketInputsDir: (ticket) =>
|
|
8089
|
-
ticketActualDir: (ticket) =>
|
|
8464
|
+
manifest: join9(root, "manifest.json"),
|
|
8465
|
+
lock: join9(root, ".lock"),
|
|
8466
|
+
deterministicScores: join9(root, "deterministic-scores.json"),
|
|
8467
|
+
judgeScores: join9(root, "judge-scores.json"),
|
|
8468
|
+
report: join9(root, "report.md"),
|
|
8469
|
+
summary: join9(root, "summary.json"),
|
|
8470
|
+
inputsDir: join9(root, "inputs"),
|
|
8471
|
+
actualDir: join9(root, "actual"),
|
|
8472
|
+
ticketInputsDir: (ticket) => join9(root, "inputs", ticket),
|
|
8473
|
+
ticketActualDir: (ticket) => join9(root, "actual", ticket)
|
|
8090
8474
|
};
|
|
8091
8475
|
}
|
|
8092
8476
|
|
|
8093
8477
|
// src/eval/types.ts
|
|
8094
|
-
import { z as
|
|
8478
|
+
import { z as z3 } from "zod";
|
|
8095
8479
|
var STAGES = ["feature-from-story", "script-from-feature", "diagnose-failure"];
|
|
8096
|
-
var StageSchema =
|
|
8097
|
-
var VerdictSchema =
|
|
8098
|
-
var PromptVersionsSchema =
|
|
8099
|
-
"feature-from-story":
|
|
8100
|
-
"script-from-feature":
|
|
8101
|
-
"diagnose-failure":
|
|
8102
|
-
"eval-rubric":
|
|
8480
|
+
var StageSchema = z3.enum(STAGES);
|
|
8481
|
+
var VerdictSchema = z3.enum(["PASS", "FAIL", "NA"]);
|
|
8482
|
+
var PromptVersionsSchema = z3.object({
|
|
8483
|
+
"feature-from-story": z3.string(),
|
|
8484
|
+
"script-from-feature": z3.string(),
|
|
8485
|
+
"diagnose-failure": z3.string(),
|
|
8486
|
+
"eval-rubric": z3.string()
|
|
8103
8487
|
});
|
|
8104
|
-
var ManifestSchema =
|
|
8105
|
-
run_id:
|
|
8106
|
-
started_at:
|
|
8107
|
-
git_sha:
|
|
8108
|
-
tickets:
|
|
8109
|
-
stages:
|
|
8110
|
-
ticket_stages:
|
|
8488
|
+
var ManifestSchema = z3.object({
|
|
8489
|
+
run_id: z3.string(),
|
|
8490
|
+
started_at: z3.string(),
|
|
8491
|
+
git_sha: z3.string(),
|
|
8492
|
+
tickets: z3.array(z3.string()).min(1),
|
|
8493
|
+
stages: z3.array(StageSchema).min(1),
|
|
8494
|
+
ticket_stages: z3.record(z3.string(), z3.array(StageSchema).min(1)),
|
|
8111
8495
|
prompt_versions: PromptVersionsSchema,
|
|
8112
|
-
flags:
|
|
8113
|
-
force:
|
|
8496
|
+
flags: z3.object({
|
|
8497
|
+
force: z3.boolean(),
|
|
8114
8498
|
only_prompt: StageSchema.nullable(),
|
|
8115
|
-
only_ticket:
|
|
8116
|
-
judge_only:
|
|
8499
|
+
only_ticket: z3.string().nullable(),
|
|
8500
|
+
judge_only: z3.boolean()
|
|
8117
8501
|
})
|
|
8118
8502
|
});
|
|
8119
|
-
var DimensionSchema =
|
|
8120
|
-
name:
|
|
8503
|
+
var DimensionSchema = z3.object({
|
|
8504
|
+
name: z3.string(),
|
|
8121
8505
|
verdict: VerdictSchema,
|
|
8122
|
-
notes:
|
|
8506
|
+
notes: z3.string()
|
|
8123
8507
|
});
|
|
8124
|
-
var JudgmentSchema =
|
|
8508
|
+
var JudgmentSchema = z3.object({
|
|
8125
8509
|
stage: StageSchema,
|
|
8126
|
-
ticket:
|
|
8127
|
-
dimensions:
|
|
8510
|
+
ticket: z3.string(),
|
|
8511
|
+
dimensions: z3.array(DimensionSchema).min(1)
|
|
8128
8512
|
});
|
|
8129
|
-
var JudgeScoresSchema =
|
|
8130
|
-
run_id:
|
|
8131
|
-
judgments:
|
|
8513
|
+
var JudgeScoresSchema = z3.object({
|
|
8514
|
+
run_id: z3.string(),
|
|
8515
|
+
judgments: z3.array(JudgmentSchema)
|
|
8132
8516
|
});
|
|
8133
|
-
var DeterministicEntrySchema =
|
|
8134
|
-
ticket:
|
|
8517
|
+
var DeterministicEntrySchema = z3.object({
|
|
8518
|
+
ticket: z3.string(),
|
|
8135
8519
|
stage: StageSchema,
|
|
8136
|
-
passed:
|
|
8137
|
-
checks:
|
|
8138
|
-
error:
|
|
8520
|
+
passed: z3.boolean(),
|
|
8521
|
+
checks: z3.array(z3.string()),
|
|
8522
|
+
error: z3.string().optional()
|
|
8139
8523
|
});
|
|
8140
|
-
var DeterministicScoresSchema =
|
|
8141
|
-
run_id:
|
|
8142
|
-
entries:
|
|
8524
|
+
var DeterministicScoresSchema = z3.object({
|
|
8525
|
+
run_id: z3.string(),
|
|
8526
|
+
entries: z3.array(DeterministicEntrySchema)
|
|
8143
8527
|
});
|
|
8144
|
-
var ResultSchema =
|
|
8145
|
-
ticket:
|
|
8528
|
+
var ResultSchema = z3.object({
|
|
8529
|
+
ticket: z3.string(),
|
|
8146
8530
|
stage: StageSchema,
|
|
8147
|
-
deterministic:
|
|
8148
|
-
passed:
|
|
8149
|
-
checks:
|
|
8150
|
-
error:
|
|
8531
|
+
deterministic: z3.object({
|
|
8532
|
+
passed: z3.boolean(),
|
|
8533
|
+
checks: z3.array(z3.string()),
|
|
8534
|
+
error: z3.string().optional()
|
|
8151
8535
|
}),
|
|
8152
|
-
judge:
|
|
8153
|
-
passed:
|
|
8154
|
-
dimensions:
|
|
8155
|
-
score:
|
|
8536
|
+
judge: z3.object({
|
|
8537
|
+
passed: z3.boolean(),
|
|
8538
|
+
dimensions: z3.array(DimensionSchema),
|
|
8539
|
+
score: z3.number().min(0).max(1)
|
|
8156
8540
|
}).nullable(),
|
|
8157
|
-
skipped:
|
|
8541
|
+
skipped: z3.boolean().optional()
|
|
8158
8542
|
});
|
|
8159
|
-
var SummarySchema =
|
|
8160
|
-
run_id:
|
|
8161
|
-
git_sha:
|
|
8543
|
+
var SummarySchema = z3.object({
|
|
8544
|
+
run_id: z3.string(),
|
|
8545
|
+
git_sha: z3.string(),
|
|
8162
8546
|
prompt_versions: PromptVersionsSchema,
|
|
8163
|
-
results:
|
|
8164
|
-
overall:
|
|
8165
|
-
passed:
|
|
8166
|
-
failed:
|
|
8167
|
-
total:
|
|
8168
|
-
score:
|
|
8547
|
+
results: z3.array(ResultSchema),
|
|
8548
|
+
overall: z3.object({
|
|
8549
|
+
passed: z3.number().int().nonnegative(),
|
|
8550
|
+
failed: z3.number().int().nonnegative(),
|
|
8551
|
+
total: z3.number().int().nonnegative(),
|
|
8552
|
+
score: z3.number().min(0).max(1)
|
|
8169
8553
|
})
|
|
8170
8554
|
});
|
|
8171
8555
|
|
|
8172
8556
|
// src/bin-internal/eval-deterministic.ts
|
|
8173
8557
|
function checkFeatureFromStory(actualFeaturePath) {
|
|
8174
|
-
if (!
|
|
8558
|
+
if (!existsSync10(actualFeaturePath)) {
|
|
8175
8559
|
return { passed: false, checks: ["validate-feature"], error: "actual missing: test.feature" };
|
|
8176
8560
|
}
|
|
8177
8561
|
try {
|
|
8178
|
-
const r = validateGherkin(
|
|
8562
|
+
const r = validateGherkin(readFileSync7(actualFeaturePath, "utf8"));
|
|
8179
8563
|
if (r.ok)
|
|
8180
8564
|
return { passed: true, checks: ["validate-feature"] };
|
|
8181
8565
|
return {
|
|
@@ -8188,31 +8572,31 @@ function checkFeatureFromStory(actualFeaturePath) {
|
|
|
8188
8572
|
}
|
|
8189
8573
|
}
|
|
8190
8574
|
function checkScriptFromFeature(actualTicketDir) {
|
|
8191
|
-
const specPath =
|
|
8192
|
-
if (!
|
|
8575
|
+
const specPath = join10(actualTicketDir, "spec.ts");
|
|
8576
|
+
if (!existsSync10(specPath)) {
|
|
8193
8577
|
return { passed: false, checks: ["file-presence"], error: "actual missing: spec.ts" };
|
|
8194
8578
|
}
|
|
8195
8579
|
return { passed: true, checks: ["file-presence"] };
|
|
8196
8580
|
}
|
|
8197
8581
|
function checkDiagnoseFailure(inputsTicketDir, actualTicketDir) {
|
|
8198
|
-
const inputPath =
|
|
8199
|
-
const actualPath =
|
|
8200
|
-
if (!
|
|
8582
|
+
const inputPath = join10(inputsTicketDir, "classifier-input.json");
|
|
8583
|
+
const actualPath = join10(actualTicketDir, "classification.json");
|
|
8584
|
+
if (!existsSync10(actualPath)) {
|
|
8201
8585
|
return {
|
|
8202
8586
|
passed: false,
|
|
8203
8587
|
checks: ["bucket-match"],
|
|
8204
8588
|
error: "actual missing: classification.json"
|
|
8205
8589
|
};
|
|
8206
8590
|
}
|
|
8207
|
-
if (!
|
|
8591
|
+
if (!existsSync10(inputPath)) {
|
|
8208
8592
|
return {
|
|
8209
8593
|
passed: false,
|
|
8210
8594
|
checks: ["bucket-match"],
|
|
8211
8595
|
error: "inputs missing: classifier-input.json"
|
|
8212
8596
|
};
|
|
8213
8597
|
}
|
|
8214
|
-
const golden = JSON.parse(
|
|
8215
|
-
const actual = JSON.parse(
|
|
8598
|
+
const golden = JSON.parse(readFileSync7(inputPath, "utf8"));
|
|
8599
|
+
const actual = JSON.parse(readFileSync7(actualPath, "utf8"));
|
|
8216
8600
|
const goldScens = golden.scenarios ?? [];
|
|
8217
8601
|
const actScens = actual.scenarios ?? [];
|
|
8218
8602
|
const mismatches = [];
|
|
@@ -8242,11 +8626,11 @@ async function evalDeterministicCmd(argv, opts = {}) {
|
|
|
8242
8626
|
return 1;
|
|
8243
8627
|
}
|
|
8244
8628
|
const paths = resolveEvalPaths(cwd, runId);
|
|
8245
|
-
if (!
|
|
8629
|
+
if (!existsSync10(paths.manifest)) {
|
|
8246
8630
|
console.error(`[xera:eval-deterministic] missing manifest.json at ${paths.manifest}`);
|
|
8247
8631
|
return 1;
|
|
8248
8632
|
}
|
|
8249
|
-
const manifest = ManifestSchema.parse(JSON.parse(
|
|
8633
|
+
const manifest = ManifestSchema.parse(JSON.parse(readFileSync7(paths.manifest, "utf8")));
|
|
8250
8634
|
const entries = [];
|
|
8251
8635
|
for (const [ticket, ticketStages] of Object.entries(manifest.ticket_stages)) {
|
|
8252
8636
|
for (const stage of ticketStages) {
|
|
@@ -8254,7 +8638,7 @@ async function evalDeterministicCmd(argv, opts = {}) {
|
|
|
8254
8638
|
const actualDir = paths.ticketActualDir(ticket);
|
|
8255
8639
|
let result;
|
|
8256
8640
|
if (stage === "feature-from-story") {
|
|
8257
|
-
result = checkFeatureFromStory(
|
|
8641
|
+
result = checkFeatureFromStory(join10(actualDir, "test.feature"));
|
|
8258
8642
|
} else if (stage === "script-from-feature") {
|
|
8259
8643
|
result = checkScriptFromFeature(actualDir);
|
|
8260
8644
|
} else {
|
|
@@ -8281,13 +8665,13 @@ async function evalDeterministicCmd(argv, opts = {}) {
|
|
|
8281
8665
|
// src/bin-internal/eval-prepare.ts
|
|
8282
8666
|
import {
|
|
8283
8667
|
copyFileSync,
|
|
8284
|
-
existsSync as
|
|
8668
|
+
existsSync as existsSync12,
|
|
8285
8669
|
mkdirSync as mkdirSync4,
|
|
8286
|
-
readdirSync as
|
|
8287
|
-
readFileSync as
|
|
8670
|
+
readdirSync as readdirSync5,
|
|
8671
|
+
readFileSync as readFileSync9,
|
|
8288
8672
|
writeFileSync as writeFileSync4
|
|
8289
8673
|
} from "fs";
|
|
8290
|
-
import { join as
|
|
8674
|
+
import { join as join11 } from "path";
|
|
8291
8675
|
|
|
8292
8676
|
// src/eval/run-id.ts
|
|
8293
8677
|
import { execSync } from "child_process";
|
|
@@ -8312,11 +8696,11 @@ function generateRunId(opts = {}) {
|
|
|
8312
8696
|
}
|
|
8313
8697
|
|
|
8314
8698
|
// src/lock/file-lock.ts
|
|
8315
|
-
import { existsSync as
|
|
8699
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
8316
8700
|
import { hostname } from "os";
|
|
8317
8701
|
import { dirname as dirname2 } from "path";
|
|
8318
8702
|
function acquireLock(path, runId) {
|
|
8319
|
-
if (
|
|
8703
|
+
if (existsSync11(path))
|
|
8320
8704
|
return false;
|
|
8321
8705
|
mkdirSync3(dirname2(path), { recursive: true });
|
|
8322
8706
|
const data = {
|
|
@@ -8333,13 +8717,13 @@ function acquireLock(path, runId) {
|
|
|
8333
8717
|
}
|
|
8334
8718
|
}
|
|
8335
8719
|
function releaseLock(path) {
|
|
8336
|
-
if (
|
|
8720
|
+
if (existsSync11(path))
|
|
8337
8721
|
unlinkSync(path);
|
|
8338
8722
|
}
|
|
8339
8723
|
function readLock(path) {
|
|
8340
|
-
if (!
|
|
8724
|
+
if (!existsSync11(path))
|
|
8341
8725
|
return null;
|
|
8342
|
-
return JSON.parse(
|
|
8726
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
8343
8727
|
}
|
|
8344
8728
|
function isLockStale(path) {
|
|
8345
8729
|
const lock = readLock(path);
|
|
@@ -8360,7 +8744,7 @@ function forceUnlock(path) {
|
|
|
8360
8744
|
}
|
|
8361
8745
|
|
|
8362
8746
|
// src/bin-internal/eval-prepare.ts
|
|
8363
|
-
function
|
|
8747
|
+
function parseFlags2(argv) {
|
|
8364
8748
|
const flags = { force: false, only_prompt: null, only_ticket: null };
|
|
8365
8749
|
for (const arg of argv) {
|
|
8366
8750
|
if (arg === "--force")
|
|
@@ -8380,42 +8764,42 @@ function parseFlags(argv) {
|
|
|
8380
8764
|
return flags;
|
|
8381
8765
|
}
|
|
8382
8766
|
function readPromptVersion(repoRoot, name) {
|
|
8383
|
-
const path =
|
|
8384
|
-
if (!
|
|
8767
|
+
const path = join11(repoRoot, "packages/prompts", `${name}.md`);
|
|
8768
|
+
if (!existsSync12(path))
|
|
8385
8769
|
return "0.0.0";
|
|
8386
|
-
const text =
|
|
8770
|
+
const text = readFileSync9(path, "utf8");
|
|
8387
8771
|
const m = /^version:\s*(\S+)\s*$/m.exec(text);
|
|
8388
8772
|
return m?.[1] ?? "0.0.0";
|
|
8389
8773
|
}
|
|
8390
8774
|
function discoverEvalTickets(repoRoot) {
|
|
8391
|
-
const root =
|
|
8392
|
-
if (!
|
|
8775
|
+
const root = join11(repoRoot, "fixtures/golden-eval");
|
|
8776
|
+
if (!existsSync12(root))
|
|
8393
8777
|
return [];
|
|
8394
8778
|
const out = [];
|
|
8395
|
-
for (const entry of
|
|
8779
|
+
for (const entry of readdirSync5(root, { withFileTypes: true })) {
|
|
8396
8780
|
if (!entry.isDirectory())
|
|
8397
8781
|
continue;
|
|
8398
8782
|
if (entry.name === "README.md" || entry.name.startsWith("."))
|
|
8399
8783
|
continue;
|
|
8400
|
-
const dir =
|
|
8401
|
-
const metaPath =
|
|
8402
|
-
if (!
|
|
8784
|
+
const dir = join11(root, entry.name);
|
|
8785
|
+
const metaPath = join11(dir, "meta.json");
|
|
8786
|
+
if (!existsSync12(metaPath))
|
|
8403
8787
|
continue;
|
|
8404
|
-
const meta = JSON.parse(
|
|
8788
|
+
const meta = JSON.parse(readFileSync9(metaPath, "utf8"));
|
|
8405
8789
|
out.push({ id: meta.id, dir, stages: meta.stages });
|
|
8406
8790
|
}
|
|
8407
8791
|
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
8408
8792
|
}
|
|
8409
8793
|
function discoverClassifierTickets(repoRoot) {
|
|
8410
|
-
const root =
|
|
8411
|
-
if (!
|
|
8794
|
+
const root = join11(repoRoot, "fixtures/golden-tickets");
|
|
8795
|
+
if (!existsSync12(root))
|
|
8412
8796
|
return [];
|
|
8413
8797
|
const out = [];
|
|
8414
|
-
for (const entry of
|
|
8798
|
+
for (const entry of readdirSync5(root, { withFileTypes: true })) {
|
|
8415
8799
|
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
8416
8800
|
continue;
|
|
8417
|
-
const path =
|
|
8418
|
-
const data = JSON.parse(
|
|
8801
|
+
const path = join11(root, entry.name);
|
|
8802
|
+
const data = JSON.parse(readFileSync9(path, "utf8"));
|
|
8419
8803
|
if (typeof data.ticket === "string")
|
|
8420
8804
|
out.push({ id: data.ticket, path });
|
|
8421
8805
|
}
|
|
@@ -8423,7 +8807,7 @@ function discoverClassifierTickets(repoRoot) {
|
|
|
8423
8807
|
}
|
|
8424
8808
|
async function evalPrepareCmd(argv, opts = {}) {
|
|
8425
8809
|
const repoRoot = opts.cwd ?? process.cwd();
|
|
8426
|
-
const flags =
|
|
8810
|
+
const flags = parseFlags2(argv);
|
|
8427
8811
|
if ("error" in flags) {
|
|
8428
8812
|
console.error(`[xera:eval-prepare] ${flags.error}`);
|
|
8429
8813
|
return 1;
|
|
@@ -8474,7 +8858,7 @@ async function evalPrepareCmd(argv, opts = {}) {
|
|
|
8474
8858
|
...opts.getGitSha ? { getGitSha: opts.getGitSha } : {}
|
|
8475
8859
|
});
|
|
8476
8860
|
const paths = resolveEvalPaths(repoRoot, runId);
|
|
8477
|
-
if (
|
|
8861
|
+
if (existsSync12(paths.root) && !flags.force) {
|
|
8478
8862
|
console.error(`[xera:eval-prepare] run dir already exists: ${paths.root}. Pass --force to re-run.`);
|
|
8479
8863
|
return 1;
|
|
8480
8864
|
}
|
|
@@ -8486,13 +8870,13 @@ async function evalPrepareCmd(argv, opts = {}) {
|
|
|
8486
8870
|
const evalT = evalTickets.find((t) => t.id === ticket);
|
|
8487
8871
|
const classT = classifierTickets.find((t) => t.id === ticket);
|
|
8488
8872
|
if (evalT) {
|
|
8489
|
-
copyFileSync(
|
|
8490
|
-
const featurePath =
|
|
8491
|
-
if (
|
|
8492
|
-
copyFileSync(featurePath,
|
|
8873
|
+
copyFileSync(join11(evalT.dir, "story.md"), join11(ticketInputs, "story.md"));
|
|
8874
|
+
const featurePath = join11(evalT.dir, "golden/test.feature");
|
|
8875
|
+
if (existsSync12(featurePath))
|
|
8876
|
+
copyFileSync(featurePath, join11(ticketInputs, "test.feature"));
|
|
8493
8877
|
}
|
|
8494
8878
|
if (classT) {
|
|
8495
|
-
copyFileSync(classT.path,
|
|
8879
|
+
copyFileSync(classT.path, join11(ticketInputs, "classifier-input.json"));
|
|
8496
8880
|
}
|
|
8497
8881
|
}
|
|
8498
8882
|
const now = (opts.now ?? (() => new Date))();
|
|
@@ -8528,7 +8912,7 @@ async function evalPrepareCmd(argv, opts = {}) {
|
|
|
8528
8912
|
}
|
|
8529
8913
|
|
|
8530
8914
|
// src/bin-internal/eval-report.ts
|
|
8531
|
-
import { existsSync as
|
|
8915
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
8532
8916
|
function scoreJudgment(j) {
|
|
8533
8917
|
const nonNa = j.dimensions.filter((d) => d.verdict !== "NA");
|
|
8534
8918
|
if (nonNa.length === 0)
|
|
@@ -8583,22 +8967,22 @@ async function evalReportCmd(argv, opts = {}) {
|
|
|
8583
8967
|
return 1;
|
|
8584
8968
|
}
|
|
8585
8969
|
const paths = resolveEvalPaths(cwd, runId);
|
|
8586
|
-
if (!
|
|
8970
|
+
if (!existsSync13(paths.manifest)) {
|
|
8587
8971
|
console.error(`[xera:eval-report] missing manifest.json at ${paths.manifest}`);
|
|
8588
8972
|
return 1;
|
|
8589
8973
|
}
|
|
8590
|
-
const manifest = ManifestSchema.parse(JSON.parse(
|
|
8974
|
+
const manifest = ManifestSchema.parse(JSON.parse(readFileSync10(paths.manifest, "utf8")));
|
|
8591
8975
|
try {
|
|
8592
8976
|
let det;
|
|
8593
8977
|
let judge;
|
|
8594
8978
|
try {
|
|
8595
|
-
det = DeterministicScoresSchema.parse(JSON.parse(
|
|
8979
|
+
det = DeterministicScoresSchema.parse(JSON.parse(readFileSync10(paths.deterministicScores, "utf8")));
|
|
8596
8980
|
} catch (err) {
|
|
8597
8981
|
console.error(`[xera:eval-report] invalid deterministic-scores.json: ${err.message}`);
|
|
8598
8982
|
return 2;
|
|
8599
8983
|
}
|
|
8600
8984
|
try {
|
|
8601
|
-
judge = JudgeScoresSchema.parse(JSON.parse(
|
|
8985
|
+
judge = JudgeScoresSchema.parse(JSON.parse(readFileSync10(paths.judgeScores, "utf8")));
|
|
8602
8986
|
} catch (err) {
|
|
8603
8987
|
console.error(`[xera:eval-report] invalid judge-scores.json: ${err.message}`);
|
|
8604
8988
|
return 2;
|
|
@@ -8670,40 +9054,77 @@ async function evalReportCmd(argv, opts = {}) {
|
|
|
8670
9054
|
}
|
|
8671
9055
|
|
|
8672
9056
|
// src/bin-internal/exec.ts
|
|
8673
|
-
import { existsSync as
|
|
8674
|
-
import { join as
|
|
9057
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
|
|
9058
|
+
import { join as join14 } from "path";
|
|
8675
9059
|
import { chromium } from "@playwright/test";
|
|
8676
9060
|
import { runAuthSetup, runPlaywright, stagePlaywrightState } from "@xera-ai/web";
|
|
8677
9061
|
|
|
9062
|
+
// src/artifact/meta.ts
|
|
9063
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
9064
|
+
import { dirname as dirname3 } from "path";
|
|
9065
|
+
import { z as z4 } from "zod";
|
|
9066
|
+
var MetaJsonSchema = z4.object({
|
|
9067
|
+
ticket: z4.string(),
|
|
9068
|
+
adapter: z4.string(),
|
|
9069
|
+
xera_version: z4.string(),
|
|
9070
|
+
prompts_version: z4.string(),
|
|
9071
|
+
fetched_at: z4.string().optional(),
|
|
9072
|
+
story_hash: z4.string().optional(),
|
|
9073
|
+
feature_generated_at: z4.string().optional(),
|
|
9074
|
+
feature_generated_from_story_hash: z4.string().optional(),
|
|
9075
|
+
feature_hash: z4.string().optional(),
|
|
9076
|
+
script_generated_at: z4.string().optional(),
|
|
9077
|
+
script_generated_from_feature_hash: z4.string().optional(),
|
|
9078
|
+
script_warnings: z4.array(z4.string()).optional()
|
|
9079
|
+
});
|
|
9080
|
+
function readMeta(path) {
|
|
9081
|
+
if (!existsSync14(path))
|
|
9082
|
+
return null;
|
|
9083
|
+
return MetaJsonSchema.parse(JSON.parse(readFileSync11(path, "utf8")));
|
|
9084
|
+
}
|
|
9085
|
+
function writeMeta(path, meta) {
|
|
9086
|
+
mkdirSync5(dirname3(path), { recursive: true });
|
|
9087
|
+
writeFileSync6(path, JSON.stringify(meta, null, 2));
|
|
9088
|
+
}
|
|
9089
|
+
function updateMeta(path, patch) {
|
|
9090
|
+
const existing = readMeta(path);
|
|
9091
|
+
if (!existing) {
|
|
9092
|
+
throw new Error(`meta.json not found at ${path}; cannot update`);
|
|
9093
|
+
}
|
|
9094
|
+
const next = { ...existing, ...patch };
|
|
9095
|
+
writeMeta(path, next);
|
|
9096
|
+
return next;
|
|
9097
|
+
}
|
|
9098
|
+
|
|
8678
9099
|
// src/artifact/paths.ts
|
|
8679
|
-
import { join as
|
|
9100
|
+
import { join as join12 } from "path";
|
|
8680
9101
|
var TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
|
|
8681
9102
|
function resolveArtifactPaths(repoRoot, ticket) {
|
|
8682
9103
|
if (!TICKET_RE.test(ticket)) {
|
|
8683
9104
|
throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
|
|
8684
9105
|
}
|
|
8685
|
-
const ticketDir =
|
|
9106
|
+
const ticketDir = join12(repoRoot, ".xera", ticket);
|
|
8686
9107
|
return {
|
|
8687
9108
|
ticketDir,
|
|
8688
|
-
storyPath:
|
|
8689
|
-
featurePath:
|
|
8690
|
-
specPath:
|
|
8691
|
-
pageObjectsDir:
|
|
8692
|
-
runsDir:
|
|
8693
|
-
metaPath:
|
|
8694
|
-
statusPath:
|
|
8695
|
-
logPath:
|
|
8696
|
-
lockPath:
|
|
8697
|
-
authDir:
|
|
9109
|
+
storyPath: join12(ticketDir, "story.md"),
|
|
9110
|
+
featurePath: join12(ticketDir, "test.feature"),
|
|
9111
|
+
specPath: join12(ticketDir, "spec.ts"),
|
|
9112
|
+
pageObjectsDir: join12(ticketDir, "page-objects"),
|
|
9113
|
+
runsDir: join12(ticketDir, "runs"),
|
|
9114
|
+
metaPath: join12(ticketDir, "meta.json"),
|
|
9115
|
+
statusPath: join12(ticketDir, "status.json"),
|
|
9116
|
+
logPath: join12(ticketDir, "xera.log"),
|
|
9117
|
+
lockPath: join12(ticketDir, ".lock"),
|
|
9118
|
+
authDir: join12(repoRoot, ".xera", ".auth"),
|
|
8698
9119
|
runPath: (runId) => {
|
|
8699
|
-
const runDir =
|
|
9120
|
+
const runDir = join12(ticketDir, "runs", runId);
|
|
8700
9121
|
return {
|
|
8701
9122
|
runDir,
|
|
8702
|
-
reportJsonPath:
|
|
8703
|
-
tracePath:
|
|
8704
|
-
normalizedPath:
|
|
8705
|
-
screenshotsDir:
|
|
8706
|
-
videoDir:
|
|
9123
|
+
reportJsonPath: join12(runDir, "report.json"),
|
|
9124
|
+
tracePath: join12(runDir, "trace.zip"),
|
|
9125
|
+
normalizedPath: join12(runDir, "normalized.json"),
|
|
9126
|
+
screenshotsDir: join12(runDir, "screenshots"),
|
|
9127
|
+
videoDir: join12(runDir, "videos")
|
|
8707
9128
|
};
|
|
8708
9129
|
}
|
|
8709
9130
|
};
|
|
@@ -8714,7 +9135,7 @@ function generateRunId2(now = new Date) {
|
|
|
8714
9135
|
|
|
8715
9136
|
// src/auth/refresh.ts
|
|
8716
9137
|
var RE = /^(\d+)([hms])$/;
|
|
8717
|
-
function
|
|
9138
|
+
function parseDuration2(d) {
|
|
8718
9139
|
const m = RE.exec(d);
|
|
8719
9140
|
if (!m)
|
|
8720
9141
|
throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
|
|
@@ -8729,8 +9150,8 @@ function parseDuration(d) {
|
|
|
8729
9150
|
function needsRefresh(entry, policy, now = new Date) {
|
|
8730
9151
|
if (!entry)
|
|
8731
9152
|
return true;
|
|
8732
|
-
const ttlMs =
|
|
8733
|
-
const bufMs =
|
|
9153
|
+
const ttlMs = parseDuration2(policy.ttl);
|
|
9154
|
+
const bufMs = parseDuration2(policy.refreshBuffer);
|
|
8734
9155
|
const createdAt = new Date(entry.created_at).getTime();
|
|
8735
9156
|
if (now.getTime() - createdAt > ttlMs)
|
|
8736
9157
|
return true;
|
|
@@ -8741,9 +9162,9 @@ function needsRefresh(entry, policy, now = new Date) {
|
|
|
8741
9162
|
}
|
|
8742
9163
|
|
|
8743
9164
|
// src/auth/state.ts
|
|
8744
|
-
import { existsSync as
|
|
8745
|
-
import { join as
|
|
8746
|
-
import { z as
|
|
9165
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
9166
|
+
import { join as join13 } from "path";
|
|
9167
|
+
import { z as z5 } from "zod";
|
|
8747
9168
|
|
|
8748
9169
|
// src/auth/encrypt.ts
|
|
8749
9170
|
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
@@ -8800,123 +9221,39 @@ function resolveAuthKey() {
|
|
|
8800
9221
|
}
|
|
8801
9222
|
|
|
8802
9223
|
// src/auth/state.ts
|
|
8803
|
-
var AuthStateEntrySchema =
|
|
8804
|
-
role:
|
|
8805
|
-
strategy:
|
|
8806
|
-
created_at:
|
|
8807
|
-
expires_at:
|
|
8808
|
-
payload:
|
|
9224
|
+
var AuthStateEntrySchema = z5.object({
|
|
9225
|
+
role: z5.string(),
|
|
9226
|
+
strategy: z5.enum(["storageState", "apiToken"]),
|
|
9227
|
+
created_at: z5.string(),
|
|
9228
|
+
expires_at: z5.string(),
|
|
9229
|
+
payload: z5.record(z5.string(), z5.unknown())
|
|
8809
9230
|
});
|
|
8810
9231
|
function pathFor(authDir, role) {
|
|
8811
|
-
return
|
|
9232
|
+
return join13(authDir, `${role}.json`);
|
|
8812
9233
|
}
|
|
8813
9234
|
function writeAuthState(authDir, entry) {
|
|
8814
|
-
|
|
9235
|
+
mkdirSync6(authDir, { recursive: true });
|
|
8815
9236
|
const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
|
|
8816
|
-
|
|
9237
|
+
writeFileSync7(pathFor(authDir, entry.role), ct);
|
|
8817
9238
|
}
|
|
8818
9239
|
function readAuthState(authDir, role) {
|
|
8819
9240
|
const p = pathFor(authDir, role);
|
|
8820
|
-
if (!
|
|
9241
|
+
if (!existsSync15(p))
|
|
8821
9242
|
return null;
|
|
8822
|
-
const txt =
|
|
9243
|
+
const txt = readFileSync12(p, "utf8");
|
|
8823
9244
|
const plain = decrypt(txt, resolveAuthKey());
|
|
8824
9245
|
return AuthStateEntrySchema.parse(JSON.parse(plain));
|
|
8825
9246
|
}
|
|
8826
9247
|
|
|
8827
|
-
// src/config/load.ts
|
|
8828
|
-
import { existsSync as existsSync10 } from "fs";
|
|
8829
|
-
import { join as join9 } from "path";
|
|
8830
|
-
import { pathToFileURL } from "url";
|
|
8831
|
-
|
|
8832
|
-
// src/config/schema.ts
|
|
8833
|
-
import { z as z4 } from "zod";
|
|
8834
|
-
var AuthRoleSchema = z4.object({
|
|
8835
|
-
envEmail: z4.string().min(1),
|
|
8836
|
-
envPassword: z4.string().min(1)
|
|
8837
|
-
});
|
|
8838
|
-
var AuthSchema = z4.object({
|
|
8839
|
-
strategy: z4.enum(["storageState", "apiToken", "none"]).default("none"),
|
|
8840
|
-
ttl: z4.string().default("8h"),
|
|
8841
|
-
refreshBuffer: z4.string().default("30m"),
|
|
8842
|
-
setupScript: z4.string().optional(),
|
|
8843
|
-
roles: z4.record(z4.string(), AuthRoleSchema).default({})
|
|
8844
|
-
});
|
|
8845
|
-
var WebSchema = z4.object({
|
|
8846
|
-
baseUrl: z4.record(z4.string(), z4.string().url()).refine((m) => Object.keys(m).length > 0, {
|
|
8847
|
-
message: "baseUrl must have at least one environment"
|
|
8848
|
-
}),
|
|
8849
|
-
defaultEnv: z4.string(),
|
|
8850
|
-
auth: AuthSchema.prefault({}),
|
|
8851
|
-
testData: z4.object({
|
|
8852
|
-
users: z4.record(z4.string(), z4.object({ fromAuth: z4.string() })).default({})
|
|
8853
|
-
}).prefault({})
|
|
8854
|
-
}).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
|
|
8855
|
-
message: "defaultEnv must exist in baseUrl map",
|
|
8856
|
-
path: ["defaultEnv"]
|
|
8857
|
-
});
|
|
8858
|
-
var JiraSchema = z4.object({
|
|
8859
|
-
baseUrl: z4.string().url(),
|
|
8860
|
-
projectKeys: z4.array(z4.string().min(1)).min(1),
|
|
8861
|
-
fields: z4.object({
|
|
8862
|
-
story: z4.string().min(1),
|
|
8863
|
-
acceptanceCriteria: z4.string().optional(),
|
|
8864
|
-
attachments: z4.string().default("attachment")
|
|
8865
|
-
})
|
|
8866
|
-
});
|
|
8867
|
-
var AISchema = z4.object({
|
|
8868
|
-
livePageSnapshot: z4.boolean().default(true),
|
|
8869
|
-
confidenceThreshold: z4.enum(["low", "medium", "high"]).default("medium"),
|
|
8870
|
-
maxRetries: z4.object({
|
|
8871
|
-
typecheck: z4.number().int().min(0).max(5).default(2),
|
|
8872
|
-
lint: z4.number().int().min(0).max(5).default(2),
|
|
8873
|
-
validateFeature: z4.number().int().min(0).max(5).default(2)
|
|
8874
|
-
}).prefault({})
|
|
8875
|
-
}).prefault({});
|
|
8876
|
-
var ReportingSchema = z4.object({
|
|
8877
|
-
language: z4.enum(["en", "vi"]).default("en"),
|
|
8878
|
-
postToJira: z4.boolean().default(true),
|
|
8879
|
-
transition: z4.object({
|
|
8880
|
-
onPass: z4.string().nullable().default(null),
|
|
8881
|
-
onFail: z4.string().nullable().default(null)
|
|
8882
|
-
}).prefault({}),
|
|
8883
|
-
artifactLinks: z4.enum(["git", "local"]).default("git")
|
|
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({});
|
|
8891
|
-
var XeraConfigSchema = z4.object({
|
|
8892
|
-
jira: JiraSchema,
|
|
8893
|
-
web: WebSchema,
|
|
8894
|
-
ai: AISchema,
|
|
8895
|
-
reporting: ReportingSchema,
|
|
8896
|
-
run: RunSchema.prefault({}),
|
|
8897
|
-
adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
|
|
8898
|
-
});
|
|
8899
|
-
|
|
8900
|
-
// src/config/load.ts
|
|
8901
|
-
async function loadConfig(cwd) {
|
|
8902
|
-
const path = join9(cwd, "xera.config.ts");
|
|
8903
|
-
if (!existsSync10(path)) {
|
|
8904
|
-
throw new Error(`xera.config.ts not found in ${cwd}`);
|
|
8905
|
-
}
|
|
8906
|
-
const mod = await import(pathToFileURL(path).href);
|
|
8907
|
-
const raw = mod.default ?? mod;
|
|
8908
|
-
return XeraConfigSchema.parse(raw);
|
|
8909
|
-
}
|
|
8910
|
-
|
|
8911
9248
|
// src/logging/ndjson-logger.ts
|
|
8912
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
8913
|
-
import { dirname as
|
|
9249
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync13 } from "fs";
|
|
9250
|
+
import { dirname as dirname4 } from "path";
|
|
8914
9251
|
|
|
8915
9252
|
class NdjsonLogger {
|
|
8916
9253
|
path;
|
|
8917
9254
|
constructor(path) {
|
|
8918
9255
|
this.path = path;
|
|
8919
|
-
|
|
9256
|
+
mkdirSync7(dirname4(path), { recursive: true });
|
|
8920
9257
|
}
|
|
8921
9258
|
log(payload) {
|
|
8922
9259
|
const entry = { ts: new Date().toISOString(), ...payload };
|
|
@@ -8924,9 +9261,9 @@ class NdjsonLogger {
|
|
|
8924
9261
|
`);
|
|
8925
9262
|
}
|
|
8926
9263
|
static readAll(path) {
|
|
8927
|
-
if (!
|
|
9264
|
+
if (!existsSync16(path))
|
|
8928
9265
|
return [];
|
|
8929
|
-
const txt =
|
|
9266
|
+
const txt = readFileSync13(path, "utf8").trim();
|
|
8930
9267
|
if (!txt)
|
|
8931
9268
|
return [];
|
|
8932
9269
|
return txt.split(`
|
|
@@ -8941,6 +9278,8 @@ async function execCmd(argv) {
|
|
|
8941
9278
|
console.error("[xera:exec] usage: exec <TICKET>");
|
|
8942
9279
|
return 1;
|
|
8943
9280
|
}
|
|
9281
|
+
const grepIdx = argv.indexOf("--grep");
|
|
9282
|
+
const grep = grepIdx > -1 ? argv[grepIdx + 1] : undefined;
|
|
8944
9283
|
const cwd = process.cwd();
|
|
8945
9284
|
const config = await loadConfig(cwd);
|
|
8946
9285
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
@@ -8959,14 +9298,41 @@ async function execCmd(argv) {
|
|
|
8959
9298
|
}
|
|
8960
9299
|
const t0 = Date.now();
|
|
8961
9300
|
try {
|
|
8962
|
-
|
|
9301
|
+
const meta = readMeta(paths.metaPath);
|
|
9302
|
+
const adapter = meta?.adapter ?? "web";
|
|
9303
|
+
if (adapter === "http") {
|
|
9304
|
+
if (!config.http) {
|
|
9305
|
+
throw new Error("http adapter requires http config block");
|
|
9306
|
+
}
|
|
9307
|
+
const env = process.env["XERA_ENV"] ?? config.http.defaultEnv;
|
|
9308
|
+
const { HttpAdapter } = await import("@xera-ai/http");
|
|
9309
|
+
const result = await HttpAdapter.execute({
|
|
9310
|
+
ticketDir: paths.ticketDir,
|
|
9311
|
+
config,
|
|
9312
|
+
runId,
|
|
9313
|
+
env
|
|
9314
|
+
});
|
|
9315
|
+
log.log({
|
|
9316
|
+
step: "exec.complete",
|
|
9317
|
+
runId,
|
|
9318
|
+
outcome: result.outcome,
|
|
9319
|
+
elapsedMs: Date.now() - t0
|
|
9320
|
+
});
|
|
9321
|
+
console.log(`[xera:exec] runId=${runId} outcome=${result.outcome}`);
|
|
9322
|
+
return result.outcome === "PASS" ? 0 : 3;
|
|
9323
|
+
}
|
|
9324
|
+
if (!config.web) {
|
|
9325
|
+
throw new Error("web adapter requires web config block");
|
|
9326
|
+
}
|
|
9327
|
+
const webConfig = config.web;
|
|
9328
|
+
if (webConfig.auth.strategy === "storageState" && webConfig.auth.setupScript) {
|
|
8963
9329
|
const browser = await chromium.launch();
|
|
8964
9330
|
try {
|
|
8965
|
-
for (const [roleName, roleCreds] of Object.entries(
|
|
9331
|
+
for (const [roleName, roleCreds] of Object.entries(webConfig.auth.roles)) {
|
|
8966
9332
|
const entry = readAuthState(paths.authDir, roleName);
|
|
8967
9333
|
if (needsRefresh(entry, {
|
|
8968
|
-
ttl:
|
|
8969
|
-
refreshBuffer:
|
|
9334
|
+
ttl: webConfig.auth.ttl,
|
|
9335
|
+
refreshBuffer: webConfig.auth.refreshBuffer
|
|
8970
9336
|
})) {
|
|
8971
9337
|
const email = process.env[roleCreds.envEmail];
|
|
8972
9338
|
const password = process.env[roleCreds.envPassword];
|
|
@@ -8977,7 +9343,7 @@ async function execCmd(argv) {
|
|
|
8977
9343
|
await runAuthSetup({
|
|
8978
9344
|
role: roleName,
|
|
8979
9345
|
creds: { email, password },
|
|
8980
|
-
setupScriptPath:
|
|
9346
|
+
setupScriptPath: join14(cwd, webConfig.auth.setupScript),
|
|
8981
9347
|
authDir: paths.authDir,
|
|
8982
9348
|
browser
|
|
8983
9349
|
});
|
|
@@ -8988,23 +9354,23 @@ async function execCmd(argv) {
|
|
|
8988
9354
|
await browser.close();
|
|
8989
9355
|
}
|
|
8990
9356
|
}
|
|
8991
|
-
if (
|
|
8992
|
-
for (const roleName of Object.keys(
|
|
9357
|
+
if (webConfig.auth.strategy === "storageState") {
|
|
9358
|
+
for (const roleName of Object.keys(webConfig.auth.roles)) {
|
|
8993
9359
|
if (readAuthState(paths.authDir, roleName)) {
|
|
8994
9360
|
stagePlaywrightState(paths.authDir, roleName);
|
|
8995
9361
|
}
|
|
8996
9362
|
}
|
|
8997
9363
|
}
|
|
8998
|
-
const cfgPath =
|
|
8999
|
-
if (!
|
|
9364
|
+
const cfgPath = join14(cwd, "playwright.config.ts");
|
|
9365
|
+
if (!existsSync17(cfgPath)) {
|
|
9000
9366
|
console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
|
|
9001
9367
|
return 1;
|
|
9002
9368
|
}
|
|
9003
9369
|
const runDir = paths.runPath(runId).runDir;
|
|
9004
|
-
|
|
9005
|
-
const envName = process.env.XERA_ENV ??
|
|
9006
|
-
const baseURL =
|
|
9007
|
-
const reportJsonPath =
|
|
9370
|
+
mkdirSync8(runDir, { recursive: true });
|
|
9371
|
+
const envName = process.env.XERA_ENV ?? webConfig.defaultEnv;
|
|
9372
|
+
const baseURL = webConfig.baseUrl[envName] ?? webConfig.baseUrl[webConfig.defaultEnv];
|
|
9373
|
+
const reportJsonPath = join14(runDir, "report.json");
|
|
9008
9374
|
log.log({ step: "exec.start", runId, env: envName, baseURL });
|
|
9009
9375
|
const r = await runPlaywright({
|
|
9010
9376
|
specPath: paths.specPath,
|
|
@@ -9014,7 +9380,8 @@ async function execCmd(argv) {
|
|
|
9014
9380
|
XERA_BASE_URL: baseURL,
|
|
9015
9381
|
XERA_ENV: envName,
|
|
9016
9382
|
PLAYWRIGHT_JSON_OUTPUT_NAME: reportJsonPath
|
|
9017
|
-
}
|
|
9383
|
+
},
|
|
9384
|
+
...grep && { grep }
|
|
9018
9385
|
});
|
|
9019
9386
|
log.log({ step: "exec.done", runId, exit: r.exitCode, ms: Date.now() - t0 });
|
|
9020
9387
|
console.log(`[xera:exec] runId=${runId} outcome=${r.outcome}`);
|
|
@@ -9029,84 +9396,47 @@ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
|
9029
9396
|
import { dirname as dirname5 } from "path";
|
|
9030
9397
|
|
|
9031
9398
|
// src/artifact/hash.ts
|
|
9032
|
-
import { createHash as
|
|
9033
|
-
import { existsSync as
|
|
9399
|
+
import { createHash as createHash4 } from "crypto";
|
|
9400
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14 } from "fs";
|
|
9034
9401
|
function hashString(s) {
|
|
9035
|
-
return `sha256:${
|
|
9402
|
+
return `sha256:${createHash4("sha256").update(s).digest("hex")}`;
|
|
9036
9403
|
}
|
|
9037
9404
|
function hashFile(path) {
|
|
9038
|
-
return hashString(
|
|
9405
|
+
return hashString(readFileSync14(path, "utf8"));
|
|
9039
9406
|
}
|
|
9040
9407
|
function hashFileIfExists(path) {
|
|
9041
|
-
if (!
|
|
9408
|
+
if (!existsSync18(path))
|
|
9042
9409
|
return null;
|
|
9043
9410
|
return hashFile(path);
|
|
9044
9411
|
}
|
|
9045
9412
|
|
|
9046
|
-
// src/artifact/meta.ts
|
|
9047
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
9048
|
-
import { dirname as dirname4 } from "path";
|
|
9049
|
-
import { z as z5 } from "zod";
|
|
9050
|
-
var MetaJsonSchema = z5.object({
|
|
9051
|
-
ticket: z5.string(),
|
|
9052
|
-
adapter: z5.string(),
|
|
9053
|
-
xera_version: z5.string(),
|
|
9054
|
-
prompts_version: z5.string(),
|
|
9055
|
-
fetched_at: z5.string().optional(),
|
|
9056
|
-
story_hash: z5.string().optional(),
|
|
9057
|
-
feature_generated_at: z5.string().optional(),
|
|
9058
|
-
feature_generated_from_story_hash: z5.string().optional(),
|
|
9059
|
-
feature_hash: z5.string().optional(),
|
|
9060
|
-
script_generated_at: z5.string().optional(),
|
|
9061
|
-
script_generated_from_feature_hash: z5.string().optional(),
|
|
9062
|
-
script_warnings: z5.array(z5.string()).optional()
|
|
9063
|
-
});
|
|
9064
|
-
function readMeta(path) {
|
|
9065
|
-
if (!existsSync14(path))
|
|
9066
|
-
return null;
|
|
9067
|
-
return MetaJsonSchema.parse(JSON.parse(readFileSync12(path, "utf8")));
|
|
9068
|
-
}
|
|
9069
|
-
function writeMeta(path, meta) {
|
|
9070
|
-
mkdirSync8(dirname4(path), { recursive: true });
|
|
9071
|
-
writeFileSync7(path, JSON.stringify(meta, null, 2));
|
|
9072
|
-
}
|
|
9073
|
-
function updateMeta(path, patch) {
|
|
9074
|
-
const existing = readMeta(path);
|
|
9075
|
-
if (!existing) {
|
|
9076
|
-
throw new Error(`meta.json not found at ${path}; cannot update`);
|
|
9077
|
-
}
|
|
9078
|
-
const next = { ...existing, ...patch };
|
|
9079
|
-
writeMeta(path, next);
|
|
9080
|
-
return next;
|
|
9081
|
-
}
|
|
9082
|
-
|
|
9083
9413
|
// src/jira/mcp-backend.ts
|
|
9084
|
-
import { existsSync as
|
|
9414
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
|
|
9085
9415
|
import { tmpdir } from "os";
|
|
9086
|
-
import { join as
|
|
9416
|
+
import { join as join15 } from "path";
|
|
9087
9417
|
var MCP_ENV = "XERA_MCP_JIRA";
|
|
9088
9418
|
async function createMcpBackend(_baseUrl) {
|
|
9089
9419
|
if (process.env[MCP_ENV] !== "1")
|
|
9090
9420
|
return null;
|
|
9091
|
-
const tmpDir =
|
|
9421
|
+
const tmpDir = join15(tmpdir(), "xera-mcp");
|
|
9092
9422
|
mkdirSync9(tmpDir, { recursive: true });
|
|
9093
9423
|
return {
|
|
9094
9424
|
backend: "mcp",
|
|
9095
9425
|
async fetchTicket(key, _fields) {
|
|
9096
|
-
const cachePath =
|
|
9097
|
-
if (!
|
|
9426
|
+
const cachePath = join15(tmpDir, `${key}.json`);
|
|
9427
|
+
if (!existsSync19(cachePath)) {
|
|
9098
9428
|
throw new Error(`MCP-mode fetch requires the skill to first call mcp__atlassian__getJiraIssue and write ${cachePath}. ` + `If you are running this directly, unset ${MCP_ENV} to use REST.`);
|
|
9099
9429
|
}
|
|
9100
|
-
const parsed = JSON.parse(
|
|
9430
|
+
const parsed = JSON.parse(readFileSync15(cachePath, "utf8"));
|
|
9101
9431
|
return parsed;
|
|
9102
9432
|
},
|
|
9103
9433
|
async postComment(key, body) {
|
|
9104
|
-
const outPath =
|
|
9434
|
+
const outPath = join15(tmpDir, `${key}.comment.json`);
|
|
9105
9435
|
writeFileSync8(outPath, JSON.stringify({ key, body }));
|
|
9106
9436
|
return { id: "mcp-pending" };
|
|
9107
9437
|
},
|
|
9108
9438
|
async transitionStatus(key, statusName) {
|
|
9109
|
-
const outPath =
|
|
9439
|
+
const outPath = join15(tmpDir, `${key}.transition.json`);
|
|
9110
9440
|
writeFileSync8(outPath, JSON.stringify({ key, statusName }));
|
|
9111
9441
|
},
|
|
9112
9442
|
async listFields(_sampleKey) {
|
|
@@ -9275,55 +9605,14 @@ function renderStory(t) {
|
|
|
9275
9605
|
`);
|
|
9276
9606
|
}
|
|
9277
9607
|
|
|
9278
|
-
// src/bin-internal/
|
|
9279
|
-
|
|
9280
|
-
import { existsSync as existsSync18, readdirSync as readdirSync5 } from "fs";
|
|
9281
|
-
import { join as join14 } from "path";
|
|
9282
|
-
async function backfillTicket(repoRoot, ticket, dryRun) {
|
|
9283
|
-
const storyPath = join14(repoRoot, ".xera", ticket, "story.md");
|
|
9284
|
-
if (!existsSync18(storyPath))
|
|
9285
|
-
return 0;
|
|
9286
|
-
const { recordFetch: recordFetch2 } = await Promise.resolve().then(() => (init_graph_record(), exports_graph_record));
|
|
9287
|
-
if (dryRun) {
|
|
9288
|
-
console.log(`[backfill dry-run] would backfill ${ticket}`);
|
|
9289
|
-
return 0;
|
|
9290
|
-
}
|
|
9291
|
-
await recordScriptImpl(repoRoot, ticket);
|
|
9292
|
-
await recordFetch2(repoRoot, ticket);
|
|
9293
|
-
return 0;
|
|
9294
|
-
}
|
|
9295
|
-
async function graphBackfillCmd(argv) {
|
|
9296
|
-
const dryRun = argv.includes("--dry-run");
|
|
9297
|
-
const repoRoot = process.cwd();
|
|
9298
|
-
const xeraDir = join14(repoRoot, ".xera");
|
|
9299
|
-
if (!existsSync18(xeraDir)) {
|
|
9300
|
-
console.log("[backfill] no .xera/ directory");
|
|
9301
|
-
return 0;
|
|
9302
|
-
}
|
|
9303
|
-
const tickets = [];
|
|
9304
|
-
for (const entry of readdirSync5(xeraDir, { withFileTypes: true })) {
|
|
9305
|
-
if (!entry.isDirectory())
|
|
9306
|
-
continue;
|
|
9307
|
-
if (entry.name === "graph")
|
|
9308
|
-
continue;
|
|
9309
|
-
if (entry.name.startsWith("."))
|
|
9310
|
-
continue;
|
|
9311
|
-
if (!/^[A-Z]+-\d+$/.test(entry.name))
|
|
9312
|
-
continue;
|
|
9313
|
-
tickets.push(entry.name);
|
|
9314
|
-
}
|
|
9315
|
-
console.log(`[backfill] found ${tickets.length} tickets`);
|
|
9316
|
-
for (const t of tickets)
|
|
9317
|
-
await backfillTicket(repoRoot, t, dryRun);
|
|
9318
|
-
console.log(`[backfill] done`);
|
|
9319
|
-
return 0;
|
|
9320
|
-
}
|
|
9608
|
+
// src/bin-internal/index.ts
|
|
9609
|
+
init_graph_backfill();
|
|
9321
9610
|
|
|
9322
9611
|
// src/graph/enrich.ts
|
|
9323
9612
|
init_store();
|
|
9324
9613
|
init_ulid();
|
|
9325
|
-
import { existsSync as
|
|
9326
|
-
import { join as
|
|
9614
|
+
import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
|
|
9615
|
+
import { join as join16 } from "path";
|
|
9327
9616
|
import { z as z6 } from "zod";
|
|
9328
9617
|
var MAX_SIMILAR_EDGES = 10;
|
|
9329
9618
|
var MIN_CONFIDENCE = 0.7;
|
|
@@ -9345,8 +9634,8 @@ var mk2 = (actor, type, payload) => ({
|
|
|
9345
9634
|
payload
|
|
9346
9635
|
});
|
|
9347
9636
|
async function enrichTicket(repoRoot, ticketId, opts) {
|
|
9348
|
-
const inputPath =
|
|
9349
|
-
if (!
|
|
9637
|
+
const inputPath = join16(repoRoot, ".xera", ticketId, "enrichment-input.json");
|
|
9638
|
+
if (!existsSync20(inputPath)) {
|
|
9350
9639
|
throw new Error(`enrichment-input.json not found at ${inputPath}`);
|
|
9351
9640
|
}
|
|
9352
9641
|
const raw = JSON.parse(readFileSync16(inputPath, "utf8"));
|
|
@@ -9421,7 +9710,7 @@ function filterByTicket(snap, ticket) {
|
|
|
9421
9710
|
};
|
|
9422
9711
|
return out;
|
|
9423
9712
|
}
|
|
9424
|
-
function
|
|
9713
|
+
function renderText2(snap) {
|
|
9425
9714
|
const out = [];
|
|
9426
9715
|
out.push(`Graph snapshot \u2014 ${snap.event_count} events`);
|
|
9427
9716
|
out.push(`Tickets: ${Object.keys(snap.tickets).length}`);
|
|
@@ -9450,7 +9739,7 @@ async function graphQueryCmd(argv) {
|
|
|
9450
9739
|
if (format === "json")
|
|
9451
9740
|
process.stdout.write(JSON.stringify(snap, null, 2));
|
|
9452
9741
|
else
|
|
9453
|
-
process.stdout.write(
|
|
9742
|
+
process.stdout.write(renderText2(snap));
|
|
9454
9743
|
return 0;
|
|
9455
9744
|
}
|
|
9456
9745
|
|
|
@@ -9459,11 +9748,11 @@ init_graph_record();
|
|
|
9459
9748
|
|
|
9460
9749
|
// src/bin-internal/graph-render.ts
|
|
9461
9750
|
import { mkdirSync as mkdirSync11, renameSync as renameSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
9462
|
-
import { dirname as dirname7, join as
|
|
9751
|
+
import { dirname as dirname7, join as join18 } from "path";
|
|
9463
9752
|
|
|
9464
9753
|
// src/graph/render.ts
|
|
9465
9754
|
import { readFileSync as readFileSync17 } from "fs";
|
|
9466
|
-
import { dirname as dirname6, join as
|
|
9755
|
+
import { dirname as dirname6, join as join17 } from "path";
|
|
9467
9756
|
import { fileURLToPath } from "url";
|
|
9468
9757
|
var COLORS = {
|
|
9469
9758
|
ticket: "#3B82F6",
|
|
@@ -9682,9 +9971,9 @@ function transformForVisNetwork(snap, opts) {
|
|
|
9682
9971
|
}
|
|
9683
9972
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
9684
9973
|
var __dirname2 = dirname6(__filename2);
|
|
9685
|
-
var TEMPLATES_DIR =
|
|
9974
|
+
var TEMPLATES_DIR = join17(__dirname2, "templates");
|
|
9686
9975
|
function loadTemplate(name) {
|
|
9687
|
-
return readFileSync17(
|
|
9976
|
+
return readFileSync17(join17(TEMPLATES_DIR, name), "utf8");
|
|
9688
9977
|
}
|
|
9689
9978
|
function statsToHuman(s) {
|
|
9690
9979
|
return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
|
|
@@ -9730,7 +10019,7 @@ async function graphRenderCmd(argv) {
|
|
|
9730
10019
|
depth = parseDepth(argv[++i]);
|
|
9731
10020
|
}
|
|
9732
10021
|
const repoRoot = process.cwd();
|
|
9733
|
-
const finalPath = outPath ??
|
|
10022
|
+
const finalPath = outPath ?? join18(repoRoot, ".xera/graph.html");
|
|
9734
10023
|
const snap = deriveSnapshot(loadAllEvents(repoRoot));
|
|
9735
10024
|
const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
|
|
9736
10025
|
const performanceMode = decidePerformanceMode(totalNodeCount);
|
|
@@ -9782,8 +10071,8 @@ async function graphSnapshotCmd(argv) {
|
|
|
9782
10071
|
}
|
|
9783
10072
|
|
|
9784
10073
|
// src/bin-internal/heal-prepare.ts
|
|
9785
|
-
import { existsSync as
|
|
9786
|
-
import { join as
|
|
10074
|
+
import { existsSync as existsSync21, readdirSync as readdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
|
|
10075
|
+
import { join as join19 } from "path";
|
|
9787
10076
|
import { scrubFreeText } from "@xera-ai/web";
|
|
9788
10077
|
|
|
9789
10078
|
// ../../node_modules/.bun/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
|
@@ -10211,7 +10500,7 @@ function classifyKind(raw) {
|
|
|
10211
10500
|
return "other";
|
|
10212
10501
|
}
|
|
10213
10502
|
function extractDomSnapshot(tracePath) {
|
|
10214
|
-
if (!
|
|
10503
|
+
if (!existsSync21(tracePath))
|
|
10215
10504
|
return "";
|
|
10216
10505
|
const buf = readFileSync18(tracePath);
|
|
10217
10506
|
const entries = unzipSync(buf);
|
|
@@ -10261,12 +10550,12 @@ function extractDomSnapshot(tracePath) {
|
|
|
10261
10550
|
return scrubFreeText(html);
|
|
10262
10551
|
}
|
|
10263
10552
|
function findPomLine(ticketDir, rawLocator) {
|
|
10264
|
-
const pomDir =
|
|
10553
|
+
const pomDir = join19(ticketDir, "page-objects");
|
|
10265
10554
|
const candidates = [];
|
|
10266
|
-
if (
|
|
10555
|
+
if (existsSync21(pomDir)) {
|
|
10267
10556
|
for (const name of readdirSync6(pomDir)) {
|
|
10268
10557
|
if (name.endsWith(".ts"))
|
|
10269
|
-
candidates.push(
|
|
10558
|
+
candidates.push(join19(pomDir, name));
|
|
10270
10559
|
}
|
|
10271
10560
|
}
|
|
10272
10561
|
for (const file of candidates) {
|
|
@@ -10308,13 +10597,13 @@ function findGherkinStep(featureText, rawLocator) {
|
|
|
10308
10597
|
}
|
|
10309
10598
|
function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
10310
10599
|
const paths = resolveArtifactPaths(repoRoot, ticket);
|
|
10311
|
-
const classifierPath =
|
|
10600
|
+
const classifierPath = join19(paths.ticketDir, "classifier-input.json");
|
|
10312
10601
|
const classifier = JSON.parse(readFileSync18(classifierPath, "utf8"));
|
|
10313
10602
|
const cls = classifier.scenarios.find((s) => s.name === scenarioName);
|
|
10314
10603
|
if (!cls)
|
|
10315
10604
|
throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
|
|
10316
|
-
const runDir =
|
|
10317
|
-
const normalized = JSON.parse(readFileSync18(
|
|
10605
|
+
const runDir = join19(paths.runsDir, runId);
|
|
10606
|
+
const normalized = JSON.parse(readFileSync18(join19(runDir, "normalized.json"), "utf8"));
|
|
10318
10607
|
const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
|
|
10319
10608
|
if (!normSc?.failure)
|
|
10320
10609
|
throw new Error(`no failure recorded for scenario "${scenarioName}"`);
|
|
@@ -10327,7 +10616,7 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
|
|
|
10327
10616
|
const pomLoc = findPomLine(paths.ticketDir, raw);
|
|
10328
10617
|
const featureText = readFileSync18(paths.featurePath, "utf8");
|
|
10329
10618
|
const gherkinStep = findGherkinStep(featureText, raw);
|
|
10330
|
-
const domSnapshotAtFailure = extractDomSnapshot(
|
|
10619
|
+
const domSnapshotAtFailure = extractDomSnapshot(join19(runDir, "trace.zip"));
|
|
10331
10620
|
return {
|
|
10332
10621
|
ticket,
|
|
10333
10622
|
runId,
|
|
@@ -10347,7 +10636,7 @@ async function healPrepareCmd(argv) {
|
|
|
10347
10636
|
try {
|
|
10348
10637
|
const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
|
|
10349
10638
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
10350
|
-
const outPath =
|
|
10639
|
+
const outPath = join19(paths.runsDir, runId, "heal-input.json");
|
|
10351
10640
|
writeFileSync11(outPath, JSON.stringify(result, null, 2));
|
|
10352
10641
|
console.log(`[xera:heal-prepare] wrote ${outPath}`);
|
|
10353
10642
|
return 0;
|
|
@@ -10359,7 +10648,7 @@ async function healPrepareCmd(argv) {
|
|
|
10359
10648
|
|
|
10360
10649
|
// src/bin-internal/impact-prepare.ts
|
|
10361
10650
|
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
|
|
10362
|
-
import { join as
|
|
10651
|
+
import { join as join20 } from "path";
|
|
10363
10652
|
|
|
10364
10653
|
// src/graph/impact.ts
|
|
10365
10654
|
var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
|
|
@@ -10609,11 +10898,11 @@ async function impactPrepareCmd(argv) {
|
|
|
10609
10898
|
scenarios,
|
|
10610
10899
|
generatedAt: new Date().toISOString()
|
|
10611
10900
|
};
|
|
10612
|
-
const impactDir =
|
|
10901
|
+
const impactDir = join20(repoRoot, ".xera/impact");
|
|
10613
10902
|
mkdirSync12(impactDir, { recursive: true });
|
|
10614
|
-
writeFileSync12(
|
|
10903
|
+
writeFileSync12(join20(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
|
|
10615
10904
|
if (!quiet) {
|
|
10616
|
-
writeFileSync12(
|
|
10905
|
+
writeFileSync12(join20(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
|
|
10617
10906
|
}
|
|
10618
10907
|
return 0;
|
|
10619
10908
|
}
|
|
@@ -10638,9 +10927,8 @@ async function lintCmd(argv) {
|
|
|
10638
10927
|
}
|
|
10639
10928
|
|
|
10640
10929
|
// src/bin-internal/normalize.ts
|
|
10641
|
-
import { existsSync as
|
|
10642
|
-
import { join as
|
|
10643
|
-
import { normalizeRun } from "@xera-ai/web";
|
|
10930
|
+
import { existsSync as existsSync22, readdirSync as readdirSync7 } from "fs";
|
|
10931
|
+
import { join as join21 } from "path";
|
|
10644
10932
|
async function normalizeCmd(argv) {
|
|
10645
10933
|
const ticket = argv[0];
|
|
10646
10934
|
if (!ticket) {
|
|
@@ -10654,22 +10942,31 @@ async function normalizeCmd(argv) {
|
|
|
10654
10942
|
console.error("[xera:normalize] no run found");
|
|
10655
10943
|
return 1;
|
|
10656
10944
|
}
|
|
10657
|
-
const runDir =
|
|
10658
|
-
if (!
|
|
10945
|
+
const runDir = join21(paths.runsDir, runId);
|
|
10946
|
+
if (!existsSync22(runDir)) {
|
|
10659
10947
|
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
10660
10948
|
return 1;
|
|
10661
10949
|
}
|
|
10950
|
+
const meta = readMeta(paths.metaPath);
|
|
10951
|
+
const adapter = meta?.adapter ?? "web";
|
|
10952
|
+
if (adapter === "http") {
|
|
10953
|
+
const { normalizeHttpRun } = await import("@xera-ai/http");
|
|
10954
|
+
await normalizeHttpRun({ runId, runDir });
|
|
10955
|
+
console.log(`[xera:normalize] wrote normalized.json (http)`);
|
|
10956
|
+
return 0;
|
|
10957
|
+
}
|
|
10958
|
+
const { normalizeRun } = await import("@xera-ai/web");
|
|
10662
10959
|
const r = await normalizeRun({ runId, runDir });
|
|
10663
10960
|
console.log(`[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`);
|
|
10664
10961
|
return 0;
|
|
10665
10962
|
}
|
|
10666
10963
|
|
|
10667
10964
|
// src/bin-internal/post.ts
|
|
10668
|
-
import { existsSync as
|
|
10669
|
-
import { join as
|
|
10965
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20 } from "fs";
|
|
10966
|
+
import { join as join22 } from "path";
|
|
10670
10967
|
|
|
10671
10968
|
// src/artifact/status.ts
|
|
10672
|
-
import { existsSync as
|
|
10969
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
|
|
10673
10970
|
import { dirname as dirname8 } from "path";
|
|
10674
10971
|
import { z as z7 } from "zod";
|
|
10675
10972
|
var ClassificationEnum = z7.enum([
|
|
@@ -10678,7 +10975,10 @@ var ClassificationEnum = z7.enum([
|
|
|
10678
10975
|
"SELECTOR_DRIFT",
|
|
10679
10976
|
"FLAKY",
|
|
10680
10977
|
"TEST_BUG",
|
|
10681
|
-
"TEST_OUTDATED"
|
|
10978
|
+
"TEST_OUTDATED",
|
|
10979
|
+
"CONTRACT_DRIFT",
|
|
10980
|
+
"RATE_LIMITED",
|
|
10981
|
+
"AUTH_EXPIRED"
|
|
10682
10982
|
]);
|
|
10683
10983
|
var ResultEnum = z7.enum(["PASS", "FAIL"]);
|
|
10684
10984
|
var ConfidenceEnum = z7.enum(["low", "medium", "high"]);
|
|
@@ -10704,7 +11004,7 @@ var StatusJsonSchema = z7.object({
|
|
|
10704
11004
|
});
|
|
10705
11005
|
var HISTORY_CAP = 20;
|
|
10706
11006
|
function readStatus(path) {
|
|
10707
|
-
if (!
|
|
11007
|
+
if (!existsSync23(path))
|
|
10708
11008
|
return null;
|
|
10709
11009
|
return StatusJsonSchema.parse(JSON.parse(readFileSync19(path, "utf8")));
|
|
10710
11010
|
}
|
|
@@ -10736,8 +11036,8 @@ async function postCmd(argv) {
|
|
|
10736
11036
|
return 0;
|
|
10737
11037
|
}
|
|
10738
11038
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
10739
|
-
const draftPath =
|
|
10740
|
-
if (!
|
|
11039
|
+
const draftPath = join22(paths.ticketDir, "jira-comment.draft.md");
|
|
11040
|
+
if (!existsSync24(draftPath)) {
|
|
10741
11041
|
console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
|
|
10742
11042
|
return 1;
|
|
10743
11043
|
}
|
|
@@ -10769,12 +11069,15 @@ async function promoteCmd(argv) {
|
|
|
10769
11069
|
}
|
|
10770
11070
|
|
|
10771
11071
|
// src/bin-internal/report.ts
|
|
10772
|
-
import { existsSync as
|
|
10773
|
-
import { join as
|
|
11072
|
+
import { existsSync as existsSync26, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
|
|
11073
|
+
import { join as join23 } from "path";
|
|
10774
11074
|
|
|
10775
11075
|
// src/classifier/aggregate.ts
|
|
10776
11076
|
var CLASS_PRIORITY = [
|
|
10777
11077
|
"REAL_BUG",
|
|
11078
|
+
"CONTRACT_DRIFT",
|
|
11079
|
+
"AUTH_EXPIRED",
|
|
11080
|
+
"RATE_LIMITED",
|
|
10778
11081
|
"TEST_OUTDATED",
|
|
10779
11082
|
"TEST_BUG",
|
|
10780
11083
|
"SELECTOR_DRIFT",
|
|
@@ -10801,6 +11104,134 @@ function aggregateScenarios(scenarios) {
|
|
|
10801
11104
|
return { overall: chosen, overallConfidence: minConf, scenarios };
|
|
10802
11105
|
}
|
|
10803
11106
|
|
|
11107
|
+
// src/classifier/auth-expired.ts
|
|
11108
|
+
function jwtExpPast(jwt, now) {
|
|
11109
|
+
const parts = jwt.split(".");
|
|
11110
|
+
if (parts.length !== 3)
|
|
11111
|
+
return false;
|
|
11112
|
+
try {
|
|
11113
|
+
const payloadB64 = parts[1];
|
|
11114
|
+
if (!payloadB64)
|
|
11115
|
+
return false;
|
|
11116
|
+
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
11117
|
+
return typeof payload.exp === "number" && payload.exp * 1000 < now;
|
|
11118
|
+
} catch {
|
|
11119
|
+
return false;
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
function classifyAuthExpired(input) {
|
|
11123
|
+
const has401 = input.calls.some((c) => c.status === 401);
|
|
11124
|
+
if (!has401)
|
|
11125
|
+
return null;
|
|
11126
|
+
const now = Date.now();
|
|
11127
|
+
for (const [role, entry] of Object.entries(input.authFiles)) {
|
|
11128
|
+
const fileExpired = new Date(entry.expires_at).getTime() < now;
|
|
11129
|
+
const jwtExpired = entry.type === "bearer" && jwtExpPast(entry.token, now);
|
|
11130
|
+
if (fileExpired || jwtExpired) {
|
|
11131
|
+
return {
|
|
11132
|
+
class: "AUTH_EXPIRED",
|
|
11133
|
+
rationale: `HTTP 401 captured; auth file for role '${role}' is past expiry. Run: bun run xera:auth-setup --role ${role}`
|
|
11134
|
+
};
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
return null;
|
|
11138
|
+
}
|
|
11139
|
+
|
|
11140
|
+
// src/classifier/contract-drift.ts
|
|
11141
|
+
function matchPath(specPaths, actualUrl) {
|
|
11142
|
+
const path = actualUrl.split("?")[0] ?? actualUrl;
|
|
11143
|
+
for (const tmpl of specPaths) {
|
|
11144
|
+
const re = new RegExp(`^${tmpl.replace(/\{[^}]+\}/g, "[^/]+")}$`);
|
|
11145
|
+
if (re.test(path))
|
|
11146
|
+
return tmpl;
|
|
11147
|
+
}
|
|
11148
|
+
return null;
|
|
11149
|
+
}
|
|
11150
|
+
function matchesSchema(body, schema) {
|
|
11151
|
+
if (!schema)
|
|
11152
|
+
return true;
|
|
11153
|
+
if (schema.type === "object") {
|
|
11154
|
+
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
11155
|
+
return false;
|
|
11156
|
+
const obj = body;
|
|
11157
|
+
for (const req of schema.required ?? []) {
|
|
11158
|
+
if (!(req in obj))
|
|
11159
|
+
return false;
|
|
11160
|
+
}
|
|
11161
|
+
return true;
|
|
11162
|
+
}
|
|
11163
|
+
if (schema.type === "array")
|
|
11164
|
+
return Array.isArray(body);
|
|
11165
|
+
if (schema.type === "string")
|
|
11166
|
+
return typeof body === "string";
|
|
11167
|
+
if (schema.type === "integer" || schema.type === "number")
|
|
11168
|
+
return typeof body === "number";
|
|
11169
|
+
if (schema.type === "boolean")
|
|
11170
|
+
return typeof body === "boolean";
|
|
11171
|
+
if (schema.type === "null")
|
|
11172
|
+
return body === null;
|
|
11173
|
+
return true;
|
|
11174
|
+
}
|
|
11175
|
+
var VERBS = ["get", "post", "put", "patch", "delete"];
|
|
11176
|
+
function isVerb(s) {
|
|
11177
|
+
return VERBS.includes(s);
|
|
11178
|
+
}
|
|
11179
|
+
function classifyContractDrift(input) {
|
|
11180
|
+
if (input.openapi === null)
|
|
11181
|
+
return null;
|
|
11182
|
+
const specPaths = Object.keys(input.openapi.paths);
|
|
11183
|
+
for (const call of input.calls) {
|
|
11184
|
+
const tmpl = matchPath(specPaths, call.url);
|
|
11185
|
+
if (!tmpl) {
|
|
11186
|
+
return {
|
|
11187
|
+
class: "CONTRACT_DRIFT",
|
|
11188
|
+
rationale: `Endpoint ${call.method} ${call.url} not found in OpenAPI`
|
|
11189
|
+
};
|
|
11190
|
+
}
|
|
11191
|
+
const methodLower = call.method.toLowerCase();
|
|
11192
|
+
if (!isVerb(methodLower)) {
|
|
11193
|
+
return {
|
|
11194
|
+
class: "CONTRACT_DRIFT",
|
|
11195
|
+
rationale: `Method ${call.method} not supported by classifier for ${tmpl}`
|
|
11196
|
+
};
|
|
11197
|
+
}
|
|
11198
|
+
const pathItem = input.openapi.paths[tmpl];
|
|
11199
|
+
const op = pathItem?.[methodLower];
|
|
11200
|
+
if (!op) {
|
|
11201
|
+
return {
|
|
11202
|
+
class: "CONTRACT_DRIFT",
|
|
11203
|
+
rationale: `${call.method} not defined for ${tmpl} in OpenAPI`
|
|
11204
|
+
};
|
|
11205
|
+
}
|
|
11206
|
+
const respDef = op.responses?.[String(call.status)];
|
|
11207
|
+
if (!respDef) {
|
|
11208
|
+
return {
|
|
11209
|
+
class: "CONTRACT_DRIFT",
|
|
11210
|
+
rationale: `Status ${call.status} not enumerated for ${call.method} ${tmpl} in OpenAPI`
|
|
11211
|
+
};
|
|
11212
|
+
}
|
|
11213
|
+
const schema = respDef.content?.["application/json"]?.schema;
|
|
11214
|
+
if (!matchesSchema(call.respBody, schema)) {
|
|
11215
|
+
return {
|
|
11216
|
+
class: "CONTRACT_DRIFT",
|
|
11217
|
+
rationale: `Response body for ${call.method} ${tmpl} (${call.status}) does not match OpenAPI schema`
|
|
11218
|
+
};
|
|
11219
|
+
}
|
|
11220
|
+
}
|
|
11221
|
+
return null;
|
|
11222
|
+
}
|
|
11223
|
+
|
|
11224
|
+
// src/classifier/rate-limited.ts
|
|
11225
|
+
function classifyRateLimited(input) {
|
|
11226
|
+
const hit = input.calls.find((c) => c.status === 429);
|
|
11227
|
+
if (!hit)
|
|
11228
|
+
return null;
|
|
11229
|
+
return {
|
|
11230
|
+
class: "RATE_LIMITED",
|
|
11231
|
+
rationale: `Captured HTTP 429 on ${hit.method} ${hit.url}`
|
|
11232
|
+
};
|
|
11233
|
+
}
|
|
11234
|
+
|
|
10804
11235
|
// src/graph/classify.ts
|
|
10805
11236
|
var DEFAULT_THRESHOLD = 0.7;
|
|
10806
11237
|
var SHORT_CIRCUIT = ["FLAKY", "PASS"];
|
|
@@ -10905,11 +11336,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
|
|
|
10905
11336
|
}
|
|
10906
11337
|
|
|
10907
11338
|
// src/reporter/status-writer.ts
|
|
10908
|
-
import { existsSync as
|
|
11339
|
+
import { existsSync as existsSync25 } from "fs";
|
|
10909
11340
|
function writeStatusFromClassification(path, input) {
|
|
10910
11341
|
const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
|
|
10911
11342
|
const entry = { ts: input.runTs, result, class: input.classification.overall };
|
|
10912
|
-
if (!
|
|
11343
|
+
if (!existsSync25(path)) {
|
|
10913
11344
|
writeStatus(path, {
|
|
10914
11345
|
ticket: input.ticket,
|
|
10915
11346
|
lastRun: input.runTs,
|
|
@@ -10941,11 +11372,70 @@ async function reportCmd(argv) {
|
|
|
10941
11372
|
console.error("[xera:report] usage: report <TICKET> --input=<classifier-output.json>");
|
|
10942
11373
|
return 1;
|
|
10943
11374
|
}
|
|
10944
|
-
const
|
|
11375
|
+
const cwd = process.cwd();
|
|
11376
|
+
const paths = resolveArtifactPaths(cwd, ticket);
|
|
10945
11377
|
const input = JSON.parse(readFileSync21(inputArg.slice("--input=".length), "utf8"));
|
|
10946
|
-
|
|
10947
|
-
const
|
|
10948
|
-
|
|
11378
|
+
let httpRuleOverride = null;
|
|
11379
|
+
const meta = readMeta(paths.metaPath);
|
|
11380
|
+
if (meta?.adapter === "http") {
|
|
11381
|
+
const config = await loadConfig(cwd);
|
|
11382
|
+
if (config.http) {
|
|
11383
|
+
const normalizedPath = join23(paths.ticketDir, "runs", input.runId, "normalized.json");
|
|
11384
|
+
if (existsSync26(normalizedPath)) {
|
|
11385
|
+
const norm = JSON.parse(readFileSync21(normalizedPath, "utf8"));
|
|
11386
|
+
const calls = norm.http?.calls ?? [];
|
|
11387
|
+
const rate = classifyRateLimited({ calls });
|
|
11388
|
+
if (rate)
|
|
11389
|
+
httpRuleOverride = rate;
|
|
11390
|
+
if (!httpRuleOverride) {
|
|
11391
|
+
const authFiles = {};
|
|
11392
|
+
const httpAuthDir = join23(cwd, ".xera", ".auth", "http");
|
|
11393
|
+
for (const role of Object.keys(config.http.auth.roles)) {
|
|
11394
|
+
const entry = readAuthState(httpAuthDir, role);
|
|
11395
|
+
if (entry) {
|
|
11396
|
+
const p = entry.payload;
|
|
11397
|
+
if (typeof p.token === "string" && typeof p.type === "string") {
|
|
11398
|
+
authFiles[role] = {
|
|
11399
|
+
token: p.token,
|
|
11400
|
+
type: p.type,
|
|
11401
|
+
expires_at: entry.expires_at
|
|
11402
|
+
};
|
|
11403
|
+
}
|
|
11404
|
+
}
|
|
11405
|
+
}
|
|
11406
|
+
const authExp = classifyAuthExpired({ calls, authFiles });
|
|
11407
|
+
if (authExp)
|
|
11408
|
+
httpRuleOverride = authExp;
|
|
11409
|
+
}
|
|
11410
|
+
if (!httpRuleOverride && config.http.spec) {
|
|
11411
|
+
const { loadOpenApi } = await import("@xera-ai/http");
|
|
11412
|
+
const openapi = await loadOpenApi(config.http.spec);
|
|
11413
|
+
if (openapi) {
|
|
11414
|
+
const drift = classifyContractDrift({
|
|
11415
|
+
calls: calls.map((c) => ({
|
|
11416
|
+
method: c.method,
|
|
11417
|
+
url: c.url,
|
|
11418
|
+
status: c.status,
|
|
11419
|
+
respBody: c.respBody
|
|
11420
|
+
})),
|
|
11421
|
+
openapi
|
|
11422
|
+
});
|
|
11423
|
+
if (drift)
|
|
11424
|
+
httpRuleOverride = drift;
|
|
11425
|
+
}
|
|
11426
|
+
}
|
|
11427
|
+
}
|
|
11428
|
+
}
|
|
11429
|
+
}
|
|
11430
|
+
const scenariosForAggregation = httpRuleOverride ? input.scenarios.map((s) => s.outcome === "FAIL" ? {
|
|
11431
|
+
...s,
|
|
11432
|
+
class: httpRuleOverride.class,
|
|
11433
|
+
rationale: httpRuleOverride.rationale,
|
|
11434
|
+
confidence: "high"
|
|
11435
|
+
} : s) : input.scenarios;
|
|
11436
|
+
const aggregated = aggregateScenarios(scenariosForAggregation);
|
|
11437
|
+
const decisionsPath = join23(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
|
|
11438
|
+
const decisions = existsSync26(decisionsPath) ? JSON.parse(readFileSync21(decisionsPath, "utf8")) : {};
|
|
10949
11439
|
const graph = deriveSnapshot(loadAllEvents(process.cwd()));
|
|
10950
11440
|
const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
10951
11441
|
const scenarioIdByName = {};
|
|
@@ -10993,7 +11483,7 @@ async function reportCmd(argv) {
|
|
|
10993
11483
|
xeraVersion: "0.1.0",
|
|
10994
11484
|
promptsVersion: "1.0.0"
|
|
10995
11485
|
});
|
|
10996
|
-
const draftPath =
|
|
11486
|
+
const draftPath = join23(paths.ticketDir, "jira-comment.draft.md");
|
|
10997
11487
|
writeFileSync14(draftPath, md);
|
|
10998
11488
|
console.log(`[xera:report] wrote status.json and ${draftPath}`);
|
|
10999
11489
|
return 0;
|
|
@@ -11061,7 +11551,7 @@ async function unlockCmd(argv) {
|
|
|
11061
11551
|
}
|
|
11062
11552
|
|
|
11063
11553
|
// src/bin-internal/validate-feature.ts
|
|
11064
|
-
import { existsSync as
|
|
11554
|
+
import { existsSync as existsSync27, readFileSync as readFileSync22 } from "fs";
|
|
11065
11555
|
import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
|
|
11066
11556
|
async function validateFeatureCmd(argv) {
|
|
11067
11557
|
const ticket = argv[0];
|
|
@@ -11070,7 +11560,7 @@ async function validateFeatureCmd(argv) {
|
|
|
11070
11560
|
return 1;
|
|
11071
11561
|
}
|
|
11072
11562
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
11073
|
-
if (!
|
|
11563
|
+
if (!existsSync27(paths.featurePath)) {
|
|
11074
11564
|
console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
|
|
11075
11565
|
return 1;
|
|
11076
11566
|
}
|
|
@@ -11086,6 +11576,8 @@ async function validateFeatureCmd(argv) {
|
|
|
11086
11576
|
|
|
11087
11577
|
// src/bin-internal/index.ts
|
|
11088
11578
|
var COMMANDS = {
|
|
11579
|
+
"auth-setup": authSetupCmd,
|
|
11580
|
+
disputes: disputesCmd,
|
|
11089
11581
|
doctor: doctorCmd,
|
|
11090
11582
|
"eval-deterministic": evalDeterministicCmd,
|
|
11091
11583
|
"eval-prepare": evalPrepareCmd,
|