Skip to main content

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:

PhaseWhereWhat
Pass 1validate_links_async preamblerglob 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 .md resolution → InMemoryPathResolver.resolve()
  • Non-.md asset 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:

ConfigurationRoot structure
No i18ndocs/*.md only
Folder modedocs/ + i18n/<locale>/docusaurus-plugin-content-docs/current/
Suffix modedocs/*.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 subprocess to 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")