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>/ |
| 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
def check(self, vsm, config):
if not (docs_root / resolved_path).exists():
yield Finding(...)
# ✅ Correct — asks the VSM
def check(self, vsm, config):
if route_info.status == RouteStatus.ORPHAN_AND_ABSENT:
yield Finding(...)
Ghost Routes are pages generated by the engine 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,zensical.toml, 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: