lastlight-evals 0.10.0 → 0.12.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 (56) hide show
  1. package/dashboard/dist/assets/index-B4TAaIUR.css +1 -0
  2. package/dashboard/dist/assets/index-LFdFc6Z9.js +109 -0
  3. package/dashboard/dist/index.html +2 -2
  4. package/datasets/aacr-bench/README.md +146 -0
  5. package/datasets/pr-review/README.md +41 -0
  6. package/datasets/pr-review/anchors.json +3709 -0
  7. package/dist/add-case.js.map +1 -1
  8. package/dist/arm.js +14 -5
  9. package/dist/arm.js.map +1 -1
  10. package/dist/bootstrap.js +14 -0
  11. package/dist/bootstrap.js.map +1 -1
  12. package/dist/clean.js.map +1 -1
  13. package/dist/config.js +59 -20
  14. package/dist/config.js.map +1 -1
  15. package/dist/fake-github.js +72 -0
  16. package/dist/fake-github.js.map +1 -1
  17. package/dist/grade.js +308 -64
  18. package/dist/grade.js.map +1 -1
  19. package/dist/init.js.map +1 -1
  20. package/dist/judge.js +19 -0
  21. package/dist/judge.js.map +1 -1
  22. package/dist/mechanism.test.js +303 -13
  23. package/dist/mechanism.test.js.map +1 -1
  24. package/dist/metrics.js +88 -9
  25. package/dist/metrics.js.map +1 -1
  26. package/dist/paths.js +135 -1
  27. package/dist/paths.js.map +1 -1
  28. package/dist/phase-models.js +56 -0
  29. package/dist/phase-models.js.map +1 -0
  30. package/dist/pool.js +32 -0
  31. package/dist/pool.js.map +1 -0
  32. package/dist/pool.test.js +180 -0
  33. package/dist/pool.test.js.map +1 -0
  34. package/dist/pr-context.js +77 -6
  35. package/dist/pr-context.js.map +1 -1
  36. package/dist/repeats.test.js +317 -0
  37. package/dist/repeats.test.js.map +1 -0
  38. package/dist/report.js +99 -5
  39. package/dist/report.js.map +1 -1
  40. package/dist/review-metrics.js +571 -0
  41. package/dist/review-metrics.js.map +1 -0
  42. package/dist/review-metrics.test.js +634 -0
  43. package/dist/review-metrics.test.js.map +1 -0
  44. package/dist/review-pipeline-stats.js +455 -0
  45. package/dist/review-pipeline-stats.js.map +1 -0
  46. package/dist/review-pipeline-stats.test.js +413 -0
  47. package/dist/review-pipeline-stats.test.js.map +1 -0
  48. package/dist/run-instance.js +176 -45
  49. package/dist/run-instance.js.map +1 -1
  50. package/dist/run.js +637 -122
  51. package/dist/run.js.map +1 -1
  52. package/dist/schema.js +22 -1
  53. package/dist/schema.js.map +1 -1
  54. package/package.json +9 -8
  55. package/dashboard/dist/assets/index-B1jNgf2P.js +0 -313
  56. package/dashboard/dist/assets/index-Sic0jI7y.css +0 -1
package/dist/judge.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"judge.js","sourceRoot":"","sources":["../src/judge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"judge.js","sourceRoot":"","sources":["../src/judge.ts"],"names":[],"mappings":"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"}
@@ -13,16 +13,17 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { join } from "node:path";
15
15
  import { tmpdir } from "node:os";
16
- import { startFakeGitHub } from "./fake-github.js";
16
+ import { startFakeGitHub, closingIssueNumbers } from "./fake-github.js";
17
17
  import { appliedRepoConfigKeys, loadRepoConfigFixture, resolveEvalRepoConfig, } from "./repo-config.js";
18
18
  import { seedWorkspace, seedWorkspaceFromGit, prFilesFromGit, injectRepoContext } from "./seed.js";
19
19
  import { gitDiffAgainstBase } from "./run-instance.js";
20
20
  import { execFileSync } from "node:child_process";
21
21
  import { gradeExecution, gradeBehavioral, gradeTriage, gradeReview, gradeMarkers, fBeta } from "./grade.js";
22
22
  import { prContextPatch } from "./pr-context.js";
23
- import { defaultFixConfig } from "lastlight-core/evals";
23
+ import { defaultFixConfig, resolveReviewGitHubClient } from "lastlight-core/evals";
24
24
  import { computeMartianRanking } from "./report.js";
25
25
  import { loadMergedConfig, resolvePhaseModel } from "./config.js";
26
+ import { modelTemplateForRow } from "./phase-models.js";
26
27
  import { modelsArm, configArm, releaseOverlayGuard } from "./arm.js";
27
28
  import { collectMetrics } from "./metrics.js";
28
29
  const staticAuth = { getToken: async () => "fake-token", expiresAt: null, canRefresh: false };
@@ -625,6 +626,74 @@ describe("config run type — per-step model resolution (config.ts)", () => {
625
626
  // 5. Template referencing an unset key → falls through to phase/default.
626
627
  expect(resolvePhaseModel("{{models.missing}}", "executor", models)).toBe("m-default");
627
628
  });
629
+ it("renders {{#if}} conditional templates via core's engine (the adjudicate fallback pair)", () => {
630
+ // pr-review.yaml's `adjudicate` model — an {{#if}}/{{#if !x}} pair that
631
+ // falls back to models.review when models.review-adjudicate is unset. The
632
+ // old bare-variable regex left the {{#if}} blocks un-rendered, so the
633
+ // recorded PhaseMetric.model was literal template residue, not a model id.
634
+ const tpl = "{{#if models.review-adjudicate}}{{models.review-adjudicate}}{{/if}}" +
635
+ "{{#if !models.review-adjudicate}}{{models.review}}{{/if}}";
636
+ const models = { default: "m-default", review: "m-review" };
637
+ // Unset → the {{#if !x}} branch renders models.review.
638
+ expect(resolvePhaseModel(tpl, "adjudicate", models)).toBe("m-review");
639
+ // Set → the pinned value wins and the negated branch renders empty.
640
+ expect(resolvePhaseModel(tpl, "adjudicate", { ...models, "review-adjudicate": "m-pinned" })).toBe("m-pinned");
641
+ // Both unset → both branches render empty → falls through to the default
642
+ // (matching core: an empty render is falsy, resolver → fallback).
643
+ expect(resolvePhaseModel(tpl, "adjudicate", { default: "m-default" })).toBe("m-default");
644
+ });
645
+ it("resolvePhaseModel branch precedence: template → models[label] → models[parent] → default", () => {
646
+ const models = {
647
+ default: "m-default",
648
+ survey: "m-survey",
649
+ survey_branch_contract: "m-contract-pin",
650
+ };
651
+ // Core's fanout resolver (`fanout.ts resolveModelVariant`) tries the branch
652
+ // LABEL as the task name, then the parent phase as fallbackTask.
653
+ expect(resolvePhaseModel(undefined, "survey_branch_contract", models, "survey")).toBe("m-contract-pin");
654
+ expect(resolvePhaseModel(undefined, "survey_branch_tests", models, "survey")).toBe("m-survey");
655
+ expect(resolvePhaseModel(undefined, "other_branch_x", models, "other")).toBe("m-default");
656
+ // A rendered template beats both map lookups.
657
+ expect(resolvePhaseModel("{{models.survey}}", "survey_branch_contract", models, "survey")).toBe("m-survey");
658
+ });
659
+ });
660
+ describe("modelTemplateForRow — ledger label → YAML model template (phase-models.ts)", () => {
661
+ // A pr-review-shaped def: a fanout phase whose `model:` template must govern
662
+ // every `<parent>_branch_<name>` row, plus one branch with its own override.
663
+ const phases = [
664
+ { name: "seed" },
665
+ {
666
+ name: "survey",
667
+ model: "{{models.review-survey}}",
668
+ branches: [{ name: "contract" }, { name: "tests", model: "{{models.survey-tests}}" }],
669
+ },
670
+ { name: "review", model: "{{models.review}}" },
671
+ ];
672
+ it("maps declared phase names to their own template (old behaviour)", () => {
673
+ expect(modelTemplateForRow(phases, "survey")).toEqual({
674
+ template: "{{models.review-survey}}",
675
+ fallbackPhase: undefined,
676
+ });
677
+ expect(modelTemplateForRow(phases, "seed")).toEqual({ template: undefined, fallbackPhase: undefined });
678
+ });
679
+ it("maps branch rows to the parent's template with the parent as fallback task", () => {
680
+ for (const label of ["survey_branch_contract", "survey_branch_contract_retry", "survey_branch_contract_check"]) {
681
+ expect(modelTemplateForRow(phases, label)).toEqual({
682
+ template: "{{models.review-survey}}",
683
+ fallbackPhase: "survey",
684
+ });
685
+ }
686
+ });
687
+ it("a branch-level model: override beats the parent template (forward-looking)", () => {
688
+ expect(modelTemplateForRow(phases, "survey_branch_tests")).toEqual({
689
+ template: "{{models.survey-tests}}",
690
+ fallbackPhase: "survey",
691
+ });
692
+ });
693
+ it("loop-derived and unknown labels keep the old no-template behaviour", () => {
694
+ expect(modelTemplateForRow(phases, "review_fix_1")).toEqual({ template: undefined, fallbackPhase: undefined });
695
+ expect(modelTemplateForRow(phases, "nonexistent")).toEqual({ template: undefined, fallbackPhase: undefined });
696
+ });
628
697
  });
629
698
  describe("Arm seam — model-selection adapters (arm.ts)", () => {
630
699
  // A stand-in core root (just the one file loadMergedConfig reads) + an overlay
@@ -662,6 +731,26 @@ describe("Arm seam — model-selection adapters (arm.ts)", () => {
662
731
  it("activate() is a no-op (no overlay to switch)", () => {
663
732
  expect(() => modelsArm("m", "f").activate()).not.toThrow();
664
733
  });
734
+ // A forced model is not the whole arm: the overlay's `review:` policy is a
735
+ // deployment fact `--model` says nothing about, and `review.analysis.enabled`
736
+ // is what switches the review evidence pipeline on. A `models` run with an
737
+ // overlay must carry it, or `--model X --overlay wp3` silently runs baseline.
738
+ it("carries the overlay's review policy; no overlay ⇒ undefined", () => {
739
+ const { root, overlay } = makeRoots();
740
+ try {
741
+ // The overlay `makeRoots` writes has no `review:` — absent, not empty.
742
+ expect(modelsArm("m", "f", overlay).review).toBeUndefined();
743
+ expect(modelsArm("m", "f").review).toBeUndefined();
744
+ writeFileSync(join(overlay, "config.yaml"), "models:\n default: openai/gpt-5.4-mini\nreview:\n postsCheck: true\n analysis:\n enabled: true\n surveyPasses: 6\n");
745
+ const arm = modelsArm("m", "f", overlay);
746
+ expect(arm.review).toEqual({ postsCheck: true, analysis: { enabled: true, surveyPasses: 6 } });
747
+ // Config arms read the same block from the same file.
748
+ expect(configArm(root, overlay).review).toEqual(arm.review);
749
+ }
750
+ finally {
751
+ rmSync(root, { recursive: true, force: true });
752
+ }
753
+ });
665
754
  });
666
755
  describe("configArm — a deployment's per-step config drives selection", () => {
667
756
  it("prepare() patches ctx.models/variants and returns the merged maps + default", () => {
@@ -713,6 +802,68 @@ describe("Arm seam — model-selection adapters (arm.ts)", () => {
713
802
  rmSync(root, { recursive: true, force: true });
714
803
  }
715
804
  });
805
+ it("records fan-out BRANCH rows with the survey model, not models.default (money trap 12, deterministic-pr-levers.md)", () => {
806
+ const { root, overlay } = makeRoots();
807
+ try {
808
+ // A wp-shaped overlay: surveys pinned to haiku, review to sonnet.
809
+ writeFileSync(join(overlay, "config.yaml"), [
810
+ "models:",
811
+ " default: anthropic/claude-sonnet-4-6",
812
+ " review-survey: anthropic/claude-haiku-4-5",
813
+ " survey-tests: openai/gpt-5.5-mini",
814
+ " review: anthropic/claude-sonnet-4-6",
815
+ ].join("\n") + "\n");
816
+ const arm = configArm(root, overlay);
817
+ const phases = [
818
+ {
819
+ name: "survey",
820
+ model: "{{models.review-survey}}",
821
+ branches: [{ name: "contract" }, { name: "tests", model: "{{models.survey-tests}}" }],
822
+ },
823
+ { name: "adjudicate", model: "{{models.review-survey}}" },
824
+ ];
825
+ // The exact recording path run-instance.ts takes for a wf.phases row:
826
+ // ledger label → modelTemplateForRow → arm.recordPhaseModel.
827
+ const record = (label) => {
828
+ const { template, fallbackPhase } = modelTemplateForRow(phases, label);
829
+ return arm.recordPhaseModel(template, label, fallbackPhase);
830
+ };
831
+ // Branch rows inherit the parent's template — the measured bug recorded
832
+ // these as models.default (sonnet) while the sessions ran haiku.
833
+ expect(record("survey_branch_contract")).toBe("anthropic/claude-haiku-4-5");
834
+ expect(record("survey_branch_contract_retry")).toBe("anthropic/claude-haiku-4-5");
835
+ // A branch-level `model:` override wins over the parent's (forward-looking).
836
+ expect(record("survey_branch_tests")).toBe("openai/gpt-5.5-mini");
837
+ // The parent row itself is unchanged.
838
+ expect(record("survey")).toBe("anthropic/claude-haiku-4-5");
839
+ // A generic-loop iteration re-runs ITS OWN phase — `adjudicate_iter_1`
840
+ // is the adjudicate phase, template and all. The measured bug (say-side
841
+ // ladder, 2026-08-25): the row stamped models.default while the session
842
+ // envelope ran the template's answer.
843
+ expect(record("adjudicate_iter_1")).toBe("anthropic/claude-haiku-4-5");
844
+ expect(record("adjudicate_iter_1_check")).toBe("anthropic/claude-haiku-4-5");
845
+ }
846
+ finally {
847
+ rmSync(root, { recursive: true, force: true });
848
+ }
849
+ });
850
+ it("records the adjudicate {{#if}} fallback pair as a real model id, never template residue", () => {
851
+ const { root, overlay } = makeRoots();
852
+ try {
853
+ // The literal template from apps/server/workflows/pr-review.yaml.
854
+ const tpl = "{{#if models.review-adjudicate}}{{models.review-adjudicate}}{{/if}}" +
855
+ "{{#if !models.review-adjudicate}}{{models.review}}{{/if}}";
856
+ // review-adjudicate UNSET → falls back to models.review.
857
+ writeFileSync(join(overlay, "config.yaml"), "models:\n default: openai/gpt-5.4-mini\n review: anthropic/claude-sonnet-4-6\n");
858
+ expect(configArm(root, overlay).recordPhaseModel(tpl, "adjudicate")).toBe("anthropic/claude-sonnet-4-6");
859
+ // review-adjudicate SET → the pinned value.
860
+ writeFileSync(join(overlay, "config.yaml"), "models:\n default: openai/gpt-5.4-mini\n review: anthropic/claude-sonnet-4-6\n review-adjudicate: openai/gpt-5.5\n");
861
+ expect(configArm(root, overlay).recordPhaseModel(tpl, "adjudicate")).toBe("openai/gpt-5.5");
862
+ }
863
+ finally {
864
+ rmSync(root, { recursive: true, force: true });
865
+ }
866
+ });
716
867
  });
717
868
  describe("overlay guard — the process-global asset root (ADR 0001)", () => {
718
869
  it("throws when a second, different overlay activates while one is in use; release clears it", () => {
@@ -974,8 +1125,8 @@ describe("PR context — core's own projection, not a copy", () => {
974
1125
  body: "Bumps tiny-case.",
975
1126
  branch: "dependabot/npm_and_yarn/tiny-case-3",
976
1127
  };
977
- it("projects the CI evidence the fix prompts render", () => {
978
- const ctx = prContextPatch({
1128
+ it("projects the CI evidence the fix prompts render", async () => {
1129
+ const ctx = await prContextPatch({
979
1130
  ...args,
980
1131
  seed: {
981
1132
  checks_state: "failing",
@@ -1001,8 +1152,8 @@ describe("PR context — core's own projection, not a copy", () => {
1001
1152
  expect(String(ctx.verifyScript)).toContain(".git/");
1002
1153
  expect(String(ctx.notesFile)).toContain(".git/");
1003
1154
  });
1004
- it("carries the flaky promotion the third attempt turns on", () => {
1005
- const ctx = prContextPatch({
1155
+ it("carries the flaky promotion the third attempt turns on", async () => {
1156
+ const ctx = await prContextPatch({
1006
1157
  ...args,
1007
1158
  seed: { attempt: 3, flaky_deferrals: 2, checks_state: "failing" },
1008
1159
  });
@@ -1012,32 +1163,171 @@ describe("PR context — core's own projection, not a copy", () => {
1012
1163
  expect(ctx.flakyDeferrals).toBe(2);
1013
1164
  expect(ctx.maxFlakyDeferrals).toBe(defaultFixConfig().maxFlakyDeferrals);
1014
1165
  });
1015
- it("projects the merge gate's verdict AND its reason, from one decision", () => {
1016
- const green = prContextPatch({
1166
+ it("projects the merge gate's verdict AND its reason, from one decision", async () => {
1167
+ const green = await prContextPatch({
1017
1168
  ...args,
1018
1169
  seed: { checks_state: "passing", settled_check_count: 3 },
1019
1170
  });
1020
1171
  expect(green.mayMerge).toBe(true);
1021
1172
  expect(typeof green.mayMergeReason).toBe("string");
1022
- const pending = prContextPatch({ ...args, seed: { checks_state: "pending" } });
1173
+ const pending = await prContextPatch({ ...args, seed: { checks_state: "pending" } });
1023
1174
  expect(pending.mayMerge).toBe(false);
1024
1175
  // The reason is produced BY the decision — the panel, the log line and the
1025
1176
  // prompt are three renderings of it, never three prose variants.
1026
1177
  expect(String(pending.mayMergeReason).length).toBeGreaterThan(0);
1027
1178
  });
1028
- it("seeds the policy blocks a prompt renders, under the case's overrides", () => {
1029
- const ctx = prContextPatch({ ...args, seed: { dependencies: { autoMergeMaxImpact: "low" } } });
1179
+ it("seeds the policy blocks a prompt renders, under the case's overrides", async () => {
1180
+ const ctx = await prContextPatch({ ...args, seed: { dependencies: { autoMergeMaxImpact: "low" } } });
1030
1181
  expect(ctx.dependencies.autoMergeMaxImpact).toBe("low");
1031
1182
  // Everything the case did NOT override is the shipped default, so a case
1032
1183
  // states only what it is actually testing.
1033
1184
  expect(ctx.fix.maxAttempts).toBe(defaultFixConfig().maxAttempts);
1034
1185
  });
1035
- it("a case with no seed still gets a coherent snapshot", () => {
1036
- const ctx = prContextPatch(args);
1186
+ it("a case with no seed still gets a coherent snapshot", async () => {
1187
+ const ctx = await prContextPatch(args);
1037
1188
  expect(ctx.attempt).toBe(1);
1038
1189
  expect(ctx.ciSection).toBe("");
1039
1190
  expect(ctx.prNumber).toBe(412);
1040
1191
  });
1192
+ // The review evidence pipeline's ONE switch. `analysisEnabled` is what every
1193
+ // WP3 phase in `pr-review.yaml` gates on (`skip_if: "analysisEnabled != true"`),
1194
+ // and it comes from the ARM's overlay `config.yaml`, never from gold — so this
1195
+ // pair of assertions IS the two-arm comparison in miniature.
1196
+ it("the arm's review policy switches the evidence pipeline on, and its absence leaves it off", async () => {
1197
+ // baseline/config.yaml: a `review:` block with no `analysis` — the pipeline
1198
+ // stays off, with no per-case special-casing.
1199
+ const off = await prContextPatch({ ...args, review: { postsCheck: true } });
1200
+ expect(off.analysisEnabled).toBeUndefined();
1201
+ expect(off.prBody).toBeUndefined();
1202
+ // wp3/config.yaml: the same block plus `analysis.enabled`.
1203
+ const on = await prContextPatch({
1204
+ ...args,
1205
+ review: { postsCheck: true, analysis: { enabled: true, maxObligations: 40, surveyPasses: 6 } },
1206
+ });
1207
+ expect(on.analysisEnabled).toBe("true");
1208
+ expect(on.prBody).toBe(args.body);
1209
+ // No arm policy at all is byte-identical to a policy that names no analysis.
1210
+ expect((await prContextPatch(args)).analysisEnabled).toBeUndefined();
1211
+ });
1212
+ // The `spec` family's SECOND END. It went inert twice: once when `pr-review`
1213
+ // was excluded from `prContextPatch` entirely, and again after that exclusion
1214
+ // was lifted — because production reads the changed-file list from
1215
+ // `listPullRequestFilePaths` in `resolveSpecContext`, which the eval never
1216
+ // calls. `buildSpecObligations` then correctly refuses to emit a one-ended
1217
+ // seed (IRIS: a half mechanism measured WORSE than no seed), so all six
1218
+ // obligations vanish and the branch spends a model call saying it cannot work.
1219
+ //
1220
+ // Both failures were silent in the sense that mattered: the run went green.
1221
+ const withAnalysis = { enabled: true, maxObligations: 40, surveyPasses: 6 };
1222
+ const specBody = "### What & why\n\nCloses #1586.\n\n### Acceptance criteria\n\n- [ ] Silent login must not show the Google popup on a returning session\n";
1223
+ it("harness-derived changed files give the spec axis its second end", async () => {
1224
+ const ctx = await prContextPatch({
1225
+ ...args,
1226
+ body: specBody,
1227
+ review: { analysis: withAnalysis },
1228
+ changedFiles: ["src/auth/redirect-sign-in.ts", "src/auth/session.ts"],
1229
+ });
1230
+ const block = ctx.specObligations;
1231
+ expect(block).toBeDefined();
1232
+ // The obligation names both ends: the criterion verbatim, and a changed file.
1233
+ expect(block).toContain("Silent login must not show the Google popup");
1234
+ expect(block).toContain("src/auth/redirect-sign-in.ts");
1235
+ expect(block).not.toContain("changed-file list could not be read");
1236
+ });
1237
+ it("without them the axis degrades LOUDLY rather than silently passing", async () => {
1238
+ const ctx = await prContextPatch({ ...args, body: specBody, review: { analysis: withAnalysis } });
1239
+ // Locked decision 6: "could not look" and "looked and it is fine" are
1240
+ // different facts, so the block still renders and says which.
1241
+ expect(ctx.specObligations).toContain("changed-file list could not be read");
1242
+ expect(ctx.specObligations).toContain("That is NOT a pass");
1243
+ });
1244
+ it("a case seeding [] means 'changes nothing', not 'could not read'", async () => {
1245
+ const ctx = await prContextPatch({
1246
+ ...args,
1247
+ body: specBody,
1248
+ review: { analysis: withAnalysis },
1249
+ seed: { changed_files: [] },
1250
+ changedFiles: ["src/auth/session.ts"],
1251
+ });
1252
+ // The seed wins over the harness-derived set, and its degraded message is
1253
+ // the other one — a `||` fallback here would silently swap the two.
1254
+ expect(ctx.specObligations).toContain("changes no files");
1255
+ expect(ctx.specObligations).not.toContain("src/auth/session.ts");
1256
+ });
1257
+ it("the arm wins over a case's own review seed — gold can never flip an arm", async () => {
1258
+ const ctx = await prContextPatch({
1259
+ ...args,
1260
+ seed: { review: { analysis: { enabled: true } } },
1261
+ review: { analysis: { enabled: false } },
1262
+ });
1263
+ expect(ctx.analysisEnabled).toBeUndefined();
1264
+ });
1265
+ // BOTH ends, from the path production uses. The two above cover the
1266
+ // harness-derived fallback; this one covers what actually ships: a real
1267
+ // `GitHubClient` pointed at the fake, with core's own `resolveSpecContext`
1268
+ // doing the GraphQL + REST reads. Each end went missing separately, so the
1269
+ // assertion is deliberately on the CONJUNCTION — a criterion traceable to the
1270
+ // linked ISSUE (not merely the PR body) and a changed file, in one block.
1271
+ it("routes both ends through core's resolver against the fake (the production path)", async () => {
1272
+ const fake = await startFakeGitHub({
1273
+ owner: "acme",
1274
+ repo: "widgets",
1275
+ issues: [
1276
+ {
1277
+ number: 1586,
1278
+ title: "Authentication standardisation: silent sign-in",
1279
+ // The criterion lives ONLY here, never in the PR body — so a run that
1280
+ // read the body alone cannot produce it, and this test would fail.
1281
+ body: "Acceptance criteria\n\n- [ ] Backend enforces the nearform.com domain server-side (non-nearform accounts get 403)\n",
1282
+ },
1283
+ ],
1284
+ pulls: [
1285
+ {
1286
+ number: 412,
1287
+ title: "feat: silent sign-in",
1288
+ body: "### What & why\n\nCloses #1586. Adds the silent transport.\n",
1289
+ base_ref: "main",
1290
+ head_ref: "feat/sso",
1291
+ base_commit: "a".repeat(40),
1292
+ head_commit: "b".repeat(40),
1293
+ },
1294
+ ],
1295
+ });
1296
+ try {
1297
+ fake.setPullFiles(412, [
1298
+ { filename: "src/auth/session.ts", status: "modified", additions: 12, deletions: 2, changes: 14, sha: "c".repeat(40) },
1299
+ ]);
1300
+ const ctx = await prContextPatch({
1301
+ repo: "acme/widgets",
1302
+ prNumber: 412,
1303
+ title: "feat: silent sign-in",
1304
+ body: "### What & why\n\nCloses #1586. Adds the silent transport.\n",
1305
+ branch: "feat/sso",
1306
+ review: { analysis: withAnalysis },
1307
+ github: resolveReviewGitHubClient({ githubApiBaseUrl: fake.url }),
1308
+ });
1309
+ const block = ctx.specObligations;
1310
+ expect(block).toBeDefined();
1311
+ expect(block).toContain("nearform.com domain server-side");
1312
+ expect(block).toContain("src/auth/session.ts");
1313
+ expect(block).not.toContain("changed-file list could not be read");
1314
+ }
1315
+ finally {
1316
+ await fake.close();
1317
+ }
1318
+ });
1319
+ // GitHub's own boundary: a closing keyword links, a bare reference does not.
1320
+ // Getting this wrong would seed the axis with criteria from an issue the PR
1321
+ // never promised to satisfy — a confident obligation about the wrong "what
1322
+ // was asked", which is worse than no obligation.
1323
+ it("resolves closing keywords only, not bare references", () => {
1324
+ expect(closingIssueNumbers("Closes #12")).toEqual([12]);
1325
+ expect(closingIssueNumbers("fixes #7 and resolved #8")).toEqual([7, 8]);
1326
+ expect(closingIssueNumbers("Closes https://github.com/o/r/issues/99")).toEqual([99]);
1327
+ expect(closingIssueNumbers("Closes #5. Also closes #5.")).toEqual([5]);
1328
+ expect(closingIssueNumbers("See #12, part of #13, related to #14")).toEqual([]);
1329
+ expect(closingIssueNumbers("no links here")).toEqual([]);
1330
+ });
1041
1331
  });
1042
1332
  // ── Marker grading (fix / dependency-merge) ─────────────────────────────────
1043
1333
  describe("gradeMarkers", () => {