Files
kandev/docs/decisions/2026-08-01-architecture-lint-budgets.md
T
2026-08-03 22:40:29 +01:00

4.6 KiB

ADR-2026-08-01-architecture-lint-budgets: Architecture Lint Budgets and Compatibility Expiry

Status: accepted (amended 2026-08-02) Date: 2026-08-01 Area: infra

Context

Kandev documents important package and state-ownership boundaries in AGENTS.md and ADRs, but documentation alone does not prevent a fast-moving codebase from adding new violations. Existing runtime, task, and frontend state-composition debt is too large to fail all at once. Compatibility aliases and fallbacks also need consistent ownership and a concrete removal trigger.

This is internal CI and repository-process behavior. It changes no product behavior, so no product spec is needed.

Decision

Accepted architecture boundaries may be enforced with deterministic, dependency-free repository checks backed by explicit reviewed finding allowlists under config/architecture-lint/. Existing debt is grandfathered by exact path and import or source-marker identity. New findings fail, removed findings make their exemptions stale, and CI compares each baseline with the target branch so its reviewed allowlist may only shrink. Normal lint never rewrites baselines.

Each rule owns one scanner module under scripts/architecture_lint/rules/, one focused test module under scripts/architecture_lint_tests/, and one baseline JSON file. A small explicit registry is the only central rule list. Shared runner modules own repository discovery, baseline comparison, diagnostics, and compatibility-ledger validation and contain no rule-specific scan conditions. A rule whose baseline is absent from the target branch may bootstrap its initial reviewed entries; after merge, that per-rule baseline becomes shrink-only.

Architecture-lint executable code is repository tooling owned under scripts/; reviewed baselines and the compatibility ledger are durable repository configuration owned under config/. The file under .github/workflows/ is only a CI adapter that invokes the same codebase-owned entrypoint used by Make and pre-commit. GitHub-specific directories do not own the linter's implementation or policy data.

The initial checks enforce the documented agent-runtime import seam, the rule that shared task code must not depend on Office models, and the narrow root Zustand composition boundary. Diagnostics must identify the rule, source location, and intended replacement seam.

Intentional compatibility exceptions are registered in config/architecture-lint/compatibility-ledger.json. Every entry requires a stable identifier and source locator, reason, owner, introduction date or version, removal condition, and target removal date or version. Date-based entries fail after expiry, and entries fail when their tracked path or marker disappears. New compatibility behavior must register explicitly; broad keyword discovery is not part of this foundation.

Consequences

Pull requests cannot silently increase these known forms of architecture debt, while existing cleanup can land incrementally. Baseline and ledger edits become visible review decisions, and contributors receive migration guidance at the violating line. Source refactors that move an allowed finding or compatibility marker require an intentional metadata update, which adds some maintenance but prevents dead exemptions from accumulating.

Additional rules should be added only after their canonical contract is accepted. This decision does not establish an aggregate architecture score or speculative checks for transport catalogs, typed events, or generic compatibility keywords.

Alternatives Considered

  • Documentation-only enforcement. Rejected because it depends on every contributor and reviewer remembering every boundary on every change.
  • Immediately fail all current violations. Rejected because it would couple this safety net to a large cleanup and make adoption impractical.
  • Use a raw aggregate architecture score. Rejected because deleting one violation could hide a different newly introduced violation.
  • Allow permanently growing exception lists. Rejected because allowlists without shrink-only comparison turn temporary debt into an unbounded bypass mechanism.
  • Keep all scanners, tests, and findings in central files. Rejected because progressive migrations would make the linter and baseline increasingly difficult to review and would create merge-conflict hotspots between unrelated architecture rules.
  • Place linter implementation and policy under .github/. Rejected because the checks are repository tooling used locally, in pre-commit, and in CI; GitHub Actions is only one caller and should not own the code or durable configuration.