Dependency findings
Dependency findings consume the import graph that crimes builds
once per scan. They flag architecture-shaped problems that
file-by-file detectors can’t see: cycles, deep cross-package reaches,
modules with too many neighbours, and architecture.layers rule
violations.
For the wire format, see docs/json-schema.md.
For the agent workflow that consumes findings, see
docs/agent-usage.md.
What ships
Section titled “What ships”Finding.type | Charge | Severity range | Confidence |
|---|---|---|---|
layer_violation | Layer Border Crossing | medium-high | 0.85 |
circular_dependency | Tangled Imports | medium-high | 0.90 |
deep_import | Deep Import Abuse | low-medium | 0.70-0.85 |
high_fan_in_fan_out | Crowded Module | low-medium | 0.70-0.80 |
dangling_template_reference | Dangling Template Reference | medium | 0.75-0.80 |
overridden_variable | Overruled Variable | low-medium | 0.80-0.85 |
All six use the standard Finding shape and add an imports_limited
flag to ScanReport when the graph hit its performance budget: same
shape as HotspotsReport.history_limited. Crowded Module counts code
edges only: a component importing its stylesheet, or a template including
a script, is not the coupling it measures.
Layer Border Crossing (layer_violation)
Section titled “Layer Border Crossing (layer_violation)”What it detects. An import that crosses a forbidden boundary in
crimes.config.json architecture.layers + architecture.rules.
The config defines named layers by file glob and explicit
from → cannotImport rules; the detector emits one finding per
imported file that violates a rule.
Example evidence.
file src/ui/PricingPage.tsx imports src/db/billing.tslayer "ui" cannotImport from "db"matching rule: { from: "ui", cannotImport: ["db", "infrastructure"] }Why it matters. Architecture rules are usually enforced
informally: a tribal “UI components don’t talk to the database”
norm. Coding agents have no access to that norm, so the first edit
that pulls a db query into a component looks reasonable in isolation
and gets merged. Encoding the rule in crimes.config.json makes the
boundary review-able.
Configuration knobs. architecture.layers[].name /
architecture.layers[].pattern (glob, e.g. "src/ui/**") and
architecture.rules[].from / architecture.rules[].cannotImport.
Layers default to “no rules”, so adding the section is opt-in. See
docs/configuration.md.
Tangled Imports (circular_dependency)
Section titled “Tangled Imports (circular_dependency)”What it detects. Strongly-connected components in the import
graph with ≥ 2 files. A 2-file cycle (a.ts ↔ b.ts) is flagged at
medium severity; ≥ 3 files at high severity.
Example evidence.
2-file cycle: src/billing.ts → src/coupon.ts → src/billing.tsimports at: billing.ts:14 → coupon.ts; coupon.ts:7 → billing.tsWhy it matters. Circular imports usually mean the two files share too much state. Beyond bundler quirks (TDZ errors, hoisting oddities), the cycle is a code-review smell: every PR that touches either file has to consider the other. Breaking the cycle into a shared types module or a one-way dependency makes the relationship explicit.
Suggested fix. Identify the genuinely shared declarations (types, constants) and move them to a third file both can import. If the cycle is between sibling modules, consider whether one should own the other rather than peer with it.
Deep Import Abuse (deep_import)
Section titled “Deep Import Abuse (deep_import)”What it detects. Imports that reach more than n segments deep
into another package’s source tree: typically
@org/pkg/src/internal/util/format. The detector applies to
workspace-relative imports too.
Example evidence.
import path: ../../../../core/src/internal/format6 segments deepcrosses workspace boundary into packages/coreWhy it matters. Deep imports bypass a package’s public surface and pin the consumer to internal implementation details. The next refactor of the internal file breaks every consumer that reached in, even though nothing about the package’s public API changed.
Suggested fix. Add the function to the package’s public
index.ts and import it from there. If the function isn’t ready to
be public, the import was probably premature: find a cheaper
surface to depend on.
Crowded Module (high_fan_in_fan_out)
Section titled “Crowded Module (high_fan_in_fan_out)”What it detects. Files with unusually high fan-in (number of
files importing this one) and/or local fan-out (number of resolved local
imports). With at least five graph files, the detector uses the repository’s
95th percentile, with a floor of five edges. The 99th percentile has a floor
of eight and can promote the finding to medium. Mostly type-only fan-in stays
low unless fan-out independently warrants promotion. These cutoffs are
currently implementation constants, not configuration options.
Example evidence.
fan-out: 5 imports (p95 cutoff: 5, p99: 8)Why it matters. Many importers can spread a change; many imports expose
the file to changes in its dependencies. These directions have different
implications. scores.blast_radius measures incoming direct/transitive
importers, not the number of dependencies the file imports. A test that
imports five modules and has no importers can have zero blast radius.
Suggested action. Read the relevant consumers and dependencies before editing. Shared hubs and integration tests can be connected by design; restructuring needs an evidenced problem beyond the edge count. Preserve useful regression checks. Removing assertions or moving them into a temporary command solely to clear this finding loses durable coverage. Review and record intentional connectivity through triage when appropriate.
Dangling Template Reference (dangling_template_reference)
Section titled “Dangling Template Reference (dangling_template_reference)”What it detects. JSP references that resolve to nothing, found while the JSP pack builds its part of the graph (0.31.0). Four claims:
missing_include: a literal<%@ include %>,<jsp:include>or<jsp:forward>target that does not exist;missing_tag: a tag whose taglib prefix is bound to atagdirwith no matching.tagfile;missing_taglib: ataglib urithat names a.tldpath that does not exist;tag_contract: a call to a tag file or TLD tag that omits an attribute declaredrequired="true", or passes one the tag does not declare (unless it declaresdynamic-attributes). Attributes supplied as<jsp:attribute name="…">children count as passed.
Example evidence.
reference: `/WEB-INF/includes/moved-away.jspf` (line 2)expected at: web/src/main/webapp/WEB-INF/includes/moved-away.jspfthe directory exists; the file does notWhy it matters. JSP is translated on the first request, not when the application is built. A moved fragment or a renamed tag attribute compiles, passes every Java test, and then fails each request for the page. An agent that renames an attribute in a tag file has no build step to tell it which callers it broke.
What it deliberately does not claim. A reference whose directory is
missing is read as a resource supplied from outside the repository — a
library jar’s META-INF/resources, a WAR overlay — and is not reported.
On a 1,089-template estate every unresolved include had that shape. A
relative path in a fragment, or in a library page, is resolved from the
pages that include it, and not judged at all when nothing in the
repository includes it. Paths computed at request time (${…},
<%= … %>) are not resolved. A taglib URI is a logical name (/struts-tags
is one) unless it names a .tld file. Nothing about scriptlets, escaping
or markup style is reported.
Suggested action. Point the reference at the file that exists, or
remove it; for a contract break, pass the attributes the tag requires and
only those it declares. crimes context <tag file> lists the pages that
call a tag, including through prefixes declared in shared fragments.
Overruled Variable (overridden_variable)
Section titled “Overruled Variable (overridden_variable)”What it detects. A top-level LESS variable definition that a later
definition overrides in every bundle that compiles it (0.31.0). LESS
evaluates variables lazily, and the last definition in a scope wins for
every use — including uses written above it. Bundles are LESS files no
other LESS file imports; each one’s evaluation order is simulated from its
imports in source order. Claims: different_value (medium — an override
that never applies) and same_value (low — a redundant copy).
Example evidence.
defined here: `@asset-path: '../../'` (line 5)overridden by: `@asset-path: '../'` at styles/constants.less:13bundles: styles/standalone.less, styles/print.less, styles/embed.lessno bundle that compiles this file uses this definitionWhy it matters. The file shows a value that is never used. An agent asked to change it edits the line it can see, and nothing changes — or, for an override written before the import that undoes it, the intended value silently never took effect.
What it deliberately does not claim. A base value that one bundle
overrides and another uses — the theme pattern — is live and not
reported. Imports inside a block are not followed: what they define is
scoped to the block. (css) imports and .css paths are not evaluated.
Vendored and generated stylesheets are not read at all, so customising a
vendored framework’s variables is not reported against it. A bundle whose
simulation exceeds its budget ((multiple) imports can re-evaluate files
without bound) is abandoned and counts every definition it reached as live.
Suggested action. Decide which value is intended. Move an override after the import that redefines it, or delete the dead definition.