crimes v0.17.0 — calibration, and the first wire-format change
Theme: the tool turned on itself. Every change here came from
crimes scanning crimes, and two of them are bugs the product found
in its own source.
This is the first release to move schema_version — 0.3.0 → 0.4.0.
The bump buys one thing: a suppression can now name the finding its
author actually looked at.
Read this if you have pinned suppressions or a baseline
Section titled “Read this if you have pinned suppressions or a baseline”Entries for magic_domain_literal_scatter, exact_duplicate_block,
and near_duplicate_block stop matching and need re-recording. Every
other finding type is byte-identical to 0.16.0 — same fingerprints,
same entries, nothing to do.
$ crimes feedback recheckwill surface the affected entries with the reason. Re-record each with a
fresh crimes ignore, and re-run crimes baseline save.
An un-migrated file still loads — the accepted schema window is
0.1.0 through 0.4.0. It just matches fewer findings for those three
types: the old fingerprint reads as fixed, the new one as new.
Full field-by-field notes:
Migrating from 0.3.0 to 0.4.0.
Why the fingerprint had to change
Section titled “Why the fingerprint had to change”A fingerprint was <type>::<file>::<symbol-or-empty>. That is unique
for almost every detector, and not unique for three of them.
magic_domain_literal_scatter reports one finding per scattered
literal and carries no symbol at all. exact_duplicate_block and
near_duplicate_block report one finding per duplicate group, anchored
on the lex-first file of the group — and one file is routinely the
anchor of more than one group.
On a self-scan that produced 3 colliding fingerprints covering 7
findings. The known limitation documented in fingerprint.ts said the
consequence was that crimes diff conflates them. The sharper one is
suppression targeting:
crimes ignoreon the"property"finding also silently suppresses"subprocess", because they are the same fingerprint.
A user suppressing one thing got a second thing suppressed without being told. That is a safety property, not a cosmetic one.
The fix, and the fix that was rejected
Section titled “The fix, and the fix that was rejected”Setting symbol to the literal value would have worked mechanically.
It was rejected: fingerprint.ts documents symbol as naming a
specific declaration, and a string literal is not a declaration.
Overloading it would have silently changed what the field means for
every consumer reading it.
A per-detector patch was rejected too. The bug is in the fingerprint function’s inputs, so it gets one general answer:
interface Finding { symbol?: string; /** Tiebreaker when (type, file, symbol) is not unique. */ discriminator?: string;}<type>::<file>::<symbol-or-empty>[::<discriminator>]The segment is omitted entirely when unset, so the overwhelming majority of fingerprints are unchanged strings. Only the detectors that were colliding change shape.
Each colliding detector had a natural value already to hand: the literal for scatter, the 12-character body hash for the duplicate-block pair — the same characters already printed in its evidence line, so a fingerprint and the finding it names can be matched up by eye.
The rule a detector must follow is documented on the field: the value has to be stable across scans of the same code. A counter or a per-scan index would satisfy the type and break every pinned entry the moment an unrelated finding appeared.
Self-scan collisions: 3 → 0.
large_file learns that prose is not code
Section titled “large_file learns that prose is not code”large_file had two shapes, domain and test_file. Everything else
— including your README, your schema reference, your ADRs — was
measured against the 300-line domain-code budget and charged as a God
File for being thorough.
| Shape | Threshold | Severity at threshold | at 2× |
|---|---|---|---|
domain | config (default 300) | medium | high |
test_file | 1500 | low | medium |
docs | 1000 | low | medium |
docs matches .md, .mdx, .markdown, .rst, .adoc,
.asciidoc, .txt. Configurable as thresholds.largeFile.docs.
Two boundaries held on purpose:
- Data formats are not docs.
.json,.yaml,.csvstay on the domain budget. A 3000-line config file is a finding worth having, and stretching “it isn’t code” to cover it would trade real precision for a quieter scan. agent_risksits abovetest_file, belowdomain. An oversized document is a genuine context cost — an agent told to follow it has to load the whole thing to find the paragraph that applies — so it does not get the test-file discount.
This repo had been carrying 19 **/*.md suppressions as an explicit
placeholder for this shape. All 19 are gone. Fourteen had already
stopped matching anything; five now surface as honest low findings
instead of being hidden. That is the intended outcome — a shape that
silenced exactly what the suppression silenced would be the suppression
with extra steps.
Two bugs crimes found in crimes
Section titled “Two bugs crimes found in crimes”Unbounded fan-out. buildFunctionHashIndex and buildJsxShapeIndex
each Promise.all-ed one readFile per candidate file. On a repo with
20k source files that asks the OS for 20k descriptors at once and fails
with EMFILE. The product’s own unbounded_async_fanout detector
flagged both, and was right.
Both now read through a bounded worker pool. The limit is sized against
the descriptor budget rather than the CPU count — the work is a read
plus a parse, so the pool exists to keep the fd count flat, not to
saturate cores — and sits an order of magnitude under the default
ulimit -n on macOS and Linux.
Note what was not done: the detector’s bounded-helper recognition was
not widened to whitelist the new helper. It stops firing because the
Promise.all is genuinely gone.
Non-reproducible evidence. exact_duplicate_block was not
deterministic across runs on an unchanged tree. Three consecutive scans
of the same commit agreed on finding identity and severity counts, but
the same anchor file reported hash 3dbfcb76d2cc… across 6 file(s) on
one run and hash 3d33dfe315b3… across 9 file(s) on another.
Same root cause: the index inserted into its maps from inside the
Promise.all callbacks, so map order tracked which read finished
first, and a function belonging to more than one duplicate group picked
its group by iteration order. Hits are now collected per file and
inserted in sorted order, and both duplicate detectors sort hash keys
before iterating.
Identity was always stable, so baselines and diff were never
affected. What was broken was the evidence string a user reads, which
is supposed to be reproducible.
Also in this release
Section titled “Also in this release”crimes feedback recheckknows about all of it. The per-detector release-notes map gained0.17entries for all four affected detectors, so a resurfaced suppression explains why it came back instead of falling through to “detector behaviour unchanged”.dependency_provenance_gaphonours!negation in pnpmpackages:and npm/yarnworkspacesglobs. A package the package manager deliberately excludes is no longer compared against a lockfile that correctly does not contain it. Found by the detector reporting it about this repo.docs/calibration-followups.mdis closed out. Every open item has a disposition and the evidence behind it, including two whose recorded status turned out to be wrong.
Eval baseline
Section titled “Eval baseline”96/96 combinations, complete matrix.
| agent | 0.16.0 | 0.17.0 |
|---|---|---|
| claude | 0.84 | 0.84 |
| codex | 0.58 | 0.57 |
This is a product delta, not a measurement correction. Nothing in
the scorer, the judge prompts, the scenario rubrics, or the fixture
finding sets changed. What moved is what crimes reports, and scanning
the fixtures with both builds isolates it exactly: four markdown files
reclassify from high to low in 03-node-cli-tool, and one drops
below threshold in 04-monorepo. Every other fixture is
finding-for-finding identical — 330 findings to 329.
Neither aggregate move is claimable as a change. The fixtures whose findings actually moved are TS/JS repos whose markdown was never the subject of a scenario, so the delta this baseline records is real but sits almost entirely outside what the scenarios ask about. That is the expected shape for a change that reclassifies prose.
Verification
Section titled “Verification”pnpm verify # format:check + lint + build + typecheck + test1873 tests across six packages, up from 1828 at 0.16.0 (that span
also covers the !-negation fix and the eval-runner resume work). New
coverage in this release: the
bounded-concurrency helper (in-flight ceiling, input-order results,
serial fallback on a non-positive limit), the discriminator’s
back-compat guarantee that an unset value produces the byte-identical
pre-0.4.0 string, per-detector fingerprint separation, a five-run
determinism guard on exact_duplicate_block, and the full docs shape
including the deliberate data-format exclusions.