`crimes@0.5.0` — Suppressions, config, and explainability
Draft release notes for the GitHub Release tagged
v0.5.0. The body below is what should go in the Releases page when you cut the tag — that triggers.github/workflows/release.ymland publishes to npm via Trusted Publishing.
crimes@0.5.0 is the product-surface release: four new commands
and a real config story, without touching any of the detectors that
shipped in 0.4.0. Real-repo trials of 0.4.0 surfaced three coupled
gaps:
- Teams couldn’t adopt
crimeswithout fighting legitimate exceptions — they needed per-finding suppressions with a reason, not just the repo-wide baseline. - Teams couldn’t tune
crimesto their conventions — they needed config knobs (per-shapelarge_function, IA alias seeds, per-detector enable/disable) the schema didn’t yet expose. - Agents reading a charge couldn’t ask “but why?” — they needed an explain command between “I see the finding” and “I commit to fix or suppress”.
All three land together in this release. The wedge is unchanged: local, open-source, agent-native. Deterministic, no LLM, no API key, no network access.
All additions are additive and backwards-compatible — no
schema_version bump, no required field changes, no CLI behaviour
regressions for callers that don’t opt in.
Headline changes
Section titled “Headline changes”crimes init — bootstrap crimes.config.json
Section titled “crimes init — bootstrap crimes.config.json”npx crimes init# → Wrote crimes.config.json (40 lines). Tweak include/exclude/thresholds and commit.Writes a starter crimes.config.json with sensible defaults, an
inline $schema URL for IDE validation, and pointers at the new
knobs. Refuses to overwrite an existing file unless you pass
--force. Non-interactive on purpose.
crimes ignore — deliberate, reviewable per-finding exceptions
Section titled “crimes ignore — deliberate, reviewable per-finding exceptions”crimes ignore large_function::src/billing.ts::generateInvoice \ --reason "Legacy billing module — rewrite tracked in #1234."# → Suppressed large_function::src/billing.ts::generateInvoice in .crimes/suppressions.json.Writes to .crimes/suppressions.json, fingerprint-keyed with a
required, non-empty reason. The file is intended to be committed
and reviewed in PRs — every suppression is a deliberate, evidence-
backed choice rather than a reflex. See
docs/suppressions.md.
Accepts either form on input:
- per-scan id (
crime_00005) — resolved to a fingerprint via a fresh scan, then persisted by fingerprint (ids reassign every scan and are useless on disk). - stable fingerprint (
<type>::<file>::<symbol>) — same identitycrimes diffandcrimes baselinealready use.
--file, --dry-run, and --no-verify available. Re-suppressing
the same fingerprint updates reason instead of duplicating.
crimes unignore — remove a suppression by fingerprint
Section titled “crimes unignore — remove a suppression by fingerprint”crimes unignore large_function::src/billing.ts::generateInvoice# → Removed large_function::src/billing.ts::generateInvoice from .crimes/suppressions.json.Symmetric to crimes ignore. Takes a stable fingerprint only —
once suppressed, there is no per-scan id to look up. Supports
--file <path> and --dry-run. Empty suppressions: [] is left
in place rather than deleting the file, so the frame is visible
to reviewers. See docs/suppressions.md.
crimes audit-suppressions — sort by age, flag stale or vague reasons
Section titled “crimes audit-suppressions — sort by age, flag stale or vague reasons”crimes audit-suppressionscrimes audit-suppressions --format jsonLists every suppression sorted oldest first, with age_days and a
per-entry concerns: ("stale" | "short_reason" | "vague_reason")[]
array. Thresholds: 180 days for stale, 16 characters for
short_reason; the vague-reason check matches tmp/todo/wip/
fixme/noisy/legacy/later/skip/ignore as a leading
keyword, plus too noisy and we know …. Emits a new
report_type: "audit_suppressions" — see
docs/json-schema.md.
The mitigation for the largest plan risk (“suppressions becoming a
dumping ground”). Wire it into a quarterly review, or into a
nightly CI job that watches flagged_count over time.
crimes explain — long-form rationale per finding
Section titled “crimes explain — long-form rationale per finding”crimes scan -f json > scan.jsoncrimes explain crime_00005 --from scan.jsonPrints detector.description, a one-paragraph why_it_matters,
the finding’s evidence, suggested actions, related files, and the
verbatim crimes ignore command line the user can copy if they
decide to live with the finding. Deterministic — every string is
either on the finding, baked into the detector at build time, or
constructed from the fingerprint. No LLM, no network. See
docs/explain.md.
Two input modes:
--from <scan.json>— reads a saved scan instead of re-scanning. Canonical agent flow:crimes scan -f json > scan.json && crimes explain crime_00005 --from scan.json.- (default) — fresh scan against cwd, then look up. Slower but standalone.
crimes diff --fail-on new-high | new-medium
Section titled “crimes diff --fail-on new-high | new-medium”Completes the M4 CI-gate trio. The remaining unshipped flag finally
lands; mirrors crimes verdict --fail-on’s thresholds minus
worse (diff doesn’t carry a verdict).
crimes diff origin/main...HEAD --fail-on new-high# → exit 1 when any new finding has severity "high"For a hard CI gate you now have four equivalent options sharing the
same 0 pass / 1 blocked / 2 usage exit contract:
crimes scan --changed --fail-on <severity>crimes baseline check --fail-on <severity>crimes diff <base...head> --fail-on new-high | new-mediumcrimes verdict --fail-on worse | new-high | new-medium
Config extension — per-shape thresholds, alias groups, detector toggles
Section titled “Config extension — per-shape thresholds, alias groups, detector toggles”crimes.config.json (validated with zod — malformed values exit
2 with the precise key path) now accepts:
thresholds.largeFunction.<shape>— per-shape overrides fordomain/route_handler/react_component/page_export/test_callback/unknown.domainwins over the legacythresholds.largeFunctionLineswhen both are set.detectors.enable/detectors.disable— allowlist / blocklist on detector ids. Unknown ids exit2(typos shouldn’t silently no-op).ia.aliasGroups— additive to the built-inDEFAULT_ALIAS_GROUPS. Seed product-specific vocabulary (e.g.dataset/corpus/collection).suppressions.path— override the.crimes/suppressions.jsonlocation.architecture.layers/architecture.rules— reserved placeholder. Schema-validated but not consumed in0.5.0; the shape mirrorsPRD.md§18 so the eventual dependency-graph detector lands without revising the schema again.
See docs/configuration.md for the full
reference and worked examples.
--show-suppressed on every report-listing command
Section titled “--show-suppressed on every report-listing command”scan, context, baseline check, diff, and verdict all
honour --show-suppressed. Default behaviour filters matched
findings out and emits suppressed_count; the flag re-surfaces them
annotated with suppressed: true and suppression_reason. The
gate (--fail-on) always ignores suppressed entries regardless of
display.
Schema additions
Section titled “Schema additions”All optional and additive. No schema_version bump.
Per-finding
Section titled “Per-finding”interface Finding { // … existing fields unchanged suppressed?: true; suppression_reason?: string;}Only set when --show-suppressed is on. Gate evaluation always
ignores findings with suppressed === true.
Per-report
Section titled “Per-report”ScanReport / ContextReport / BaselineCheckReport / DiffReport / VerdictReport { // … existing fields unchanged suppressed_count?: number;}
DiffReport { // … existing fields unchanged fail_on?: "new-high" | "new-medium"; failed?: boolean;}suppressed_count is only present when ≥1 suppression matched in
this invocation. Absent otherwise (equivalent to “no suppressions
configured” for downstream consumers).
New report types
Section titled “New report types”ExplainReport(report_type: "explain") — output ofcrimes explain. Carries the matchedFinding, the detector’sdescription+charge, thewhy_it_mattersparagraph, and the verbatimcrimes ignorecommand line.Suppressions(report_type: "suppressions") — on-disk shape of.crimes/suppressions.json. Documented injson-schema.md.
CI implications
Section titled “CI implications”Suppressions are applied before every --fail-on evaluation. A
suppressed finding never trips a gate, regardless of severity or
the command. See docs/ci.md.
Add this to your CI workflow when adopting suppressions:
- name: crimes diff gate run: npx crimes diff origin/${{ github.base_ref }}...HEAD --fail-on new-highMigration / upgrade notes
Section titled “Migration / upgrade notes”- Existing
crimes.config.jsonfiles keep working. Every new field is optional; missing keys take defaults. - Existing
.crimes/baseline.jsonfiles keep working. The baseline workflow is unchanged. - JSON consumers continue to work. No required fields changed,
no field types changed,
schema_versionstays at"0.1.0". The new optional fields are documented under the existing stability guarantees.
What didn’t ship
Section titled “What didn’t ship”- Per-finding
scores.churn/scores.test_gap/scores.blast_radius— M2 work. The scoring contract change touches every detector and deserves its own minor release. - Suppression expiry / owner —
expires_atandownerwere considered for the suppressions file but deferred. Revisit once teams have lived with the basic file for a release. architecture.layersruntime enforcement — schema lands now; the dependency-graph detector that consumes it is its own release.
Thanks
Section titled “Thanks”This release closes the M4 polish gap that’s been deferred since
0.2.0. Special thanks to the agents and humans whose 0.4.0
real-repo trials surfaced the demand for deliberate per-finding
exceptions — none of this lands without their patience.
The wedge is unchanged: deterministic, local, JSON-first. Build once, run everywhere, no API key required.