Context
fathom validate (src/fathom/cli.py:120-173) parses each YAML document and runs per-document Pydantic validation via validate_document (delegated from _validate_document at cli.py:109). It never cross-references files. fathom compile (cli.py:254) is likewise file-by-file. There is no cross-construct analysis anywhere in src/fathom/ — a grep for unreachable|dead.rule|shadow|conflict|overlap returns only unrelated hits (CSS box-shadows, a grpc_server.py:169 comment, a studio/panels.py:442 doc comment).
As a result, authoring mistakes that span files pass validate cleanly and only surface as opaque CLIPS build errors at load time. The runtime contract confirms this: Engine.load_rules (engine.py:652) only checks that a ruleset's module is registered (engine.py:679); every other defect (a rule pattern referencing a slot the template doesn't declare, a focus order naming a missing module, a condition using a never-bound variable) falls through to _safe_build and raises a cryptic CLIPS error — the same class of footgun behind the Engine.reset() CSTRCPSR bug.
The data model already supports the needed cross-checks: FactPattern.template + ConditionEntry.slot/.bind (models.py:165, models.py:101), TemplateDefinition.slots (models.py:86), RulesetDefinition.module (models.py:285), ModuleDefinition (models.py:304), and the Engine registries template_registry/module_registry/rule_registry (engine.py:222-234).
Task
Add fathom lint <rules_path> that loads all constructs (parse + register, without building CLIPS) and reports cross-construct issues:
- Rule LHS pattern references a template/slot not declared in any template (cross-check
FactPattern.template + ConditionEntry.slot against template_registry).
focus order names a module with no rules or an undeclared module (against module_registry).
- Rule condition uses a variable never declared via a
bind.
- Optional: shadowed rules (same module, identical normalized LHS) and clearly-unreachable rules (LHS constrains a slot to a value outside the template's allowed enum).
Emit findings with file + rule + slot names, classified warning vs error; support --json; exit non-zero on errors only.
Where
src/fathom/cli.py — new lint command (alongside validate/compile).
src/fathom/compiler.py / src/fathom/engine.py — reuse parse + registry plumbing; the registries needed already exist on Engine.
tests/test_cli.py — new cases + fixture packs with intentional defects.
docs/reference/cli/index.md (+ a new docs/reference/cli/lint.md) — document the command.
Acceptance criteria
Note: distinct from #82 (hot-reload + signed bundle impact analysis), which concerns runtime bundle swaps, not author-time static analysis.
Size: L
Context
fathom validate(src/fathom/cli.py:120-173) parses each YAML document and runs per-document Pydantic validation viavalidate_document(delegated from_validate_documentatcli.py:109). It never cross-references files.fathom compile(cli.py:254) is likewise file-by-file. There is no cross-construct analysis anywhere insrc/fathom/— a grep forunreachable|dead.rule|shadow|conflict|overlapreturns only unrelated hits (CSS box-shadows, agrpc_server.py:169comment, astudio/panels.py:442doc comment).As a result, authoring mistakes that span files pass
validatecleanly and only surface as opaque CLIPS build errors at load time. The runtime contract confirms this:Engine.load_rules(engine.py:652) only checks that a ruleset'smoduleis registered (engine.py:679); every other defect (a rule pattern referencing a slot the template doesn't declare, afocusorder naming a missing module, a condition using a never-bound variable) falls through to_safe_buildand raises a cryptic CLIPS error — the same class of footgun behind theEngine.reset()CSTRCPSR bug.The data model already supports the needed cross-checks:
FactPattern.template+ConditionEntry.slot/.bind(models.py:165,models.py:101),TemplateDefinition.slots(models.py:86),RulesetDefinition.module(models.py:285),ModuleDefinition(models.py:304), and theEngineregistriestemplate_registry/module_registry/rule_registry(engine.py:222-234).Task
Add
fathom lint <rules_path>that loads all constructs (parse + register, without building CLIPS) and reports cross-construct issues:FactPattern.template+ConditionEntry.slotagainsttemplate_registry).focusorder names a module with no rules or an undeclared module (againstmodule_registry).bind.Emit findings with file + rule + slot names, classified warning vs error; support
--json; exit non-zero on errors only.Where
src/fathom/cli.py— newlintcommand (alongsidevalidate/compile).src/fathom/compiler.py/src/fathom/engine.py— reuse parse + registry plumbing; the registries needed already exist onEngine.tests/test_cli.py— new cases + fixture packs with intentional defects.docs/reference/cli/index.md(+ a newdocs/reference/cli/lint.md) — document the command.Acceptance criteria
focusnames a missing module reports it.--jsonoutput parses; non-zero exit only on errors (warnings exit 0).uv run ruff check,uv run mypy src/, anduv run pytestpass.Note: distinct from #82 (hot-reload + signed bundle impact analysis), which concerns runtime bundle swaps, not author-time static analysis.
Size: L