@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.
Files changed (60) hide show
  1. package/dist/artifact/status.d.ts +12 -0
  2. package/dist/artifact/status.d.ts.map +1 -1
  3. package/dist/bin/internal.js +1059 -567
  4. package/dist/bin-internal/auth-setup.d.ts +2 -0
  5. package/dist/bin-internal/auth-setup.d.ts.map +1 -0
  6. package/dist/bin-internal/disputes.d.ts +2 -0
  7. package/dist/bin-internal/disputes.d.ts.map +1 -0
  8. package/dist/bin-internal/doctor.d.ts +1 -1
  9. package/dist/bin-internal/doctor.d.ts.map +1 -1
  10. package/dist/bin-internal/exec.d.ts.map +1 -1
  11. package/dist/bin-internal/graph-record-script.d.ts.map +1 -1
  12. package/dist/bin-internal/graph-record.d.ts.map +1 -1
  13. package/dist/bin-internal/index.d.ts.map +1 -1
  14. package/dist/bin-internal/normalize.d.ts.map +1 -1
  15. package/dist/bin-internal/report.d.ts.map +1 -1
  16. package/dist/bin-internal/verify-prompts.d.ts.map +1 -1
  17. package/dist/classifier/aggregate.d.ts.map +1 -1
  18. package/dist/classifier/auth-expired.d.ts +12 -0
  19. package/dist/classifier/auth-expired.d.ts.map +1 -0
  20. package/dist/classifier/contract-drift.d.ts +35 -0
  21. package/dist/classifier/contract-drift.d.ts.map +1 -0
  22. package/dist/classifier/rate-limited.d.ts +15 -0
  23. package/dist/classifier/rate-limited.d.ts.map +1 -0
  24. package/dist/config/schema.d.ts +32 -3
  25. package/dist/config/schema.d.ts.map +1 -1
  26. package/dist/graph/schema.d.ts +9 -0
  27. package/dist/graph/schema.d.ts.map +1 -1
  28. package/dist/graph/store.d.ts.map +1 -1
  29. package/dist/graph/types.d.ts +2 -1
  30. package/dist/graph/types.d.ts.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/scrub/index.d.ts +2 -0
  34. package/dist/scrub/index.d.ts.map +1 -0
  35. package/dist/scrub/rules.d.ts +12 -0
  36. package/dist/scrub/rules.d.ts.map +1 -0
  37. package/dist/src/index.js +110 -5
  38. package/package.json +4 -3
  39. package/src/artifact/status.ts +3 -0
  40. package/src/bin-internal/auth-setup.ts +116 -0
  41. package/src/bin-internal/disputes.ts +88 -0
  42. package/src/bin-internal/doctor.ts +13 -1
  43. package/src/bin-internal/exec.ts +45 -9
  44. package/src/bin-internal/graph-record-script.ts +37 -8
  45. package/src/bin-internal/graph-record.ts +3 -0
  46. package/src/bin-internal/index.ts +4 -0
  47. package/src/bin-internal/normalize.ts +13 -1
  48. package/src/bin-internal/report.ts +94 -2
  49. package/src/bin-internal/verify-prompts.ts +2 -1
  50. package/src/classifier/aggregate.ts +3 -0
  51. package/src/classifier/auth-expired.ts +44 -0
  52. package/src/classifier/contract-drift.ts +111 -0
  53. package/src/classifier/rate-limited.ts +25 -0
  54. package/src/config/schema.ts +52 -9
  55. package/src/graph/schema.ts +3 -0
  56. package/src/graph/store.ts +8 -1
  57. package/src/graph/types.ts +5 -1
  58. package/src/index.ts +2 -0
  59. package/src/scrub/index.ts +1 -0
  60. package/src/scrub/rules.ts +69 -0
@@ -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 = join(repoRoot, ".xera/graph/events");
24
+ const eventsDir = join3(repoRoot, ".xera/graph/events");
25
25
  return {
26
26
  eventsDir,
27
- snapshotFile: join(repoRoot, ".xera/graph/snapshot.json"),
28
- costLog: join(repoRoot, ".xera/cost-log.jsonl"),
29
- eventsMonthDir: (yyyyMm) => join(eventsDir, yyyyMm),
30
- eventFile: (ulid, skill, ticketId, yyyyMm) => join(eventsDir, yyyyMm, `${ulid}-${skill}-${ticketId}.jsonl`)
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 = z.literal(SCHEMA_VERSION);
54
- iso = z.string().datetime({ offset: false });
55
- ticketFetched = z.object({
56
- ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
57
- summary: z.string(),
58
- ac: z.array(z.string()),
59
- jiraLinks: z.array(z.object({
60
- ticketId: z.string().regex(/^[A-Z][A-Z0-9]*-\d+$/),
61
- relation: z.enum(["blocks", "duplicates", "relates", "supersedes"])
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: z.string(),
64
- modifiesAreas: z.array(z.string().regex(/^[a-z0-9-]+$/))
63
+ storyHash: z2.string(),
64
+ modifiesAreas: z2.array(z2.string().regex(/^[a-z0-9-]+$/))
65
65
  }).passthrough();
66
- ticketEnriched = z.object({
67
- ticketId: z.string(),
66
+ ticketEnriched = z2.object({
67
+ ticketId: z2.string(),
68
68
  enrichedAt: iso,
69
- similarCount: z.number().int().nonnegative()
69
+ similarCount: z2.number().int().nonnegative()
70
70
  }).passthrough();
71
- scenarioGenerated = z.object({
72
- scenarioId: z.string(),
73
- ticketId: z.string(),
74
- name: z.string(),
75
- gherkin: z.string(),
76
- priority: z.enum(["p0", "p1", "p2"]),
77
- featureHash: z.string(),
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 = z.object({
81
- pomId: z.string(),
82
- ticketId: z.string(),
83
- filePath: z.string(),
84
- route: z.string(),
85
- locators: z.array(z.string()),
86
- scope: z.enum(["local", "shared"])
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 = z.object({
89
- pomId: z.string(),
90
- fromPath: z.string(),
91
- toPath: z.string()
88
+ pomPromoted = z2.object({
89
+ pomId: z2.string(),
90
+ fromPath: z2.string(),
91
+ toPath: z2.string()
92
92
  }).passthrough();
93
- runCompleted = z.object({
94
- scenarioId: z.string(),
95
- ticketId: z.string(),
96
- runId: z.string(),
97
- status: z.enum(["pass", "fail"]),
98
- traceId: z.string().optional(),
99
- runtime: z.number().nonnegative()
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 = z.enum([
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 = z.object({
110
- scenarioId: z.string(),
111
- runId: z.string(),
112
+ runClassified = z2.object({
113
+ scenarioId: z2.string(),
114
+ runId: z2.string(),
112
115
  classification,
113
- confidence: z.enum(["low", "medium", "high"])
116
+ confidence: z2.enum(["low", "medium", "high"])
114
117
  }).passthrough();
115
- classificationDisputed = z.object({
116
- runId: z.string(),
117
- scenarioId: z.string(),
118
+ classificationDisputed = z2.object({
119
+ runId: z2.string(),
120
+ scenarioId: z2.string(),
118
121
  originalClassification: classification,
119
122
  disputedTo: classification,
120
- qaActor: z.string(),
121
- qaReason: z.string().optional()
123
+ qaActor: z2.string(),
124
+ qaReason: z2.string().optional()
122
125
  }).passthrough();
123
- edgeDiscovered = z.object({
124
- kind: z.enum(["tests", "uses", "covers", "modifies", "jira-linked", "similar", "ran"]),
125
- from: z.string(),
126
- to: z.string(),
127
- confidence: z.number().min(0).max(1).optional(),
128
- source: z.string()
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: z.string().min(20),
134
+ event_id: z2.string().min(20),
132
135
  schema_version: schemaV,
133
136
  ts: iso,
134
- actor: z.string()
137
+ actor: z2.string()
135
138
  };
136
- EventSchema = z.discriminatedUnion("type", [
137
- z.object({ ...base, type: z.literal("ticket.fetched"), payload: ticketFetched }),
138
- z.object({ ...base, type: z.literal("ticket.enriched"), payload: ticketEnriched }),
139
- z.object({ ...base, type: z.literal("scenario.generated"), payload: scenarioGenerated }),
140
- z.object({ ...base, type: z.literal("pom.generated"), payload: pomGenerated }),
141
- z.object({ ...base, type: z.literal("pom.promoted"), payload: pomPromoted }),
142
- z.object({ ...base, type: z.literal("run.completed"), payload: runCompleted }),
143
- z.object({ ...base, type: z.literal("run.classified"), payload: runClassified }),
144
- z.object({
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: z.literal("classification.disputed"),
149
+ type: z2.literal("classification.disputed"),
147
150
  payload: classificationDisputed
148
151
  }),
149
- z.object({ ...base, type: z.literal("edge.discovered"), payload: edgeDiscovered })
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 existsSync2,
157
- mkdirSync as mkdirSync2,
159
+ existsSync as existsSync3,
160
+ mkdirSync,
158
161
  readdirSync,
159
- readFileSync as readFileSync2,
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
- mkdirSync2(monthDir, { recursive: true });
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 (!existsSync2(paths.eventsDir))
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 = readFileSync2(file, "utf8").split(`
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
- mkdirSync2(dirname(paths.snapshotFile), { recursive: true });
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 (!existsSync2(paths.snapshotFile))
365
+ if (!existsSync3(paths.snapshotFile))
356
366
  return null;
357
367
  try {
358
- return JSON.parse(readFileSync2(paths.snapshotFile, "utf8"));
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 createHash3 } from "crypto";
433
- import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync14 } from "fs";
434
- import { basename, join as join12 } from "path";
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 currentTagPriority = "p1";
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
- currentTagPriority = tag;
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
- scenarios.push({
457
- name,
458
- priority: currentTagPriority,
459
- gherkin: lines.slice(start, i).join(`
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 (!existsSync16(dir))
486
+ if (!existsSync6(dir))
471
487
  return [];
472
- return readdirSync4(dir).filter((f) => f.endsWith(".ts")).map((f) => join12(dir, f));
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 = join12(repoRoot, ".xera", ticket);
500
- const featurePath = join12(ticketDir, "feature", `${ticket}.feature`);
501
- const specPath = join12(ticketDir, "tests", `${ticket}.spec.ts`);
502
- const pomDir = join12(ticketDir, "poms");
503
- if (!existsSync16(featurePath)) {
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 = readFileSync14(featurePath, "utf8");
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 = readFileSync14(pomFile, "utf8");
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 (existsSync16(specPath)) {
542
- const specContent = readFileSync14(specPath, "utf8");
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) => createHash3("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) => ({
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 createHash4 } from "crypto";
7584
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
7585
- import { basename as basename2, join as join13 } from "path";
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 createHash4("sha1").update(s).digest("hex");
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 = join13(repoRoot, ".xera", ticket, "story.md");
7610
- if (!existsSync17(path))
7647
+ const path = join6(repoRoot, ".xera", ticket, "story.md");
7648
+ if (!existsSync7(path))
7611
7649
  return null;
7612
- const raw = readFileSync15(path, "utf8");
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 = join13(repoRoot, ".xera", ticket, "graph-input.json");
7620
- if (!existsSync17(path))
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(readFileSync15(path, "utf8"));
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 = join13(repoRoot, ".xera", ticket, "runs", runId, "reporter.json");
7672
- if (!existsSync17(reporterPath)) {
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(readFileSync15(reporterPath, "utf8"));
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 = join13(repoRoot, ".xera", ticket, "runs", runId, "classifier-output.json");
7695
- if (!existsSync17(classifyPath)) {
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(readFileSync15(classifyPath, "utf8"));
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 parseFlags2(args) {
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 = parseFlags2(rest);
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 = parseFlags2(rest);
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, parseFlags2(rest));
7820
+ return recordPromote(repoRoot, parseFlags(rest));
7783
7821
  }
7784
7822
  case "dispute": {
7785
- const flags = parseFlags2(rest);
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 existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
7834
- import { join as join3 } from "path";
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 (!existsSync(paths.costLog))
8214
+ if (!existsSync4(paths.costLog))
7843
8215
  return result;
7844
8216
  const cutoff = Date.now() - daysBack * 86400 * 1000;
7845
- for (const line of readFileSync(paths.costLog, "utf8").split(`
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 existsSync3, readFileSync as readFileSync3 } from "fs";
7873
- import { join as join2 } from "path";
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 = join2(repoRoot, "packages/prompts");
8258
+ const promptsDir = join4(repoRoot, "packages/prompts");
7886
8259
  const results = [];
7887
8260
  for (const filename of IN_SCOPE_PROMPTS) {
7888
- const path = join2(promptsDir, filename);
7889
- if (!existsSync3(path)) {
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 = join3(repoRoot, "fixtures/golden-eval");
7945
- if (!existsSync4(root))
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 = readdirSync2(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
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 = join3(root, entry.name);
7957
- const metaPath = join3(dir, "meta.json");
7958
- if (!existsSync4(metaPath)) {
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(readFileSync4(metaPath, "utf8"));
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 (!existsSync4(join3(dir, "story.md")))
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 (!existsSync4(join3(dir, rel))) {
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 = join3(repoRoot, "packages/prompts/eval-rubric.md");
7993
- if (!existsSync4(path))
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 = readFileSync4(path, "utf8");
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 = join3(repoRoot, "packages/skills/xera-eval.md");
8006
- if (!existsSync4(path))
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 = readFileSync4(path, "utf8");
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 = join3(repoRoot, "package.json");
8018
- if (!existsSync4(path))
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(readFileSync4(path, "utf8"));
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(_argv, opts = {}) {
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 = join3(repoRoot, ".xera");
8045
- if (existsSync4(xeraDir)) {
8046
- const ticketDirs = readdirSync2(xeraDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name));
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 existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
8071
- import { join as join5 } from "path";
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 join4 } from "path";
8459
+ import { join as join9 } from "path";
8076
8460
  function resolveEvalPaths(cwd, runId) {
8077
- const root = join4(cwd, ".xera", "eval", runId);
8461
+ const root = join9(cwd, ".xera", "eval", runId);
8078
8462
  return {
8079
8463
  root,
8080
- manifest: join4(root, "manifest.json"),
8081
- lock: join4(root, ".lock"),
8082
- deterministicScores: join4(root, "deterministic-scores.json"),
8083
- judgeScores: join4(root, "judge-scores.json"),
8084
- report: join4(root, "report.md"),
8085
- summary: join4(root, "summary.json"),
8086
- inputsDir: join4(root, "inputs"),
8087
- actualDir: join4(root, "actual"),
8088
- ticketInputsDir: (ticket) => join4(root, "inputs", ticket),
8089
- ticketActualDir: (ticket) => join4(root, "actual", 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 z2 } from "zod";
8478
+ import { z as z3 } from "zod";
8095
8479
  var STAGES = ["feature-from-story", "script-from-feature", "diagnose-failure"];
8096
- var StageSchema = z2.enum(STAGES);
8097
- var VerdictSchema = z2.enum(["PASS", "FAIL", "NA"]);
8098
- var PromptVersionsSchema = z2.object({
8099
- "feature-from-story": z2.string(),
8100
- "script-from-feature": z2.string(),
8101
- "diagnose-failure": z2.string(),
8102
- "eval-rubric": z2.string()
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 = z2.object({
8105
- run_id: z2.string(),
8106
- started_at: z2.string(),
8107
- git_sha: z2.string(),
8108
- tickets: z2.array(z2.string()).min(1),
8109
- stages: z2.array(StageSchema).min(1),
8110
- ticket_stages: z2.record(z2.string(), z2.array(StageSchema).min(1)),
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: z2.object({
8113
- force: z2.boolean(),
8496
+ flags: z3.object({
8497
+ force: z3.boolean(),
8114
8498
  only_prompt: StageSchema.nullable(),
8115
- only_ticket: z2.string().nullable(),
8116
- judge_only: z2.boolean()
8499
+ only_ticket: z3.string().nullable(),
8500
+ judge_only: z3.boolean()
8117
8501
  })
8118
8502
  });
8119
- var DimensionSchema = z2.object({
8120
- name: z2.string(),
8503
+ var DimensionSchema = z3.object({
8504
+ name: z3.string(),
8121
8505
  verdict: VerdictSchema,
8122
- notes: z2.string()
8506
+ notes: z3.string()
8123
8507
  });
8124
- var JudgmentSchema = z2.object({
8508
+ var JudgmentSchema = z3.object({
8125
8509
  stage: StageSchema,
8126
- ticket: z2.string(),
8127
- dimensions: z2.array(DimensionSchema).min(1)
8510
+ ticket: z3.string(),
8511
+ dimensions: z3.array(DimensionSchema).min(1)
8128
8512
  });
8129
- var JudgeScoresSchema = z2.object({
8130
- run_id: z2.string(),
8131
- judgments: z2.array(JudgmentSchema)
8513
+ var JudgeScoresSchema = z3.object({
8514
+ run_id: z3.string(),
8515
+ judgments: z3.array(JudgmentSchema)
8132
8516
  });
8133
- var DeterministicEntrySchema = z2.object({
8134
- ticket: z2.string(),
8517
+ var DeterministicEntrySchema = z3.object({
8518
+ ticket: z3.string(),
8135
8519
  stage: StageSchema,
8136
- passed: z2.boolean(),
8137
- checks: z2.array(z2.string()),
8138
- error: z2.string().optional()
8520
+ passed: z3.boolean(),
8521
+ checks: z3.array(z3.string()),
8522
+ error: z3.string().optional()
8139
8523
  });
8140
- var DeterministicScoresSchema = z2.object({
8141
- run_id: z2.string(),
8142
- entries: z2.array(DeterministicEntrySchema)
8524
+ var DeterministicScoresSchema = z3.object({
8525
+ run_id: z3.string(),
8526
+ entries: z3.array(DeterministicEntrySchema)
8143
8527
  });
8144
- var ResultSchema = z2.object({
8145
- ticket: z2.string(),
8528
+ var ResultSchema = z3.object({
8529
+ ticket: z3.string(),
8146
8530
  stage: StageSchema,
8147
- deterministic: z2.object({
8148
- passed: z2.boolean(),
8149
- checks: z2.array(z2.string()),
8150
- error: z2.string().optional()
8531
+ deterministic: z3.object({
8532
+ passed: z3.boolean(),
8533
+ checks: z3.array(z3.string()),
8534
+ error: z3.string().optional()
8151
8535
  }),
8152
- judge: z2.object({
8153
- passed: z2.boolean(),
8154
- dimensions: z2.array(DimensionSchema),
8155
- score: z2.number().min(0).max(1)
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: z2.boolean().optional()
8541
+ skipped: z3.boolean().optional()
8158
8542
  });
8159
- var SummarySchema = z2.object({
8160
- run_id: z2.string(),
8161
- git_sha: z2.string(),
8543
+ var SummarySchema = z3.object({
8544
+ run_id: z3.string(),
8545
+ git_sha: z3.string(),
8162
8546
  prompt_versions: PromptVersionsSchema,
8163
- results: z2.array(ResultSchema),
8164
- overall: z2.object({
8165
- passed: z2.number().int().nonnegative(),
8166
- failed: z2.number().int().nonnegative(),
8167
- total: z2.number().int().nonnegative(),
8168
- score: z2.number().min(0).max(1)
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 (!existsSync5(actualFeaturePath)) {
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(readFileSync5(actualFeaturePath, "utf8"));
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 = join5(actualTicketDir, "spec.ts");
8192
- if (!existsSync5(specPath)) {
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 = join5(inputsTicketDir, "classifier-input.json");
8199
- const actualPath = join5(actualTicketDir, "classification.json");
8200
- if (!existsSync5(actualPath)) {
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 (!existsSync5(inputPath)) {
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(readFileSync5(inputPath, "utf8"));
8215
- const actual = JSON.parse(readFileSync5(actualPath, "utf8"));
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 (!existsSync5(paths.manifest)) {
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(readFileSync5(paths.manifest, "utf8")));
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(join5(actualDir, "test.feature"));
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 existsSync7,
8668
+ existsSync as existsSync12,
8285
8669
  mkdirSync as mkdirSync4,
8286
- readdirSync as readdirSync3,
8287
- readFileSync as readFileSync7,
8670
+ readdirSync as readdirSync5,
8671
+ readFileSync as readFileSync9,
8288
8672
  writeFileSync as writeFileSync4
8289
8673
  } from "fs";
8290
- import { join as join6 } from "path";
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 existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
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 (existsSync6(path))
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 (existsSync6(path))
8720
+ if (existsSync11(path))
8337
8721
  unlinkSync(path);
8338
8722
  }
8339
8723
  function readLock(path) {
8340
- if (!existsSync6(path))
8724
+ if (!existsSync11(path))
8341
8725
  return null;
8342
- return JSON.parse(readFileSync6(path, "utf8"));
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 parseFlags(argv) {
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 = join6(repoRoot, "packages/prompts", `${name}.md`);
8384
- if (!existsSync7(path))
8767
+ const path = join11(repoRoot, "packages/prompts", `${name}.md`);
8768
+ if (!existsSync12(path))
8385
8769
  return "0.0.0";
8386
- const text = readFileSync7(path, "utf8");
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 = join6(repoRoot, "fixtures/golden-eval");
8392
- if (!existsSync7(root))
8775
+ const root = join11(repoRoot, "fixtures/golden-eval");
8776
+ if (!existsSync12(root))
8393
8777
  return [];
8394
8778
  const out = [];
8395
- for (const entry of readdirSync3(root, { withFileTypes: true })) {
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 = join6(root, entry.name);
8401
- const metaPath = join6(dir, "meta.json");
8402
- if (!existsSync7(metaPath))
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(readFileSync7(metaPath, "utf8"));
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 = join6(repoRoot, "fixtures/golden-tickets");
8411
- if (!existsSync7(root))
8794
+ const root = join11(repoRoot, "fixtures/golden-tickets");
8795
+ if (!existsSync12(root))
8412
8796
  return [];
8413
8797
  const out = [];
8414
- for (const entry of readdirSync3(root, { withFileTypes: true })) {
8798
+ for (const entry of readdirSync5(root, { withFileTypes: true })) {
8415
8799
  if (!entry.isFile() || !entry.name.endsWith(".json"))
8416
8800
  continue;
8417
- const path = join6(root, entry.name);
8418
- const data = JSON.parse(readFileSync7(path, "utf8"));
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 = parseFlags(argv);
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 (existsSync7(paths.root) && !flags.force) {
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(join6(evalT.dir, "story.md"), join6(ticketInputs, "story.md"));
8490
- const featurePath = join6(evalT.dir, "golden/test.feature");
8491
- if (existsSync7(featurePath))
8492
- copyFileSync(featurePath, join6(ticketInputs, "test.feature"));
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, join6(ticketInputs, "classifier-input.json"));
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 existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
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 (!existsSync8(paths.manifest)) {
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(readFileSync8(paths.manifest, "utf8")));
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(readFileSync8(paths.deterministicScores, "utf8")));
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(readFileSync8(paths.judgeScores, "utf8")));
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 existsSync12, mkdirSync as mkdirSync7 } from "fs";
8674
- import { join as join10 } from "path";
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 join7 } from "path";
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 = join7(repoRoot, ".xera", ticket);
9106
+ const ticketDir = join12(repoRoot, ".xera", ticket);
8686
9107
  return {
8687
9108
  ticketDir,
8688
- storyPath: join7(ticketDir, "story.md"),
8689
- featurePath: join7(ticketDir, "test.feature"),
8690
- specPath: join7(ticketDir, "spec.ts"),
8691
- pageObjectsDir: join7(ticketDir, "page-objects"),
8692
- runsDir: join7(ticketDir, "runs"),
8693
- metaPath: join7(ticketDir, "meta.json"),
8694
- statusPath: join7(ticketDir, "status.json"),
8695
- logPath: join7(ticketDir, "xera.log"),
8696
- lockPath: join7(ticketDir, ".lock"),
8697
- authDir: join7(repoRoot, ".xera", ".auth"),
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 = join7(ticketDir, "runs", runId);
9120
+ const runDir = join12(ticketDir, "runs", runId);
8700
9121
  return {
8701
9122
  runDir,
8702
- reportJsonPath: join7(runDir, "report.json"),
8703
- tracePath: join7(runDir, "trace.zip"),
8704
- normalizedPath: join7(runDir, "normalized.json"),
8705
- screenshotsDir: join7(runDir, "screenshots"),
8706
- videoDir: join7(runDir, "videos")
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 parseDuration(d) {
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 = parseDuration(policy.ttl);
8733
- const bufMs = parseDuration(policy.refreshBuffer);
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 existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
8745
- import { join as join8 } from "path";
8746
- import { z as z3 } from "zod";
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 = z3.object({
8804
- role: z3.string(),
8805
- strategy: z3.enum(["storageState", "apiToken"]),
8806
- created_at: z3.string(),
8807
- expires_at: z3.string(),
8808
- payload: z3.record(z3.string(), z3.unknown())
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 join8(authDir, `${role}.json`);
9232
+ return join13(authDir, `${role}.json`);
8812
9233
  }
8813
9234
  function writeAuthState(authDir, entry) {
8814
- mkdirSync5(authDir, { recursive: true });
9235
+ mkdirSync6(authDir, { recursive: true });
8815
9236
  const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
8816
- writeFileSync6(pathFor(authDir, entry.role), ct);
9237
+ writeFileSync7(pathFor(authDir, entry.role), ct);
8817
9238
  }
8818
9239
  function readAuthState(authDir, role) {
8819
9240
  const p = pathFor(authDir, role);
8820
- if (!existsSync9(p))
9241
+ if (!existsSync15(p))
8821
9242
  return null;
8822
- const txt = readFileSync9(p, "utf8");
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 existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync10 } from "fs";
8913
- import { dirname as dirname3 } from "path";
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
- mkdirSync6(dirname3(path), { recursive: true });
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 (!existsSync11(path))
9264
+ if (!existsSync16(path))
8928
9265
  return [];
8929
- const txt = readFileSync10(path, "utf8").trim();
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
- if (config.web.auth.strategy === "storageState" && config.web.auth.setupScript) {
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(config.web.auth.roles)) {
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: config.web.auth.ttl,
8969
- refreshBuffer: config.web.auth.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: join10(cwd, config.web.auth.setupScript),
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 (config.web.auth.strategy === "storageState") {
8992
- for (const roleName of Object.keys(config.web.auth.roles)) {
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 = join10(cwd, "playwright.config.ts");
8999
- if (!existsSync12(cfgPath)) {
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
- mkdirSync7(runDir, { recursive: true });
9005
- const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
9006
- const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv];
9007
- const reportJsonPath = join10(runDir, "report.json");
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 createHash2 } from "crypto";
9033
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
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:${createHash2("sha256").update(s).digest("hex")}`;
9402
+ return `sha256:${createHash4("sha256").update(s).digest("hex")}`;
9036
9403
  }
9037
9404
  function hashFile(path) {
9038
- return hashString(readFileSync11(path, "utf8"));
9405
+ return hashString(readFileSync14(path, "utf8"));
9039
9406
  }
9040
9407
  function hashFileIfExists(path) {
9041
- if (!existsSync13(path))
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 existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
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 join11 } from "path";
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 = join11(tmpdir(), "xera-mcp");
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 = join11(tmpDir, `${key}.json`);
9097
- if (!existsSync15(cachePath)) {
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(readFileSync13(cachePath, "utf8"));
9430
+ const parsed = JSON.parse(readFileSync15(cachePath, "utf8"));
9101
9431
  return parsed;
9102
9432
  },
9103
9433
  async postComment(key, body) {
9104
- const outPath = join11(tmpDir, `${key}.comment.json`);
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 = join11(tmpDir, `${key}.transition.json`);
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/graph-backfill.ts
9279
- init_graph_record_script();
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 existsSync19, readFileSync as readFileSync16 } from "fs";
9326
- import { join as join15 } from "path";
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 = join15(repoRoot, ".xera", ticketId, "enrichment-input.json");
9349
- if (!existsSync19(inputPath)) {
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 renderText(snap) {
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(renderText(snap));
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 join17 } from "path";
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 join16 } from "path";
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 = join16(__dirname2, "templates");
9974
+ var TEMPLATES_DIR = join17(__dirname2, "templates");
9686
9975
  function loadTemplate(name) {
9687
- return readFileSync17(join16(TEMPLATES_DIR, name), "utf8");
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 ?? join17(repoRoot, ".xera/graph.html");
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 existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
9786
- import { join as join18 } from "path";
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 (!existsSync20(tracePath))
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 = join18(ticketDir, "page-objects");
10553
+ const pomDir = join19(ticketDir, "page-objects");
10265
10554
  const candidates = [];
10266
- if (existsSync20(pomDir)) {
10555
+ if (existsSync21(pomDir)) {
10267
10556
  for (const name of readdirSync6(pomDir)) {
10268
10557
  if (name.endsWith(".ts"))
10269
- candidates.push(join18(pomDir, name));
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 = join18(paths.ticketDir, "classifier-input.json");
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 = join18(paths.runsDir, runId);
10317
- const normalized = JSON.parse(readFileSync18(join18(runDir, "normalized.json"), "utf8"));
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(join18(runDir, "trace.zip"));
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 = join18(paths.runsDir, runId, "heal-input.json");
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 join19 } from "path";
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 = join19(repoRoot, ".xera/impact");
10901
+ const impactDir = join20(repoRoot, ".xera/impact");
10613
10902
  mkdirSync12(impactDir, { recursive: true });
10614
- writeFileSync12(join19(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10903
+ writeFileSync12(join20(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10615
10904
  if (!quiet) {
10616
- writeFileSync12(join19(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
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 existsSync21, readdirSync as readdirSync7 } from "fs";
10642
- import { join as join20 } from "path";
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 = join20(paths.runsDir, runId);
10658
- if (!existsSync21(runDir)) {
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 existsSync23, readFileSync as readFileSync20 } from "fs";
10669
- import { join as join21 } from "path";
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 existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
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 (!existsSync22(path))
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 = join21(paths.ticketDir, "jira-comment.draft.md");
10740
- if (!existsSync23(draftPath)) {
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 existsSync25, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
10773
- import { join as join22 } from "path";
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 existsSync24 } from "fs";
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 (!existsSync24(path)) {
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 paths = resolveArtifactPaths(process.cwd(), ticket);
11375
+ const cwd = process.cwd();
11376
+ const paths = resolveArtifactPaths(cwd, ticket);
10945
11377
  const input = JSON.parse(readFileSync21(inputArg.slice("--input=".length), "utf8"));
10946
- const aggregated = aggregateScenarios(input.scenarios);
10947
- const decisionsPath = join22(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
10948
- const decisions = existsSync25(decisionsPath) ? JSON.parse(readFileSync21(decisionsPath, "utf8")) : {};
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 = join22(paths.ticketDir, "jira-comment.draft.md");
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 existsSync26, readFileSync as readFileSync22 } from "fs";
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 (!existsSync26(paths.featurePath)) {
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,