Skip to content

gate: cf-import-contract — layering direction under contract#2

Merged
Antawari merged 4 commits into
mainfrom
gate/dip
Jun 11, 2026
Merged

gate: cf-import-contract — layering direction under contract#2
Antawari merged 4 commits into
mainfrom
gate/dip

Conversation

@Antawari

Copy link
Copy Markdown
Contributor

cf-import-contract — layering direction under contract

New gate in the battery: every consumer repo commits ONE exhaustive import contract (import-linter form, [tool.importlinter] in pyproject.toml) plus an ignore-count baseline, and the gate holds it in three passes — contract-file lint, lint-imports at the kit-pinned version (import-linter==2.11, exact pin so the parsed oracle cannot drift), and a companion AST scan for module-level dynamic imports the static contract cannot see.

Budget

  • Enumerated ignores only, ratchet shrink-only. ignore_imports entries are named edges, never wildcards (CONTRACT_LINT_WILDCARD); their count ratchets against the committed import-contract-baseline.json ({"ignored_imports": N}) and an absent baseline means zero, never a free pass (CONTRACT_LINT_RATCHET).
  • Stale ignores self-clean. unmatched_ignore_imports_alerting is pinned to "error" — an ignore entry matching no real edge fails (CONTRACT_STALE_IGNORE), so the baseline can only shrink.
  • Pinned settings may not be loosened. exclude_type_checking_imports = true fails the contract file itself (CONTRACT_LINT_PINNED_CONFIG).
  • Exhaustiveness. Every top-level package under the resolved source root must be named in some contract clause (CONTRACT_LINT_EXHAUSTIVENESS) — kills rename-evasion and vacuity.
  • Exit codes: 0 clean · 1 violation · 2 the gate itself could not run (typed GateError on stderr — environment trouble is never a violation verdict).
  • Honest scope is disclosed in the gate docstring: this is a layering-direction proxy; abstraction quality stays review-tier, contract content is review-gated at mount.

Control rods (eleven, through the CLI's parsed-output surface)

R1 module-level upward edge → CONTRACT_BROKEN with the named edge + line · R2 enumerated ignore entry passes with the entry printed · R3 packages with no contract → CONTRACT_MISSING · R4 function-body deferred upward import still fails · R5 TYPE_CHECKING-guarded upward import still fails · R6 wildcard ignore fails · R7 unnamed top-level package fails exhaustiveness · R8 module-level dynamic import fails · R9 stale ignore entry fails · R10 exclude_type_checking_imports = true fails · R11 uninstalled src-layout package is a typed config error (exit 2), never a violation verdict.

Self-run (the gauge submits to its own gate)

Pre-registered prediction: the kit would fail its own new gate. Confirmed verbatim:

CONTRACT_MISSING: top-level package(s) cf_quality carry no committed import contract ([tool.importlinter] in pyproject.toml)

Resolution: fixed, not baselined (the gate's only baseline surface is the ignore-count ratchet; a missing contract has no baseline path). The kit now commits its own contract — the six gate modules sit above repo_config above errors — and self-ci runs cf-import-contract --root .. The contract was refuted live: an injected errors -> repo_config edge draws CONTRACT_BROKEN: cf_quality.errors -> cf_quality.repo_config (l.86); reverting restores green.

Verification

  • Suite: 296 passed (295 from the gate build + 1 new clean-wheel regression test).
  • Full battery green, no-cache: ruff check · ruff format · sticky · file-budget · recursion · exemptions · import-contract · mypy (0 errors).
  • Clean-wheel harsh oracle (non-editable install, no source tree): CONTRACT_MISSING fixture exits 1 with the verdict; a contracted clean fixture exits 0; an injected upward edge exits 1 with the named edge — and the new packaging test pins the verdict path.
  • Also caught and fixed in review: ruff I001/format drift in the gate's test battery, present at the previous commit but masked by a stale ruff cache.

Open fork for the maintainer

cf-import-contract is wired into self-ci only. Mounting it into the reusable quality-gate.yml would immediately fail every consumer repo that has not yet committed a contract — that rollout (and the per-repo contract content review) is a maintainer decision, deliberately not taken here.

Do not merge — pending maintainer review.

Antawari and others added 4 commits June 11, 2026 01:55
…(RED: gate not yet built)

Failing-first battery for cf-import-contract, the layering-direction gate:

- PASS 1 contract-file lint (before lint-imports): wildcard ignore_imports
  entries fail; ignore count ratchets shrink-only against a committed
  import-contract-baseline.json (absent = zero); every top-level package
  under the source root must be named in a contract clause (exhaustiveness);
  pinned settings may not be loosened (exclude_type_checking_imports=true,
  unmatched alerting downgraded).
- PASS 2 lint-imports at the kit-pinned version: CONTRACT_BROKEN with named
  edge + line; enumerated ignore entries pass with the entry printed;
  stale ignores fail CONTRACT_STALE_IGNORE; packages-with-no-contract fail
  CONTRACT_MISSING; environment/config failures (uninstalled src-layout
  package, unreadable config or baseline) surface as typed
  CONTRACT_CONFIG_ERROR exit 2, never as a violation verdict.
- PASS 3 companion AST scan: module-level importlib.import_module /
  __import__ inside contract-protected layers fail CONTRACT_DYNAMIC_IMPORT;
  function-body dynamic loading is out of scope by design.

Control rods R1..R11 built inline as fixtures; kit wiring pinned (console
script cf-import-contract, import-linter== dev pin, blessed
templates/import-contract.toml carrying the pinned settings, honest-scope
docstring disclosure). All behaviors anchored against import-linter 2.11
observed output (edge form 'core -> tenants.acme (l.1)'; unmatched-ignore
default alerting=error; 'Could not find package' on uninstalled src layout).

Suite state at this commit: 262 passed, this file RED
(ModuleNotFoundError: cf_quality.import_contract).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…5 passed)

Implements the three-pass import-contract gate against the committed
failing-first battery (33 tests, all green; full suite 295 passed):

- PASS 1 lint_contract: wildcard ignore_imports fail CONTRACT_LINT_WILDCARD;
  ignore count ratchets shrink-only against import-contract-baseline.json
  (absent = zero) as CONTRACT_LINT_RATCHET; every top-level package under the
  source root must be named in a contract clause
  (CONTRACT_LINT_EXHAUSTIVENESS); pinned settings may not be loosened
  (CONTRACT_LINT_PINNED_CONFIG). Unreadable contract/baseline raise typed
  CONTRACT_CONFIG_ERROR. Pass-1 verdicts land BEFORE lint-imports runs.
- PASS 2 subprocess run of the kit-pinned lint-imports (cwd = repo root so
  flat layouts resolve; --no-cache). Verdict classification anchored against
  import-linter 2.11 observed output: BROKEN section edges re-emitted under
  CONTRACT_BROKEN ('core -> tenants.acme (l.1)'); 'No matches for ignored
  import' is CONTRACT_STALE_IGNORE; anything verdict-less (e.g. "Could not
  find package" on an uninstalled src layout) is typed CONTRACT_CONFIG_ERROR
  exit 2, never a violation. Honored enumerated ignores are printed on pass.
- PASS 3 scan_dynamic_imports: module-level importlib.import_module /
  __import__ inside contract-protected layers fail CONTRACT_DYNAMIC_IMPORT;
  function bodies are out of scope by design (iterative AST walk, no descent
  into function scopes).

Kit wiring: console script cf-import-contract registered; import-linter==2.11
pinned exactly in [dev] (the pass-2 oracle may not drift); blessed
templates/import-contract.toml ships the pinned settings; honest
layering-direction-proxy scope disclosed in the module docstring.

The kit submits to its own gauge: the new S603 subprocess suppression is
registered in exemptions.json with frozen_count bumped 1 -> 2 (visible
ratchet decision, approver pending maintainer ratification); ruff check +
format, mypy (0 errors), cf-file-budget (399 < 500 lines),
cf-recursion-check, cf-exemptions all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… format) — present at the previous commit but masked by a stale ruff cache; surfaced by a no-cache battery run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ACT_MISSING)

Running cf-import-contract against this repo itself reported
CONTRACT_MISSING for cf_quality — the gauge did not yet carry the
contract it ships. Fix, not baseline (the gate's only baseline surface
is the ignore-count ratchet; a missing contract has no baseline path):

- pyproject.toml: the kit's own [tool.importlinter] contract — the six
  gate modules sit above repo_config above errors, pinned settings per
  templates/import-contract.toml. Refuted against the live oracle: an
  injected errors -> repo_config edge draws CONTRACT_BROKEN with the
  named edge + line; reverting restores green.
- self-ci: the battery now runs cf-import-contract --root . so the
  self-policing survives in CI, not just in one verifier run.
- test_packaging.py: the clean-wheel harsh oracle extended to the new
  console script — a non-editable install still delivers the
  CONTRACT_MISSING verdict (exit 1), never an ImportError crash.

Suite: 296 passed. Battery: ruff/format/sticky/file-budget/recursion/
exemptions/import-contract/mypy all green, no-cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Antawari Antawari marked this pull request as ready for review June 11, 2026 14:59
@Antawari Antawari merged commit af1d086 into main Jun 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant