Skip to content

`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.yml and 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:

  1. Teams couldn’t adopt crimes without fighting legitimate exceptions — they needed per-finding suppressions with a reason, not just the repo-wide baseline.
  2. Teams couldn’t tune crimes to their conventions — they needed config knobs (per-shape large_function, IA alias seeds, per-detector enable/disable) the schema didn’t yet expose.
  3. 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.

crimes init — bootstrap crimes.config.json

Section titled “crimes init — bootstrap crimes.config.json”
Terminal window
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”
Terminal window
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 identity crimes diff and crimes baseline already 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”
Terminal window
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”
Terminal window
crimes audit-suppressions
crimes audit-suppressions --format json

Lists 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”
Terminal window
crimes scan -f json > scan.json
crimes explain crime_00005 --from scan.json

Prints 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).

Terminal window
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-medium
  • crimes 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 for domain / route_handler / react_component / page_export / test_callback / unknown. domain wins over the legacy thresholds.largeFunctionLines when both are set.
  • detectors.enable / detectors.disable — allowlist / blocklist on detector ids. Unknown ids exit 2 (typos shouldn’t silently no-op).
  • ia.aliasGroups — additive to the built-in DEFAULT_ALIAS_GROUPS. Seed product-specific vocabulary (e.g. dataset / corpus / collection).
  • suppressions.path — override the .crimes/suppressions.json location.
  • architecture.layers / architecture.rules — reserved placeholder. Schema-validated but not consumed in 0.5.0; the shape mirrors PRD.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.

All optional and additive. No schema_version bump.

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.

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).

  • ExplainReport (report_type: "explain") — output of crimes explain. Carries the matched Finding, the detector’s description + charge, the why_it_matters paragraph, and the verbatim crimes ignore command line.
  • Suppressions (report_type: "suppressions") — on-disk shape of .crimes/suppressions.json. Documented in json-schema.md.

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-high
  • Existing crimes.config.json files keep working. Every new field is optional; missing keys take defaults.
  • Existing .crimes/baseline.json files keep working. The baseline workflow is unchanged.
  • JSON consumers continue to work. No required fields changed, no field types changed, schema_version stays at "0.1.0". The new optional fields are documented under the existing stability guarantees.
  • 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 / ownerexpires_at and owner were considered for the suppressions file but deferred. Revisit once teams have lived with the basic file for a release.
  • architecture.layers runtime enforcement — schema lands now; the dependency-graph detector that consumes it is its own release.

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.