@xera-ai/core 0.4.4 → 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 (50) 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 +822 -464
  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/exec.d.ts.map +1 -1
  7. package/dist/bin-internal/graph-record.d.ts.map +1 -1
  8. package/dist/bin-internal/index.d.ts.map +1 -1
  9. package/dist/bin-internal/normalize.d.ts.map +1 -1
  10. package/dist/bin-internal/report.d.ts.map +1 -1
  11. package/dist/bin-internal/verify-prompts.d.ts.map +1 -1
  12. package/dist/classifier/aggregate.d.ts.map +1 -1
  13. package/dist/classifier/auth-expired.d.ts +12 -0
  14. package/dist/classifier/auth-expired.d.ts.map +1 -0
  15. package/dist/classifier/contract-drift.d.ts +35 -0
  16. package/dist/classifier/contract-drift.d.ts.map +1 -0
  17. package/dist/classifier/rate-limited.d.ts +15 -0
  18. package/dist/classifier/rate-limited.d.ts.map +1 -0
  19. package/dist/config/schema.d.ts +32 -3
  20. package/dist/config/schema.d.ts.map +1 -1
  21. package/dist/graph/schema.d.ts +9 -0
  22. package/dist/graph/schema.d.ts.map +1 -1
  23. package/dist/graph/types.d.ts +1 -1
  24. package/dist/graph/types.d.ts.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/scrub/index.d.ts +2 -0
  28. package/dist/scrub/index.d.ts.map +1 -0
  29. package/dist/scrub/rules.d.ts +12 -0
  30. package/dist/scrub/rules.d.ts.map +1 -0
  31. package/dist/src/index.js +109 -4
  32. package/package.json +4 -3
  33. package/src/artifact/status.ts +3 -0
  34. package/src/bin-internal/auth-setup.ts +116 -0
  35. package/src/bin-internal/exec.ts +42 -9
  36. package/src/bin-internal/graph-record.ts +3 -0
  37. package/src/bin-internal/index.ts +2 -0
  38. package/src/bin-internal/normalize.ts +13 -1
  39. package/src/bin-internal/report.ts +94 -2
  40. package/src/bin-internal/verify-prompts.ts +2 -1
  41. package/src/classifier/aggregate.ts +3 -0
  42. package/src/classifier/auth-expired.ts +44 -0
  43. package/src/classifier/contract-drift.ts +111 -0
  44. package/src/classifier/rate-limited.ts +25 -0
  45. package/src/config/schema.ts +51 -8
  46. package/src/graph/schema.ts +3 -0
  47. package/src/graph/types.ts +4 -1
  48. package/src/index.ts +2 -0
  49. package/src/scrub/index.ts +1 -0
  50. 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,110 +50,113 @@ 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,
159
+ existsSync as existsSync3,
157
160
  mkdirSync,
158
161
  readdirSync,
159
162
  readFileSync,
@@ -180,7 +183,7 @@ function appendEvents(repoRoot, events, opts) {
180
183
  }
181
184
  function loadAllEvents(repoRoot) {
182
185
  const paths = graphPaths(repoRoot);
183
- if (!existsSync(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 })) {
@@ -359,7 +362,7 @@ function writeSnapshot(repoRoot, snap) {
359
362
  }
360
363
  function loadSnapshot(repoRoot) {
361
364
  const paths = graphPaths(repoRoot);
362
- if (!existsSync(paths.snapshotFile))
365
+ if (!existsSync3(paths.snapshotFile))
363
366
  return null;
364
367
  try {
365
368
  return JSON.parse(readFileSync(paths.snapshotFile, "utf8"));
@@ -437,8 +440,8 @@ __export(exports_graph_record_script, {
437
440
  recordScriptImpl: () => recordScriptImpl
438
441
  });
439
442
  import { createHash as createHash2 } from "crypto";
440
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
441
- import { basename, join as join3 } from "path";
443
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
444
+ import { basename, join as join5 } from "path";
442
445
  function inferPriority(name, gherkin) {
443
446
  const haystack = `${name} ${gherkin}`.toLowerCase();
444
447
  for (const kw of P0_KEYWORDS) {
@@ -480,9 +483,9 @@ function parseFeature(text) {
480
483
  return scenarios;
481
484
  }
482
485
  function listPomFiles(dir) {
483
- if (!existsSync4(dir))
486
+ if (!existsSync6(dir))
484
487
  return [];
485
- return readdirSync2(dir).filter((f) => f.endsWith(".ts")).map((f) => join3(dir, f));
488
+ return readdirSync2(dir).filter((f) => f.endsWith(".ts")).map((f) => join5(dir, f));
486
489
  }
487
490
  function extractRoute(pomContent) {
488
491
  const m = pomContent.match(/goto\s*\(\s*['"]([^'"]+)['"]/);
@@ -509,11 +512,11 @@ function extractPomUsage(specContent) {
509
512
  return [...names];
510
513
  }
511
514
  async function recordScriptImpl(repoRoot, ticket) {
512
- const ticketDir = join3(repoRoot, ".xera", ticket);
513
- const featurePath = join3(ticketDir, "feature", `${ticket}.feature`);
514
- const specPath = join3(ticketDir, "tests", `${ticket}.spec.ts`);
515
- const pomDir = join3(ticketDir, "poms");
516
- if (!existsSync4(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)) {
517
520
  console.error(`[graph-record script] feature missing`);
518
521
  return 1;
519
522
  }
@@ -551,7 +554,7 @@ async function recordScriptImpl(repoRoot, ticket) {
551
554
  };
552
555
  events.push(mk("xera-script", "pom.generated", pg));
553
556
  }
554
- if (existsSync4(specPath)) {
557
+ if (existsSync6(specPath)) {
555
558
  const specContent = readFileSync4(specPath, "utf8");
556
559
  const usedPoms = extractPomUsage(specContent);
557
560
  for (const scenario of scenarios) {
@@ -7616,8 +7619,8 @@ __export(exports_graph_record, {
7616
7619
  graphRecordCmd: () => graphRecordCmd
7617
7620
  });
7618
7621
  import { createHash as createHash3 } from "crypto";
7619
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
7620
- import { basename as basename2, join as join4 } from "path";
7622
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
7623
+ import { basename as basename2, join as join6 } from "path";
7621
7624
  function nowIso2() {
7622
7625
  return new Date().toISOString();
7623
7626
  }
@@ -7641,8 +7644,8 @@ function makeEvent(actor, type, payload) {
7641
7644
  };
7642
7645
  }
7643
7646
  function readStoryFrontmatter(repoRoot, ticket) {
7644
- const path = join4(repoRoot, ".xera", ticket, "story.md");
7645
- if (!existsSync5(path))
7647
+ const path = join6(repoRoot, ".xera", ticket, "story.md");
7648
+ if (!existsSync7(path))
7646
7649
  return null;
7647
7650
  const raw = readFileSync5(path, "utf8");
7648
7651
  const m = raw.match(/^---\n([\s\S]*?)\n---/);
@@ -7651,8 +7654,8 @@ function readStoryFrontmatter(repoRoot, ticket) {
7651
7654
  return $parse(m[1]);
7652
7655
  }
7653
7656
  function readGraphInput(repoRoot, ticket) {
7654
- const path = join4(repoRoot, ".xera", ticket, "graph-input.json");
7655
- if (!existsSync5(path))
7657
+ const path = join6(repoRoot, ".xera", ticket, "graph-input.json");
7658
+ if (!existsSync7(path))
7656
7659
  return { modifiesAreas: [] };
7657
7660
  try {
7658
7661
  return JSON.parse(readFileSync5(path, "utf8"));
@@ -7703,8 +7706,8 @@ async function recordScript(repoRoot, ticket) {
7703
7706
  return recordScriptImpl2(repoRoot, ticket);
7704
7707
  }
7705
7708
  async function recordExec(repoRoot, ticket, runId) {
7706
- const reporterPath = join4(repoRoot, ".xera", ticket, "runs", runId, "reporter.json");
7707
- if (!existsSync5(reporterPath)) {
7709
+ const reporterPath = join6(repoRoot, ".xera", ticket, "runs", runId, "reporter.json");
7710
+ if (!existsSync7(reporterPath)) {
7708
7711
  console.error(`[graph-record exec] reporter.json missing`);
7709
7712
  return 1;
7710
7713
  }
@@ -7726,8 +7729,8 @@ async function recordExec(repoRoot, ticket, runId) {
7726
7729
  return 0;
7727
7730
  }
7728
7731
  async function recordClassify(repoRoot, ticket, runId) {
7729
- const classifyPath = join4(repoRoot, ".xera", ticket, "runs", runId, "classifier-output.json");
7730
- if (!existsSync5(classifyPath)) {
7732
+ const classifyPath = join6(repoRoot, ".xera", ticket, "runs", runId, "classifier-output.json");
7733
+ if (!existsSync7(classifyPath)) {
7731
7734
  console.error(`[graph-record classify] classifier-output.json missing`);
7732
7735
  return 1;
7733
7736
  }
@@ -7834,7 +7837,10 @@ async function graphRecordCmd(argv) {
7834
7837
  "SELECTOR_DRIFT",
7835
7838
  "FLAKY",
7836
7839
  "PASS",
7837
- "TEST_OUTDATED"
7840
+ "TEST_OUTDATED",
7841
+ "CONTRACT_DRIFT",
7842
+ "RATE_LIMITED",
7843
+ "AUTH_EXPIRED"
7838
7844
  ];
7839
7845
  if (!validClass.includes(from) || !validClass.includes(to)) {
7840
7846
  console.error(`[graph-record dispute] --from and --to must be one of: ${validClass.join(", ")}`);
@@ -7869,11 +7875,11 @@ var exports_graph_backfill = {};
7869
7875
  __export(exports_graph_backfill, {
7870
7876
  graphBackfillCmd: () => graphBackfillCmd
7871
7877
  });
7872
- import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
7873
- import { join as join5 } from "path";
7878
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
7879
+ import { join as join7 } from "path";
7874
7880
  async function backfillTicket(repoRoot, ticket, dryRun) {
7875
- const storyPath = join5(repoRoot, ".xera", ticket, "story.md");
7876
- if (!existsSync6(storyPath))
7881
+ const storyPath = join7(repoRoot, ".xera", ticket, "story.md");
7882
+ if (!existsSync8(storyPath))
7877
7883
  return 0;
7878
7884
  const { recordFetch: recordFetch2 } = await Promise.resolve().then(() => (init_graph_record(), exports_graph_record));
7879
7885
  if (dryRun) {
@@ -7887,8 +7893,8 @@ async function backfillTicket(repoRoot, ticket, dryRun) {
7887
7893
  async function graphBackfillCmd(argv) {
7888
7894
  const dryRun = argv.includes("--dry-run");
7889
7895
  const repoRoot = process.cwd();
7890
- const xeraDir = join5(repoRoot, ".xera");
7891
- if (!existsSync6(xeraDir)) {
7896
+ const xeraDir = join7(repoRoot, ".xera");
7897
+ if (!existsSync8(xeraDir)) {
7892
7898
  console.log("[backfill] no .xera/ directory");
7893
7899
  return 0;
7894
7900
  }
@@ -7914,6 +7920,212 @@ var init_graph_backfill = __esm(() => {
7914
7920
  init_graph_record_script();
7915
7921
  });
7916
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
+
7917
8129
  // src/bin-internal/disputes.ts
7918
8130
  init_store();
7919
8131
  function parseDuration(s) {
@@ -7990,16 +8202,16 @@ async function disputesCmd(argv) {
7990
8202
  }
7991
8203
 
7992
8204
  // src/bin-internal/doctor.ts
7993
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
7994
- import { join as join6 } from "path";
8205
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
8206
+ import { join as join8 } from "path";
7995
8207
 
7996
8208
  // src/graph/cost.ts
7997
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "fs";
8209
+ import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "fs";
7998
8210
  init_paths();
7999
8211
  function summarizeCost(repoRoot, daysBack) {
8000
8212
  const paths = graphPaths(repoRoot);
8001
8213
  const result = { totalCalls: 0, totalUsd: 0, bySkill: {}, windowDays: daysBack };
8002
- if (!existsSync2(paths.costLog))
8214
+ if (!existsSync4(paths.costLog))
8003
8215
  return result;
8004
8216
  const cutoff = Date.now() - daysBack * 86400 * 1000;
8005
8217
  for (const line of readFileSync2(paths.costLog, "utf8").split(`
@@ -8029,11 +8241,12 @@ function summarizeCost(repoRoot, daysBack) {
8029
8241
  init_store();
8030
8242
 
8031
8243
  // src/bin-internal/verify-prompts.ts
8032
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
8033
- import { join as join2 } from "path";
8244
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
8245
+ import { join as join4 } from "path";
8034
8246
  var IN_SCOPE_PROMPTS = [
8035
8247
  "feature-from-story.md",
8036
- "script-from-feature.md",
8248
+ "script-from-feature-web.md",
8249
+ "script-from-feature-http.md",
8037
8250
  "heal-locator.md",
8038
8251
  "extract-areas.md",
8039
8252
  "similarity-match.md",
@@ -8042,11 +8255,11 @@ var IN_SCOPE_PROMPTS = [
8042
8255
  var REQUIRED_SECTION_HEADING = "## Handling untrusted input";
8043
8256
  var REQUIRED_KEYWORDS = ["UNTRUSTED", "injection-follow", "<XR_"];
8044
8257
  function verifyPrompts(repoRoot) {
8045
- const promptsDir = join2(repoRoot, "packages/prompts");
8258
+ const promptsDir = join4(repoRoot, "packages/prompts");
8046
8259
  const results = [];
8047
8260
  for (const filename of IN_SCOPE_PROMPTS) {
8048
- const path = join2(promptsDir, filename);
8049
- if (!existsSync3(path)) {
8261
+ const path = join4(promptsDir, filename);
8262
+ if (!existsSync5(path)) {
8050
8263
  results.push({
8051
8264
  ok: false,
8052
8265
  message: `${filename}: file missing at packages/prompts/${filename}`
@@ -8101,8 +8314,8 @@ function frontmatterField(content, field) {
8101
8314
  return m?.[1] ?? null;
8102
8315
  }
8103
8316
  function checkGoldenEvalDir(repoRoot) {
8104
- const root = join6(repoRoot, "fixtures/golden-eval");
8105
- if (!existsSync7(root))
8317
+ const root = join8(repoRoot, "fixtures/golden-eval");
8318
+ if (!existsSync9(root))
8106
8319
  return [{ ok: false, message: "fixtures/golden-eval/ does not exist" }];
8107
8320
  const dirs = readdirSync4(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("."));
8108
8321
  const results = [];
@@ -8113,9 +8326,9 @@ function checkGoldenEvalDir(repoRoot) {
8113
8326
  });
8114
8327
  }
8115
8328
  for (const entry of dirs) {
8116
- const dir = join6(root, entry.name);
8117
- const metaPath = join6(dir, "meta.json");
8118
- if (!existsSync7(metaPath)) {
8329
+ const dir = join8(root, entry.name);
8330
+ const metaPath = join8(dir, "meta.json");
8331
+ if (!existsSync9(metaPath)) {
8119
8332
  results.push({ ok: false, message: `${entry.name}: meta.json missing` });
8120
8333
  continue;
8121
8334
  }
@@ -8132,12 +8345,12 @@ function checkGoldenEvalDir(repoRoot) {
8132
8345
  const stages = Array.isArray(meta.stages) ? meta.stages : [];
8133
8346
  if (stages.length === 0)
8134
8347
  results.push({ ok: false, message: `${entry.name}: meta.stages is empty` });
8135
- if (!existsSync7(join6(dir, "story.md")))
8348
+ if (!existsSync9(join8(dir, "story.md")))
8136
8349
  results.push({ ok: false, message: `${entry.name}: story.md missing` });
8137
8350
  for (const stage of stages) {
8138
8351
  const required = REQUIRED_FILES_PER_STAGE[stage] ?? [];
8139
8352
  for (const rel of required) {
8140
- if (!existsSync7(join6(dir, rel))) {
8353
+ if (!existsSync9(join8(dir, rel))) {
8141
8354
  results.push({
8142
8355
  ok: false,
8143
8356
  message: `${meta.id ?? entry.name}: stage "${stage}" declared but ${rel} missing`
@@ -8149,8 +8362,8 @@ function checkGoldenEvalDir(repoRoot) {
8149
8362
  return results;
8150
8363
  }
8151
8364
  function checkRubricPrompt(repoRoot) {
8152
- const path = join6(repoRoot, "packages/prompts/eval-rubric.md");
8153
- if (!existsSync7(path))
8365
+ const path = join8(repoRoot, "packages/prompts/eval-rubric.md");
8366
+ if (!existsSync9(path))
8154
8367
  return [{ ok: false, message: "packages/prompts/eval-rubric.md missing" }];
8155
8368
  const text = readFileSync6(path, "utf8");
8156
8369
  const id = frontmatterField(text, "id");
@@ -8162,8 +8375,8 @@ function checkRubricPrompt(repoRoot) {
8162
8375
  return [];
8163
8376
  }
8164
8377
  function checkEvalSkill(repoRoot) {
8165
- const path = join6(repoRoot, "packages/skills/xera-eval.md");
8166
- if (!existsSync7(path))
8378
+ const path = join8(repoRoot, "packages/skills/xera-eval.md");
8379
+ if (!existsSync9(path))
8167
8380
  return [{ ok: false, message: "packages/skills/xera-eval.md missing" }];
8168
8381
  const text = readFileSync6(path, "utf8");
8169
8382
  if (!frontmatterField(text, "name"))
@@ -8174,8 +8387,8 @@ function checkPromptInjectionPreamble(repoRoot) {
8174
8387
  return verifyPrompts(repoRoot);
8175
8388
  }
8176
8389
  function checkRootScripts(repoRoot) {
8177
- const path = join6(repoRoot, "package.json");
8178
- if (!existsSync7(path))
8390
+ const path = join8(repoRoot, "package.json");
8391
+ if (!existsSync9(path))
8179
8392
  return [{ ok: false, message: "root package.json missing" }];
8180
8393
  const pkg = JSON.parse(readFileSync6(path, "utf8"));
8181
8394
  const scripts = pkg.scripts ?? {};
@@ -8202,8 +8415,8 @@ async function doctorCmd(argv, opts = {}) {
8202
8415
  if (top)
8203
8416
  console.log(` Top skill: ${top[0]} (${top[1].calls} calls, $${top[1].usd.toFixed(2)})`);
8204
8417
  }
8205
- const xeraDir = join6(repoRoot, ".xera");
8206
- if (existsSync7(xeraDir)) {
8418
+ const xeraDir = join8(repoRoot, ".xera");
8419
+ if (existsSync9(xeraDir)) {
8207
8420
  const ticketDirs = readdirSync4(xeraDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Z]+-\d+$/.test(e.name));
8208
8421
  if (ticketDirs.length > 0) {
8209
8422
  const events = loadAllEvents(repoRoot);
@@ -8238,111 +8451,111 @@ async function doctorCmd(argv, opts = {}) {
8238
8451
  }
8239
8452
 
8240
8453
  // src/bin-internal/eval-deterministic.ts
8241
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
8242
- import { join as join8 } from "path";
8454
+ import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
8455
+ import { join as join10 } from "path";
8243
8456
  import { validateGherkin } from "@xera-ai/web";
8244
8457
 
8245
8458
  // src/eval/paths.ts
8246
- import { join as join7 } from "path";
8459
+ import { join as join9 } from "path";
8247
8460
  function resolveEvalPaths(cwd, runId) {
8248
- const root = join7(cwd, ".xera", "eval", runId);
8461
+ const root = join9(cwd, ".xera", "eval", runId);
8249
8462
  return {
8250
8463
  root,
8251
- manifest: join7(root, "manifest.json"),
8252
- lock: join7(root, ".lock"),
8253
- deterministicScores: join7(root, "deterministic-scores.json"),
8254
- judgeScores: join7(root, "judge-scores.json"),
8255
- report: join7(root, "report.md"),
8256
- summary: join7(root, "summary.json"),
8257
- inputsDir: join7(root, "inputs"),
8258
- actualDir: join7(root, "actual"),
8259
- ticketInputsDir: (ticket) => join7(root, "inputs", ticket),
8260
- ticketActualDir: (ticket) => join7(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)
8261
8474
  };
8262
8475
  }
8263
8476
 
8264
8477
  // src/eval/types.ts
8265
- import { z as z2 } from "zod";
8478
+ import { z as z3 } from "zod";
8266
8479
  var STAGES = ["feature-from-story", "script-from-feature", "diagnose-failure"];
8267
- var StageSchema = z2.enum(STAGES);
8268
- var VerdictSchema = z2.enum(["PASS", "FAIL", "NA"]);
8269
- var PromptVersionsSchema = z2.object({
8270
- "feature-from-story": z2.string(),
8271
- "script-from-feature": z2.string(),
8272
- "diagnose-failure": z2.string(),
8273
- "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()
8274
8487
  });
8275
- var ManifestSchema = z2.object({
8276
- run_id: z2.string(),
8277
- started_at: z2.string(),
8278
- git_sha: z2.string(),
8279
- tickets: z2.array(z2.string()).min(1),
8280
- stages: z2.array(StageSchema).min(1),
8281
- 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)),
8282
8495
  prompt_versions: PromptVersionsSchema,
8283
- flags: z2.object({
8284
- force: z2.boolean(),
8496
+ flags: z3.object({
8497
+ force: z3.boolean(),
8285
8498
  only_prompt: StageSchema.nullable(),
8286
- only_ticket: z2.string().nullable(),
8287
- judge_only: z2.boolean()
8499
+ only_ticket: z3.string().nullable(),
8500
+ judge_only: z3.boolean()
8288
8501
  })
8289
8502
  });
8290
- var DimensionSchema = z2.object({
8291
- name: z2.string(),
8503
+ var DimensionSchema = z3.object({
8504
+ name: z3.string(),
8292
8505
  verdict: VerdictSchema,
8293
- notes: z2.string()
8506
+ notes: z3.string()
8294
8507
  });
8295
- var JudgmentSchema = z2.object({
8508
+ var JudgmentSchema = z3.object({
8296
8509
  stage: StageSchema,
8297
- ticket: z2.string(),
8298
- dimensions: z2.array(DimensionSchema).min(1)
8510
+ ticket: z3.string(),
8511
+ dimensions: z3.array(DimensionSchema).min(1)
8299
8512
  });
8300
- var JudgeScoresSchema = z2.object({
8301
- run_id: z2.string(),
8302
- judgments: z2.array(JudgmentSchema)
8513
+ var JudgeScoresSchema = z3.object({
8514
+ run_id: z3.string(),
8515
+ judgments: z3.array(JudgmentSchema)
8303
8516
  });
8304
- var DeterministicEntrySchema = z2.object({
8305
- ticket: z2.string(),
8517
+ var DeterministicEntrySchema = z3.object({
8518
+ ticket: z3.string(),
8306
8519
  stage: StageSchema,
8307
- passed: z2.boolean(),
8308
- checks: z2.array(z2.string()),
8309
- error: z2.string().optional()
8520
+ passed: z3.boolean(),
8521
+ checks: z3.array(z3.string()),
8522
+ error: z3.string().optional()
8310
8523
  });
8311
- var DeterministicScoresSchema = z2.object({
8312
- run_id: z2.string(),
8313
- entries: z2.array(DeterministicEntrySchema)
8524
+ var DeterministicScoresSchema = z3.object({
8525
+ run_id: z3.string(),
8526
+ entries: z3.array(DeterministicEntrySchema)
8314
8527
  });
8315
- var ResultSchema = z2.object({
8316
- ticket: z2.string(),
8528
+ var ResultSchema = z3.object({
8529
+ ticket: z3.string(),
8317
8530
  stage: StageSchema,
8318
- deterministic: z2.object({
8319
- passed: z2.boolean(),
8320
- checks: z2.array(z2.string()),
8321
- error: z2.string().optional()
8531
+ deterministic: z3.object({
8532
+ passed: z3.boolean(),
8533
+ checks: z3.array(z3.string()),
8534
+ error: z3.string().optional()
8322
8535
  }),
8323
- judge: z2.object({
8324
- passed: z2.boolean(),
8325
- dimensions: z2.array(DimensionSchema),
8326
- 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)
8327
8540
  }).nullable(),
8328
- skipped: z2.boolean().optional()
8541
+ skipped: z3.boolean().optional()
8329
8542
  });
8330
- var SummarySchema = z2.object({
8331
- run_id: z2.string(),
8332
- git_sha: z2.string(),
8543
+ var SummarySchema = z3.object({
8544
+ run_id: z3.string(),
8545
+ git_sha: z3.string(),
8333
8546
  prompt_versions: PromptVersionsSchema,
8334
- results: z2.array(ResultSchema),
8335
- overall: z2.object({
8336
- passed: z2.number().int().nonnegative(),
8337
- failed: z2.number().int().nonnegative(),
8338
- total: z2.number().int().nonnegative(),
8339
- 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)
8340
8553
  })
8341
8554
  });
8342
8555
 
8343
8556
  // src/bin-internal/eval-deterministic.ts
8344
8557
  function checkFeatureFromStory(actualFeaturePath) {
8345
- if (!existsSync8(actualFeaturePath)) {
8558
+ if (!existsSync10(actualFeaturePath)) {
8346
8559
  return { passed: false, checks: ["validate-feature"], error: "actual missing: test.feature" };
8347
8560
  }
8348
8561
  try {
@@ -8359,23 +8572,23 @@ function checkFeatureFromStory(actualFeaturePath) {
8359
8572
  }
8360
8573
  }
8361
8574
  function checkScriptFromFeature(actualTicketDir) {
8362
- const specPath = join8(actualTicketDir, "spec.ts");
8363
- if (!existsSync8(specPath)) {
8575
+ const specPath = join10(actualTicketDir, "spec.ts");
8576
+ if (!existsSync10(specPath)) {
8364
8577
  return { passed: false, checks: ["file-presence"], error: "actual missing: spec.ts" };
8365
8578
  }
8366
8579
  return { passed: true, checks: ["file-presence"] };
8367
8580
  }
8368
8581
  function checkDiagnoseFailure(inputsTicketDir, actualTicketDir) {
8369
- const inputPath = join8(inputsTicketDir, "classifier-input.json");
8370
- const actualPath = join8(actualTicketDir, "classification.json");
8371
- if (!existsSync8(actualPath)) {
8582
+ const inputPath = join10(inputsTicketDir, "classifier-input.json");
8583
+ const actualPath = join10(actualTicketDir, "classification.json");
8584
+ if (!existsSync10(actualPath)) {
8372
8585
  return {
8373
8586
  passed: false,
8374
8587
  checks: ["bucket-match"],
8375
8588
  error: "actual missing: classification.json"
8376
8589
  };
8377
8590
  }
8378
- if (!existsSync8(inputPath)) {
8591
+ if (!existsSync10(inputPath)) {
8379
8592
  return {
8380
8593
  passed: false,
8381
8594
  checks: ["bucket-match"],
@@ -8413,7 +8626,7 @@ async function evalDeterministicCmd(argv, opts = {}) {
8413
8626
  return 1;
8414
8627
  }
8415
8628
  const paths = resolveEvalPaths(cwd, runId);
8416
- if (!existsSync8(paths.manifest)) {
8629
+ if (!existsSync10(paths.manifest)) {
8417
8630
  console.error(`[xera:eval-deterministic] missing manifest.json at ${paths.manifest}`);
8418
8631
  return 1;
8419
8632
  }
@@ -8425,7 +8638,7 @@ async function evalDeterministicCmd(argv, opts = {}) {
8425
8638
  const actualDir = paths.ticketActualDir(ticket);
8426
8639
  let result;
8427
8640
  if (stage === "feature-from-story") {
8428
- result = checkFeatureFromStory(join8(actualDir, "test.feature"));
8641
+ result = checkFeatureFromStory(join10(actualDir, "test.feature"));
8429
8642
  } else if (stage === "script-from-feature") {
8430
8643
  result = checkScriptFromFeature(actualDir);
8431
8644
  } else {
@@ -8452,13 +8665,13 @@ async function evalDeterministicCmd(argv, opts = {}) {
8452
8665
  // src/bin-internal/eval-prepare.ts
8453
8666
  import {
8454
8667
  copyFileSync,
8455
- existsSync as existsSync10,
8668
+ existsSync as existsSync12,
8456
8669
  mkdirSync as mkdirSync4,
8457
8670
  readdirSync as readdirSync5,
8458
8671
  readFileSync as readFileSync9,
8459
8672
  writeFileSync as writeFileSync4
8460
8673
  } from "fs";
8461
- import { join as join9 } from "path";
8674
+ import { join as join11 } from "path";
8462
8675
 
8463
8676
  // src/eval/run-id.ts
8464
8677
  import { execSync } from "child_process";
@@ -8483,11 +8696,11 @@ function generateRunId(opts = {}) {
8483
8696
  }
8484
8697
 
8485
8698
  // src/lock/file-lock.ts
8486
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
8699
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
8487
8700
  import { hostname } from "os";
8488
8701
  import { dirname as dirname2 } from "path";
8489
8702
  function acquireLock(path, runId) {
8490
- if (existsSync9(path))
8703
+ if (existsSync11(path))
8491
8704
  return false;
8492
8705
  mkdirSync3(dirname2(path), { recursive: true });
8493
8706
  const data = {
@@ -8504,11 +8717,11 @@ function acquireLock(path, runId) {
8504
8717
  }
8505
8718
  }
8506
8719
  function releaseLock(path) {
8507
- if (existsSync9(path))
8720
+ if (existsSync11(path))
8508
8721
  unlinkSync(path);
8509
8722
  }
8510
8723
  function readLock(path) {
8511
- if (!existsSync9(path))
8724
+ if (!existsSync11(path))
8512
8725
  return null;
8513
8726
  return JSON.parse(readFileSync8(path, "utf8"));
8514
8727
  }
@@ -8551,16 +8764,16 @@ function parseFlags2(argv) {
8551
8764
  return flags;
8552
8765
  }
8553
8766
  function readPromptVersion(repoRoot, name) {
8554
- const path = join9(repoRoot, "packages/prompts", `${name}.md`);
8555
- if (!existsSync10(path))
8767
+ const path = join11(repoRoot, "packages/prompts", `${name}.md`);
8768
+ if (!existsSync12(path))
8556
8769
  return "0.0.0";
8557
8770
  const text = readFileSync9(path, "utf8");
8558
8771
  const m = /^version:\s*(\S+)\s*$/m.exec(text);
8559
8772
  return m?.[1] ?? "0.0.0";
8560
8773
  }
8561
8774
  function discoverEvalTickets(repoRoot) {
8562
- const root = join9(repoRoot, "fixtures/golden-eval");
8563
- if (!existsSync10(root))
8775
+ const root = join11(repoRoot, "fixtures/golden-eval");
8776
+ if (!existsSync12(root))
8564
8777
  return [];
8565
8778
  const out = [];
8566
8779
  for (const entry of readdirSync5(root, { withFileTypes: true })) {
@@ -8568,9 +8781,9 @@ function discoverEvalTickets(repoRoot) {
8568
8781
  continue;
8569
8782
  if (entry.name === "README.md" || entry.name.startsWith("."))
8570
8783
  continue;
8571
- const dir = join9(root, entry.name);
8572
- const metaPath = join9(dir, "meta.json");
8573
- if (!existsSync10(metaPath))
8784
+ const dir = join11(root, entry.name);
8785
+ const metaPath = join11(dir, "meta.json");
8786
+ if (!existsSync12(metaPath))
8574
8787
  continue;
8575
8788
  const meta = JSON.parse(readFileSync9(metaPath, "utf8"));
8576
8789
  out.push({ id: meta.id, dir, stages: meta.stages });
@@ -8578,14 +8791,14 @@ function discoverEvalTickets(repoRoot) {
8578
8791
  return out.sort((a, b) => a.id.localeCompare(b.id));
8579
8792
  }
8580
8793
  function discoverClassifierTickets(repoRoot) {
8581
- const root = join9(repoRoot, "fixtures/golden-tickets");
8582
- if (!existsSync10(root))
8794
+ const root = join11(repoRoot, "fixtures/golden-tickets");
8795
+ if (!existsSync12(root))
8583
8796
  return [];
8584
8797
  const out = [];
8585
8798
  for (const entry of readdirSync5(root, { withFileTypes: true })) {
8586
8799
  if (!entry.isFile() || !entry.name.endsWith(".json"))
8587
8800
  continue;
8588
- const path = join9(root, entry.name);
8801
+ const path = join11(root, entry.name);
8589
8802
  const data = JSON.parse(readFileSync9(path, "utf8"));
8590
8803
  if (typeof data.ticket === "string")
8591
8804
  out.push({ id: data.ticket, path });
@@ -8645,7 +8858,7 @@ async function evalPrepareCmd(argv, opts = {}) {
8645
8858
  ...opts.getGitSha ? { getGitSha: opts.getGitSha } : {}
8646
8859
  });
8647
8860
  const paths = resolveEvalPaths(repoRoot, runId);
8648
- if (existsSync10(paths.root) && !flags.force) {
8861
+ if (existsSync12(paths.root) && !flags.force) {
8649
8862
  console.error(`[xera:eval-prepare] run dir already exists: ${paths.root}. Pass --force to re-run.`);
8650
8863
  return 1;
8651
8864
  }
@@ -8657,13 +8870,13 @@ async function evalPrepareCmd(argv, opts = {}) {
8657
8870
  const evalT = evalTickets.find((t) => t.id === ticket);
8658
8871
  const classT = classifierTickets.find((t) => t.id === ticket);
8659
8872
  if (evalT) {
8660
- copyFileSync(join9(evalT.dir, "story.md"), join9(ticketInputs, "story.md"));
8661
- const featurePath = join9(evalT.dir, "golden/test.feature");
8662
- if (existsSync10(featurePath))
8663
- copyFileSync(featurePath, join9(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"));
8664
8877
  }
8665
8878
  if (classT) {
8666
- copyFileSync(classT.path, join9(ticketInputs, "classifier-input.json"));
8879
+ copyFileSync(classT.path, join11(ticketInputs, "classifier-input.json"));
8667
8880
  }
8668
8881
  }
8669
8882
  const now = (opts.now ?? (() => new Date))();
@@ -8699,7 +8912,7 @@ async function evalPrepareCmd(argv, opts = {}) {
8699
8912
  }
8700
8913
 
8701
8914
  // src/bin-internal/eval-report.ts
8702
- import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
8915
+ import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
8703
8916
  function scoreJudgment(j) {
8704
8917
  const nonNa = j.dimensions.filter((d) => d.verdict !== "NA");
8705
8918
  if (nonNa.length === 0)
@@ -8754,7 +8967,7 @@ async function evalReportCmd(argv, opts = {}) {
8754
8967
  return 1;
8755
8968
  }
8756
8969
  const paths = resolveEvalPaths(cwd, runId);
8757
- if (!existsSync11(paths.manifest)) {
8970
+ if (!existsSync13(paths.manifest)) {
8758
8971
  console.error(`[xera:eval-report] missing manifest.json at ${paths.manifest}`);
8759
8972
  return 1;
8760
8973
  }
@@ -8841,40 +9054,77 @@ async function evalReportCmd(argv, opts = {}) {
8841
9054
  }
8842
9055
 
8843
9056
  // src/bin-internal/exec.ts
8844
- import { existsSync as existsSync15, mkdirSync as mkdirSync7 } from "fs";
8845
- import { join as join13 } from "path";
9057
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
9058
+ import { join as join14 } from "path";
8846
9059
  import { chromium } from "@playwright/test";
8847
9060
  import { runAuthSetup, runPlaywright, stagePlaywrightState } from "@xera-ai/web";
8848
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
+
8849
9099
  // src/artifact/paths.ts
8850
- import { join as join10 } from "path";
9100
+ import { join as join12 } from "path";
8851
9101
  var TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
8852
9102
  function resolveArtifactPaths(repoRoot, ticket) {
8853
9103
  if (!TICKET_RE.test(ticket)) {
8854
9104
  throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
8855
9105
  }
8856
- const ticketDir = join10(repoRoot, ".xera", ticket);
9106
+ const ticketDir = join12(repoRoot, ".xera", ticket);
8857
9107
  return {
8858
9108
  ticketDir,
8859
- storyPath: join10(ticketDir, "story.md"),
8860
- featurePath: join10(ticketDir, "test.feature"),
8861
- specPath: join10(ticketDir, "spec.ts"),
8862
- pageObjectsDir: join10(ticketDir, "page-objects"),
8863
- runsDir: join10(ticketDir, "runs"),
8864
- metaPath: join10(ticketDir, "meta.json"),
8865
- statusPath: join10(ticketDir, "status.json"),
8866
- logPath: join10(ticketDir, "xera.log"),
8867
- lockPath: join10(ticketDir, ".lock"),
8868
- authDir: join10(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"),
8869
9119
  runPath: (runId) => {
8870
- const runDir = join10(ticketDir, "runs", runId);
9120
+ const runDir = join12(ticketDir, "runs", runId);
8871
9121
  return {
8872
9122
  runDir,
8873
- reportJsonPath: join10(runDir, "report.json"),
8874
- tracePath: join10(runDir, "trace.zip"),
8875
- normalizedPath: join10(runDir, "normalized.json"),
8876
- screenshotsDir: join10(runDir, "screenshots"),
8877
- videoDir: join10(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")
8878
9128
  };
8879
9129
  }
8880
9130
  };
@@ -8912,9 +9162,9 @@ function needsRefresh(entry, policy, now = new Date) {
8912
9162
  }
8913
9163
 
8914
9164
  // src/auth/state.ts
8915
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
8916
- import { join as join11 } from "path";
8917
- 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";
8918
9168
 
8919
9169
  // src/auth/encrypt.ts
8920
9170
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
@@ -8971,123 +9221,39 @@ function resolveAuthKey() {
8971
9221
  }
8972
9222
 
8973
9223
  // src/auth/state.ts
8974
- var AuthStateEntrySchema = z3.object({
8975
- role: z3.string(),
8976
- strategy: z3.enum(["storageState", "apiToken"]),
8977
- created_at: z3.string(),
8978
- expires_at: z3.string(),
8979
- 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())
8980
9230
  });
8981
9231
  function pathFor(authDir, role) {
8982
- return join11(authDir, `${role}.json`);
9232
+ return join13(authDir, `${role}.json`);
8983
9233
  }
8984
9234
  function writeAuthState(authDir, entry) {
8985
- mkdirSync5(authDir, { recursive: true });
9235
+ mkdirSync6(authDir, { recursive: true });
8986
9236
  const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
8987
- writeFileSync6(pathFor(authDir, entry.role), ct);
9237
+ writeFileSync7(pathFor(authDir, entry.role), ct);
8988
9238
  }
8989
9239
  function readAuthState(authDir, role) {
8990
9240
  const p = pathFor(authDir, role);
8991
- if (!existsSync12(p))
9241
+ if (!existsSync15(p))
8992
9242
  return null;
8993
- const txt = readFileSync11(p, "utf8");
9243
+ const txt = readFileSync12(p, "utf8");
8994
9244
  const plain = decrypt(txt, resolveAuthKey());
8995
9245
  return AuthStateEntrySchema.parse(JSON.parse(plain));
8996
9246
  }
8997
9247
 
8998
- // src/config/load.ts
8999
- import { existsSync as existsSync13 } from "fs";
9000
- import { join as join12 } from "path";
9001
- import { pathToFileURL } from "url";
9002
-
9003
- // src/config/schema.ts
9004
- import { z as z4 } from "zod";
9005
- var AuthRoleSchema = z4.object({
9006
- envEmail: z4.string().min(1),
9007
- envPassword: z4.string().min(1)
9008
- });
9009
- var AuthSchema = z4.object({
9010
- strategy: z4.enum(["storageState", "apiToken", "none"]).default("none"),
9011
- ttl: z4.string().default("8h"),
9012
- refreshBuffer: z4.string().default("30m"),
9013
- setupScript: z4.string().optional(),
9014
- roles: z4.record(z4.string(), AuthRoleSchema).default({})
9015
- });
9016
- var WebSchema = z4.object({
9017
- baseUrl: z4.record(z4.string(), z4.string().url()).refine((m) => Object.keys(m).length > 0, {
9018
- message: "baseUrl must have at least one environment"
9019
- }),
9020
- defaultEnv: z4.string(),
9021
- auth: AuthSchema.prefault({}),
9022
- testData: z4.object({
9023
- users: z4.record(z4.string(), z4.object({ fromAuth: z4.string() })).default({})
9024
- }).prefault({})
9025
- }).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
9026
- message: "defaultEnv must exist in baseUrl map",
9027
- path: ["defaultEnv"]
9028
- });
9029
- var JiraSchema = z4.object({
9030
- baseUrl: z4.string().url(),
9031
- projectKeys: z4.array(z4.string().min(1)).min(1),
9032
- fields: z4.object({
9033
- story: z4.string().min(1),
9034
- acceptanceCriteria: z4.string().optional(),
9035
- attachments: z4.string().default("attachment")
9036
- })
9037
- });
9038
- var AISchema = z4.object({
9039
- livePageSnapshot: z4.boolean().default(true),
9040
- confidenceThreshold: z4.enum(["low", "medium", "high"]).default("medium"),
9041
- maxRetries: z4.object({
9042
- typecheck: z4.number().int().min(0).max(5).default(2),
9043
- lint: z4.number().int().min(0).max(5).default(2),
9044
- validateFeature: z4.number().int().min(0).max(5).default(2)
9045
- }).prefault({})
9046
- }).prefault({});
9047
- var ReportingSchema = z4.object({
9048
- language: z4.enum(["en", "vi"]).default("en"),
9049
- postToJira: z4.boolean().default(true),
9050
- transition: z4.object({
9051
- onPass: z4.string().nullable().default(null),
9052
- onFail: z4.string().nullable().default(null)
9053
- }).prefault({}),
9054
- artifactLinks: z4.enum(["git", "local"]).default("git")
9055
- }).prefault({});
9056
- var RunSchema = z4.object({
9057
- autoImpact: z4.object({
9058
- enabled: z4.boolean().default(true),
9059
- threshold: z4.number().nonnegative().default(8)
9060
- }).prefault({})
9061
- }).prefault({});
9062
- var XeraConfigSchema = z4.object({
9063
- jira: JiraSchema,
9064
- web: WebSchema,
9065
- ai: AISchema,
9066
- reporting: ReportingSchema,
9067
- run: RunSchema.prefault({}),
9068
- adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
9069
- });
9070
-
9071
- // src/config/load.ts
9072
- async function loadConfig(cwd) {
9073
- const path = join12(cwd, "xera.config.ts");
9074
- if (!existsSync13(path)) {
9075
- throw new Error(`xera.config.ts not found in ${cwd}`);
9076
- }
9077
- const mod = await import(pathToFileURL(path).href);
9078
- const raw = mod.default ?? mod;
9079
- return XeraConfigSchema.parse(raw);
9080
- }
9081
-
9082
9248
  // src/logging/ndjson-logger.ts
9083
- import { appendFileSync as appendFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync12 } from "fs";
9084
- 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";
9085
9251
 
9086
9252
  class NdjsonLogger {
9087
9253
  path;
9088
9254
  constructor(path) {
9089
9255
  this.path = path;
9090
- mkdirSync6(dirname3(path), { recursive: true });
9256
+ mkdirSync7(dirname4(path), { recursive: true });
9091
9257
  }
9092
9258
  log(payload) {
9093
9259
  const entry = { ts: new Date().toISOString(), ...payload };
@@ -9095,9 +9261,9 @@ class NdjsonLogger {
9095
9261
  `);
9096
9262
  }
9097
9263
  static readAll(path) {
9098
- if (!existsSync14(path))
9264
+ if (!existsSync16(path))
9099
9265
  return [];
9100
- const txt = readFileSync12(path, "utf8").trim();
9266
+ const txt = readFileSync13(path, "utf8").trim();
9101
9267
  if (!txt)
9102
9268
  return [];
9103
9269
  return txt.split(`
@@ -9132,14 +9298,41 @@ async function execCmd(argv) {
9132
9298
  }
9133
9299
  const t0 = Date.now();
9134
9300
  try {
9135
- 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) {
9136
9329
  const browser = await chromium.launch();
9137
9330
  try {
9138
- for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
9331
+ for (const [roleName, roleCreds] of Object.entries(webConfig.auth.roles)) {
9139
9332
  const entry = readAuthState(paths.authDir, roleName);
9140
9333
  if (needsRefresh(entry, {
9141
- ttl: config.web.auth.ttl,
9142
- refreshBuffer: config.web.auth.refreshBuffer
9334
+ ttl: webConfig.auth.ttl,
9335
+ refreshBuffer: webConfig.auth.refreshBuffer
9143
9336
  })) {
9144
9337
  const email = process.env[roleCreds.envEmail];
9145
9338
  const password = process.env[roleCreds.envPassword];
@@ -9150,7 +9343,7 @@ async function execCmd(argv) {
9150
9343
  await runAuthSetup({
9151
9344
  role: roleName,
9152
9345
  creds: { email, password },
9153
- setupScriptPath: join13(cwd, config.web.auth.setupScript),
9346
+ setupScriptPath: join14(cwd, webConfig.auth.setupScript),
9154
9347
  authDir: paths.authDir,
9155
9348
  browser
9156
9349
  });
@@ -9161,23 +9354,23 @@ async function execCmd(argv) {
9161
9354
  await browser.close();
9162
9355
  }
9163
9356
  }
9164
- if (config.web.auth.strategy === "storageState") {
9165
- 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)) {
9166
9359
  if (readAuthState(paths.authDir, roleName)) {
9167
9360
  stagePlaywrightState(paths.authDir, roleName);
9168
9361
  }
9169
9362
  }
9170
9363
  }
9171
- const cfgPath = join13(cwd, "playwright.config.ts");
9172
- if (!existsSync15(cfgPath)) {
9364
+ const cfgPath = join14(cwd, "playwright.config.ts");
9365
+ if (!existsSync17(cfgPath)) {
9173
9366
  console.error(`[xera:exec] missing ${cfgPath}. Run \`xera init\` to scaffold it, then re-run.`);
9174
9367
  return 1;
9175
9368
  }
9176
9369
  const runDir = paths.runPath(runId).runDir;
9177
- mkdirSync7(runDir, { recursive: true });
9178
- const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
9179
- const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv];
9180
- const reportJsonPath = join13(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");
9181
9374
  log.log({ step: "exec.start", runId, env: envName, baseURL });
9182
9375
  const r = await runPlaywright({
9183
9376
  specPath: paths.specPath,
@@ -9204,83 +9397,46 @@ import { dirname as dirname5 } from "path";
9204
9397
 
9205
9398
  // src/artifact/hash.ts
9206
9399
  import { createHash as createHash4 } from "crypto";
9207
- import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
9400
+ import { existsSync as existsSync18, readFileSync as readFileSync14 } from "fs";
9208
9401
  function hashString(s) {
9209
9402
  return `sha256:${createHash4("sha256").update(s).digest("hex")}`;
9210
9403
  }
9211
9404
  function hashFile(path) {
9212
- return hashString(readFileSync13(path, "utf8"));
9405
+ return hashString(readFileSync14(path, "utf8"));
9213
9406
  }
9214
9407
  function hashFileIfExists(path) {
9215
- if (!existsSync16(path))
9408
+ if (!existsSync18(path))
9216
9409
  return null;
9217
9410
  return hashFile(path);
9218
9411
  }
9219
9412
 
9220
- // src/artifact/meta.ts
9221
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
9222
- import { dirname as dirname4 } from "path";
9223
- import { z as z5 } from "zod";
9224
- var MetaJsonSchema = z5.object({
9225
- ticket: z5.string(),
9226
- adapter: z5.string(),
9227
- xera_version: z5.string(),
9228
- prompts_version: z5.string(),
9229
- fetched_at: z5.string().optional(),
9230
- story_hash: z5.string().optional(),
9231
- feature_generated_at: z5.string().optional(),
9232
- feature_generated_from_story_hash: z5.string().optional(),
9233
- feature_hash: z5.string().optional(),
9234
- script_generated_at: z5.string().optional(),
9235
- script_generated_from_feature_hash: z5.string().optional(),
9236
- script_warnings: z5.array(z5.string()).optional()
9237
- });
9238
- function readMeta(path) {
9239
- if (!existsSync17(path))
9240
- return null;
9241
- return MetaJsonSchema.parse(JSON.parse(readFileSync14(path, "utf8")));
9242
- }
9243
- function writeMeta(path, meta) {
9244
- mkdirSync8(dirname4(path), { recursive: true });
9245
- writeFileSync7(path, JSON.stringify(meta, null, 2));
9246
- }
9247
- function updateMeta(path, patch) {
9248
- const existing = readMeta(path);
9249
- if (!existing) {
9250
- throw new Error(`meta.json not found at ${path}; cannot update`);
9251
- }
9252
- const next = { ...existing, ...patch };
9253
- writeMeta(path, next);
9254
- return next;
9255
- }
9256
-
9257
9413
  // src/jira/mcp-backend.ts
9258
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
9414
+ import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
9259
9415
  import { tmpdir } from "os";
9260
- import { join as join14 } from "path";
9416
+ import { join as join15 } from "path";
9261
9417
  var MCP_ENV = "XERA_MCP_JIRA";
9262
9418
  async function createMcpBackend(_baseUrl) {
9263
9419
  if (process.env[MCP_ENV] !== "1")
9264
9420
  return null;
9265
- const tmpDir = join14(tmpdir(), "xera-mcp");
9421
+ const tmpDir = join15(tmpdir(), "xera-mcp");
9266
9422
  mkdirSync9(tmpDir, { recursive: true });
9267
9423
  return {
9268
9424
  backend: "mcp",
9269
9425
  async fetchTicket(key, _fields) {
9270
- const cachePath = join14(tmpDir, `${key}.json`);
9271
- if (!existsSync18(cachePath)) {
9426
+ const cachePath = join15(tmpDir, `${key}.json`);
9427
+ if (!existsSync19(cachePath)) {
9272
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.`);
9273
9429
  }
9274
9430
  const parsed = JSON.parse(readFileSync15(cachePath, "utf8"));
9275
9431
  return parsed;
9276
9432
  },
9277
9433
  async postComment(key, body) {
9278
- const outPath = join14(tmpDir, `${key}.comment.json`);
9434
+ const outPath = join15(tmpDir, `${key}.comment.json`);
9279
9435
  writeFileSync8(outPath, JSON.stringify({ key, body }));
9280
9436
  return { id: "mcp-pending" };
9281
9437
  },
9282
9438
  async transitionStatus(key, statusName) {
9283
- const outPath = join14(tmpDir, `${key}.transition.json`);
9439
+ const outPath = join15(tmpDir, `${key}.transition.json`);
9284
9440
  writeFileSync8(outPath, JSON.stringify({ key, statusName }));
9285
9441
  },
9286
9442
  async listFields(_sampleKey) {
@@ -9455,8 +9611,8 @@ init_graph_backfill();
9455
9611
  // src/graph/enrich.ts
9456
9612
  init_store();
9457
9613
  init_ulid();
9458
- import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
9459
- import { join as join15 } from "path";
9614
+ import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
9615
+ import { join as join16 } from "path";
9460
9616
  import { z as z6 } from "zod";
9461
9617
  var MAX_SIMILAR_EDGES = 10;
9462
9618
  var MIN_CONFIDENCE = 0.7;
@@ -9478,8 +9634,8 @@ var mk2 = (actor, type, payload) => ({
9478
9634
  payload
9479
9635
  });
9480
9636
  async function enrichTicket(repoRoot, ticketId, opts) {
9481
- const inputPath = join15(repoRoot, ".xera", ticketId, "enrichment-input.json");
9482
- if (!existsSync19(inputPath)) {
9637
+ const inputPath = join16(repoRoot, ".xera", ticketId, "enrichment-input.json");
9638
+ if (!existsSync20(inputPath)) {
9483
9639
  throw new Error(`enrichment-input.json not found at ${inputPath}`);
9484
9640
  }
9485
9641
  const raw = JSON.parse(readFileSync16(inputPath, "utf8"));
@@ -9592,11 +9748,11 @@ init_graph_record();
9592
9748
 
9593
9749
  // src/bin-internal/graph-render.ts
9594
9750
  import { mkdirSync as mkdirSync11, renameSync as renameSync2, writeFileSync as writeFileSync10 } from "fs";
9595
- import { dirname as dirname7, join as join17 } from "path";
9751
+ import { dirname as dirname7, join as join18 } from "path";
9596
9752
 
9597
9753
  // src/graph/render.ts
9598
9754
  import { readFileSync as readFileSync17 } from "fs";
9599
- import { dirname as dirname6, join as join16 } from "path";
9755
+ import { dirname as dirname6, join as join17 } from "path";
9600
9756
  import { fileURLToPath } from "url";
9601
9757
  var COLORS = {
9602
9758
  ticket: "#3B82F6",
@@ -9815,9 +9971,9 @@ function transformForVisNetwork(snap, opts) {
9815
9971
  }
9816
9972
  var __filename2 = fileURLToPath(import.meta.url);
9817
9973
  var __dirname2 = dirname6(__filename2);
9818
- var TEMPLATES_DIR = join16(__dirname2, "templates");
9974
+ var TEMPLATES_DIR = join17(__dirname2, "templates");
9819
9975
  function loadTemplate(name) {
9820
- return readFileSync17(join16(TEMPLATES_DIR, name), "utf8");
9976
+ return readFileSync17(join17(TEMPLATES_DIR, name), "utf8");
9821
9977
  }
9822
9978
  function statsToHuman(s) {
9823
9979
  return `${s.tickets} tickets \xB7 ${s.scenarios} scenarios \xB7 ${s.poms} POMs \xB7 ${s.edges} edges`;
@@ -9863,7 +10019,7 @@ async function graphRenderCmd(argv) {
9863
10019
  depth = parseDepth(argv[++i]);
9864
10020
  }
9865
10021
  const repoRoot = process.cwd();
9866
- const finalPath = outPath ?? join17(repoRoot, ".xera/graph.html");
10022
+ const finalPath = outPath ?? join18(repoRoot, ".xera/graph.html");
9867
10023
  const snap = deriveSnapshot(loadAllEvents(repoRoot));
9868
10024
  const totalNodeCount = Object.keys(snap.tickets).length + Object.keys(snap.scenarios).length + Object.keys(snap.poms).length + Object.keys(snap.areas).length;
9869
10025
  const performanceMode = decidePerformanceMode(totalNodeCount);
@@ -9915,8 +10071,8 @@ async function graphSnapshotCmd(argv) {
9915
10071
  }
9916
10072
 
9917
10073
  // src/bin-internal/heal-prepare.ts
9918
- import { existsSync as existsSync20, readdirSync as readdirSync6, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
9919
- 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";
9920
10076
  import { scrubFreeText } from "@xera-ai/web";
9921
10077
 
9922
10078
  // ../../node_modules/.bun/fflate@0.8.3/node_modules/fflate/esm/index.mjs
@@ -10344,7 +10500,7 @@ function classifyKind(raw) {
10344
10500
  return "other";
10345
10501
  }
10346
10502
  function extractDomSnapshot(tracePath) {
10347
- if (!existsSync20(tracePath))
10503
+ if (!existsSync21(tracePath))
10348
10504
  return "";
10349
10505
  const buf = readFileSync18(tracePath);
10350
10506
  const entries = unzipSync(buf);
@@ -10394,12 +10550,12 @@ function extractDomSnapshot(tracePath) {
10394
10550
  return scrubFreeText(html);
10395
10551
  }
10396
10552
  function findPomLine(ticketDir, rawLocator) {
10397
- const pomDir = join18(ticketDir, "page-objects");
10553
+ const pomDir = join19(ticketDir, "page-objects");
10398
10554
  const candidates = [];
10399
- if (existsSync20(pomDir)) {
10555
+ if (existsSync21(pomDir)) {
10400
10556
  for (const name of readdirSync6(pomDir)) {
10401
10557
  if (name.endsWith(".ts"))
10402
- candidates.push(join18(pomDir, name));
10558
+ candidates.push(join19(pomDir, name));
10403
10559
  }
10404
10560
  }
10405
10561
  for (const file of candidates) {
@@ -10441,13 +10597,13 @@ function findGherkinStep(featureText, rawLocator) {
10441
10597
  }
10442
10598
  function healPrepare(repoRoot, ticket, runId, scenarioName) {
10443
10599
  const paths = resolveArtifactPaths(repoRoot, ticket);
10444
- const classifierPath = join18(paths.ticketDir, "classifier-input.json");
10600
+ const classifierPath = join19(paths.ticketDir, "classifier-input.json");
10445
10601
  const classifier = JSON.parse(readFileSync18(classifierPath, "utf8"));
10446
10602
  const cls = classifier.scenarios.find((s) => s.name === scenarioName);
10447
10603
  if (!cls)
10448
10604
  throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
10449
- const runDir = join18(paths.runsDir, runId);
10450
- 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"));
10451
10607
  const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
10452
10608
  if (!normSc?.failure)
10453
10609
  throw new Error(`no failure recorded for scenario "${scenarioName}"`);
@@ -10460,7 +10616,7 @@ function healPrepare(repoRoot, ticket, runId, scenarioName) {
10460
10616
  const pomLoc = findPomLine(paths.ticketDir, raw);
10461
10617
  const featureText = readFileSync18(paths.featurePath, "utf8");
10462
10618
  const gherkinStep = findGherkinStep(featureText, raw);
10463
- const domSnapshotAtFailure = extractDomSnapshot(join18(runDir, "trace.zip"));
10619
+ const domSnapshotAtFailure = extractDomSnapshot(join19(runDir, "trace.zip"));
10464
10620
  return {
10465
10621
  ticket,
10466
10622
  runId,
@@ -10480,7 +10636,7 @@ async function healPrepareCmd(argv) {
10480
10636
  try {
10481
10637
  const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
10482
10638
  const paths = resolveArtifactPaths(process.cwd(), ticket);
10483
- const outPath = join18(paths.runsDir, runId, "heal-input.json");
10639
+ const outPath = join19(paths.runsDir, runId, "heal-input.json");
10484
10640
  writeFileSync11(outPath, JSON.stringify(result, null, 2));
10485
10641
  console.log(`[xera:heal-prepare] wrote ${outPath}`);
10486
10642
  return 0;
@@ -10492,7 +10648,7 @@ async function healPrepareCmd(argv) {
10492
10648
 
10493
10649
  // src/bin-internal/impact-prepare.ts
10494
10650
  import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
10495
- import { join as join19 } from "path";
10651
+ import { join as join20 } from "path";
10496
10652
 
10497
10653
  // src/graph/impact.ts
10498
10654
  var PRIORITY_WEIGHT = { p0: 3, p1: 2, p2: 1 };
@@ -10742,11 +10898,11 @@ async function impactPrepareCmd(argv) {
10742
10898
  scenarios,
10743
10899
  generatedAt: new Date().toISOString()
10744
10900
  };
10745
- const impactDir = join19(repoRoot, ".xera/impact");
10901
+ const impactDir = join20(repoRoot, ".xera/impact");
10746
10902
  mkdirSync12(impactDir, { recursive: true });
10747
- writeFileSync12(join19(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10903
+ writeFileSync12(join20(impactDir, `${ticket}.json`), JSON.stringify(report, null, 2));
10748
10904
  if (!quiet) {
10749
- writeFileSync12(join19(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
10905
+ writeFileSync12(join20(impactDir, `${ticket}.md`), renderImpactMarkdown(report));
10750
10906
  }
10751
10907
  return 0;
10752
10908
  }
@@ -10771,9 +10927,8 @@ async function lintCmd(argv) {
10771
10927
  }
10772
10928
 
10773
10929
  // src/bin-internal/normalize.ts
10774
- import { existsSync as existsSync21, readdirSync as readdirSync7 } from "fs";
10775
- import { join as join20 } from "path";
10776
- import { normalizeRun } from "@xera-ai/web";
10930
+ import { existsSync as existsSync22, readdirSync as readdirSync7 } from "fs";
10931
+ import { join as join21 } from "path";
10777
10932
  async function normalizeCmd(argv) {
10778
10933
  const ticket = argv[0];
10779
10934
  if (!ticket) {
@@ -10787,22 +10942,31 @@ async function normalizeCmd(argv) {
10787
10942
  console.error("[xera:normalize] no run found");
10788
10943
  return 1;
10789
10944
  }
10790
- const runDir = join20(paths.runsDir, runId);
10791
- if (!existsSync21(runDir)) {
10945
+ const runDir = join21(paths.runsDir, runId);
10946
+ if (!existsSync22(runDir)) {
10792
10947
  console.error(`[xera:normalize] runs/${runId} missing`);
10793
10948
  return 1;
10794
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");
10795
10959
  const r = await normalizeRun({ runId, runDir });
10796
10960
  console.log(`[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`);
10797
10961
  return 0;
10798
10962
  }
10799
10963
 
10800
10964
  // src/bin-internal/post.ts
10801
- import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
10802
- import { join as join21 } from "path";
10965
+ import { existsSync as existsSync24, readFileSync as readFileSync20 } from "fs";
10966
+ import { join as join22 } from "path";
10803
10967
 
10804
10968
  // src/artifact/status.ts
10805
- 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";
10806
10970
  import { dirname as dirname8 } from "path";
10807
10971
  import { z as z7 } from "zod";
10808
10972
  var ClassificationEnum = z7.enum([
@@ -10811,7 +10975,10 @@ var ClassificationEnum = z7.enum([
10811
10975
  "SELECTOR_DRIFT",
10812
10976
  "FLAKY",
10813
10977
  "TEST_BUG",
10814
- "TEST_OUTDATED"
10978
+ "TEST_OUTDATED",
10979
+ "CONTRACT_DRIFT",
10980
+ "RATE_LIMITED",
10981
+ "AUTH_EXPIRED"
10815
10982
  ]);
10816
10983
  var ResultEnum = z7.enum(["PASS", "FAIL"]);
10817
10984
  var ConfidenceEnum = z7.enum(["low", "medium", "high"]);
@@ -10837,7 +11004,7 @@ var StatusJsonSchema = z7.object({
10837
11004
  });
10838
11005
  var HISTORY_CAP = 20;
10839
11006
  function readStatus(path) {
10840
- if (!existsSync22(path))
11007
+ if (!existsSync23(path))
10841
11008
  return null;
10842
11009
  return StatusJsonSchema.parse(JSON.parse(readFileSync19(path, "utf8")));
10843
11010
  }
@@ -10869,8 +11036,8 @@ async function postCmd(argv) {
10869
11036
  return 0;
10870
11037
  }
10871
11038
  const paths = resolveArtifactPaths(cwd, ticket);
10872
- const draftPath = join21(paths.ticketDir, "jira-comment.draft.md");
10873
- if (!existsSync23(draftPath)) {
11039
+ const draftPath = join22(paths.ticketDir, "jira-comment.draft.md");
11040
+ if (!existsSync24(draftPath)) {
10874
11041
  console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
10875
11042
  return 1;
10876
11043
  }
@@ -10902,12 +11069,15 @@ async function promoteCmd(argv) {
10902
11069
  }
10903
11070
 
10904
11071
  // src/bin-internal/report.ts
10905
- import { existsSync as existsSync25, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
10906
- 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";
10907
11074
 
10908
11075
  // src/classifier/aggregate.ts
10909
11076
  var CLASS_PRIORITY = [
10910
11077
  "REAL_BUG",
11078
+ "CONTRACT_DRIFT",
11079
+ "AUTH_EXPIRED",
11080
+ "RATE_LIMITED",
10911
11081
  "TEST_OUTDATED",
10912
11082
  "TEST_BUG",
10913
11083
  "SELECTOR_DRIFT",
@@ -10934,6 +11104,134 @@ function aggregateScenarios(scenarios) {
10934
11104
  return { overall: chosen, overallConfidence: minConf, scenarios };
10935
11105
  }
10936
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
+
10937
11235
  // src/graph/classify.ts
10938
11236
  var DEFAULT_THRESHOLD = 0.7;
10939
11237
  var SHORT_CIRCUIT = ["FLAKY", "PASS"];
@@ -11038,11 +11336,11 @@ xera v${input.xeraVersion} \u2022 prompts v${input.promptsVersion}`;
11038
11336
  }
11039
11337
 
11040
11338
  // src/reporter/status-writer.ts
11041
- import { existsSync as existsSync24 } from "fs";
11339
+ import { existsSync as existsSync25 } from "fs";
11042
11340
  function writeStatusFromClassification(path, input) {
11043
11341
  const result = input.classification.overall === "PASS" ? "PASS" : "FAIL";
11044
11342
  const entry = { ts: input.runTs, result, class: input.classification.overall };
11045
- if (!existsSync24(path)) {
11343
+ if (!existsSync25(path)) {
11046
11344
  writeStatus(path, {
11047
11345
  ticket: input.ticket,
11048
11346
  lastRun: input.runTs,
@@ -11074,11 +11372,70 @@ async function reportCmd(argv) {
11074
11372
  console.error("[xera:report] usage: report <TICKET> --input=<classifier-output.json>");
11075
11373
  return 1;
11076
11374
  }
11077
- const paths = resolveArtifactPaths(process.cwd(), ticket);
11375
+ const cwd = process.cwd();
11376
+ const paths = resolveArtifactPaths(cwd, ticket);
11078
11377
  const input = JSON.parse(readFileSync21(inputArg.slice("--input=".length), "utf8"));
11079
- const aggregated = aggregateScenarios(input.scenarios);
11080
- const decisionsPath = join22(paths.ticketDir, "runs", input.runId, "outdated-decisions.json");
11081
- 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")) : {};
11082
11439
  const graph = deriveSnapshot(loadAllEvents(process.cwd()));
11083
11440
  const normalizeScenarioName = (name) => name.trim().toLowerCase().replace(/\s+/g, " ");
11084
11441
  const scenarioIdByName = {};
@@ -11126,7 +11483,7 @@ async function reportCmd(argv) {
11126
11483
  xeraVersion: "0.1.0",
11127
11484
  promptsVersion: "1.0.0"
11128
11485
  });
11129
- const draftPath = join22(paths.ticketDir, "jira-comment.draft.md");
11486
+ const draftPath = join23(paths.ticketDir, "jira-comment.draft.md");
11130
11487
  writeFileSync14(draftPath, md);
11131
11488
  console.log(`[xera:report] wrote status.json and ${draftPath}`);
11132
11489
  return 0;
@@ -11194,7 +11551,7 @@ async function unlockCmd(argv) {
11194
11551
  }
11195
11552
 
11196
11553
  // src/bin-internal/validate-feature.ts
11197
- import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
11554
+ import { existsSync as existsSync27, readFileSync as readFileSync22 } from "fs";
11198
11555
  import { validateGherkin as validateGherkin2 } from "@xera-ai/web";
11199
11556
  async function validateFeatureCmd(argv) {
11200
11557
  const ticket = argv[0];
@@ -11203,7 +11560,7 @@ async function validateFeatureCmd(argv) {
11203
11560
  return 1;
11204
11561
  }
11205
11562
  const paths = resolveArtifactPaths(process.cwd(), ticket);
11206
- if (!existsSync26(paths.featurePath)) {
11563
+ if (!existsSync27(paths.featurePath)) {
11207
11564
  console.error(`[xera:validate-feature] missing ${paths.featurePath}`);
11208
11565
  return 1;
11209
11566
  }
@@ -11219,6 +11576,7 @@ async function validateFeatureCmd(argv) {
11219
11576
 
11220
11577
  // src/bin-internal/index.ts
11221
11578
  var COMMANDS = {
11579
+ "auth-setup": authSetupCmd,
11222
11580
  disputes: disputesCmd,
11223
11581
  doctor: doctorCmd,
11224
11582
  "eval-deterministic": evalDeterministicCmd,