executable-stories-formatters 1.20.0 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -87,6 +87,7 @@ __export(src_exports, {
87
87
  computeTestMetrics: () => computeTestMetrics,
88
88
  copyMarkdownAssets: () => copyMarkdownAssets,
89
89
  createAnchor: () => createAnchor,
90
+ createJevClient: () => createJevClient,
90
91
  createPrCommentSummary: () => createPrCommentSummary,
91
92
  createReportGenerator: () => createReportGenerator,
92
93
  createTestRailProvider: () => createTestRailProvider,
@@ -99,6 +100,9 @@ __export(src_exports, {
99
100
  diffRuns: () => diffRuns,
100
101
  diffStoryReports: () => diffStoryReports,
101
102
  emptyLockfile: () => emptyLockfile,
103
+ enrichGoal: () => enrichGoal,
104
+ enrichReview: () => enrichReview,
105
+ enrichTriage: () => enrichTriage,
102
106
  findGitDir: () => findGitDir,
103
107
  formatDuration: () => import_duration2.formatDuration,
104
108
  generateRunComparison: () => generateRunComparison,
@@ -113,6 +117,7 @@ __export(src_exports, {
113
117
  isProviderName: () => isProviderName,
114
118
  isReviewableSource: () => isReviewableSource,
115
119
  isTestFile: () => isTestFile,
120
+ jevFromEnv: () => jevFromEnv,
116
121
  joinNameAndExt: () => joinNameAndExt,
117
122
  listScenarios: () => listScenarios,
118
123
  loadHistory: () => loadHistory,
@@ -5146,6 +5151,50 @@ function sourceBaseKey(sourceFile) {
5146
5151
  return dot > slash ? sourceFile.slice(0, dot) : sourceFile;
5147
5152
  }
5148
5153
 
5154
+ // src/jev.ts
5155
+ var JEV_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
5156
+ var JEV_MODEL = "jev-latest";
5157
+ function createJevClient(options) {
5158
+ const {
5159
+ apiKey,
5160
+ model = JEV_MODEL,
5161
+ endpoint = JEV_ENDPOINT,
5162
+ fetch = globalThis.fetch,
5163
+ timeoutMs = 1e4
5164
+ } = options;
5165
+ return {
5166
+ model,
5167
+ // One attempt per question set; add backoff on 429/529 if a loop hits rate limits.
5168
+ async ask(state, questions) {
5169
+ const response = await fetch(endpoint, {
5170
+ method: "POST",
5171
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
5172
+ body: JSON.stringify({ model, state, questions }),
5173
+ signal: AbortSignal.timeout(timeoutMs)
5174
+ });
5175
+ if (!response.ok) throw new Error(`Jev ${response.status}: ${(await response.text()).slice(0, 200)}`);
5176
+ const body = await response.json();
5177
+ if (!body.answers) throw new Error("Jev response has no answers");
5178
+ return body.answers;
5179
+ }
5180
+ };
5181
+ }
5182
+ function jevFromEnv(env = process.env) {
5183
+ const apiKey = env.JEV_API_KEY;
5184
+ if (!apiKey) return void 0;
5185
+ return createJevClient({
5186
+ apiKey,
5187
+ ...env.JEV_MODEL ? { model: env.JEV_MODEL } : {},
5188
+ ...env.JEV_ENDPOINT ? { endpoint: env.JEV_ENDPOINT } : {}
5189
+ });
5190
+ }
5191
+ function asChoice(answer) {
5192
+ return answer?.type === "choice" ? answer : void 0;
5193
+ }
5194
+ function asNoul(answer) {
5195
+ return answer?.type === "noul" ? answer.noul : void 0;
5196
+ }
5197
+
5149
5198
  // src/review/build-review.ts
5150
5199
  var STRENGTH_RANK = {
5151
5200
  none: 0,
@@ -5346,6 +5395,61 @@ function buildReview(run, context = { changedFiles: [] }) {
5346
5395
  codeDiffs: (context.codeDiffs ?? []).map((d) => buildCodeDiff(d, run, context))
5347
5396
  };
5348
5397
  }
5398
+ var REVIEW_CHANGE_TYPE_MIN_CONFIDENCE = 0.6;
5399
+ var PATCH_EXCERPT_CHARS = 4e3;
5400
+ var CHANGE_TYPE_CRITERIA = {
5401
+ feature: "new user-visible behaviour or capability",
5402
+ bugfix: "corrects behaviour that was wrong",
5403
+ refactor: "restructures code without changing behaviour",
5404
+ perf: "same behaviour, faster or cheaper",
5405
+ deps: "dependency, toolchain, or lockfile change"
5406
+ };
5407
+ async function enrichReview(review, jev) {
5408
+ const untagged = review.claims.filter((c) => c.changeType === "unknown" && c.coversFiles.length > 0);
5409
+ if (untagged.length === 0) return review;
5410
+ const hunksByPath = /* @__PURE__ */ new Map();
5411
+ for (const group of review.context.codeDiffs ?? []) {
5412
+ for (const file of parseUnifiedDiff(group.patch)) {
5413
+ const path15 = file.newPath ?? file.oldPath;
5414
+ if (!path15) continue;
5415
+ const text2 = file.hunks.flatMap((h) => h.lines.map((l) => (l.kind === "add" ? "+" : l.kind === "del" ? "-" : " ") + l.text));
5416
+ hunksByPath.set(path15, [...hunksByPath.get(path15) ?? [], ...text2]);
5417
+ }
5418
+ }
5419
+ const inferred = /* @__PURE__ */ new Map();
5420
+ await Promise.all(
5421
+ untagged.map(async (claim) => {
5422
+ const patch = claim.coversFiles.flatMap((f) => hunksByPath.get(f) ?? []).join("\n").slice(0, PATCH_EXCERPT_CHARS);
5423
+ const answers = await jev.ask(
5424
+ {
5425
+ scenario: claim.scenario,
5426
+ steps: claim.testCase.story.steps.map((s) => `${s.keyword} ${s.text}`),
5427
+ ...claim.intent ? { intent: claim.intent } : {},
5428
+ changedFiles: claim.coversFiles,
5429
+ ...patch ? { patch } : {}
5430
+ },
5431
+ {
5432
+ changeType: {
5433
+ type: "choice",
5434
+ instructions: "What kind of change does this scenario prove?",
5435
+ criteria: CHANGE_TYPE_CRITERIA
5436
+ }
5437
+ }
5438
+ );
5439
+ const answer = asChoice(answers.changeType);
5440
+ if (!answer || answer.confidence < REVIEW_CHANGE_TYPE_MIN_CONFIDENCE) return;
5441
+ const changeType = answer.choice;
5442
+ if (VALID_CHANGE_TYPES.has(changeType)) inferred.set(claim.id, { changeType, confidence: answer.confidence });
5443
+ })
5444
+ );
5445
+ return {
5446
+ ...review,
5447
+ claims: review.claims.map((claim) => {
5448
+ const hit = inferred.get(claim.id);
5449
+ return hit ? { ...claim, changeType: hit.changeType, changeTypeConfidence: hit.confidence } : claim;
5450
+ })
5451
+ };
5452
+ }
5349
5453
  function codeDiffDiagnostics(review) {
5350
5454
  const issues = [];
5351
5455
  for (const evidence of review.codeDiffs) {
@@ -9679,9 +9783,41 @@ function buildGoal(args, _deps = {}) {
9679
9783
  requirements,
9680
9784
  regressions,
9681
9785
  regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
9682
- ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
9786
+ ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations, advisories: [] }
9683
9787
  };
9684
9788
  }
9789
+ var GOAL_WEAKENED_MIN_PROBABILITY = 0.75;
9790
+ var stepText = (tc) => tc.story.steps.map((s) => `${s.keyword} ${s.text}`);
9791
+ async function enrichGoal(report, args, jev) {
9792
+ const { baseline } = args;
9793
+ if (!baseline || !report.ratchet.enforced) return report;
9794
+ const current = new Map(args.run.testCases.map((tc) => [tc.id, tc]));
9795
+ const flagged = new Set(report.ratchet.violations.map((v) => v.id));
9796
+ const rewritten = baseline.testCases.flatMap((base) => {
9797
+ const now = current.get(base.id);
9798
+ if (!now || flagged.has(base.id)) return [];
9799
+ const before = stepText(base);
9800
+ const after = stepText(now);
9801
+ return before.join("\n") === after.join("\n") ? [] : [{ base, before, after }];
9802
+ });
9803
+ const advisories = (await Promise.all(
9804
+ rewritten.map(async ({ base, before, after }) => {
9805
+ const answers = await jev.ask(
9806
+ { baseline: before, now: after },
9807
+ {
9808
+ weakened: {
9809
+ type: "noul",
9810
+ instructions: "Does the NOW scenario check less than the BASELINE scenario: fewer or weaker assertions, looser expectations, or a removed check?"
9811
+ }
9812
+ }
9813
+ );
9814
+ const p = asNoul(answers.weakened);
9815
+ if (p === void 0 || p < GOAL_WEAKENED_MIN_PROBABILITY) return [];
9816
+ return [{ id: base.id, title: base.story.scenario, kind: "weakened", detail: `jev ${p.toFixed(2)}: steps rewritten, checks less` }];
9817
+ })
9818
+ )).flat();
9819
+ return { ...report, ratchet: { ...report.ratchet, advisories } };
9820
+ }
9685
9821
  function evaluate(selector, matched) {
9686
9822
  const passed = matched.filter((tc) => tc.status === "passed").length;
9687
9823
  const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
@@ -9723,6 +9859,9 @@ function renderGoal(report, format) {
9723
9859
  lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
9724
9860
  }
9725
9861
  }
9862
+ for (const v of report.ratchet.advisories) {
9863
+ lines.push(` advisory ${v.kind}: ${v.title} (${v.detail})`);
9864
+ }
9726
9865
  }
9727
9866
  return lines.join("\n");
9728
9867
  }
@@ -9792,6 +9931,55 @@ function buildTriage(args, _deps = {}) {
9792
9931
  items
9793
9932
  };
9794
9933
  }
9934
+ var TRIAGE_SUGGEST_MIN_PROBABILITY = 0.5;
9935
+ var FAILURE_KIND_CRITERIA = {
9936
+ product: "the application code under test behaves wrongly",
9937
+ test: "the scenario, assertion, fixture, or test data is wrong or stale",
9938
+ infra: "environment, network, timeout, resource, or flaky-timing failure"
9939
+ };
9940
+ async function enrichTriage(report, testCases, jev, codeowners) {
9941
+ const candidates = new Set(testCases.flatMap((tc) => tc.story.covers ?? []));
9942
+ for (const rule of codeowners ?? []) candidates.add(rule.pattern);
9943
+ const criteria = Object.fromEntries([...candidates].map((path15) => [path15, null]));
9944
+ const byId = new Map(testCases.map((tc) => [tc.id, tc]));
9945
+ const items = await Promise.all(
9946
+ report.items.map(async (item) => {
9947
+ if (item.covers.length > 0) return item;
9948
+ const tc = byId.get(item.id);
9949
+ const answers = await jev.ask(
9950
+ {
9951
+ scenario: item.scenario,
9952
+ steps: tc?.story.steps.map((s) => `${s.keyword} ${s.text}`) ?? [],
9953
+ testFile: item.location,
9954
+ error: item.errorMessage ?? null
9955
+ },
9956
+ {
9957
+ kind: {
9958
+ type: "choice",
9959
+ instructions: "What is most likely broken, given this failing scenario and its error?",
9960
+ criteria: FAILURE_KIND_CRITERIA
9961
+ },
9962
+ ...candidates.size > 0 ? {
9963
+ covers: {
9964
+ type: "choice",
9965
+ instructions: "Which of these product paths does the fix for this failure most likely land in?",
9966
+ criteria
9967
+ }
9968
+ } : {}
9969
+ }
9970
+ );
9971
+ const kind = asChoice(answers.kind);
9972
+ const covers = asChoice(answers.covers);
9973
+ const probability = covers ? covers.probabilities[covers.choice] ?? 0 : 0;
9974
+ return {
9975
+ ...item,
9976
+ ...kind && kind.choice in FAILURE_KIND_CRITERIA ? { failureKind: { kind: kind.choice, confidence: kind.confidence } } : {},
9977
+ ...covers && probability >= TRIAGE_SUGGEST_MIN_PROBABILITY ? { suggestedCovers: { path: covers.choice, probability } } : {}
9978
+ };
9979
+ })
9980
+ );
9981
+ return { ...report, items };
9982
+ }
9795
9983
  function renderTriage(report, format, options = {}) {
9796
9984
  if (format === "json") return JSON.stringify(report, null, 2);
9797
9985
  if (report.items.length === 0) {
@@ -9809,9 +9997,14 @@ function renderTriage(report, format, options = {}) {
9809
9997
  }
9810
9998
  if (item.covers.length > 0) {
9811
9999
  lines.push(` fix: ${item.covers.join(", ")}`);
10000
+ } else if (item.suggestedCovers) {
10001
+ lines.push(` fix: ${item.suggestedCovers.path}? (jev ${item.suggestedCovers.probability.toFixed(2)}, no covers declared)`);
9812
10002
  } else {
9813
10003
  lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
9814
10004
  }
10005
+ if (item.failureKind) {
10006
+ lines.push(` kind: ${item.failureKind.kind} (jev ${item.failureKind.confidence.toFixed(2)})`);
10007
+ }
9815
10008
  if (item.tickets.length > 0) {
9816
10009
  lines.push(` ticket: ${item.tickets.join(", ")}`);
9817
10010
  }
@@ -10062,7 +10255,8 @@ function renderClaim(lines, claim) {
10062
10255
  lines.push("");
10063
10256
  lines.push(`- File: \`${claim.sourceFile}:${claim.sourceLine}\``);
10064
10257
  if (claim.changeType !== "unknown") {
10065
- lines.push(`- Change: \`${claim.changeType}\``);
10258
+ const inferred = claim.changeTypeConfidence === void 0 ? "" : ` _(inferred, jev ${claim.changeTypeConfidence.toFixed(2)})_`;
10259
+ lines.push(`- Change: \`${claim.changeType}\`${inferred}`);
10066
10260
  }
10067
10261
  const tickets = claim.testCase.story.tickets ?? [];
10068
10262
  if (tickets.length > 0) {
@@ -10784,6 +10978,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
10784
10978
  computeTestMetrics,
10785
10979
  copyMarkdownAssets,
10786
10980
  createAnchor,
10981
+ createJevClient,
10787
10982
  createPrCommentSummary,
10788
10983
  createReportGenerator,
10789
10984
  createTestRailProvider,
@@ -10796,6 +10991,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
10796
10991
  diffRuns,
10797
10992
  diffStoryReports,
10798
10993
  emptyLockfile,
10994
+ enrichGoal,
10995
+ enrichReview,
10996
+ enrichTriage,
10799
10997
  findGitDir,
10800
10998
  formatDuration,
10801
10999
  generateRunComparison,
@@ -10810,6 +11008,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
10810
11008
  isProviderName,
10811
11009
  isReviewableSource,
10812
11010
  isTestFile,
11011
+ jevFromEnv,
10813
11012
  joinNameAndExt,
10814
11013
  listScenarios,
10815
11014
  loadHistory,