Core Laws of the Scanner
These rules protect the performance and determinism guarantees of src/zenzic/core/. Any modification to the analysis core must respect these invariants.
Zero I/O in the Hot Path
src/zenzic/core/ must never call Path.exists(), Path.is_file(), open(), or any other filesystem or subprocess operation inside a per-link or per-file loop.
The two permitted I/O phases are:
| Phase | Where | What |
|---|---|---|
| Pass 1 | validate_links_async preamble | rglob traversal to build md_contents and known_assets |
InMemoryPathResolver construction | __init__ | Building _lookup_map from the pre-read content dict |
Everything after Pass 1 must use only in-memory data structures:
- Internal
.mdresolution →InMemoryPathResolver.resolve() - Non-
.mdasset resolution →asset_str in known_assets(frozenset[str], O(1))
i18n Determinism
src/zenzic/core/ must produce identical findings and identical exit codes in all three i18n configurations:
| Configuration | Root structure |
|---|---|
| No i18n | docs/*.md only |
| Folder mode | docs/ + i18n/<locale>/docusaurus-plugin-content-docs/current/ |
| Suffix mode | docs/*.md + docs/*.it.md |
Any check that produces different findings depending on locale configuration has a bug. Locale detection happens in the adapter layer; core must be locale-agnostic.
Ghost Route Awareness
Any check that validates links or routes must query the VSM, not the filesystem:
# ❌ Grade-1 violation — asks the filesystem, misses Ghost Routes
if not (docs_root / resolved_path).exists():
yield Finding(...)
# ✅ Correct — asks the VSM
if route_info.status == RouteStatus.ORPHAN_AND_ABSENT:
yield Finding(...)
Ghost Routes are pages generated by Docusaurus at build time (tag listings, paginated indexes, author pages) that have no physical Markdown source on disk. A filesystem check always reports them as broken.
VSM Sovereignty
When building or querying the navigation model:
- Use only the adapter's
get_nav_paths()/get_route_info()surface. - Never parse
mkdocs.yml,docusaurus.config.ts, or any other engine config file directly inside a check. That responsibility belongs exclusively to the adapter. - Never call
subprocessto run the build engine. Zenzic reads config as data, not as executable code.
Adapter Contract
When a check needs adapter data, it must query the adapter instance:
# ✅ Correct — use the adapter
route_info = adapter.get_route_info(rel_path)
# ❌ Wrong — never parse mkdocs.yml for locale data inside a check
with open("mkdocs.yml") as f:
config = yaml.safe_load(f)
locale = config.get("plugins", {}).get("i18n", {}).get("default_locale", "en")