`crimes@0.8.0` — Extended Lens: Date, Naming, Hot-Path, and Asset Crimes
Draft release notes for the GitHub Release tagged
v0.8.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.8.0 extends the lens to four families of “common-sense”
crimes that mainstream linters don’t catch: timezone-and-date crimes,
narrow naming-tier crimes, hot-path I/O crimes, and asset-weight
crimes. One config feature plus thirteen new detectors land
together. Detector count: 34 → 47. Five accumulated patch bumps
(0.7.6 → 0.7.15) roll up into this release.
- One new config feature.
detectors.options.<id>— per-detector exemption values, sitting betweendetectors.disable(kills the detector everywhere) andcrimes ignore(suppresses one specific finding). Each detector registers its own zod schema; typos surface at config-load time. - Date / time family (5 detectors).
timezone_unsafe_parse,mixed_utc_local_methods,locale_drift,dst_naive_arithmetic,date_string_concat. The silent-bug class linters most consistently miss. - Naming-tier family (2 detectors).
boolean_naming_drift,singular_plural_type_mismatch. Names that lie to readers and agents. - Hot-path / portability family (3 detectors).
sync_io_in_hotpath,hardcoded_local_path,hardcoded_localhost. Patterns that survive code review and break in production. - Asset family (3 detectors) — first non-source detectors.
oversized_raster,raster_should_be_vector,svg_with_embedded_raster. A new second-pass pipeline lets crimes walk images alongside source without disturbing the existing parsed-AST contract. - Eight new eval scenarios spread across all five scenario kinds — one per detector that warrants its own scenario. Total per agent: 30 → 38.
- Zero medium-or-higher self-scan findings from the new detectors. Self-scan stays clean.
Schema: schema_version stays at "0.1.0". Every new detector
emits the existing Finding shape; new parser fields are additive on
ParsedFile. Existing scan JSON files load unchanged.
The wedge
Section titled “The wedge”The 0.3.0 → 0.7.5 arc built out structural, IA, duplication, frontend, and calibration coverage. The 0.8.0 thesis:
Extend the lens to cover four families of “common-sense” crimes that linters don’t catch and humans / agents repeatedly ship.
Every new detector here was audited against ESLint / Biome / Knip /
typescript-eslint / Semgrep / SonarQube before keeping. The rule:
either no mainstream tool catches the pattern, or the tool exists but
is rarely configured and our angle (change-risk or agent-risk) is
sharper than the linter angle. Three candidates from the original
brainstorm were rejected during research because ESLint covers them
cleanly (@ts-ignore discipline, as any, await in loop); two
more deferred to 0.9.0 with type-info work (date_equality_misuse,
exported_stop_word_name); one dropped as too rare to matter in
modern TS code (catch_rethrow_no_context).
Research, decisions, and ship-vs-defer rationale all recorded at
.planning/0.8.0-extended-lens.md
and
.planning/0.8.0-RESEARCH.md.
What shipped
Section titled “What shipped”Per-detector exemption config
Section titled “Per-detector exemption config”detectors.options.<id> is the third tier of suppression alongside
detectors.disable and crimes ignore:
{ "detectors": { "options": { "boolean_naming_drift": { "allowedNames": ["pristine", "processed"] }, "hardcoded_local_path": { "allowedPaths": ["/Users/ci-runner/cache"] }, "oversized_raster": { "allowedPaths": ["public/hero/"] } } }}Each detector registers an optional optionsSchema: z.ZodType on its
declaration; the config loader validates every supplied options block
against the registered schema at load time. Typos for both detector
ids (“hardcoded_local_pth”) and option keys (“allowdPaths”) raise
ConfigParseError (exit code 2) — never silently no-op. Consumed by
every 0.8.0 detector with built-in exemption surface, plus
retro-fitted onto the existing detector list where it improves
configurability.
Date / time family (5 detectors)
Section titled “Date / time family (5 detectors)”Prototyped against the crimes monorepo, the bundled fixture, and
~600 files of external open-source TS/JS before keeping (see
Investigation 8 of the research doc for the FP-surface evidence).
Two candidates from the original brainstorm — date_equality_misuse
and date_string_concat v2 — required type info to be robust and
deferred to 0.9.0.
timezone_unsafe_parse(“Timezone Roulette”) — flagsnew Date("…")whose string argument has noZand no±HH:MMoffset.YYYY-MM-DDis read as UTC midnight;YYYY-MM-DDTHH:MM:SSwithout zone is read as local time. Either way the developer is betting on a timezone the runtime won’t honour. Severity medium-high, confidence 0.90. Per-project allowlist viadetectors.options.timezone_unsafe_parse.allowedLiterals.mixed_utc_local_methods(“Half-UTC, Half-Local”) — flags Date instances whereget*UTC*/get*methods of opposing families are read on the same receiver identifier in the same file. Silent bug class: tests pass in UTC, production drifts by the host’s offset. Severity high.locale_drift(“Host-Locale Drift”) — flags.toLocaleDateString()/.toLocaleString()/.toLocaleTimeString()invoked without a locale argument. Output depends on the host’s default locale; user-facing renderers need an explicit pick.dst_naive_arithmetic(“DST-Naive Day Math”) — flags+ 86400000/+ 604800000(day / week / 28-day / 30-day / year millisecond constants) and folded equivalents like24 * 60 * 60 * 1000. Day-level millisecond arithmetic silently misfires on DST transition days.date_string_concat(“Date String Sewing”) — flags"…" + d.getUTCMonth()and the reverse — hand-rolled date string assembly. Smell rather than guaranteed bug, but a tell that the project should reach forIntl.DateTimeFormatortoISOString().
Naming-tier family (2 detectors)
Section titled “Naming-tier family (2 detectors)”boolean_naming_drift(“Unprefixed Boolean”) — declarations annotated: booleanor initialised from a boolean expression (!x/a === b/ etc.) whose name lacks theis/has/should/can/will/did/was/were/are/… prefix family. Ships with a built-in React-state allowlist (loading,ready,active,disabled,expanded,pending,open,closed,visible,hidden,selected,focused,dirty,valid,submitting,editing,dragging,hovering,checked,busy,empty,full,online,offline,mounted,unmounted) so idiomatic React doesn’t trip the detector. User extensions viadetectors.options.boolean_naming_drift.allowedNames. Severity low-medium, confidence 0.80.singular_plural_type_mismatch(“Plural Mismatch”) — declarations where the name’s plural shape disagrees with the type’s array shape (users: User,invoice: Invoice[],users: Array<User>). v1 fires on bare identifier and simple- array annotations only — aliased and generic types deferred to 0.9.0 type-info work. Hand-rolled pluraliser (~60 lines, no dependency) plus uncountable-noun allowlist (data,information,news,software,staff, …). Severity low-medium, confidence 0.70.
Hot-path / portability family (3 detectors)
Section titled “Hot-path / portability family (3 detectors)”sync_io_in_hotpath(“Sync I/O in Hot Path”) — node:fs*Syncmethods (readFileSync,writeFileSync,existsSync,statSync,readdirSync, …) and the synchronous process- spawning helpers (execSync,spawnSync,execFileSync) invoked inside route handlers, page exports, React components, or domain functions. Test-callback and CLI-registrar ancestors anywhere in the enclosing chain suppress the finding (sync I/O in those shapes is either intentional or test-only). Severity caps at high for request surfaces; pure-domain findings stay low so a CLI’s own legitimate sync I/O doesn’t dominate the default report.hardcoded_local_path(“Localhost-on-Disk”) —/Users/<name>/…,/home/<name>/…, WindowsC:\Users\<name>\…(back- and forward-slashed). Skips files underscripts/,examples/,fixtures/,test/,tests/. Severity medium-high, confidence 0.90. Per-project allowlists viadetectors.options.hardcoded_local_path.allowedPaths.hardcoded_localhost(“Dev-Server URL”) —localhost:NNNN,127.0.0.1:NNNN,0.0.0.0:NNNN,[::1]:NNNNin non-test, non-config source. Skips.env*,*.config.*,docker-compose*,Dockerfile*,README*.md,CHANGELOG*.md, and thescripts//examples//docs//fixtures//test//tests//__tests__/directories. Severity medium-high, confidence 0.90.
Asset family (3 detectors) — first non-source detectors
Section titled “Asset family (3 detectors) — first non-source detectors”The asset pass is the first crimes pipeline that operates on files
other than TS/JS source. Source detectors stay on the parsed-AST
contract; asset detectors get a slimmer AssetDetectorContext
carrying { file, absolutePath, extension, byteSize, read(), config }
where read() is lazy and per-file cached. The two pools share one
detectors.options.<id> namespace and one detectors.enable /
disable list.
oversized_raster(“Oversized Raster”) — file size againstthresholds.assetWeight.{low,medium,high}Kb(defaults200 / 500 / 1000, mirroring Core Web Vitals “good / needs improvement / poor” guidance for content images). Pure-stat detector: flagging a 5 MB hero is onefs.statsyscall.raster_should_be_vector(“Icon-Sized Raster”) — PNG / JPEG / GIF whose width AND height both fit ≤ 64 px. Header-only dimension parse via a ~80-line in-tree reader (noimage-sizedependency added; WebP / AVIF return undefined from the parser and are silently skipped in v1). ConfigurableiconSizeMax.svg_with_embedded_raster(“SVG With Embedded Raster”) — SVG files containing<image href="data:image/*;base64,…">(orxlink:href). The pattern defeats the entire reason to use SVG: the vector container ships, but the actual content is a raster blob. Severity medium for one embed, high for two-plus.
Parser surfaces added
Section titled “Parser surfaces added”Additive ParsedFile fields — no schema bump, no existing detector
touched:
dateMethodCalls(phase 2a) — everyDate.prototypemethod call with receiver / family (UTC vs local) / line / arg count.dateArithmetic(phase 2a) — every+/-whose numeric operand matches a day / week / month / year millisecond constant. Folds nested*(so24 * 60 * 60 * 1000is recognised as a day).dateStringConcats(phase 2a) —"…" + d.dateMethod()and the reverse.typedDeclarations(phase 3a) — every named declaration (const / let / var / param / property) with optional type annotation text andInitializerKind. Destructuring patterns intentionally skipped — naming-tier detectors only consider simple identifier names.syncIoCalls(phase 4a) — every node:fs*Syncand child-process-sync call site, with the full chain of enclosing function-like ancestors (innermost first). Lets detectors apply their own shape policy without re-walking the AST.
Eval harness expansion
Section titled “Eval harness expansion”Following the 0.7.5 “one scenario per detector that warrants its own scenario” pattern:
refactor-01-plural-mismatch— agent proposes name-vs-type fixes for two declarations.context-01-boolean-naming— agent picks the React-state- idiom-vs-policy-violation distinction across two unprefixed booleans.bugfix-01-sync-io-hotpath— production handler stalls under concurrent load; agent identifies the per-request blocking calls and proposes async migration.plan-01-hardcoded-local-path— portability sweep; agent picksos.homedir()/process.env.*/ relative paths case-by- case rather than recommending one for everything.review-01-hardcoded-localhost— PR review of a new helper exposing alocalhost:NNNNURL; agent recommends a specific configuration mechanism, not the generic “move to config”.context-01-raster-icon— agent inspects an icon-sized PNG before reusing its pattern and explains why an SVG is the better starting point.refactor-01-svg-embedded-raster— agent refactors an SVG carrying embedded base64 rasters, splitting “this becomes vector paths” from “this becomes a standalone raster referenced by URL”.review-01-oversized-raster— agent reviews a new hero PNG, quotes the byte size from finding evidence, and picks AVIF / WebP with rationale.
Total per agent: 30 → 38. All 38 reconcile against their fixtures
(pnpm --filter evals-runner evals:verify-scenarios green). New
fixture violations live in examples/messy-ts-app/src/api/files.ts
(sync I/O + hardcoded paths + dev-server URL) and
examples/messy-ts-app/src/assets/{hero-banner.png,check-icon.png,partner-logo.svg}
(asset trio).
Scorer corrections
Section titled “Scorer corrections”Two measurement bugs surfaced during the consolidated re-run and landed in the same release:
DETECTOR_IDSin the scorer now unionsbuiltInDetectorswithbuiltInAssetDetectors. Asset-detector slugs weren’t recognised before — codexreview-01-oversized-rasterscored 0/3 despite namingoversized_rasterverbatim.extractFilePathsregex extended with asset extensions (png,jpg,jpeg,gif,webp,avif,svg). Image paths weren’t picked up by the file-reference check.
Both are pure calibration; no agent or detector behaviour changed. The 0.7.14 results dir was never committed; 0.7.15 carries the corrected baseline.
Baseline at 0.7.15
Section titled “Baseline at 0.7.15”76 runs (38 scenarios × 2 agents) in 5m 44s on the reference laptop:
| Agent | Structural pass rate | Δ vs 0.7.8 |
|---|---|---|
claude | 0.85 | +1pp |
codex | 0.74 | -4pp |
Per scenario kind:
| Kind | claude | codex | claude Δ | codex Δ |
|---|---|---|---|---|
| bugfix | 0.56 | 0.50 | -12pp | -18pp |
| context | 1.00 | 1.00 | +0pp | +0pp |
| plan | 0.94 | 0.59 | +14pp | +8pp |
| refactor | 0.89 | 0.78 | +1pp | +0pp |
| review | 0.81 | 0.76 | +0pp | -9pp |
The two visible shifts both come from the new 0.8.0 scenarios:
- bugfix down 12-18pp.
bugfix-01-sync-io-hotpathis genuinely harder than the date-family bugfix scenario — the agent has to connect “production stalls under load” to a specific blocking- call class and propose the smallest async migration. Both agents miss pieces of that. - plan up 14pp / 8pp.
plan-01-hardcoded-local-pathlands cleanly — both agents quote the literal path and split the remediation acrossos.homedir()/process.env/ relative paths cleanly. The signal: portability migrations are an agent strength, not a weakness.
Result transcripts and rubric scores at
evals/results/0.7.15/, replayable
against future scoring tweaks via pnpm run evals:replay.
What’s not in 0.8.0
Section titled “What’s not in 0.8.0”- No schema bump.
schema_versionstays at"0.1.0". Existing consumers unchanged. - No new commands. CLI surface byte-equivalent to 0.7.5.
- No type checker in
language-js. The naming-tier v1 stays syntactic — aliased and generic type annotations silently skip. v2 (graduating to type info via rawtsAPI) is on the 0.9.0 candidate list. - No
image-sizedependency. The icon-size detector uses a ~80-line in-tree header parser covering PNG / GIF / JPEG. WebP / AVIF support deferred to 0.9.0 with the dependency. - No LLM-assisted detector modes. Evals call LLMs; detectors stay deterministic.
- No Python. Still deferred per PRD §26.
Deferred to 0.9.0+
Section titled “Deferred to 0.9.0+”Three detectors emerged from research with sharp framings but required either type info or speculative evidence:
date_equality_misuse—dateA === dateBbetween two Date parameters. Syntactic v1 missed too many real cases (function parameters typedDatearen’t nameddate/d). Re-evaluate with type info.exported_stop_word_name—data/Manager/Helperexported at module top level. Exports-only prototype found zero hits across 500+ files — the signal lives at parameter and local-variable scope, where ESLint’sid-denylistalready sits.dead_exportwith edit-distance variant — a dead export whose name is within edit distance 1 of a live export (“agent picks wrong-but-close name”). Plausible but unproven; needs eval-run evidence before committing detector design.
Plus the v2 of singular_plural_type_mismatch once type info
lands.
Upgrading
Section titled “Upgrading”npm install -g crimes@0.8.0crimes --version # crimes@0.8.0npx crimes scan .If you have crimes.config.json and want to tune the new
detectors, the configurable surface is documented in
docs/configuration.md:
thresholds.assetWeight.{lowKb, mediumKb, highKb}—oversized_rasterthresholds.assets.include/assets.exclude— asset discovery scope.detectors.options.<id>— per-detector exemption values for every 0.8.0 detector with allowlist surface.
If your CI pipeline does --fail-on high on crimes scan or
crimes baseline check, new findings of severity ≥ high will gate
the build. Run crimes scan --changed --fail-on high on the
upgrade PR to see what would trip — or crimes baseline save to
pin the current state and only fail on regressions.
Notable links
Section titled “Notable links”docs/finding-types/structural.md— every date-family, naming-tier, and hot-path / portability detector documented with example evidence, severity ladder, and per-project exemption shape.docs/finding-types/assets.md— the new asset family page, plus the asset pipeline contract.docs/configuration.md— everycrimes.config.jsonknob, including the newthresholds.assetWeightandassets.include/excludesections..planning/0.8.0-extended-lens.md/.planning/0.8.0-RESEARCH.md— research, decisions, and ship-vs-defer rationale for every candidate detector considered.evals/README.md— eval harness docs (versioning policy, scenario↔fixture coverage discipline).evals/results/0.7.15/— the consolidated baseline rolled into this release.