Unclassified-module reporting

Updated Oct 18, 2026

Coverage reporting watches edges. A module no configured group claims is a different gap: every import it makes or receives sits outside group-based enforcement, so a package added without a matching prefix can drift unchecked while each existing check still passes. unclassified_modules reports the module itself:

[tool.hecate]
unclassified_modules = "error"

The setting accepts three values:

Mode Behaviour
ignore Report nothing. This is the default and preserves earlier output.
warn Report each unclassified module as a warning; the exit code stays 0.
error Report each unclassified module as an error; the check exits 1.

A finding names the module and says why it was reported:

HEC002 error sample.newthing: no architecture group matched this module

Module findings carry the fixed rule identifier HEC002, so a consumer can tell them apart from edge diagnostics, which use the configured default_rule_id. For that reason HEC002 is reserved: a configuration that sets default_rule_id = "HEC002" is rejected as invalid.

What counts as a discovered module

  • Every *.py file beneath a configured package root, including __init__.py and files with no imports at all. A new package is reported on its first run, before it grows its first import.
  • Classification reuses the ordered, first-match rule described under group tables. A dotted prefix matches at a boundary, so a group for sample.domain does not classify a lookalike sample.domainx.
  • A package __init__.py is claimed when any configured prefix lies beneath it. An initializer imports and re-exports what its submodules bind, so groups for sample.domain already cover the sample.domain package without a recursive group for sample. That containment stops at the initializer: sample.newthing is a plain module and is still reported.
  • External imports are not modules for this purpose. They continue to follow include_external_packages and the external prefixes groups declare.

Reading the output

Text output shows module findings whenever the mode is warn or error, because the configuration already asked for them. Under warn the pass notice still appears, since warnings do not change the exit code:

hecate: architecture check passed
HEC002 warning sample.newthing: no architecture group matched this module

Under error there is no pass notice, and the exit code is 1.

JSON output carries an unclassified_modules array in both reporting modes, sorted by module name. The array is present and empty when the tree is fully classified, which a consumer reads as "evaluated and clean" rather than as "not reported":

{
  "ok": false,
  "unclassified_modules": [
    {
      "message": "no architecture group matched this module",
      "module": "sample.newthing",
      "rule_id": "HEC002",
      "severity": "error",
      "source_path": "/repo/sample/newthing/__init__.py"
    }
  ]
}

Under ignore the key is absent entirely, so the default payload is unchanged. The exit code follows from the findings: warn leaves it at 0, error makes it 1.

A module finding does not change how the edges around that module are classified. An import edge with an unclassified endpoint stays outside group-based enforcement in all three modes; the finding exists to make that gap visible. See coverage reporting for how such edges are reported by strict mode.