From 9231f7d951db76d19a613b85c9402c3bf9e54333 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Fri, 3 Jul 2026 02:03:11 +0800 Subject: [PATCH 01/11] chore: add import-linter baseline gate Assisted-by: Codex:GPT-5.4 --- Makefile | 6 + api/dev/import_linter_baseline.json | 4 + api/dev/lint_imports_baseline.py | 287 ++++++++++++++++++ .../commands/test_lint_imports_baseline.py | 272 +++++++++++++++++ 4 files changed, 569 insertions(+) create mode 100644 api/dev/import_linter_baseline.json create mode 100644 api/dev/lint_imports_baseline.py create mode 100644 api/tests/unit_tests/commands/test_lint_imports_baseline.py diff --git a/Makefile b/Makefile index 801c59cc281a37..58f3f9d06d6af0 100644 --- a/Makefile +++ b/Makefile @@ -80,6 +80,7 @@ lint: @uv run --project api --dev ruff check --fix ./api @$(MAKE) api-contract-lint @uv run --directory api --dev lint-imports + @$(MAKE) api-import-baseline-lint @uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example @echo "✅ Linting complete" @@ -88,6 +89,11 @@ api-contract-lint: @uv run --project api --dev python api/dev/lint_response_contracts.py @echo "✅ Response contract lint complete" +api-import-baseline-lint: + @echo "🏗️ Checking import-linter baseline..." + @uv run --project api --dev python api/dev/lint_imports_baseline.py --baseline api/dev/import_linter_baseline.json + @echo "✅ Import baseline lint complete" + type-check: @echo "📝 Running type checks (pyrefly + mypy)..." @./dev/pyrefly-check-local $(PATH_TO_CHECK) diff --git a/api/dev/import_linter_baseline.json b/api/dev/import_linter_baseline.json new file mode 100644 index 00000000000000..9f9dfddefae718 --- /dev/null +++ b/api/dev/import_linter_baseline.json @@ -0,0 +1,4 @@ +{ + "contracts": {}, + "version": 1 +} diff --git a/api/dev/lint_imports_baseline.py b/api/dev/lint_imports_baseline.py new file mode 100644 index 00000000000000..5512d81ef48490 --- /dev/null +++ b/api/dev/lint_imports_baseline.py @@ -0,0 +1,287 @@ +"""Gate import-linter violations against a committed baseline snapshot. + +This wrapper keeps import-linter as the source of truth for architectural +contracts, then snapshots the broken direct-import edges per contract and +importer module. The default comparison mode is ``subset`` because it prevents +same-count replacements from silently regressing the architecture. A weaker +``count`` mode is also available when a team explicitly wants count-only gating. +""" + +from __future__ import annotations + +import argparse +import dataclasses +import json +import os +import sys +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal + +from importlinter import configuration +from importlinter.application import use_cases + +type ComparisonMode = Literal["subset", "count"] +type Snapshot = dict[str, dict[str, list[str]]] + +BASELINE_VERSION = 1 +DEFAULT_CONFIG_PATH = Path(__file__).resolve().parents[1] / ".importlinter" + + +@dataclass(frozen=True) +class SnapshotFailure: + contract_name: str + importer: str + baseline_count: int + current_count: int + extra_imports: tuple[str, ...] + + +def load_report(config_path: str | None = None, contract_ids: tuple[str, ...] = ()) -> Any: + """Build and return an import-linter report using the same path setup as the CLI.""" + + configuration.configure() + api_dir = str(DEFAULT_CONFIG_PATH.parent) + cwd = os.getcwd() + for candidate in (api_dir, cwd): + if candidate not in sys.path: + sys.path.insert(0, candidate) + resolved_config_path = config_path or str(DEFAULT_CONFIG_PATH) + user_options = use_cases.read_user_options(config_filename=resolved_config_path) + return use_cases.create_report( + user_options=user_options, + limit_to_contracts=contract_ids, + ) + + +def snapshot_from_report(report: Any) -> Snapshot: + """Return broken direct-import edges grouped by contract and importer module.""" + + snapshot: dict[str, dict[str, set[str]]] = {} + for contract, check in report.get_contracts_and_checks(): + if check.kept: + continue + + imports_by_importer: dict[str, set[str]] = defaultdict(set) + for importer, imported in _iter_direct_imports(check.metadata): + imports_by_importer[importer].add(imported) + + if check.metadata and not imports_by_importer: + raise ValueError( + f"Broken contract '{contract.name}' does not expose direct import edges in metadata." + ) + + if imports_by_importer: + snapshot[contract.name] = { + importer: sorted(imported_modules) + for importer, imported_modules in sorted(imports_by_importer.items()) + } + + return {contract_name: snapshot[contract_name] for contract_name in sorted(snapshot)} + + +def compare_snapshots( + current_snapshot: Snapshot, + baseline_snapshot: Snapshot, + comparison: ComparisonMode = "subset", +) -> list[SnapshotFailure]: + """Compare the current and baseline snapshots and return any regressions.""" + + failures: list[SnapshotFailure] = [] + + contract_names = sorted(set(current_snapshot) | set(baseline_snapshot)) + for contract_name in contract_names: + current_by_importer = current_snapshot.get(contract_name, {}) + baseline_by_importer = baseline_snapshot.get(contract_name, {}) + + for importer in sorted(set(current_by_importer) | set(baseline_by_importer)): + current_imports = set(current_by_importer.get(importer, [])) + baseline_imports = set(baseline_by_importer.get(importer, [])) + extra_imports = tuple(sorted(current_imports - baseline_imports)) + + if comparison == "subset": + is_failure = bool(extra_imports) + else: + is_failure = len(current_imports) > len(baseline_imports) + + if is_failure: + failures.append( + SnapshotFailure( + contract_name=contract_name, + importer=importer, + baseline_count=len(baseline_imports), + current_count=len(current_imports), + extra_imports=extra_imports, + ) + ) + + return failures + + +def load_baseline(path: Path) -> Snapshot: + """Load and validate a baseline file.""" + + payload = json.loads(path.read_text(encoding="utf-8")) + version = payload.get("version") + if version != BASELINE_VERSION: + raise ValueError( + f"Unsupported baseline version {version!r}; expected {BASELINE_VERSION}." + ) + + contracts = payload.get("contracts") + if not isinstance(contracts, dict): + raise ValueError("Baseline file must contain a 'contracts' object.") + + normalized: Snapshot = {} + for contract_name, importers in contracts.items(): + if not isinstance(contract_name, str) or not isinstance(importers, dict): + raise ValueError("Each baseline contract entry must be an object keyed by contract name.") + + normalized[contract_name] = {} + for importer, imported_modules in importers.items(): + if not isinstance(importer, str) or not isinstance(imported_modules, list): + raise ValueError("Each baseline importer entry must map to a list of modules.") + if not all(isinstance(module_name, str) for module_name in imported_modules): + raise ValueError("Each baseline imported module must be a string.") + normalized[contract_name][importer] = sorted(set(imported_modules)) + + return normalized + + +def write_baseline(path: Path, snapshot: Snapshot) -> None: + """Persist the supplied snapshot as a JSON baseline file.""" + + payload = { + "version": BASELINE_VERSION, + "contracts": snapshot, + } + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def main(argv: list[str] | None = None) -> int: + parser = build_argument_parser() + args = parser.parse_args(argv) + + baseline_path = args.baseline + current_snapshot = snapshot_from_report( + load_report(config_path=args.config, contract_ids=tuple(args.contract)) + ) + + if args.write_baseline: + write_baseline(baseline_path, current_snapshot) + _write_line(f"Wrote import baseline to {baseline_path}.") + return 0 + + baseline_snapshot = load_baseline(baseline_path) + failures = compare_snapshots( + current_snapshot=current_snapshot, + baseline_snapshot=baseline_snapshot, + comparison=args.comparison, + ) + if failures: + _print_failures(failures, comparison=args.comparison) + return 1 + + _write_line( + "Import baseline OK. " + f"Checked {sum(len(importers) for importers in current_snapshot.values())} importer entries." + ) + return 0 + + +def build_argument_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Compare current import-linter violations against a committed baseline." + ) + parser.add_argument( + "--baseline", + type=Path, + required=True, + help="Path to the committed baseline JSON file.", + ) + parser.add_argument( + "--write-baseline", + action="store_true", + help="Write the current violation snapshot to the baseline file and exit.", + ) + parser.add_argument( + "--comparison", + choices=("subset", "count"), + default="subset", + help="Comparison strategy. 'subset' is stricter and is the default.", + ) + parser.add_argument( + "--config", + help="Optional import-linter config file path.", + ) + parser.add_argument( + "--contract", + action="append", + default=[], + help="Optional contract id filter. May be passed multiple times.", + ) + return parser + + +def _iter_direct_imports(node: object, seen: set[int] | None = None): + if seen is None: + seen = set() + + if node is None or isinstance(node, (str, int, float, bool)): + return + + if dataclasses.is_dataclass(node): + yield from _iter_direct_imports(dataclasses.asdict(node), seen) + return + + if isinstance(node, dict): + marker = id(node) + if marker in seen: + return + seen.add(marker) + + importer = node.get("importer") + imported = node.get("imported") + if isinstance(importer, str) and isinstance(imported, str): + yield importer, imported + + for value in node.values(): + yield from _iter_direct_imports(value, seen) + return + + if isinstance(node, (list, tuple, set, frozenset)): + marker = id(node) + if marker in seen: + return + seen.add(marker) + + for item in node: + yield from _iter_direct_imports(item, seen) + return + + if hasattr(node, "__dict__"): + marker = id(node) + if marker in seen: + return + seen.add(marker) + yield from _iter_direct_imports(vars(node), seen) + + +def _print_failures(failures: list[SnapshotFailure], comparison: ComparisonMode) -> None: + _write_line(f"Import baseline regression detected ({comparison} mode):") + for failure in failures: + _write_line( + f"- [{failure.contract_name}] {failure.importer}: " + f"baseline={failure.baseline_count}, current={failure.current_count}" + ) + if failure.extra_imports: + _write_line(f" new imports: {', '.join(failure.extra_imports)}") + + +def _write_line(message: str) -> None: + sys.stdout.write(f"{message}\n") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/api/tests/unit_tests/commands/test_lint_imports_baseline.py b/api/tests/unit_tests/commands/test_lint_imports_baseline.py new file mode 100644 index 00000000000000..6cfc6c5c18a37d --- /dev/null +++ b/api/tests/unit_tests/commands/test_lint_imports_baseline.py @@ -0,0 +1,272 @@ +import importlib.util +import json +import sys +from dataclasses import dataclass +from pathlib import Path +from types import SimpleNamespace + + +def _load_lint_imports_baseline_module(): + api_dir = Path(__file__).parents[3] + script_path = api_dir / "dev" / "lint_imports_baseline.py" + spec = importlib.util.spec_from_file_location("lint_imports_baseline", script_path) + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +@dataclass(frozen=True) +class _FakeContract: + name: str + + +@dataclass(frozen=True) +class _ProtectedImportGroup: + top_level_module: str + illegal_links: list[dict[str, object]] + original_expression: str | None = None + + +class _FakeReport: + def __init__(self, entries: list[tuple[_FakeContract, SimpleNamespace]]) -> None: + self._entries = entries + + def get_contracts_and_checks(self): + return iter(self._entries) + + +def test_snapshot_from_report_collects_unique_direct_imports_from_nested_metadata(): + module = _load_lint_imports_baseline_module() + report = _FakeReport( + [ + ( + _FakeContract("layers"), + SimpleNamespace( + kept=False, + metadata={ + "invalid_dependencies": [ + { + "routes": [ + { + "chain": [ + { + "importer": "controllers.apps.list", + "imported": "services.apps", + "line_numbers": (3,), + }, + { + "importer": "services.apps", + "imported": "core.apps", + "line_numbers": (4,), + }, + ], + "extra_firsts": [], + "extra_lasts": [], + }, + { + "chain": [ + { + "importer": "controllers.apps.list", + "imported": "services.apps", + "line_numbers": (9,), + } + ], + "extra_firsts": [], + "extra_lasts": [], + }, + ] + } + ] + }, + ), + ), + ( + _FakeContract("protected"), + SimpleNamespace( + kept=False, + metadata={ + "illegal_imports": [ + _ProtectedImportGroup( + top_level_module="extensions.secret", + illegal_links=[ + { + "importer": "controllers.apps.list", + "imported": "extensions.secret", + "line_numbers": (12,), + } + ], + ) + ] + }, + ), + ), + ] + ) + + assert module.snapshot_from_report(report) == { + "layers": { + "controllers.apps.list": ["services.apps"], + "services.apps": ["core.apps"], + }, + "protected": { + "controllers.apps.list": ["extensions.secret"], + }, + } + + +def test_compare_snapshots_reports_new_direct_imports_in_subset_mode(): + module = _load_lint_imports_baseline_module() + + failures = module.compare_snapshots( + current_snapshot={ + "layers": { + "controllers.apps.list": ["services.apps", "services.billing"], + } + }, + baseline_snapshot={ + "layers": { + "controllers.apps.list": ["services.apps"], + } + }, + comparison="subset", + ) + + assert len(failures) == 1 + assert failures[0].contract_name == "layers" + assert failures[0].importer == "controllers.apps.list" + assert failures[0].extra_imports == ("services.billing",) + assert failures[0].baseline_count == 1 + assert failures[0].current_count == 2 + + +def test_compare_snapshots_count_mode_rejects_only_growth(): + module = _load_lint_imports_baseline_module() + + failures = module.compare_snapshots( + current_snapshot={ + "layers": { + "controllers.apps.list": ["services.apps", "services.billing", "services.audit"], + } + }, + baseline_snapshot={ + "layers": { + "controllers.apps.list": ["services.apps", "services.billing"], + } + }, + comparison="count", + ) + + assert len(failures) == 1 + assert failures[0].current_count == 3 + assert failures[0].baseline_count == 2 + + +def test_main_writes_baseline_snapshot(tmp_path: Path, monkeypatch): + module = _load_lint_imports_baseline_module() + baseline_path = tmp_path / "import-baseline.json" + + monkeypatch.setattr( + module, + "load_report", + lambda **_: _FakeReport( + [ + ( + _FakeContract("layers"), + SimpleNamespace( + kept=False, + metadata={ + "invalid_dependencies": [ + { + "routes": [ + { + "chain": [ + { + "importer": "controllers.apps.list", + "imported": "services.apps", + "line_numbers": (3,), + } + ], + "extra_firsts": [], + "extra_lasts": [], + } + ] + } + ] + }, + ), + ) + ] + ), + ) + + assert module.main(["--baseline", str(baseline_path), "--write-baseline"]) == 0 + assert json.loads(baseline_path.read_text(encoding="utf-8")) == { + "version": 1, + "contracts": { + "layers": { + "controllers.apps.list": ["services.apps"], + } + }, + } + + +def test_main_fails_on_replacement_violation_in_default_subset_mode( + tmp_path: Path, monkeypatch, capsys +): + module = _load_lint_imports_baseline_module() + baseline_path = tmp_path / "import-baseline.json" + baseline_path.write_text( + json.dumps( + { + "version": 1, + "contracts": { + "layers": { + "controllers.apps.list": ["services.apps"], + } + }, + } + ), + encoding="utf-8", + ) + + monkeypatch.setattr( + module, + "load_report", + lambda **_: _FakeReport( + [ + ( + _FakeContract("layers"), + SimpleNamespace( + kept=False, + metadata={ + "invalid_dependencies": [ + { + "routes": [ + { + "chain": [ + { + "importer": "controllers.apps.list", + "imported": "services.billing", + "line_numbers": (8,), + } + ], + "extra_firsts": [], + "extra_lasts": [], + } + ] + } + ] + }, + ), + ) + ] + ), + ) + + assert module.main(["--baseline", str(baseline_path)]) == 1 + output = capsys.readouterr().out + assert "controllers.apps.list" in output + assert "services.billing" in output From 0ca534b8232b9aefa98c51e27da077904676cc36 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:10:53 +0000 Subject: [PATCH 02/11] [autofix.ci] apply automated fixes --- api/dev/lint_imports_baseline.py | 15 ++++----------- .../commands/test_lint_imports_baseline.py | 4 +--- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/api/dev/lint_imports_baseline.py b/api/dev/lint_imports_baseline.py index 5512d81ef48490..6eed4121473adc 100644 --- a/api/dev/lint_imports_baseline.py +++ b/api/dev/lint_imports_baseline.py @@ -68,14 +68,11 @@ def snapshot_from_report(report: Any) -> Snapshot: imports_by_importer[importer].add(imported) if check.metadata and not imports_by_importer: - raise ValueError( - f"Broken contract '{contract.name}' does not expose direct import edges in metadata." - ) + raise ValueError(f"Broken contract '{contract.name}' does not expose direct import edges in metadata.") if imports_by_importer: snapshot[contract.name] = { - importer: sorted(imported_modules) - for importer, imported_modules in sorted(imports_by_importer.items()) + importer: sorted(imported_modules) for importer, imported_modules in sorted(imports_by_importer.items()) } return {contract_name: snapshot[contract_name] for contract_name in sorted(snapshot)} @@ -125,9 +122,7 @@ def load_baseline(path: Path) -> Snapshot: payload = json.loads(path.read_text(encoding="utf-8")) version = payload.get("version") if version != BASELINE_VERSION: - raise ValueError( - f"Unsupported baseline version {version!r}; expected {BASELINE_VERSION}." - ) + raise ValueError(f"Unsupported baseline version {version!r}; expected {BASELINE_VERSION}.") contracts = payload.get("contracts") if not isinstance(contracts, dict): @@ -164,9 +159,7 @@ def main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv) baseline_path = args.baseline - current_snapshot = snapshot_from_report( - load_report(config_path=args.config, contract_ids=tuple(args.contract)) - ) + current_snapshot = snapshot_from_report(load_report(config_path=args.config, contract_ids=tuple(args.contract))) if args.write_baseline: write_baseline(baseline_path, current_snapshot) diff --git a/api/tests/unit_tests/commands/test_lint_imports_baseline.py b/api/tests/unit_tests/commands/test_lint_imports_baseline.py index 6cfc6c5c18a37d..169330f10ab61f 100644 --- a/api/tests/unit_tests/commands/test_lint_imports_baseline.py +++ b/api/tests/unit_tests/commands/test_lint_imports_baseline.py @@ -213,9 +213,7 @@ def test_main_writes_baseline_snapshot(tmp_path: Path, monkeypatch): } -def test_main_fails_on_replacement_violation_in_default_subset_mode( - tmp_path: Path, monkeypatch, capsys -): +def test_main_fails_on_replacement_violation_in_default_subset_mode(tmp_path: Path, monkeypatch, capsys): module = _load_lint_imports_baseline_module() baseline_path = tmp_path / "import-baseline.json" baseline_path.write_text( From f071f5c6dc31bb7d14110faf0f1f1c836519cae6 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Fri, 3 Jul 2026 02:18:01 +0800 Subject: [PATCH 03/11] refactor: tighten import baseline typing Assisted-by: Codex:GPT-5.4 --- api/dev/lint_imports_baseline.py | 64 ++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/api/dev/lint_imports_baseline.py b/api/dev/lint_imports_baseline.py index 6eed4121473adc..7f6a070b7a0862 100644 --- a/api/dev/lint_imports_baseline.py +++ b/api/dev/lint_imports_baseline.py @@ -14,16 +14,29 @@ import json import os import sys -from collections import defaultdict +from collections.abc import Iterator from dataclasses import dataclass from pathlib import Path -from typing import Any, Literal +from typing import Any, Literal, NewType, TypedDict from importlinter import configuration from importlinter.application import use_cases type ComparisonMode = Literal["subset", "count"] -type Snapshot = dict[str, dict[str, list[str]]] +ContractName = NewType("ContractName", str) +ModuleName = NewType("ModuleName", str) +type ImportedModuleList = list[ModuleName] +type ImportedModuleSet = set[ModuleName] +type SnapshotModulesByImporter = dict[ModuleName, ImportedModuleList] +type MutableSnapshotModulesByImporter = dict[ModuleName, ImportedModuleSet] +type Snapshot = dict[ContractName, SnapshotModulesByImporter] +type ImportEdge = tuple[ModuleName, ModuleName] + + +class BaselineFile(TypedDict): + version: int + contracts: dict[str, dict[str, list[str]]] + BASELINE_VERSION = 1 DEFAULT_CONFIG_PATH = Path(__file__).resolve().parents[1] / ".importlinter" @@ -31,11 +44,11 @@ @dataclass(frozen=True) class SnapshotFailure: - contract_name: str - importer: str + contract_name: ContractName + importer: ModuleName baseline_count: int current_count: int - extra_imports: tuple[str, ...] + extra_imports: tuple[ModuleName, ...] def load_report(config_path: str | None = None, contract_ids: tuple[str, ...] = ()) -> Any: @@ -58,21 +71,22 @@ def load_report(config_path: str | None = None, contract_ids: tuple[str, ...] = def snapshot_from_report(report: Any) -> Snapshot: """Return broken direct-import edges grouped by contract and importer module.""" - snapshot: dict[str, dict[str, set[str]]] = {} + snapshot: Snapshot = {} for contract, check in report.get_contracts_and_checks(): if check.kept: continue - imports_by_importer: dict[str, set[str]] = defaultdict(set) + imports_by_importer: MutableSnapshotModulesByImporter = {} for importer, imported in _iter_direct_imports(check.metadata): - imports_by_importer[importer].add(imported) + imports_by_importer.setdefault(importer, set()).add(imported) if check.metadata and not imports_by_importer: raise ValueError(f"Broken contract '{contract.name}' does not expose direct import edges in metadata.") if imports_by_importer: - snapshot[contract.name] = { - importer: sorted(imported_modules) for importer, imported_modules in sorted(imports_by_importer.items()) + snapshot[ContractName(contract.name)] = { + importer: sorted(imported_modules) + for importer, imported_modules in sorted(imports_by_importer.items()) } return {contract_name: snapshot[contract_name] for contract_name in sorted(snapshot)} @@ -119,7 +133,7 @@ def compare_snapshots( def load_baseline(path: Path) -> Snapshot: """Load and validate a baseline file.""" - payload = json.loads(path.read_text(encoding="utf-8")) + payload: BaselineFile = json.loads(path.read_text(encoding="utf-8")) version = payload.get("version") if version != BASELINE_VERSION: raise ValueError(f"Unsupported baseline version {version!r}; expected {BASELINE_VERSION}.") @@ -133,13 +147,16 @@ def load_baseline(path: Path) -> Snapshot: if not isinstance(contract_name, str) or not isinstance(importers, dict): raise ValueError("Each baseline contract entry must be an object keyed by contract name.") - normalized[contract_name] = {} + normalized_importers: SnapshotModulesByImporter = {} for importer, imported_modules in importers.items(): if not isinstance(importer, str) or not isinstance(imported_modules, list): raise ValueError("Each baseline importer entry must map to a list of modules.") if not all(isinstance(module_name, str) for module_name in imported_modules): raise ValueError("Each baseline imported module must be a string.") - normalized[contract_name][importer] = sorted(set(imported_modules)) + normalized_importers[ModuleName(importer)] = sorted( + {ModuleName(module_name) for module_name in imported_modules} + ) + normalized[ContractName(contract_name)] = normalized_importers return normalized @@ -147,9 +164,15 @@ def load_baseline(path: Path) -> Snapshot: def write_baseline(path: Path, snapshot: Snapshot) -> None: """Persist the supplied snapshot as a JSON baseline file.""" - payload = { + payload: BaselineFile = { "version": BASELINE_VERSION, - "contracts": snapshot, + "contracts": { + contract_name: { + importer: list(imported_modules) + for importer, imported_modules in importers.items() + } + for contract_name, importers in snapshot.items() + }, } path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") @@ -217,15 +240,16 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def _iter_direct_imports(node: object, seen: set[int] | None = None): +def _iter_direct_imports(node: object, seen: set[int] | None = None) -> Iterator[ImportEdge]: if seen is None: seen = set() if node is None or isinstance(node, (str, int, float, bool)): return - if dataclasses.is_dataclass(node): - yield from _iter_direct_imports(dataclasses.asdict(node), seen) + if not isinstance(node, type) and dataclasses.is_dataclass(node): + for field in dataclasses.fields(node): + yield from _iter_direct_imports(getattr(node, field.name), seen) return if isinstance(node, dict): @@ -237,7 +261,7 @@ def _iter_direct_imports(node: object, seen: set[int] | None = None): importer = node.get("importer") imported = node.get("imported") if isinstance(importer, str) and isinstance(imported, str): - yield importer, imported + yield ModuleName(importer), ModuleName(imported) for value in node.values(): yield from _iter_direct_imports(value, seen) From 6feb4488f868768161ebb10e8f96951ada0060c2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:24:40 +0000 Subject: [PATCH 04/11] [autofix.ci] apply automated fixes --- api/dev/lint_imports_baseline.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/api/dev/lint_imports_baseline.py b/api/dev/lint_imports_baseline.py index 7f6a070b7a0862..e4dacf12972b90 100644 --- a/api/dev/lint_imports_baseline.py +++ b/api/dev/lint_imports_baseline.py @@ -85,8 +85,7 @@ def snapshot_from_report(report: Any) -> Snapshot: if imports_by_importer: snapshot[ContractName(contract.name)] = { - importer: sorted(imported_modules) - for importer, imported_modules in sorted(imports_by_importer.items()) + importer: sorted(imported_modules) for importer, imported_modules in sorted(imports_by_importer.items()) } return {contract_name: snapshot[contract_name] for contract_name in sorted(snapshot)} @@ -167,10 +166,7 @@ def write_baseline(path: Path, snapshot: Snapshot) -> None: payload: BaselineFile = { "version": BASELINE_VERSION, "contracts": { - contract_name: { - importer: list(imported_modules) - for importer, imported_modules in importers.items() - } + contract_name: {importer: list(imported_modules) for importer, imported_modules in importers.items()} for contract_name, importers in snapshot.items() }, } From 8f7cda0181f48c800a2ba82589eb8337922a2b3c Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Fri, 3 Jul 2026 02:26:37 +0800 Subject: [PATCH 05/11] refactor: parse baseline payload with pydantic Assisted-by: Codex:GPT-5.4 --- api/dev/lint_imports_baseline.py | 98 +++++++++++-------- .../commands/test_lint_imports_baseline.py | 21 ++++ 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/api/dev/lint_imports_baseline.py b/api/dev/lint_imports_baseline.py index e4dacf12972b90..423ce4963e6171 100644 --- a/api/dev/lint_imports_baseline.py +++ b/api/dev/lint_imports_baseline.py @@ -11,16 +11,16 @@ import argparse import dataclasses -import json import os import sys from collections.abc import Iterator from dataclasses import dataclass from pathlib import Path -from typing import Any, Literal, NewType, TypedDict +from typing import Any, Literal, NewType from importlinter import configuration from importlinter.application import use_cases +from pydantic import BaseModel, ConfigDict type ComparisonMode = Literal["subset", "count"] ContractName = NewType("ContractName", str) @@ -33,9 +33,12 @@ type ImportEdge = tuple[ModuleName, ModuleName] -class BaselineFile(TypedDict): +class BaselinePayload(BaseModel): + """Serialized baseline file payload.""" + version: int contracts: dict[str, dict[str, list[str]]] + model_config = ConfigDict(extra="forbid") BASELINE_VERSION = 1 @@ -51,6 +54,39 @@ class SnapshotFailure: extra_imports: tuple[ModuleName, ...] +@dataclass(frozen=True) +class BaselineDocument: + """Domain baseline document used across the file boundary.""" + + version: int + snapshot: Snapshot + + @classmethod + def from_payload(cls, payload: BaselinePayload) -> BaselineDocument: + normalized_snapshot: Snapshot = {} + for contract_name, importers in payload.contracts.items(): + normalized_importers: SnapshotModulesByImporter = {} + for importer, imported_modules in importers.items(): + normalized_importers[ModuleName(importer)] = sorted( + {ModuleName(module_name) for module_name in imported_modules} + ) + normalized_snapshot[ContractName(contract_name)] = normalized_importers + + return cls(version=payload.version, snapshot=normalized_snapshot) + + def to_payload(self) -> BaselinePayload: + return BaselinePayload( + version=self.version, + contracts={ + contract_name: { + importer: list(imported_modules) + for importer, imported_modules in importers.items() + } + for contract_name, importers in self.snapshot.items() + }, + ) + + def load_report(config_path: str | None = None, contract_ids: tuple[str, ...] = ()) -> Any: """Build and return an import-linter report using the same path setup as the CLI.""" @@ -129,48 +165,23 @@ def compare_snapshots( return failures -def load_baseline(path: Path) -> Snapshot: +def load_baseline(path: Path) -> BaselineDocument: """Load and validate a baseline file.""" - payload: BaselineFile = json.loads(path.read_text(encoding="utf-8")) - version = payload.get("version") - if version != BASELINE_VERSION: - raise ValueError(f"Unsupported baseline version {version!r}; expected {BASELINE_VERSION}.") - - contracts = payload.get("contracts") - if not isinstance(contracts, dict): - raise ValueError("Baseline file must contain a 'contracts' object.") - - normalized: Snapshot = {} - for contract_name, importers in contracts.items(): - if not isinstance(contract_name, str) or not isinstance(importers, dict): - raise ValueError("Each baseline contract entry must be an object keyed by contract name.") - - normalized_importers: SnapshotModulesByImporter = {} - for importer, imported_modules in importers.items(): - if not isinstance(importer, str) or not isinstance(imported_modules, list): - raise ValueError("Each baseline importer entry must map to a list of modules.") - if not all(isinstance(module_name, str) for module_name in imported_modules): - raise ValueError("Each baseline imported module must be a string.") - normalized_importers[ModuleName(importer)] = sorted( - {ModuleName(module_name) for module_name in imported_modules} - ) - normalized[ContractName(contract_name)] = normalized_importers - - return normalized + payload = BaselinePayload.model_validate_json(path.read_text(encoding="utf-8")) + baseline_document = BaselineDocument.from_payload(payload) + if baseline_document.version != BASELINE_VERSION: + raise ValueError( + f"Unsupported baseline version {baseline_document.version!r}; expected {BASELINE_VERSION}." + ) + return baseline_document -def write_baseline(path: Path, snapshot: Snapshot) -> None: +def write_baseline(path: Path, baseline_document: BaselineDocument) -> None: """Persist the supplied snapshot as a JSON baseline file.""" - payload: BaselineFile = { - "version": BASELINE_VERSION, - "contracts": { - contract_name: {importer: list(imported_modules) for importer, imported_modules in importers.items()} - for contract_name, importers in snapshot.items() - }, - } - path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + payload = baseline_document.to_payload() + path.write_text(payload.model_dump_json(indent=2) + "\n", encoding="utf-8") def main(argv: list[str] | None = None) -> int: @@ -181,14 +192,17 @@ def main(argv: list[str] | None = None) -> int: current_snapshot = snapshot_from_report(load_report(config_path=args.config, contract_ids=tuple(args.contract))) if args.write_baseline: - write_baseline(baseline_path, current_snapshot) + write_baseline( + baseline_path, + BaselineDocument(version=BASELINE_VERSION, snapshot=current_snapshot), + ) _write_line(f"Wrote import baseline to {baseline_path}.") return 0 - baseline_snapshot = load_baseline(baseline_path) + baseline_document = load_baseline(baseline_path) failures = compare_snapshots( current_snapshot=current_snapshot, - baseline_snapshot=baseline_snapshot, + baseline_snapshot=baseline_document.snapshot, comparison=args.comparison, ) if failures: diff --git a/api/tests/unit_tests/commands/test_lint_imports_baseline.py b/api/tests/unit_tests/commands/test_lint_imports_baseline.py index 169330f10ab61f..5d248e313e24e0 100644 --- a/api/tests/unit_tests/commands/test_lint_imports_baseline.py +++ b/api/tests/unit_tests/commands/test_lint_imports_baseline.py @@ -5,6 +5,9 @@ from pathlib import Path from types import SimpleNamespace +import pytest +from pydantic import ValidationError + def _load_lint_imports_baseline_module(): api_dir = Path(__file__).parents[3] @@ -268,3 +271,21 @@ def test_main_fails_on_replacement_violation_in_default_subset_mode(tmp_path: Pa output = capsys.readouterr().out assert "controllers.apps.list" in output assert "services.billing" in output + + +def test_load_baseline_rejects_unexpected_top_level_fields(tmp_path: Path): + module = _load_lint_imports_baseline_module() + baseline_path = tmp_path / "import-baseline.json" + baseline_path.write_text( + json.dumps( + { + "version": 1, + "contracts": {}, + "unexpected": True, + } + ), + encoding="utf-8", + ) + + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + module.load_baseline(baseline_path) From 158b8b4f49cd7fae5f475d1a5b6f6523f731d93c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:33:40 +0000 Subject: [PATCH 06/11] [autofix.ci] apply automated fixes --- api/dev/lint_imports_baseline.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/api/dev/lint_imports_baseline.py b/api/dev/lint_imports_baseline.py index 423ce4963e6171..a1f333a5313b4a 100644 --- a/api/dev/lint_imports_baseline.py +++ b/api/dev/lint_imports_baseline.py @@ -78,10 +78,7 @@ def to_payload(self) -> BaselinePayload: return BaselinePayload( version=self.version, contracts={ - contract_name: { - importer: list(imported_modules) - for importer, imported_modules in importers.items() - } + contract_name: {importer: list(imported_modules) for importer, imported_modules in importers.items()} for contract_name, importers in self.snapshot.items() }, ) @@ -171,9 +168,7 @@ def load_baseline(path: Path) -> BaselineDocument: payload = BaselinePayload.model_validate_json(path.read_text(encoding="utf-8")) baseline_document = BaselineDocument.from_payload(payload) if baseline_document.version != BASELINE_VERSION: - raise ValueError( - f"Unsupported baseline version {baseline_document.version!r}; expected {BASELINE_VERSION}." - ) + raise ValueError(f"Unsupported baseline version {baseline_document.version!r}; expected {BASELINE_VERSION}.") return baseline_document From 39aa189fe616f53400aff5583613b334630f3257 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Fri, 3 Jul 2026 02:34:27 +0800 Subject: [PATCH 07/11] refactor: simplify baseline payload types Assisted-by: Codex:GPT-5.4 --- api/dev/lint_imports_baseline.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/api/dev/lint_imports_baseline.py b/api/dev/lint_imports_baseline.py index a1f333a5313b4a..57acd4f03c91fc 100644 --- a/api/dev/lint_imports_baseline.py +++ b/api/dev/lint_imports_baseline.py @@ -27,9 +27,9 @@ ModuleName = NewType("ModuleName", str) type ImportedModuleList = list[ModuleName] type ImportedModuleSet = set[ModuleName] -type SnapshotModulesByImporter = dict[ModuleName, ImportedModuleList] -type MutableSnapshotModulesByImporter = dict[ModuleName, ImportedModuleSet] -type Snapshot = dict[ContractName, SnapshotModulesByImporter] +type ModulesByImporter = dict[ModuleName, ImportedModuleList] +type MutableModulesByImporter = dict[ModuleName, ImportedModuleSet] +type BaselineSnapshot = dict[ContractName, ModulesByImporter] type ImportEdge = tuple[ModuleName, ModuleName] @@ -37,7 +37,7 @@ class BaselinePayload(BaseModel): """Serialized baseline file payload.""" version: int - contracts: dict[str, dict[str, list[str]]] + contracts: BaselineSnapshot model_config = ConfigDict(extra="forbid") @@ -59,18 +59,16 @@ class BaselineDocument: """Domain baseline document used across the file boundary.""" version: int - snapshot: Snapshot + snapshot: BaselineSnapshot @classmethod def from_payload(cls, payload: BaselinePayload) -> BaselineDocument: - normalized_snapshot: Snapshot = {} + normalized_snapshot: BaselineSnapshot = {} for contract_name, importers in payload.contracts.items(): - normalized_importers: SnapshotModulesByImporter = {} + normalized_importers: ModulesByImporter = {} for importer, imported_modules in importers.items(): - normalized_importers[ModuleName(importer)] = sorted( - {ModuleName(module_name) for module_name in imported_modules} - ) - normalized_snapshot[ContractName(contract_name)] = normalized_importers + normalized_importers[importer] = sorted(set(imported_modules)) + normalized_snapshot[contract_name] = normalized_importers return cls(version=payload.version, snapshot=normalized_snapshot) @@ -101,15 +99,15 @@ def load_report(config_path: str | None = None, contract_ids: tuple[str, ...] = ) -def snapshot_from_report(report: Any) -> Snapshot: +def snapshot_from_report(report: Any) -> BaselineSnapshot: """Return broken direct-import edges grouped by contract and importer module.""" - snapshot: Snapshot = {} + snapshot: BaselineSnapshot = {} for contract, check in report.get_contracts_and_checks(): if check.kept: continue - imports_by_importer: MutableSnapshotModulesByImporter = {} + imports_by_importer: MutableModulesByImporter = {} for importer, imported in _iter_direct_imports(check.metadata): imports_by_importer.setdefault(importer, set()).add(imported) @@ -125,8 +123,8 @@ def snapshot_from_report(report: Any) -> Snapshot: def compare_snapshots( - current_snapshot: Snapshot, - baseline_snapshot: Snapshot, + current_snapshot: BaselineSnapshot, + baseline_snapshot: BaselineSnapshot, comparison: ComparisonMode = "subset", ) -> list[SnapshotFailure]: """Compare the current and baseline snapshots and return any regressions.""" From fd2d43a9f0e3f7e7df14cf8cc89c51867822be50 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Fri, 3 Jul 2026 02:39:08 +0800 Subject: [PATCH 08/11] refactor: tighten baseline version typing Assisted-by: Codex:GPT-5.4 --- api/dev/lint_imports_baseline.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/api/dev/lint_imports_baseline.py b/api/dev/lint_imports_baseline.py index 57acd4f03c91fc..ec397222708536 100644 --- a/api/dev/lint_imports_baseline.py +++ b/api/dev/lint_imports_baseline.py @@ -22,6 +22,7 @@ from importlinter.application import use_cases from pydantic import BaseModel, ConfigDict +type BaselineVersion = Literal[1] type ComparisonMode = Literal["subset", "count"] ContractName = NewType("ContractName", str) ModuleName = NewType("ModuleName", str) @@ -36,12 +37,11 @@ class BaselinePayload(BaseModel): """Serialized baseline file payload.""" - version: int + version: BaselineVersion = 1 contracts: BaselineSnapshot model_config = ConfigDict(extra="forbid") -BASELINE_VERSION = 1 DEFAULT_CONFIG_PATH = Path(__file__).resolve().parents[1] / ".importlinter" @@ -58,8 +58,8 @@ class SnapshotFailure: class BaselineDocument: """Domain baseline document used across the file boundary.""" - version: int snapshot: BaselineSnapshot + version: BaselineVersion = 1 @classmethod def from_payload(cls, payload: BaselinePayload) -> BaselineDocument: @@ -164,10 +164,7 @@ def load_baseline(path: Path) -> BaselineDocument: """Load and validate a baseline file.""" payload = BaselinePayload.model_validate_json(path.read_text(encoding="utf-8")) - baseline_document = BaselineDocument.from_payload(payload) - if baseline_document.version != BASELINE_VERSION: - raise ValueError(f"Unsupported baseline version {baseline_document.version!r}; expected {BASELINE_VERSION}.") - return baseline_document + return BaselineDocument.from_payload(payload) def write_baseline(path: Path, baseline_document: BaselineDocument) -> None: @@ -187,7 +184,7 @@ def main(argv: list[str] | None = None) -> int: if args.write_baseline: write_baseline( baseline_path, - BaselineDocument(version=BASELINE_VERSION, snapshot=current_snapshot), + BaselineDocument(snapshot=current_snapshot), ) _write_line(f"Wrote import baseline to {baseline_path}.") return 0 From 99abb9520977c6e4a175532f174344cc7232ad3c Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Fri, 3 Jul 2026 02:46:31 +0800 Subject: [PATCH 09/11] refactor: relocate import baseline tooling Assisted-by: Codex:GPT-5.4 --- Makefile | 2 +- .../commands/test_lint_imports_baseline.py | 4 +- ...seline.json => import_linter_baseline.json | 0 {api/dev => scripts}/lint_imports_baseline.py | 65 +++++++------------ 4 files changed, 27 insertions(+), 44 deletions(-) rename api/dev/import_linter_baseline.json => import_linter_baseline.json (100%) rename {api/dev => scripts}/lint_imports_baseline.py (84%) diff --git a/Makefile b/Makefile index 58f3f9d06d6af0..6921ccaaf025ae 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ api-contract-lint: api-import-baseline-lint: @echo "🏗️ Checking import-linter baseline..." - @uv run --project api --dev python api/dev/lint_imports_baseline.py --baseline api/dev/import_linter_baseline.json + @uv run --project api --dev python scripts/lint_imports_baseline.py --baseline import_linter_baseline.json @echo "✅ Import baseline lint complete" type-check: diff --git a/api/tests/unit_tests/commands/test_lint_imports_baseline.py b/api/tests/unit_tests/commands/test_lint_imports_baseline.py index 5d248e313e24e0..a961a085db1643 100644 --- a/api/tests/unit_tests/commands/test_lint_imports_baseline.py +++ b/api/tests/unit_tests/commands/test_lint_imports_baseline.py @@ -10,8 +10,8 @@ def _load_lint_imports_baseline_module(): - api_dir = Path(__file__).parents[3] - script_path = api_dir / "dev" / "lint_imports_baseline.py" + repo_root = Path(__file__).parents[4] + script_path = repo_root / "scripts" / "lint_imports_baseline.py" spec = importlib.util.spec_from_file_location("lint_imports_baseline", script_path) assert spec is not None module = importlib.util.module_from_spec(spec) diff --git a/api/dev/import_linter_baseline.json b/import_linter_baseline.json similarity index 100% rename from api/dev/import_linter_baseline.json rename to import_linter_baseline.json diff --git a/api/dev/lint_imports_baseline.py b/scripts/lint_imports_baseline.py similarity index 84% rename from api/dev/lint_imports_baseline.py rename to scripts/lint_imports_baseline.py index ec397222708536..cb5f48ce3fa734 100644 --- a/api/dev/lint_imports_baseline.py +++ b/scripts/lint_imports_baseline.py @@ -42,7 +42,9 @@ class BaselinePayload(BaseModel): model_config = ConfigDict(extra="forbid") -DEFAULT_CONFIG_PATH = Path(__file__).resolve().parents[1] / ".importlinter" +REPO_ROOT = Path(__file__).resolve().parents[1] +API_DIR = REPO_ROOT / "api" +DEFAULT_CONFIG_PATH = API_DIR / ".importlinter" @dataclass(frozen=True) @@ -53,40 +55,11 @@ class SnapshotFailure: current_count: int extra_imports: tuple[ModuleName, ...] - -@dataclass(frozen=True) -class BaselineDocument: - """Domain baseline document used across the file boundary.""" - - snapshot: BaselineSnapshot - version: BaselineVersion = 1 - - @classmethod - def from_payload(cls, payload: BaselinePayload) -> BaselineDocument: - normalized_snapshot: BaselineSnapshot = {} - for contract_name, importers in payload.contracts.items(): - normalized_importers: ModulesByImporter = {} - for importer, imported_modules in importers.items(): - normalized_importers[importer] = sorted(set(imported_modules)) - normalized_snapshot[contract_name] = normalized_importers - - return cls(version=payload.version, snapshot=normalized_snapshot) - - def to_payload(self) -> BaselinePayload: - return BaselinePayload( - version=self.version, - contracts={ - contract_name: {importer: list(imported_modules) for importer, imported_modules in importers.items()} - for contract_name, importers in self.snapshot.items() - }, - ) - - def load_report(config_path: str | None = None, contract_ids: tuple[str, ...] = ()) -> Any: """Build and return an import-linter report using the same path setup as the CLI.""" configuration.configure() - api_dir = str(DEFAULT_CONFIG_PATH.parent) + api_dir = str(API_DIR) cwd = os.getcwd() for candidate in (api_dir, cwd): if candidate not in sys.path: @@ -122,6 +95,19 @@ def snapshot_from_report(report: Any) -> BaselineSnapshot: return {contract_name: snapshot[contract_name] for contract_name in sorted(snapshot)} +def normalize_snapshot(snapshot: BaselineSnapshot) -> BaselineSnapshot: + """Return a stable snapshot with sorted keys and deduplicated imported modules.""" + + normalized_snapshot: BaselineSnapshot = {} + for contract_name, importers in snapshot.items(): + normalized_importers: ModulesByImporter = {} + for importer, imported_modules in importers.items(): + normalized_importers[importer] = sorted(set(imported_modules)) + normalized_snapshot[contract_name] = normalized_importers + + return {contract_name: normalized_snapshot[contract_name] for contract_name in sorted(normalized_snapshot)} + + def compare_snapshots( current_snapshot: BaselineSnapshot, baseline_snapshot: BaselineSnapshot, @@ -160,17 +146,17 @@ def compare_snapshots( return failures -def load_baseline(path: Path) -> BaselineDocument: +def load_baseline(path: Path) -> BaselineSnapshot: """Load and validate a baseline file.""" payload = BaselinePayload.model_validate_json(path.read_text(encoding="utf-8")) - return BaselineDocument.from_payload(payload) + return normalize_snapshot(payload.contracts) -def write_baseline(path: Path, baseline_document: BaselineDocument) -> None: +def write_baseline(path: Path, snapshot: BaselineSnapshot) -> None: """Persist the supplied snapshot as a JSON baseline file.""" - payload = baseline_document.to_payload() + payload = BaselinePayload(contracts=normalize_snapshot(snapshot)) path.write_text(payload.model_dump_json(indent=2) + "\n", encoding="utf-8") @@ -182,17 +168,14 @@ def main(argv: list[str] | None = None) -> int: current_snapshot = snapshot_from_report(load_report(config_path=args.config, contract_ids=tuple(args.contract))) if args.write_baseline: - write_baseline( - baseline_path, - BaselineDocument(snapshot=current_snapshot), - ) + write_baseline(baseline_path, current_snapshot) _write_line(f"Wrote import baseline to {baseline_path}.") return 0 - baseline_document = load_baseline(baseline_path) + baseline_snapshot = load_baseline(baseline_path) failures = compare_snapshots( current_snapshot=current_snapshot, - baseline_snapshot=baseline_document.snapshot, + baseline_snapshot=baseline_snapshot, comparison=args.comparison, ) if failures: From 09b1ffa3e3dc7f1aa67306078c2eda6708ae1490 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:50:17 +0000 Subject: [PATCH 10/11] [autofix.ci] apply automated fixes --- scripts/lint_imports_baseline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/lint_imports_baseline.py b/scripts/lint_imports_baseline.py index cb5f48ce3fa734..86fbe7583dfc8a 100644 --- a/scripts/lint_imports_baseline.py +++ b/scripts/lint_imports_baseline.py @@ -55,6 +55,7 @@ class SnapshotFailure: current_count: int extra_imports: tuple[ModuleName, ...] + def load_report(config_path: str | None = None, contract_ids: tuple[str, ...] = ()) -> Any: """Build and return an import-linter report using the same path setup as the CLI.""" From 17bce5be5e0eae45f7a91b050a7ae071ab155cb1 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Fri, 3 Jul 2026 02:51:24 +0800 Subject: [PATCH 11/11] refactor: narrow import path setup Assisted-by: Codex:GPT-5.4 --- scripts/lint_imports_baseline.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/scripts/lint_imports_baseline.py b/scripts/lint_imports_baseline.py index cb5f48ce3fa734..874ec557c9d16b 100644 --- a/scripts/lint_imports_baseline.py +++ b/scripts/lint_imports_baseline.py @@ -11,12 +11,11 @@ import argparse import dataclasses -import os -import sys from collections.abc import Iterator from dataclasses import dataclass from pathlib import Path from typing import Any, Literal, NewType +import sys from importlinter import configuration from importlinter.application import use_cases @@ -26,10 +25,8 @@ type ComparisonMode = Literal["subset", "count"] ContractName = NewType("ContractName", str) ModuleName = NewType("ModuleName", str) -type ImportedModuleList = list[ModuleName] -type ImportedModuleSet = set[ModuleName] -type ModulesByImporter = dict[ModuleName, ImportedModuleList] -type MutableModulesByImporter = dict[ModuleName, ImportedModuleSet] +type ModulesByImporter = dict[ModuleName, list[ModuleName]] +type MutableModulesByImporter = dict[ModuleName, set[ModuleName]] type BaselineSnapshot = dict[ContractName, ModulesByImporter] type ImportEdge = tuple[ModuleName, ModuleName] @@ -60,10 +57,9 @@ def load_report(config_path: str | None = None, contract_ids: tuple[str, ...] = configuration.configure() api_dir = str(API_DIR) - cwd = os.getcwd() - for candidate in (api_dir, cwd): - if candidate not in sys.path: - sys.path.insert(0, candidate) + if api_dir not in sys.path: + sys.path.insert(0, api_dir) + resolved_config_path = config_path or str(DEFAULT_CONFIG_PATH) user_options = use_cases.read_user_options(config_filename=resolved_config_path) return use_cases.create_report(