4"""Enforce non-negotiable first-party library architecture boundaries.
6The checker protects four independent invariants:
8* ``ra8_core`` is the foundation and cannot include another library;
9* a library cannot include another library's ``*_internal.h`` contract;
10* Domain code cannot include HAL or board-device contracts;
11* reusable libraries cannot include hosted-OS headers. POSIX, Windows, and
12 similar dependencies belong under ``port/`` and bind through a portable
13 interface instead of leaking into ``libs/``.
15The second rule is what keeps implementation seams real. A wrapper backend in
16another module must implement a public, platform-neutral backend contract; it
17cannot compile only because a broad include path exposes a sibling's ``src/``.
18The fourth rule intentionally allows ISO C headers such as ``string.h`` and
19``stdlib.h``. It rejects only hosted I/O, filesystem, threading, socket, and
20process headers whose presence makes a reusable library OS-specific.
24 check_core_layering.py # scan every first-party library
25 check_core_layering.py path/to/file.c # scan listed library files
26 check_core_layering.py --selftest # prove every rule fires/stays quiet
28Exit 0 on a clean scan, 1 on a boundary violation, and 2 when a full sweep or
29the header-ownership oracle collapses below its non-vacuity floor.
32from __future__
import annotations
36from collections.abc
import Iterable
37from dataclasses
import dataclass
38from pathlib
import Path
40sys.path.insert(0, str(Path(__file__).resolve().parent))
42from lint_targets
import is_build_output_path
43from selftest_assert
import expect, report
45REPO_ROOT = Path(__file__).resolve().parents[2]
46LIBS_ROOT = REPO_ROOT /
"libs"
48FOUNDATION_LIB =
"ra8_core"
49SOURCE_SUFFIXES = (
".c",
".h",
".cpp",
".hpp")
50INCLUDE_RE = re.compile(
r'#\s*include\s*([<"])([^">]+)[">]')
51EXCLUDE_FRAGMENTS = (
"/third_party/",
"/ra8_fonts/")
52INTERNAL_HEADER_SUFFIX =
"_internal.h"
53DOMAIN_TAG =
"[Ring 4 / Domain]"
57HOSTED_HEADERS = frozenset(
85HOSTED_HEADER_PREFIXES = (
"arpa/",
"mach/",
"netinet/",
"sys/",
"windows/")
88def _is_device_module(module: str) -> bool:
89 """Whether ``module`` owns a hardware- or board-specific contract."""
90 return module ==
"ra8_hal" or module.startswith(
"ra8_board_")
94_MIN_LIB_PATH_PARTS = 2
105SELFTEST_ARG_COUNT = 2
108def _excluded(path: Path) -> bool:
109 """Whether ``path`` is build output, SOUP, or generated font data."""
110 text = str(path).replace(
"\\",
"/")
111 return is_build_output_path(text)
or any(fragment
in text
for fragment
in EXCLUDE_FRAGMENTS)
114def _header_owners() -> dict[str, set[str]]:
115 """Map each first-party library header basename to its owning module(s)."""
116 owners: dict[str, set[str]] = {}
117 for header
in LIBS_ROOT.rglob(
"*.h"):
118 if _excluded(header):
120 rel = header.relative_to(LIBS_ROOT).parts
121 if len(rel) < _MIN_LIB_PATH_PARTS
or rel[1]
not in (
"inc",
"src"):
123 owners.setdefault(header.name, set()).add(rel[0])
127def _is_source(path: Path) -> bool:
128 """Whether ``path`` has a first-party C/C++ source/header suffix."""
129 return path.suffix
in SOURCE_SUFFIXES
132def _rel(path: Path) -> str:
133 """Return a stable repository-relative display path where possible."""
134 if path.is_relative_to(REPO_ROOT):
135 return str(path.relative_to(REPO_ROOT))
139def _module_for(path: Path) -> str |
None:
140 """Return the immediate module beneath ``libs/``, or None outside it."""
141 if not path.is_relative_to(LIBS_ROOT):
143 rel = path.relative_to(LIBS_ROOT).parts
144 return rel[0]
if rel
else None
147def _enumerate_targets(arg_paths: Iterable[str]) -> list[Path]:
148 """Expand explicit paths, or enumerate every first-party library file."""
149 args = list(arg_paths)
151 candidates: list[Path] = []
154 if not path.is_absolute():
155 path = REPO_ROOT / path
157 for suffix
in SOURCE_SUFFIXES:
158 candidates.extend(path.rglob(
"*" + suffix))
159 elif _is_source(path):
160 candidates.append(path)
163 for suffix
in SOURCE_SUFFIXES:
164 candidates.extend(LIBS_ROOT.rglob(
"*" + suffix))
166 path
for path
in candidates
if path.is_relative_to(LIBS_ROOT)
and not _excluded(path)
170def _is_hosted_header(name: str) -> bool:
171 """Whether an angle-bracket include names a hosted-OS implementation API."""
172 folded = name.lower()
173 return folded
in HOSTED_HEADERS
or folded.startswith(HOSTED_HEADER_PREFIXES)
180 owners: dict[str, set[str]],
181) -> list[tuple[str, str, int, str, tuple[str, ...]]]:
182 """Return architecture findings from one module-owned translation unit."""
183 findings: list[tuple[str, str, int, str, tuple[str, ...]]] = []
184 is_domain = DOMAIN_TAG
in text
185 for lineno, line
in enumerate(text.splitlines(), 1):
186 match = INCLUDE_RE.search(line)
189 opener, included = match.groups()
190 name = Path(included).name
191 modules = owners.get(name, set())
192 other_owners = modules - {module}
194 if module == FOUNDATION_LIB
and other_owners
and FOUNDATION_LIB
not in modules:
196 (
"CORE_UPWARD", rel_path, lineno, included, tuple(sorted(other_owners)))
199 if name.endswith(INTERNAL_HEADER_SUFFIX)
and other_owners
and module
not in modules:
201 (
"CROSS_INTERNAL", rel_path, lineno, included, tuple(sorted(other_owners)))
204 device_owners = tuple(sorted(owner
for owner
in other_owners
if _is_device_module(owner)))
205 if is_domain
and device_owners:
206 findings.append((
"DOMAIN_DEVICE", rel_path, lineno, included, device_owners))
208 if opener ==
"<" and _is_hosted_header(included):
209 findings.append((
"HOSTED_LIB", rel_path, lineno, included, ()))
214 targets: Iterable[Path], owners: dict[str, set[str]]
215) -> list[tuple[str, str, int, str, tuple[str, ...]]]:
216 """Read and scan every target, reporting unreadable files as findings."""
217 findings: list[tuple[str, str, int, str, tuple[str, ...]]] = []
219 module = _module_for(path)
223 source = path.read_text(encoding=
"utf-8", errors=
"replace")
225 findings.append((
"READ_ERROR", _rel(path), 0,
"unreadable source", ()))
227 findings.extend(_scan_text(source, _rel(path), module, owners))
231@dataclass(frozen=True)
233 """One synthetic source and its expected self-test label."""
242 rule: str, case: _SelftestCase, owners: dict[str, set[str]], failures: list[str]
244 """Require one synthetic include to trigger the selected rule."""
245 findings = _scan_text(case.source, case.rel_path, case.module, owners)
246 expect(any(finding[0] == rule
for finding
in findings), case.label, failures)
251 owners: dict[str, set[str]],
254 """Require one synthetic portable include set to remain finding-free."""
255 expect(
not _scan_text(case.source, case.rel_path, case.module, owners), case.label, failures)
258def _selftest_violations(owners: dict[str, set[str]], failures: list[str]) ->
None:
259 """Prove each forbidden dependency class is detected."""
263 '#include "ra8_gpio.h"\n',
266 "ra8_core upward dependency fires",
274 '#include "ra8_io_private_internal.h"\n',
277 "cross-library internal header fires",
285 '[Ring 4 / Domain]\n#include "ra8_gpio.h"\n',
286 "apps/shared_libs/book/x.c",
288 "Domain dependency on a device contract fires",
296 "#include <dirent.h>\n",
297 "apps/shared_libs/book/x.c",
299 "hosted filesystem header in a library fires",
306def _selftest_portable(owners: dict[str, set[str]], failures: list[str]) ->
None:
307 """Prove portable and same-module dependencies remain accepted."""
310 '#include <string.h>\n#include "ra8_err.h"\n',
311 "apps/shared_libs/book/x.c",
313 "portable ISO C and public lower-layer includes stay quiet",
320 '#include "ra8_io_private_internal.h"\n',
323 "same-module internal header stays quiet",
330 '[Ring 4 / Domain]\n#include "ra8_err.h"\n',
331 "apps/shared_libs/book/x.c",
333 "Domain dependency on a portable lower contract stays quiet",
340def _selftest() -> int:
341 """Prove all four rules fire and portable/same-module includes stay quiet."""
342 print(
"check_core_layering.py --selftest")
343 failures: list[str] = []
345 "ra8_err.h": {
"ra8_core"},
346 "ra8_gpio.h": {
"ra8_hal"},
347 "ra8_io_private_internal.h": {
"ra8_io"},
349 _selftest_violations(owners, failures)
350 _selftest_portable(owners, failures)
351 return report(failures)
354def _report(findings: list[tuple[str, str, int, str, tuple[str, ...]]]) ->
None:
355 """Print actionable diagnostics grouped by stable rule key."""
356 print(f
"check_core_layering.py: {len(findings)} architecture violation(s):\n", file=sys.stderr)
357 for rule, path, lineno, included, owners
in findings:
358 location = f
"{path}:{lineno}" if lineno > 0
else path
359 suffix = f
" (owned by {', '.join(owners)})" if owners
else ""
360 print(f
" [{rule}] {location} includes {included}{suffix}", file=sys.stderr)
362 "\nCORE_UPWARD: move the dependency above ra8_core or move a genuinely\n"
363 "shared contract into the foundation. CROSS_INTERNAL: publish a narrow\n"
364 "backend/adapter contract under the owner library's inc/ directory.\n"
365 "DOMAIN_DEVICE: invert the device dependency behind a portable interface\n"
366 "and bind it in the application composition root.\n"
367 "HOSTED_LIB: move the OS operation under port/ and inject it through a\n"
368 "platform-neutral interface. Do not add an include path or local shim\n"
369 "that hides the same dependency.",
374def main(argv: list[str]) -> int:
375 """Scan first-party libraries, or run the detector's bidirectional selftest."""
376 if len(argv) == SELFTEST_ARG_COUNT
and argv[1] ==
"--selftest":
378 if "--selftest" in argv[1:]:
379 print(
"check_core_layering.py: --selftest accepts no paths", file=sys.stderr)
383 targets = _enumerate_targets(paths)
384 if not paths
and len(targets) < FILE_FLOOR:
386 f
"check_core_layering.py: FATAL -- only {len(targets)} library file(s) "
387 f
"in scope, floor is {FILE_FLOOR}. A collapsed sweep is not clean.",
392 print(
"check_core_layering.py: no library files to scan", file=sys.stderr)
395 owners = _header_owners()
396 if len(owners) < OWNER_FLOOR:
398 f
"check_core_layering.py: FATAL -- header ownership map holds only "
399 f
"{len(owners)} header(s), floor is {OWNER_FLOOR}. A collapsed oracle "
400 "cannot establish layering.",
405 findings = _scan_targets(targets, owners)
410 f
"check_core_layering.py: {len(targets)} library file(s) scanned; "
411 "foundation, module privacy, Domain/device, and hosted-API boundaries are clean."
413 return int(bool(findings))
416if __name__ ==
"__main__":
417 sys.exit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.