solumbe Evals¶
solumbe ships these evals. They answer different questions and must not be confused:
| Eval | Entry point | Question it answers |
|---|---|---|
| Token-savings | runEval(repoPath) |
Is the context pack smaller than a naive file dump? |
| Accuracy | runRetrievalEval() |
Is the context pack right, meaning does it surface the files an agent needs, and does the risk classifier label paths/queries correctly? |
| Harness execution | runHarnessExecutionEval() |
Do the encoded setup and validation commands Solumbe inferred actually run? |
| Gate effectiveness | runGateEffectivenessEval() |
Does the real local gate allow a valid change and block known-bad changes for the expected deterministic reason? |
| History replay | runHistoryEval() |
On a real repository's own recent commits, does the pack name the files each commit changed? |
The token-savings eval is necessary but not sufficient: a randomly-ranked pack saves exactly as many tokens as a perfect one. Accuracy, harness execution, and gate effectiveness close distinct gaps in that evidence.
What the corpus measures¶
The corpus lives at evals/corpus.json and contains retrieval, risk, harness
execution, and gate-effectiveness cases.
Retrieval cases¶
Each retrieval case runs generateContextPack(query) against a fixture repo and
scores whether the labeled files land in primaryFiles:
- precision@k: of the files the pack returned (capped at
k), how many were relevant. The denominator is what was returned, notk, because solumbe packs are intentionally tiny (often 1–3 files); dividing a single correct hit by a fixedk=5would score a perfect one-file pack at 0.2 and punish concision. - recall@k: of the relevant files, how many appeared in the top
k. - MRR: reciprocal rank of the first relevant file (1.0 if the top hit is
relevant, 0 if none appear in the top
k).
A case passes when every expectedPrimary file is in the top k and, when
expectedAnyOf is given, at least one of those files appears anywhere in the
pack (primaryFiles or relatedFiles, since expectedAnyOf encodes
related-file and route↔client pairing expectations, which the engine surfaces
under relatedFiles).
Pure-fallback cases (expectedPrimary: []) assert only that the pack falls back
gracefully to a non-empty set, so their precision/recall/MRR are reported as
null and excluded from the aggregate.
The fixtures exercise: cross-language naming (TypeScript + Python + Go),
plural/singular folding (users service → users_service.py), route-shaped
queries, error-message-shaped queries that should fall back gracefully, and
multi-repo route↔client pairing.
Two fixtures are whole repositories in one non-JavaScript language:
python-orders-api (a package with absolute and relative imports and a
tests/ directory) and go-inventory-api (a go.mod module with
cmd/ and internal/ packages and a _test.go file). Each has four
cases that rank a file from its name or a symbol, and importer cases that
expect a file's only caller in the pack when the two share no name.
Known gaps¶
A retrieval or gate case can carry "knownGap": true. It records a miss
before its fix exists: the case runs like any other and the report lists it
under Known gaps as "still open", or as "now passes; remove its knownGap
marker" once a change closes it. It never counts toward the floors, the pass
counts or the exit code, so a miss can be written down the day it is found
without turning CI red. Remove the marker in the change that fixes it.
Open gap (2026-10-22): on node-upgrade-api, a Node 22 move should rank
.nvmrc. It needs version pins read as runtime requests; a Node version is not
a dependency request. The four dependency gaps recorded with it on 2026-10-20
closed when the code map began listing manifests and lockfiles (see
src/lib/dependency-request.js).
Dependency requests¶
A request that upgrades, bumps, audits or patches dependencies owns the
manifests and lockfiles, which are not source files. The
dependency-advisory-api, -web and -mobile fixtures carry the 2026-10-22
dogfood misses: a hyphenated package (shell-quote) split into words that
matched an unrelated module, a package (handlebars) that pulled in every
template, package and upgrade ranking components, and lockfiles never
ranked. Their cases expect the lockfile (and a manifest that declares a named
package) as primary; package-manager config and a manifest a transitive fix may
not touch are expected in Related Files.
Gaps from the history replay (2026-10-22), each a real miss
rebuilt as a synthetic fixture and confirmed open on develop and on the
dependency-ranking change (#325):
history-nav-protection-script(nav-protection-web): a request that names nav protection rankspublic/scripts/nav-protection.jssixth, behind the partner, domain and navigation components.history-solumbe-pre-commit-hook(solumbe-hook-web): a solumbe-managed.githooks/pre-commitis only a related file for "run solumbe gate on staged tree in pre-commit"; components named Gate lead.history-feature-flag-environment-config(feature-flag-api): switching a feature on in production should change that environment's flag file, whose keys name the mechanism (rag_answers_enabled), not the feature.
A fourth replay miss, files under src/ai/rag/ crowded out by large event,
booking and email services, did not reproduce on a fixture of a dozen files:
the RAG files ranked in the top five. It needs a repository's scale, so the
history replay is where it is measured.
Risk cases¶
Each risk case exercises the shared risk vocabulary in
src/lib/risk-paths.js so the recently-fixed false
positives/negatives surface in CI, not in production review output. The mode
field selects the predicate:
mode |
Predicate | Concept labels |
|---|---|---|
query |
conceptsFromQuery(query) |
risk-flag names (money flow, auth/security, …) |
path |
classifyPath(path) |
risk-flag names |
gate |
isGateRiskPath(path) |
["gate"] if it gates a merge, else [] |
secret |
isSecretPath(path) |
["secret"] if it is a secret file, else [] |
expectedConcepts must all be present; notExpectedConcepts must all be
absent. Encoded regression guards include:
fix payload parsing→ not money flow (thepaysubstring must not match)roles.guard.ts→ auth/security, not money flowtests/checkout.spec.ts→ no merge-gate flagconfig/dev.environments.ts→ not a secret (.envsubstring must not match).nvmrc→ configuration, and gates a merge (it picks the production Node version)yarn.lock→ no merge-gate flag (dependency churn scores zero)upgrade the organiser to the pro plan→ not dependency
Harness execution cases¶
The harnessExecution corpus encodes a small, reviewed fixture repository and
the exact commands Solumbe must infer from it. The current Node fixture proves:
npm installnpm testnpm run typechecknpm run build
For every expected command, the runner first verifies that it appears in the
right harness group, then executes it in a temporary copy of the fixture. The
source fixture is never mutated. It accepts only ordinary npm, pnpm,
yarn, or bun install/test/run forms, has a 60-second timeout per command,
and disables lifecycle scripts during the install probe. It never executes a
command inferred from a customer repository.
The current dependency-free Node fixture uses node --check behind its
typecheck script. That proves command inference and execution; it does not
claim TypeScript compiler coverage. Add a dedicated TypeScript fixture when
testing compiler semantics is the goal.
This is execution evidence for the encoded fixture commands, not a claim that every command in every possible harness is safe or runnable. Add another small fixture when extending coverage to another ecosystem or command form.
Gate-effectiveness cases¶
The gateEffectiveness corpus runs Solumbe's real local gate against reviewed,
committed changes. The corpus holds four changes that must pass:
- a valid change;
- a change aligned with its source-kind owner, which must converge;
- a valid change gated against a
mainthat gained a commit after the branch point, which must count only the change's own file; and - an edit to a file that already holds a credential, which the change did not add.
And eight that must be blocked:
- a potential secret or environment file;
- a credential value pasted into ordinary source;
- a credential added beside one already on the base;
- a high-risk schema change without independently verified PR controls;
- version metadata changed without a changelog update;
- requested validation without a base-committed validation policy;
- company ownership rules that cannot be self-certified by a local run; and
- a change that falls below the requested convergence threshold.
Each case names a fixture, a committed change-set, an expected overall verdict,
and one or more named gate checks. Expectations can assert status plus stable
summary or detail fragments. The case fails when the verdict differs, an
expected reason is missing, or the gate returns an unexpected FAIL check.
The runner copies base/ into an isolated temporary directory, creates a
deterministic Git baseline, applies one reviewed changes/<changeSet>/
directory or changes/<changeSet>.patch, stages the result, and invokes
solumbe gate --staged --json. A case with advanceBase branches the change
off the baseline, commits that reviewed change-set to main, and gates the
working tree against main, since staged and head subjects are exact two-tree
diffs by design. Allow markers are stripped before the baseline commit as well
as after the change, so an unchanged marked line reads as unchanged. The patch form lets a safety case create a
secret-like path only inside the temporary repository, without placing that
path in Solumbe's own working tree. Corpus entries cannot provide shell commands,
redirect execution outside evals/fixtures/, or use path-like change-set
names. The source fixture and customer repositories are never mutated or
executed.
This is product evidence for the encoded local-gate behaviours. Hosted CI, GitHub approvals, CODEOWNERS approval state, unresolved conversations, and the human merge decision remain separate authorities.
Read cases¶
The reads corpus labels what a request asks for: its intent (one of the
eight in src/lib/jev.js, or several where a careful reader could defend more
than one), the solumbe tool that answers it, and the files the work needs. It
grades the request read, not solumbe's ranking, so it runs one read provider at
a time over the same offline context pack the retrieval eval builds:
solumbe eval --reads --provider heuristic # solumbe's own action word, no call
solumbe eval --reads --provider jev # TYPESAFE_API_KEY
solumbe eval --reads --provider claude # ANTHROPIC_API_KEY; writes its reasoning before its answers
solumbe eval --reads --provider claude --no-reasoning # the same read, answers only
It reports intent accuracy, the share of reads that clear CHOICE_FLOOR, the
accuracy of the intent a pack would actually carry, a Brier score and
confidence bands for the stated confidence, capability accuracy, how many
expected files a read would demote, and the cost. It calls a model, so it has
no thresholds and is not part of npm run quality; results are recorded in
the calibration thesis.
tests/claude-read.test.js runs it against a mocked fetch.
History replay¶
The corpus is labelled by hand on small synthetic fixtures, and every case in
it passes, so it can catch a regression but cannot show how far the pack is
from right on real work. A commit is a labelled case that already exists: its
subject is the request, and the files it changed are the files the work
needed. solumbe eval --history replays recent commits of any repository that
way:
solumbe eval --history ../bashbop-api --commits 30 [--k 5] [--max-files 12] [--ref HEAD] [--json] [--out file]
npm run eval:history -- ../bashbop-api
For each commit it exports the parent tree with git archive into a
temporary directory, builds the pack there from the cleaned subject, and
scores the pack against the commit. The subject loses its conventional-commit
type and scope (fix(gate):), which is usually a path hint the requester would
not have given, and a trailing (#123).
What counts as expected:
- a file the commit modified, deleted or renamed (by its old path);
- not a file the commit added, which did not exist on the parent;
- not a changelog, which every change touches by convention (a lockfile is expected: for a dependency request it is the file to change, and it counts as unindexed on an engine that does not list it);
- a changed test is expected in the pack's Tests section, so it counts toward pack recall but not the primary metrics;
- a changed file the code map does not index is reported as a coverage gap and kept out of the ranking metrics, since no ranking could name it.
Merges, releases, version bumps, root commits, bot authors and commits with
more than --max-files expected files are skipped and counted by reason. A
subject seen on a newer commit (a cherry-pick or a re-landed change) is skipped
too, so one miss is not counted twice.
It reports, over the scored commits:
| Metric | Meaning |
|---|---|
| hit@k | at least one changed file in the top k primary files |
| recall@k, precision@k, MRR | as for the corpus (computeRetrievalMetrics), on the changed non-test files |
| pack recall | changed files, tests included, found anywhere in the pack |
| unindexed share | changed files the code map does not index |
and lists each commit, the misses (commits with nothing changed in the top k) and the unindexed files.
It is a measurement, not a gate. The numbers depend on the repository and the
commits it reads, and a commit's diff is a noisy label: it holds files the
request never named (a refactor carried along, a doc touched in passing), so
recall@k has a ceiling well below 1.0 and is for comparing engine versions on
the same commits, not for reading in isolation. It has no thresholds and is not
part of npm run quality. The inspected repository is only read: nothing from
it is executed, and each temporary tree and its index cache are removed.
tests/history-eval.test.js runs it on a small generated repository.
First replay (2026-10-22)¶
The first comparison: the 30 most recent eligible commits of bashbop-api at
fbd8484, the same commits scored by the engine before and after
dependency-request ranking (#325, context engine 5). k = 5.
| Engine | Commits | hit@5 | recall@5 | MRR | pack recall | unindexed |
|---|---|---|---|---|---|---|
| before #325 | 27 | 0.37 | 0.205 | 0.182 | 0.212 | 0.14 |
| with #325 | 29 | 0.414 | 0.214 | 0.239 | 0.218 | 0.061 |
Two dependency commits that scored nothing now hit: "patch handlebars, tar,
proxy-addr and other vulnerable dependencies" ranks yarn.lock first (recall
0.5, it also changed package.json), and "make the Dependabot batch install,
lint, test and build" gains yarn.lock (recall 0.2). Two more commits are
scored because manifests and lockfiles are now indexed, so the unindexed share
halves. "move to Node 22 and take the upgrades that needed it" still scores
zero; .nvmrc is the open node-upgrade-runtime-version gap.
On the same first run the engine before #325 scored hit@5 0.75 on solumbe,
0.741 on bashbop-mobile-app and 0.345 on bashbop-event-web; the synthetic
corpus scores recall@5 1.0. The misses behind the low scores are the known
gaps above, files under src/ai/rag/ crowded out by large services, plan and
spec documents edited by "record the …" commits, and translation catalogues
that every string change touches.
How thresholds work¶
The corpus header carries a thresholds block so the pass/fail bar is tunable
without touching code:
"thresholds": {
"retrieval": { "precisionAtK": 0.82, "recallAtK": 0.9, "mrr": 0.9 },
"risk": { "accuracy": 0.97 }
}
The runner computes the aggregate metrics, compares each against its floor, and
sets exitCode to 0 only when every check passes. Thresholds are set to
current-baseline-minus-slack: high enough that a real regression trips the
gate, low enough that the suite is green today. (At the recorded baseline a
single risk-case regression drops accuracy to 27/28 = 0.964, below the 0.97
floor, so the gate catches it. The floor moved from 0.96 to 0.97 when the risk
corpus grew from 22 to 28 cases, because 27/28 would otherwise pass 0.96.)
Current baseline¶
Recorded 2026-10-30 by running runRetrievalEval() against the committed
corpus (44 retrieval + 28 risk cases, plus known gaps outside the aggregate):
| Group | Metric | Value | Threshold | Pass |
|-----------|--------------|------:|----------:|:----:|
| retrieval | precisionAtK | 0.857 | 0.82 | yes |
| retrieval | recallAtK | 1.0 | 0.9 | yes |
| retrieval | mrr | 0.988 | 0.9 | yes |
| risk | accuracy | 1.0 | 0.97 | yes |
Retrieval: p@5=0.857, r@5=1.0, mrr=0.988 (44/44 cases pass)
Risk: accuracy=1.0 (28/28 cases pass)
Overall: PASS (exit 0)
The 2026-10-30 change added the symbol-stacking fixture and the
symbol-stacking-tool-registry case, from a miss on solumbe itself: asked to
trim the MCP tool definitions, the pack ranked ast-languages.js (146) far
above mcp.js (55), because each of its per-language extractors loops over a
local definitions of names, and every one matched the query again through
its body identifiers. Each further symbol matching the same words now adds
less (1, 1/2, 1/3...), and a strong match needs two query words in the
symbol's own name: words found only in a body count as weak matches, one per
word for the file. (Context engine version 6 discounted body-only words by a
tuned 0.4; version 7, 2026-10-31, replaced it with that rule, which scores the
decoy 44 against the registry's 118, and ast-languages.js 45 for the original
query, against 41 with the weight and 86 with saturation alone. The retrieval
metrics are unchanged.) Without the change the case
still passes at k=5 (the registry ranks second), so
tests/context-engine.test.js requires it first and at more than twice the
decoy's score; MRR moves from 0.977 to 0.988 with it. The case lowers p@5 from
0.865 to 0.857 because the decoy stays in Primary Files. Two other misses from
the same session (telemetry.js and tokens.js for a billed-token request)
are not fixed by this and are not yet recorded.
The 2026-10-20 change recorded the misses from a Dependabot batch and a Node
20 → 22 upgrade on bashbop-api. Two were rules and are fixed: .nvmrc,
.node-version and .tool-versions are configuration and gate a merge, and
dependency, dependencies, deps, dependabot, renovate and lockfile
in a request imply the dependency concept. upgrade, bump and package do
not, since they name pricing tiers, releases and versions as often. The rest
need manifests, lockfiles and version pins indexed, which the code map skips,
so they are recorded as known gaps (below): four node-upgrade-api retrieval
cases and the dependency-bump-converges gate case. The risk floor moved to
0.97 for the larger corpus. Retrieval aggregates are unchanged.
The 2026-10-13 change added the git-hooks fixture and the
git-hook-pre-commit-gate-step case. Git hook scripts have no extension, so
they were never indexed, and a request to change the pre-commit hook ranked
components that shared the words "step" and "gate". The case returns the hook
alone at rank 1 (p@5=1.0). Before the primary pool was scoped for hooks it
returned the hook plus both decoys (p@5=0.333), which took the aggregate to
0.819, below the floor. The floor was not changed. A global relative score
cutoff was tried at 0.5 to 0.7 of the top score and rejected: every ratio
dropped at least one labeled second owner elsewhere in the corpus.
The 2026-10-10 change added the python-orders-api and go-inventory-api
fixtures and ten cases, to measure ranking in a repository with no JavaScript,
and then Go import resolution. The 25 earlier cases are unchanged (p@5=0.861,
r@5=1.0, mrr=0.979). The ten new cases score p@5=0.767, r@5=1.0, mrr=1.0,
which moved the aggregate below the old 0.85 precision floor, so the floor was
re-set to 0.82 here and in tests/cli.test.js. What the new cases show:
- Every name and symbol case passes with the labeled file at rank 1, in both languages. The five Go cases return one primary file each. Four of the five Python cases also return one or two primary files that are not labeled (p@5=0.333 or 0.5).
- Both Python importer cases pass, but not through the import graph: the
resolver reads neither
from .tax importnorfrom app.shared.clock import, andrelatedFilesis empty. The caller is ranked as a primary file because the import line itself names the module. - The engine before Go import resolution fails
go-module-import-importer. A Go import names a package directory by module path (example.com/inventory/internal/platform), never the file, so nothing connectedinternal/stock/store.gotointernal/platform/retry.goand the pack for "change the retry backoff" heldretry.goalone. The code map now readsgo.modand records the package directories each Go file imports, and the pack's import graph links the file to that package's files, sostore.gois a related file ("imports primary file").
The 2026-10-06 change (string names a registry gives tools indexed as
registered symbols, and a named tool's implementing module pinned) left the
24 earlier cases with the same aggregate (p@5=0.87, r@5=1.0, mrr=0.978). It
added registered-tool-implementation on the new tool-registry fixture,
which returns a third, unlabeled primary file (p@5=0.667). The engine before
that change fails it: the request reaches neither src/lib/converge.js nor
the registry src/lib/mcp.js.
The 2026-10-05 change (each request word counted once, the words of a
camelCase name such as OpenPanel counted only together, and an action verb
counted only in a symbol's own name) left all 22 earlier cases with the same
aggregate (p@5=0.873, r@5=1.0, mrr=0.976). It added telemetry-repeated-words
and telemetry-openpanel-camelcase on the new telemetry-web fixture; the
aggregate moved because the first returns a third, unlabeled primary file
(p@5=0.667). The fixture is too small to reproduce the live failure, where
app/global-error.tsx fell out of the pack and a seller checkout service
became primary; tests/context-engine.test.js asserts the three mechanisms
directly, and each of those tests fails with its fix removed.
The 2026-09-27 change (named-file pinning, translation-catalog demotion and
locale folding, extension tokens dropped, runnable tests only) left all 21
earlier cases byte-identical; the aggregate moved from p@5=0.867, mrr=0.975
only because it added multi-locale-named-files-and-symbol, which scores 1.0.
The engine before that change fails the same case (p@5=0.6: two
.ts/.tsx files matched on their extension took the slots of
utils/create-event.ts and aiEventSchema.ts).
precision@5 is below 1.0 because some pairing queries (e.g. fix the rsvp
button) legitimately return two primary files when only one is labeled
expectedPrimary; the second is a correct related file, so this is expected
headroom rather than a defect.
mrr dropped from 1.0 to 0.975 when the query-interpretation heuristics (intent hints, the signup/verification and RSVP-privacy boosts, the template boost) were removed. Those rules existed to put one file at rank 1 for the three cases they were written against; without them the expected file sits at rank 2 or 3, still inside the top five. Ordering within the pack is the model's job.
How to add a case¶
-
Pick or add a fixture. Reuse a name under
fixtureRootsinevals/corpus.json, or add a small synthetic repo underevals/fixtures/and register it infixtureRoots. Keep fixtures tiny and synthetic: a few files that exercise one behavior. Do not add a.solumbe/cache to a fixture; the runner copies each fixture to a temp dir and regenerates the map from source so the committed fixtures are never mutated. -
Add the case.
Retrieval:
{
"name": "shop-refund-payment",
"query": "refund a payment",
"repoFixture": "shop-api",
"expectedPrimary": ["src/payment/checkout.service.ts"],
"expectedAnyOf": ["src/payment/stripe.webhook.ts"]
}
Multi-repo cases use "repoFixtures": ["shop-api", "web-client"] and
namespace expected paths as "<fixture>/<repo-relative-path>".
Risk:
{
"name": "query-refund-is-money",
"mode": "query",
"query": "refund a payment",
"expectedConcepts": ["money flow"],
"notExpectedConcepts": []
}
Harness execution:
{
"name": "node-install-test-typecheck-build",
"repoFixture": "harness-node",
"commands": [
{ "kind": "install", "group": "setup", "command": "npm install" },
{ "kind": "test", "group": "validate", "command": "npm test", "script": "test" }
]
}
Gate effectiveness:
{
"name": "secret-file-is-blocked",
"repoFixture": "gate-node",
"changeSet": "secret",
"expectedVerdict": "FAIL",
"expectedChecks": [
{
"name": "Secret safety",
"status": "FAIL",
"summaryIncludes": "Potential secret or environment file changed"
}
]
}
- Run it.
node --test tests/eval.test.js, or run the runner directly to see the scoreboard. If you intentionally changed behavior, re-record the baseline above and re-set the thresholds to baseline-minus-slack.
Running the eval¶
runRetrievalEval(), runHarnessExecutionEval(), and
runGateEffectivenessEval() are exported from src/lib/eval.js. All suites are
covered by tests/eval.test.js:
Run the release-gating commands directly with:
The CLI equivalents are solumbe eval --accuracy, solumbe eval --harness, and
solumbe eval --gate-effectiveness. Each exits non-zero on a failed corpus, and
npm run quality runs all three release-gating suites.