lastlight-evals 0.1.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 (41) hide show
  1. package/README.md +187 -0
  2. package/datasets/code-fix/instances.json +26 -0
  3. package/datasets/code-fix/repos/codefix__date-range-off-by-one/README.md +8 -0
  4. package/datasets/code-fix/repos/codefix__date-range-off-by-one/package.json +9 -0
  5. package/datasets/code-fix/repos/codefix__date-range-off-by-one/src/date-range.ts +25 -0
  6. package/datasets/code-fix/tests/codefix__date-range-off-by-one/date-range.test.ts +18 -0
  7. package/datasets/code-fix/tier.json +5 -0
  8. package/datasets/triage/instances.json +62 -0
  9. package/datasets/triage/tier.json +5 -0
  10. package/dist/bootstrap.js +46 -0
  11. package/dist/bootstrap.js.map +1 -0
  12. package/dist/discovery.js +100 -0
  13. package/dist/discovery.js.map +1 -0
  14. package/dist/env.js +109 -0
  15. package/dist/env.js.map +1 -0
  16. package/dist/fake-github.js +249 -0
  17. package/dist/fake-github.js.map +1 -0
  18. package/dist/grade.js +134 -0
  19. package/dist/grade.js.map +1 -0
  20. package/dist/html-report.js +325 -0
  21. package/dist/html-report.js.map +1 -0
  22. package/dist/init.js +122 -0
  23. package/dist/init.js.map +1 -0
  24. package/dist/mechanism.test.js +108 -0
  25. package/dist/mechanism.test.js.map +1 -0
  26. package/dist/metrics.js +86 -0
  27. package/dist/metrics.js.map +1 -0
  28. package/dist/paths.js +33 -0
  29. package/dist/paths.js.map +1 -0
  30. package/dist/report.js +137 -0
  31. package/dist/report.js.map +1 -0
  32. package/dist/run-instance.js +203 -0
  33. package/dist/run-instance.js.map +1 -0
  34. package/dist/run.js +476 -0
  35. package/dist/run.js.map +1 -0
  36. package/dist/schema.js +15 -0
  37. package/dist/schema.js.map +1 -0
  38. package/dist/seed.js +49 -0
  39. package/dist/seed.js.map +1 -0
  40. package/models.json +17 -0
  41. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # lastlight-evals
2
+
3
+ A standalone, **SWE-bench-compatible** eval harness for [Last
4
+ Light](https://github.com/cliftonc/lastlight) workflows. It drives the **real**
5
+ production workflows (`issue-triage`, `build`, …) — their actual prompts and
6
+ skills — against a **mocked GitHub**, grades the result deterministically, and
7
+ prints a model-comparison scorecard. It answers "what do we expect from the
8
+ agent, and which model does it best?"
9
+
10
+ Nothing here talks to real GitHub. The agent's `github_*` tool calls are served
11
+ by an in-process fake (seeded + recording), and `git push` goes to a local bare
12
+ repo. The only deviations from production are the two we can't do unattended:
13
+ approval gates are disabled and outward side-effects are mocked.
14
+
15
+ ```
16
+ instance (SWE-bench shape)
17
+ │
18
+ ├─ start fake GitHub (seeded with the issue, records every mutation)
19
+ ├─ (code-fix) seed workspace: fixture repo @ base_commit + local bare origin
20
+ ├─ load the REAL workflow YAML (issue-triage / build / …) from lastlight core
21
+ ├─ runWorkflow(sandbox:"none", githubApiBaseUrl→fake, approvalConfig:{})
22
+ └─ grade:
23
+ • execution — apply held-out tests, run them → FAIL_TO_PASS / PASS_TO_PASS
24
+ • behavioral — recorded GitHub calls vs the instance's expectations
25
+ ```
26
+
27
+ > Working on the harness itself? See `CLAUDE.md` for the seams and invariants
28
+ > (the base-URL mock, static-token mode, the no-clone seeding trick, the
29
+ > asset-bootstrap footgun, the metrics drain).
30
+
31
+ ## How it depends on Last Light
32
+
33
+ `lastlight-evals` is a thin CLI on top of the `lastlight` npm package. It imports
34
+ exactly four things from core's public `lastlight/evals` barrel —
35
+ `getWorkflow`, `runWorkflow`, `ExecutorConfig`, `TemplateContext` — plus the
36
+ `gh`-repo bootstrap helpers used by `init`. Core ships its `workflows/`,
37
+ `skills/`, and `agent-context/` in the package, so the evals run the same assets
38
+ core does.
39
+
40
+ ```bash
41
+ npm install # installs `lastlight` (and agentic-pi as a peer)
42
+ ```
43
+
44
+ **Local development against an un-published core.** Until the matching
45
+ `lastlight` version is on npm — or whenever you want to eval your working-tree
46
+ core — link a checkout:
47
+
48
+ ```bash
49
+ cd ../lastlight && npm run build && npm link
50
+ cd ../lastlight-evals && npm link lastlight
51
+ ```
52
+
53
+ Or set `LASTLIGHT_CORE_DIR=/path/to/lastlight` to point just the **asset roots**
54
+ (workflows/skills/agent-context — the bulk of what `lastlight server update`
55
+ ships) at a checkout without touching the npm dep. (The runner *code* still
56
+ comes from `node_modules/lastlight`; use `npm link` to exercise working-tree
57
+ engine code too.)
58
+
59
+ ## Run it
60
+
61
+ ```bash
62
+ # no tier args → interactively pick which tiers to run (one or all).
63
+ # Non-interactive (CI / piped) falls back to the cheapest default.
64
+ lastlight-evals run # (or: npm run eval)
65
+
66
+ # name tiers explicitly to skip the prompt
67
+ lastlight-evals run triage
68
+ lastlight-evals run code-fix # the full build cycle (heavy)
69
+ lastlight-evals run triage code-fix # both → combined tabbed report
70
+
71
+ # cross-vendor comparison (OpenAI + Anthropic + open source) — see models.json.
72
+ # Families run in PARALLEL; serial within a family. Force serial with --serial.
73
+ lastlight-evals run --compare
74
+
75
+ # pick ONE model (fuzzy-matched against models.json id/label)
76
+ lastlight-evals run triage --model haiku
77
+ lastlight-evals run triage --model glm,deepseek # a comma-list also works
78
+
79
+ # repeat each case N times; verdicts WORST-case, cost/tokens/latency MEAN
80
+ lastlight-evals run triage --runs 3
81
+
82
+ # run against an overlay repo's OWN workflows + datasets (see below)
83
+ lastlight-evals run --overlay ~/work/lastlight-instance
84
+
85
+ # add your own datasets dir without an overlay
86
+ lastlight-evals run --datasets ~/my-evals/datasets
87
+
88
+ # ad-hoc model set / focus one instance / no browser
89
+ EVAL_MODELS="openai/gpt-5.5,anthropic/claude-sonnet-4-6" lastlight-evals run
90
+ EVAL_INSTANCE=off-by-one lastlight-evals run code-fix
91
+ lastlight-evals run triage --no-open
92
+ ```
93
+
94
+ The runner opens `index.html` and **rewrites it after every run** (auto-refresh,
95
+ preserving the active tab + scroll), so you watch the scorecard fill in live.
96
+ Output lands under `./eval-results/<tiers>/` (override with `LASTLIGHT_EVALS_OUT`):
97
+
98
+ - `index.html` — styled scorecard.
99
+ - `scorecard.json` — structured roll-up per model.
100
+ - `predictions.jsonl` — SWE-bench predictions shape.
101
+
102
+ Needs a provider key (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` /
103
+ `FIREWORKS_API_KEY` / `OPENROUTER_API_KEY`) in the environment or a cwd `.env`.
104
+ The runner exits non-zero **only** if the harness itself errors — a weak model
105
+ scoring poorly is the measurement, not a build failure.
106
+
107
+ ## Your own workflows + datasets (overlays)
108
+
109
+ An **overlay** is a directory (often its own repo, like `lastlight-instance`)
110
+ that carries its own `workflows/` / `skills/` / `agent-context/` (which shadow
111
+ the core built-ins by name) and its own `evals/datasets/`. One flag wires both:
112
+
113
+ ```bash
114
+ lastlight-evals run --overlay ~/work/lastlight-instance # or LASTLIGHT_OVERLAY_DIR
115
+ ```
116
+
117
+ - Overlay **workflows/skills** are layered over core via core's asset overlay
118
+ (same mechanism the production harness uses).
119
+ - Overlay **datasets** are discovered at `<overlay>/evals/datasets/<tier>/`, and
120
+ shadow built-in tiers of the same name.
121
+ - An overlay **`evals/models.json`** is picked up automatically (or pass
122
+ `--models-file`).
123
+
124
+ ### `lastlight-evals init [dir]` — scaffold a fresh overlay+evals repo
125
+
126
+ ```bash
127
+ lastlight-evals init my-evals
128
+ cd my-evals && lastlight-evals run --overlay .
129
+ ```
130
+
131
+ Scaffolds `workflows/` `skills/` `agent-context/` (empty, to fill in),
132
+ `evals/datasets/` + `evals/models.json` (seeded from the shipped samples),
133
+ `config.yaml`, and a `.gitignore`/`README`, then offers to `git init` + create a
134
+ private GitHub repo via `gh` (reusing core's `lastlight server setup` flow).
135
+
136
+ ## Datasets & tiers
137
+
138
+ A **tier** is a directory containing `instances.json` (+ an optional `tier.json`
139
+ declaring its `defaultWorkflow`). Tiers are discovered from three roots, merged
140
+ by name with **overlay > user (`--datasets`) > built-in** precedence:
141
+
142
+ - **built-in** (shipped here): `triage` → `issue-triage`, `code-fix` → `build`.
143
+ - **user**: `--datasets <dir>` / `LASTLIGHT_EVALS_DATASETS`.
144
+ - **overlay**: `<overlay>/evals/datasets/*`.
145
+
146
+ ### Add a case
147
+
148
+ **Triage** — append to a tier's `instances.json`:
149
+
150
+ ```json
151
+ {
152
+ "instance_id": "triage__my-case",
153
+ "repo": "lastlight-evals/widget",
154
+ "workflow": "issue-triage",
155
+ "problem_statement": "short title",
156
+ "issue": { "number": 110, "title": "…", "body": "…", "labels": [] },
157
+ "triage_gold": { "category": "bug", "state": "ready-for-agent" },
158
+ "expect_github": { "labels_added": ["bug"] }
159
+ }
160
+ ```
161
+
162
+ **Code-fix** — three things keyed by `instance_id`, all under the tier dir:
163
+
164
+ ```
165
+ <tier>/instances.json # the SweBenchInstance (FAIL_TO_PASS / PASS_TO_PASS)
166
+ <tier>/repos/<id>/ # fixture repo at base_commit (NO held-out tests)
167
+ <tier>/tests/<id>/ # held-out test files, copied in at grade time
168
+ ```
169
+
170
+ A new tier just needs a directory with an `instances.json` and a `tier.json`
171
+ (`{ "name", "defaultWorkflow", "description" }`); per-instance `workflow` wins
172
+ when present.
173
+
174
+ ## Models (`models.json`)
175
+
176
+ - `default` — the single model `run` uses.
177
+ - `compare` — the cross-vendor set `--compare` fans out over. Each entry has an
178
+ `id` (the agentic-pi/pi-ai `provider/model` spec), a `label`, and an `envKey`.
179
+ **An entry only runs if its `envKey` is present**, so the compare set
180
+ auto-trims to whatever keys you have.
181
+
182
+ ## Roadmap
183
+
184
+ - **`lastlight-evals extract <owner>/<repo>#<n>`** — generate eval cases from
185
+ GitHub historical issues/PRs (issue → fixture, merged PR → held-out tests).
186
+ - Docker-backed runs; real SWE-bench Lite ingestion; per-fixture test runners.
187
+ - LLM-as-judge stays out by design — grading is deterministic.
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "instance_id": "codefix__date-range-off-by-one",
4
+ "repo": "lastlight-evals/daterange",
5
+ "workflow": "build",
6
+ "base_commit": "0000000000000000000000000000000000000000",
7
+ "problem_statement": "inclusiveDayCount is off by one\n\n`inclusiveDayCount(start, end)` should return the number of whole days in the INCLUSIVE range [start, end], but it returns one too few.\n\nExample:\n- `inclusiveDayCount(2026-01-01, 2026-01-01)` returns 0, should be 1.\n- `inclusiveDayCount(2026-01-01, 2026-01-03)` returns 2, should be 3.\n\nThe fix is in `src/date-range.ts`. `eachDay` already enumerates the inclusive range correctly, so don't change it.",
8
+ "issue": {
9
+ "number": 201,
10
+ "title": "inclusiveDayCount is off by one",
11
+ "body": "`inclusiveDayCount(start, end)` should count days in the INCLUSIVE range but returns one too few. `inclusiveDayCount(2026-01-01, 2026-01-01)` returns 0 (should be 1). The bug is in `src/date-range.ts`.",
12
+ "labels": ["bug", "ready-for-agent"],
13
+ "user": "reporter"
14
+ },
15
+ "FAIL_TO_PASS": [
16
+ "inclusive single day counts as one",
17
+ "inclusive three day span counts as three"
18
+ ],
19
+ "PASS_TO_PASS": [
20
+ "eachDay still lists every inclusive day"
21
+ ],
22
+ "expect_github": {
23
+ "pr_opened": { "base": "main", "head_is_branch": true }
24
+ }
25
+ }
26
+ ]
@@ -0,0 +1,8 @@
1
+ # daterange
2
+
3
+ A tiny inclusive date-range utility.
4
+
5
+ - `inclusiveDayCount(start, end)` — number of whole days in the inclusive range.
6
+ - `eachDay(start, end)` — every `YYYY-MM-DD` in the inclusive range.
7
+
8
+ Run the tests with `npm test` (`node --test`).
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "daterange",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Tiny date-range utility.",
6
+ "scripts": {
7
+ "test": "node --test --experimental-strip-types"
8
+ }
9
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Date-range helpers.
3
+ */
4
+
5
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
6
+
7
+ /**
8
+ * Number of whole days covered by the INCLUSIVE range [start, end].
9
+ *
10
+ * e.g. 2026-01-01 .. 2026-01-01 covers 1 day; 2026-01-01 .. 2026-01-03 covers 3.
11
+ */
12
+ export function inclusiveDayCount(start: Date, end: Date): number {
13
+ const ms = end.getTime() - start.getTime();
14
+ // BUG: drops the inclusive end day — should add 1.
15
+ return Math.round(ms / MS_PER_DAY);
16
+ }
17
+
18
+ /** All YYYY-MM-DD dates in the inclusive range, in order. */
19
+ export function eachDay(start: Date, end: Date): string[] {
20
+ const out: string[] = [];
21
+ for (let t = start.getTime(); t <= end.getTime(); t += MS_PER_DAY) {
22
+ out.push(new Date(t).toISOString().slice(0, 10));
23
+ }
24
+ return out;
25
+ }
@@ -0,0 +1,18 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { inclusiveDayCount, eachDay } from "./src/date-range.ts";
5
+
6
+ const d = (s: string): Date => new Date(`${s}T00:00:00Z`);
7
+
8
+ test("inclusive single day counts as one", () => {
9
+ assert.equal(inclusiveDayCount(d("2026-01-01"), d("2026-01-01")), 1);
10
+ });
11
+
12
+ test("inclusive three day span counts as three", () => {
13
+ assert.equal(inclusiveDayCount(d("2026-01-01"), d("2026-01-03")), 3);
14
+ });
15
+
16
+ test("eachDay still lists every inclusive day", () => {
17
+ assert.deepEqual(eachDay(d("2026-01-01"), d("2026-01-03")), ["2026-01-01", "2026-01-02", "2026-01-03"]);
18
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "code-fix",
3
+ "defaultWorkflow": "build",
4
+ "description": "Full Architect→Executor→Reviewer build cycle on a seeded fixture repo (heavy)."
5
+ }
@@ -0,0 +1,62 @@
1
+ [
2
+ {
3
+ "instance_id": "triage__bug-with-repro-ready",
4
+ "repo": "lastlight-evals/widget",
5
+ "workflow": "issue-triage",
6
+ "problem_statement": "Crash on empty config file",
7
+ "issue": {
8
+ "number": 101,
9
+ "title": "Crash on empty config file",
10
+ "body": "## Steps to reproduce\n1. Create an empty `config.yaml`\n2. Run `widget start`\n\n## Expected\nIt should start with defaults.\n\n## Actual\nIt throws `TypeError: Cannot read properties of undefined (reading 'port')` at `src/config.ts:42`.\n\nEnvironment: widget v2.1.0, Node 20. This reproduces 100% of the time.",
11
+ "labels": [],
12
+ "user": "alice"
13
+ },
14
+ "triage_gold": { "category": "bug", "state": "ready-for-agent" },
15
+ "expect_github": { "labels_added": ["bug"] }
16
+ },
17
+ {
18
+ "instance_id": "triage__bug-no-repro-needs-info",
19
+ "repo": "lastlight-evals/widget",
20
+ "workflow": "issue-triage",
21
+ "problem_statement": "It's broken",
22
+ "issue": {
23
+ "number": 102,
24
+ "title": "It doesn't work",
25
+ "body": "I tried to use the thing and it didn't work. Please fix.",
26
+ "labels": [],
27
+ "user": "bob"
28
+ },
29
+ "triage_gold": { "state": "needs-info" },
30
+ "expect_github": { "labels_added": ["needs-info"], "labels_absent": ["ready-for-agent"], "comment_matches": "need|provide|repro|version|steps" }
31
+ },
32
+ {
33
+ "instance_id": "triage__feature-well-specified",
34
+ "repo": "lastlight-evals/widget",
35
+ "workflow": "issue-triage",
36
+ "problem_statement": "Add --json flag to widget status",
37
+ "issue": {
38
+ "number": 103,
39
+ "title": "Add a --json output flag to `widget status`",
40
+ "body": "## Use case\nWe script around `widget status` in CI and parsing the human table is brittle.\n\n## Proposal\nAdd a `--json` flag that prints the same status as a JSON object: `{ \"state\": \"running\", \"uptimeSec\": 1234 }`.\n\nThe table output stays the default. This is additive and well-scoped — the status data already exists in `src/status.ts`.",
41
+ "labels": [],
42
+ "user": "carol"
43
+ },
44
+ "triage_gold": { "category": "enhancement" },
45
+ "expect_github": { "labels_added": ["enhancement"] }
46
+ },
47
+ {
48
+ "instance_id": "triage__pure-question",
49
+ "repo": "lastlight-evals/widget",
50
+ "workflow": "issue-triage",
51
+ "problem_statement": "How do I change the port?",
52
+ "issue": {
53
+ "number": 104,
54
+ "title": "How do I change the listening port?",
55
+ "body": "Quick question — what's the right way to change the port widget listens on? Is it an env var or a config key? Not a bug, just want to understand the options.",
56
+ "labels": [],
57
+ "user": "dave"
58
+ },
59
+ "triage_gold": { "category": "question" },
60
+ "expect_github": { "labels_added": ["question"], "labels_absent": ["ready-for-agent", "ready-for-human"] }
61
+ }
62
+ ]
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "triage",
3
+ "defaultWorkflow": "issue-triage",
4
+ "description": "Issue triage — labels, category, and state-role accuracy (cheap)."
5
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Core-asset bootstrap — the one thing an out-of-process eval harness MUST do
3
+ * that the in-repo version never had to.
4
+ *
5
+ * Last Light's `getWorkflow` resolves built-in workflows/skills/agent-context
6
+ * from `DEFAULT_ROOT = resolve(".")` (the process cwd). In-repo that happened to
7
+ * be the core checkout, so it "just worked". As a separate package our cwd is
8
+ * wherever the user invoked the CLI, so we MUST tell core where its assets live
9
+ * by calling `configureWorkflowAssets({ builtInRoot })` BEFORE any
10
+ * `getWorkflow`/`runWorkflow`. Forget this and workflows silently fail to
11
+ * resolve. {@link bootstrapAssets} is therefore the first call in `run`.
12
+ */
13
+ import { createRequire } from "node:module";
14
+ import { dirname } from "node:path";
15
+ import { configureWorkflowAssets } from "lastlight/evals";
16
+ /**
17
+ * The lastlight PACKAGE ROOT — the dir holding `workflows/`, `skills/`,
18
+ * `agent-context/`, `config/`, `dist/`.
19
+ *
20
+ * Default: resolve the installed `lastlight` package (a normal npm dependency).
21
+ *
22
+ * Override: `LASTLIGHT_CORE_DIR` repoints the ASSET roots at a local core
23
+ * checkout, so you can eval un-published workflow/prompt/skill edits — the bulk
24
+ * of what `lastlight server update` ships — without bumping the npm dep. Caveat:
25
+ * the imported runner CODE still comes from `node_modules/lastlight`; to also
26
+ * exercise working-tree engine code, `npm link lastlight` (or a `file:` dep).
27
+ */
28
+ export function resolveCoreRoot() {
29
+ const override = process.env.LASTLIGHT_CORE_DIR?.trim();
30
+ if (override)
31
+ return override;
32
+ const require = createRequire(import.meta.url);
33
+ return dirname(require.resolve("lastlight/package.json"));
34
+ }
35
+ /**
36
+ * Point core's asset layers at the resolved core root (+ optional overlay).
37
+ * MUST run before the first workflow access. An overlay's workflows/skills/
38
+ * agent-context shadow the built-ins by logical name — the same precedence the
39
+ * production harness uses via `LASTLIGHT_OVERLAY_DIR`.
40
+ */
41
+ export function bootstrapAssets(opts = {}) {
42
+ const builtInRoot = resolveCoreRoot();
43
+ configureWorkflowAssets({ builtInRoot, overlayRoot: opts.overlayDir });
44
+ return { builtInRoot, overlayDir: opts.overlayDir };
45
+ }
46
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,CAAC;IACxD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/C,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAOD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAgC,EAAE;IAChE,MAAM,WAAW,GAAG,eAAe,EAAE,CAAC;IACtC,uBAAuB,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;IACvE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC;AACtD,CAAC"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Tier/dataset discovery — replaces the old hardcoded `TIERS` map.
3
+ *
4
+ * A **tier** is simply a directory that contains an `instances.json`
5
+ * (alongside, for code-fix tiers, `repos/<id>/` fixtures and `tests/<id>/`
6
+ * held-out tests). Tiers are discovered from up to three roots and merged by
7
+ * name with **overlay > user > built-in** precedence — the same shadow-by-name
8
+ * model core uses for workflow assets, so a deployment can override a shipped
9
+ * tier or add entirely new ones without touching this package.
10
+ *
11
+ * 1. built-in — `<pkg>/datasets/*` (the shipped "our" samples)
12
+ * 2. user — `--datasets <dir>` / `LASTLIGHT_EVALS_DATASETS`
13
+ * 3. overlay — `<overlayDir>/evals/datasets/*` (a bootstrapped repo)
14
+ *
15
+ * Which workflow a tier runs is resolved per-instance first (the existing,
16
+ * unchanged `instance.workflow` field), then from an optional per-tier
17
+ * `tier.json` (`{ name?, defaultWorkflow, description? }`). Shipping a
18
+ * `tier.json` next to the two built-in datasets turns the former hardcoded map
19
+ * into data with zero changes to the instance files.
20
+ */
21
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
22
+ import { join, resolve } from "node:path";
23
+ /** Scan one root for immediate subdirs that hold an `instances.json`. */
24
+ function scanRoot(root, source) {
25
+ if (!root || !existsSync(root))
26
+ return [];
27
+ const out = [];
28
+ for (const entry of readdirSync(root)) {
29
+ const dir = join(root, entry);
30
+ let isDir = false;
31
+ try {
32
+ isDir = statSync(dir).isDirectory();
33
+ }
34
+ catch {
35
+ continue;
36
+ }
37
+ if (!isDir)
38
+ continue;
39
+ const instancesPath = join(dir, "instances.json");
40
+ if (!existsSync(instancesPath))
41
+ continue;
42
+ let manifest = {};
43
+ const manifestPath = join(dir, "tier.json");
44
+ if (existsSync(manifestPath)) {
45
+ try {
46
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
47
+ }
48
+ catch {
49
+ /* a malformed tier.json just means no defaultWorkflow — not fatal */
50
+ }
51
+ }
52
+ out.push({
53
+ name: manifest.name || entry,
54
+ source,
55
+ root: dir,
56
+ instancesPath,
57
+ defaultWorkflow: manifest.defaultWorkflow,
58
+ description: manifest.description,
59
+ });
60
+ }
61
+ return out;
62
+ }
63
+ /**
64
+ * Discover all tiers across the roots, overlay-wins by name. Insertion order
65
+ * (built-in first) is preserved for stable display; later sources overwrite the
66
+ * map value for a shared name.
67
+ */
68
+ export function discoverTiers(opts) {
69
+ const tiers = new Map();
70
+ const add = (list) => {
71
+ for (const t of list)
72
+ tiers.set(t.name, t);
73
+ };
74
+ add(scanRoot(opts.builtinRoot, "builtin"));
75
+ if (opts.userDatasetsDir)
76
+ add(scanRoot(resolve(opts.userDatasetsDir), "user"));
77
+ if (opts.overlayDir)
78
+ add(scanRoot(join(resolve(opts.overlayDir), "evals", "datasets"), "overlay"));
79
+ return tiers;
80
+ }
81
+ /** Load a tier's instances, with the optional `EVAL_INSTANCE` substring filter. */
82
+ export function loadInstances(tier) {
83
+ const all = JSON.parse(readFileSync(tier.instancesPath, "utf8"));
84
+ const filter = process.env.EVAL_INSTANCE?.trim();
85
+ return filter ? all.filter((i) => i.instance_id.includes(filter)) : all;
86
+ }
87
+ /**
88
+ * Resolve the workflow to run for one instance: explicit `instance.workflow`
89
+ * wins, else the tier's `defaultWorkflow`. Throws if neither is set — a loud
90
+ * misconfiguration rather than a silent wrong default.
91
+ */
92
+ export function workflowFor(tier, inst) {
93
+ const wf = inst.workflow ?? tier.defaultWorkflow;
94
+ if (!wf) {
95
+ throw new Error(`Tier "${tier.name}": instance "${inst.instance_id}" has no \`workflow\` and the ` +
96
+ `tier has no \`defaultWorkflow\` (add one to ${join(tier.root, "tier.json")}).`);
97
+ }
98
+ return wf;
99
+ }
100
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
package/dist/env.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Minimal `.env` loader for the eval harness (no dotenv dependency).
3
+ *
4
+ * Reads the repo-root `.env` (KEY=VALUE lines) and sets any keys not already
5
+ * present in `process.env`. The provider key (OPENAI_API_KEY / ANTHROPIC_…)
6
+ * lives there for local dev; the eval needs it to make real model calls.
7
+ */
8
+ import { readFileSync, existsSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { builtinModelsPath } from "./paths.js";
11
+ let loaded = false;
12
+ /**
13
+ * Active model-registry path. Defaults to the shipped `<pkg>/models.json`; a
14
+ * user/overlay can point it elsewhere via {@link setModelsPath} (run.ts wires
15
+ * `--models-file` / `<overlay>/evals/models.json`) so a deployment can ship its
16
+ * own model set the same way it ships its own datasets.
17
+ */
18
+ let modelsPath = builtinModelsPath();
19
+ /** Override the model-registry path (call once at CLI startup, before reads). */
20
+ export function setModelsPath(path) {
21
+ modelsPath = resolve(path);
22
+ }
23
+ export function loadDotEnv(root = process.cwd()) {
24
+ if (loaded)
25
+ return;
26
+ loaded = true;
27
+ const path = join(root, ".env");
28
+ if (!existsSync(path))
29
+ return;
30
+ for (const raw of readFileSync(path, "utf8").split("\n")) {
31
+ const line = raw.trim();
32
+ if (!line || line.startsWith("#"))
33
+ continue;
34
+ const eq = line.indexOf("=");
35
+ if (eq <= 0)
36
+ continue;
37
+ const key = line.slice(0, eq).trim();
38
+ let val = line.slice(eq + 1).trim();
39
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
40
+ val = val.slice(1, -1);
41
+ }
42
+ if (process.env[key] === undefined)
43
+ process.env[key] = val;
44
+ }
45
+ }
46
+ /** True if at least one provider key the eval can use is set. */
47
+ export function hasProviderKey() {
48
+ return Boolean(process.env.OPENAI_API_KEY ||
49
+ process.env.ANTHROPIC_API_KEY ||
50
+ process.env.OPENROUTER_API_KEY ||
51
+ process.env.FIREWORKS_API_KEY ||
52
+ process.env.DEEPSEEK_API_KEY);
53
+ }
54
+ function modelsConfig() {
55
+ return JSON.parse(readFileSync(modelsPath, "utf8"));
56
+ }
57
+ /** Single-model run: EVAL_MODELS override, else the `default` from models.json. */
58
+ export function evalModels() {
59
+ const raw = process.env.EVAL_MODELS?.trim();
60
+ if (raw)
61
+ return raw.split(",").map((m) => m.trim()).filter(Boolean);
62
+ return [modelsConfig().default];
63
+ }
64
+ /**
65
+ * Cross-vendor comparison set from `models.json`, filtered to the models whose
66
+ * provider key is actually present — so `npm run eval:compare` runs whatever
67
+ * you have keys for and silently skips the rest.
68
+ */
69
+ export function compareModels() {
70
+ return modelsConfig().compare.filter((m) => !m.envKey || Boolean(process.env[m.envKey]));
71
+ }
72
+ /** id → display label, from models.json (for the scorecard). */
73
+ export function modelLabels() {
74
+ const out = {};
75
+ for (const m of modelsConfig().compare)
76
+ if (m.label)
77
+ out[m.id] = m.label;
78
+ return out;
79
+ }
80
+ /** Provider family (env-key) inferred from a `provider/model` id prefix. */
81
+ export function familyForId(id) {
82
+ const provider = id.split("/")[0]?.toLowerCase() ?? "";
83
+ const map = {
84
+ openai: "OPENAI_API_KEY",
85
+ anthropic: "ANTHROPIC_API_KEY",
86
+ fireworks: "FIREWORKS_API_KEY",
87
+ openrouter: "OPENROUTER_API_KEY",
88
+ deepseek: "DEEPSEEK_API_KEY",
89
+ };
90
+ return map[provider] ?? "default";
91
+ }
92
+ /**
93
+ * Resolve a user-supplied model token (from `--model`) to a concrete entry.
94
+ * Matches a models.json `compare` id exactly, else a case-insensitive substring
95
+ * of its id or label (so `--model haiku` works); failing that, treats the token
96
+ * as a raw `provider/model` id and infers the family from its prefix. Not
97
+ * key-gated — `--model` is an explicit request, so we run it even if the key
98
+ * check would otherwise skip it (the provider call surfaces a missing key).
99
+ */
100
+ export function resolveModel(token) {
101
+ const all = modelsConfig().compare;
102
+ const v = token.toLowerCase();
103
+ const hit = all.find((m) => m.id === token) ??
104
+ all.find((m) => m.id.toLowerCase().includes(v) || m.label?.toLowerCase().includes(v));
105
+ if (hit)
106
+ return { ...hit, family: hit.envKey ?? familyForId(hit.id) };
107
+ return { id: token, family: familyForId(token) };
108
+ }
109
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"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"}