Skip to content

ci(lint): enforce architecture contracts with import-linter - #1813

Closed
zoroyihan7 wants to merge 1 commit into
mainfrom
ci/import-linter-contracts
Closed

zoroyihan7 wants to merge 1 commit into
mainfrom
ci/import-linter-contracts

Conversation

@zoroyihan7

Copy link
Copy Markdown
Contributor
  • Description: Enforce the package dependency direction as a CI gate. Adds .importlinter with seven import-linter contracts and runs them in a new import-linter (architecture contracts) job in lint.yml (import-linter pinned to 2.15, PYTHONPATH=src lint-imports --no-cache, no project install) and as a local pre-commit hook. The style guide gains a "Dependency direction" subsection under Module structure that lists the contracts.

    Contract Rule Baseline
    hl-top-layers hyperloom.cli > orchestrator > agents > inference_optimizer > common 56 exact upward imports, all from hyperloom.inference_optimizer.cli (an entry point that sits in the core layer)
    kf-only-common kernelforge imports only hyperloom.common from hyperloom none
    common-is-leaf hyperloom.common imports nothing else from the project none
    agents-independent hyperloom.agents.{critic,framework,quantization} do not import each other none
    kf-backends-independent the nine kernelforge.kernel_backends.* do not import each other none
    kf-entry-protected hyperloom.agents, inference_optimizer, common do not import kernelforge directly none
    acyclic no import cycle between sibling packages under hyperloom / kernelforge 155 exact cycle-breaking imports (the set import-linter nominates on main today)

    Both baselines set unmatched_ignore_imports_alerting = error, so they can only shrink: removing a baselined import without deleting its line fails the check. Tests and if TYPE_CHECKING: imports are exempt; imports inside functions are checked. Measured on main 880c167: 7 kept, 0 broken, about 0.6 s wall clock.

    Admin action needed for this to block merges: ruleset "main" has no required status checks today. Mark import-linter (architecture contracts) (job in workflow Lint) as a required check. Note that lint.yml has paths-ignore for Markdown/docs, so a docs-only PR never produces this check; requiring it directly would leave such PRs pending (the same is true of ruff today). Either require it through an always-run aggregate job or accept that limitation.

    Independent of ci: code-metrics gate on touched files with baseline-free checks; fold in import-linter, PR hygiene and ruff principle rules #1812 (code metrics gate): no shared files except docs/contributing/style-guide.md, where this PR only touches the Module structure section and the Related configuration files table.

  • Linked issue(s): none

  • Tests: no Python code changed. Reverse proofs, each on a scratch copy (not committed):

    • unmodified tree: Contracts: 7 kept, 0 broken, rc 0
    • hyperloom.common.timeutil lazily imports hyperloom.orchestrator.loop.coordinator: rc 1, hl-top-layers, common-is-leaf ("hyperloom.common is not allowed to import hyperloom.orchestrator"), agents-independent (indirect chain via common) and acyclic BROKEN
    • kernelforge.durable_io imports hyperloom.orchestrator.state: rc 1, only kf-only-common BROKEN ("kernelforge is not allowed to import hyperloom.orchestrator")
    • hyperloom.agents.critic imports hyperloom.agents.framework.kb: rc 1, only agents-independent BROKEN
    • kernel_backends.aiter imports kernel_backends.triton: rc 1, only kf-backends-independent BROKEN
    • hyperloom.inference_optimizer.gpu_types imports kernelforge.durable_io: rc 1, only kf-entry-protected BROKEN ("hyperloom.inference_optimizer is not allowed to import kernelforge")
    • orchestrator.state.task_registry lazily imports orchestrator.loop.coordinator: rc 1, only acyclic BROKEN ("No cycles are allowed in hyperloom.orchestrator ... .state -> .loop")
    • removed the baselined import inference_optimizer.cli.quantization -> orchestrator.phases.quantization_request_handlers: rc 1, "No matches for ignored import hyperloom.inference_optimizer.cli.quantization -> hyperloom.orchestrator.phases.quantization_request_handlers"
    • removed the baselined common.llm_config -> common.claude_oneshot imports: rc 1, "No matches for ignored import hyperloom.common.llm_config -> hyperloom.common.claude_oneshot"
    • pre-commit run import-linter --all-files: Passed; with the agents.critic -> agents.framework mutation staged: Failed with agent packages are independent BROKEN
    • actionlint .github/workflows/lint.yml, yamllint (repo config), check-yaml, codespell, reuse lint: clean
  • Size/complexity triggers crossed: none (config, workflow and docs only; .importlinter is long because it lists the 211 baseline lines explicitly)

  • If this simplifies or refactors: n/a

  • Observable effect: contributors see a new import-linter (architecture contracts) check on PRs and pushes to main; it fails, naming the contract and the import chain, when a PR adds an import that points up a layer, couples two agents or two kernel backends, makes common or kernelforge depend upward, or closes a new cycle between sibling packages. Until now these rules lived only in review and drifted silently (the layering violations and cycles in the baselines are what that cost).

  • Breaking changes: no

  • PR addresses single concern: yes

  • Root cause is upstream (Magpie/TraceLens/GEAK/IntelliKit/AgentKernelArena), ticket filed: n/a

Known limits: import-linter is static, so importlib / string-path imports are invisible; the baselines are per module pair, so another import between two modules already listed is not caught.

🤖 Generated with Claude Code

Add .importlinter with seven contracts: hyperloom top-level layers,
kernelforge depends on hyperloom.common only, hyperloom.common is a
leaf, agent packages and kernel backends are independent, only the
orchestrator and hyperloom.cli import kernelforge directly, and no
cycles between sibling packages.

Existing violations are recorded as exact ignore_imports lines (56
upward imports of hyperloom.inference_optimizer.cli, 155 cycle
breakers) with unmatched_ignore_imports_alerting = error, so the
baselines can only shrink.

Run it as a hard gate in lint.yml (import-linter==2.15 pinned) and as
a local pre-commit hook; document the rules in the style guide.

Co-Authored-By: Claude <noreply@anthropic.com>
@zoroyihan7

Copy link
Copy Markdown
Contributor Author

Superseded by #1812, which now carries this change.

@zoroyihan7 zoroyihan7 closed this Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New capability or improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant