Example Projects
The examples/ directory at the repository root contains five self-contained
projects. Each is a runnable fixture: navigate into the directory and run
zenzic check all to see its output.
git clone https://github.com/PythonWoods/zenzic
cd zenzic/examples/<name>
zenzic check all
broken-docs — Intentional Failures Fixture
Purpose: Trigger every Zenzic check at least once. Useful when debugging a new check or verifying that an error message is correctly formatted.
Expected result: FAILED — multiple check failures, exit code 1.
| Check | What triggers it |
|---|---|
| Links | Missing file, dead anchor, path traversal, absolute path, broken i18n |
| Orphans | api.md exists on disk but is absent from the nav |
| Snippets | Python block with a SyntaxError (missing colon) |
| Placeholders | api.md has only 18 words and a bare task marker |
| Assets | assets/unused.png is on disk but never referenced |
| Custom rules | ZZ-NOFIXME pattern in zenzic.toml |
cd examples/broken-docs
zenzic check all # exit 1
zenzic check all --exit-zero # exit 0 (soft-gate mode)
Engine: mkdocs. Also ships a zensical.toml to demonstrate the same fixture
under the Zensical engine.
i18n-standard — Gold Standard Bilingual Project
Purpose: Demonstrate a perfectly clean bilingual project that scores 100/100. Use this as the reference template when starting a new multilingual docs project.
Expected result: SUCCESS — all checks pass, score 100/100.
Key patterns this example demonstrates:
-
Suffix-mode i18n — translations live as
page.it.mdsiblings, never in adocs/it/subtree -
Path symmetry —
../../assets/brand/brand-kit.zipresolves identically fromboth
page.mdandpage.it.md -
Build artifact exclusion —
excluded_build_artifactslets Zenzic validatelinks to generated files without requiring them on disk
-
fail_under = 100— any regression breaks the gate
cd examples/i18n-standard
zenzic check all --strict # exit 0, score 100/100
Engine: mkdocs with i18n plugin in docs_structure: suffix mode.
security_lab — Zenzic Shield Test Fixture
Purpose: Exercise the Shield subsystem — credential detection and path traversal classification — before releases.
Expected result: FAILED — exit code 2 (Shield event; non-suppressible).
| File | What it triggers |
|---|---|
traversal.md | PathTraversal: ../../etc/passwd escapes docs/ |
attack.md | PathTraversal + seven fake credential patterns (all Shield families) |
absolute.md | Absolute paths (/assets/logo.png, /etc/passwd) |
fenced.md | Fake credentials inside unlabelled and bash fenced blocks |
cd examples/security_lab
zenzic check links --strict # exit 1 (path traversal)
zenzic check references # exit 2 (Shield: fake credentials)
zenzic check all # exit 2 (Shield takes priority)
The credentials in
attack.mdandfenced.mdare entirely synthetic — they match the regex shape but are not valid tokens for any service.
Engine: mkdocs.
standalone — Engine-Agnostic Quality Gate
Purpose: Show Zenzic running without any build engine. No mkdocs.yml,
no zensical.toml, no Hugo config. Just engine = "standalone" in zenzic.toml.
Expected result: SUCCESS — all applicable checks pass.
What works in Standalone mode:
- Links, snippets, placeholders, and assets are fully checked
[[custom_rules]]fire identically to any other modefail_underenforces a minimum quality score- The orphan check is skipped — with no declared nav there is no reference set
cd examples/standalone
zenzic check all # exit 0
Use Standalone mode for Hugo, Docusaurus, Sphinx, Astro, Jekyll, GitHub wikis, or any project that does not use MkDocs or Zensical.
plugin-scaffold-demo — Plugin SDK Living Scaffold
Purpose: Provide the exact output generated by
zenzic init --plugin plugin-scaffold-demo as a committed integration fixture.
Expected result: SUCCESS — the generated scaffold is lint-clean.
cd examples/plugin-scaffold-demo
zenzic check all # exit 0
Use this fixture to validate scaffold regressions: if this example starts failing, the SDK template has drifted.
Running the full examples suite
From the repository root, verify all examples produce their expected exit codes:
# Gold standard and standalone: must be clean
(cd examples/i18n-standard && zenzic check all --strict)
(cd examples/standalone && zenzic check all)
# Broken: must fail with exit 1
(cd examples/broken-docs && zenzic check all); [ $? -eq 1 ]
# Security lab: must exit with code 2 (Shield)
(cd examples/security_lab && zenzic check all); [ $? -eq 2 ]
# Plugin scaffold demo: generated template must be clean
(cd examples/plugin-scaffold-demo && zenzic check all)
Adapter Internals — Pedagogical Comparison
This section walks through two concrete adapter methods side-by-side.
The contrast between DocusaurusAdapter and StandaloneAdapter shows how the
adapter protocol enables engine-agnostic Core logic.
provides_index() — Does this directory have a landing page
The Core calls provides_index(directory_path) once per directory during orphan
detection. It answers: "Will the engine generate a browsable index for this
directory, so that files inside it are not structurally orphaned?"
DocusaurusAdapter.provides_index() — full engine awareness:
def provides_index(self, directory_path: Path) -> bool:
# Physical index files — Docusaurus serves these directly.
index_files = ("index.md", "index.mdx", "README.md", "README.mdx")
if any((directory_path / f).exists() for f in index_files):
return True
# _category_.json with "generated-index" link — Docusaurus auto-generates
# a category landing page even without a physical index file.
category_json = directory_path / "_category_.json"
if category_json.exists():
try:
import json as _json
data = _json.loads(category_json.read_text(encoding="utf-8"))
link = data.get("link", {})
return isinstance(link, dict) and link.get("type") == "generated-index"
except Exception:
return True # conservative: assume it provides an index
return False
StandaloneAdapter.provides_index() — zero engine assumptions:
def provides_index(self, directory_path: Path) -> bool:
# No engine config — only a plain index.md signals a landing page.
return (directory_path / "index.md").exists()
Key difference: DocusaurusAdapter knows about _category_.json and
README.mdx because those are Docusaurus conventions. StandaloneAdapter
makes no assumptions — it recognises only the universal index.md convention.
get_nav_paths() — What files are discoverable
get_nav_paths() returns the set of file paths reachable via the site's
navigation UI. A file absent from this set is a candidate for Z402
(ORPHAN_BUT_EXISTING).
DocusaurusAdapter.get_nav_paths() — three-source aggregation:
def get_nav_paths(self) -> frozenset[str]:
if self._sidebar_path is not None:
sidebar_paths = _parse_sidebars(self._sidebar_path, self._docs_root)
if sidebar_paths is not None:
# Explicit sidebar: merge with navbar paths.
# A file is REACHABLE if it appears in the sidebar OR the navbar.
return sidebar_paths | self._navbar_paths
# Autogenerated or no sidebar: all files are already REACHABLE.
return frozenset()
self._navbar_paths is populated by _parse_config_navigation() from
docusaurus.config.* — it extracts to: URL paths and docId: attributes
from navbar and footer items. A file linked only in the footer is still
considered discoverable (UX-Discoverability Law, Rule R21).
StandaloneAdapter.get_nav_paths() — intentionally empty:
def get_nav_paths(self) -> frozenset[str]:
"""Empty frozenset — no engine config means no declared nav."""
return frozenset()
When get_nav_paths() returns an empty frozenset, classify_route() treats
all files as REACHABLE. This is intentional: in Standalone mode there is no
navigation contract, so orphan detection (Z402) is disabled.
classify_route() — Is this file reachable
classify_route(rel, nav_paths) maps a source file path to its route status.
DocusaurusAdapter.classify_route() — four classification rules:
def classify_route(self, rel: Path, nav_paths: frozenset[str]) -> RouteStatus:
# Rule 1: Private/meta files (e.g. _category_.json) → IGNORED
non_sentinel_parts = [p for p in rel.parts if p != "_version_"]
if any(part.startswith("_") for part in non_sentinel_parts):
return "IGNORED"
# Version Ghost Routes: files under _version_/<label>/ are always REACHABLE.
if len(rel.parts) >= 2 and rel.parts[0] == "_version_":
return "REACHABLE"
# Rule 2: No explicit nav → autogenerated sidebar → all REACHABLE
if not nav_paths:
return "REACHABLE"
# Rule 3: Explicit nav match (sidebar or navbar)
if rel.as_posix() in nav_paths:
return "REACHABLE"
# Locale shadows inherit nav membership
if self.is_shadow_of_nav_page(rel, nav_paths):
return "REACHABLE"
# Ghost Routes: locale entry points (e.g. it/index.mdx)
if rel.name in ("index.md", "index.mdx") and len(rel.parts) == 2:
if rel.parts[0] in self._locale_dirs:
return "REACHABLE"
# Rule 4: File exists but is not discoverable via any UI entry point
return "ORPHAN_BUT_EXISTING"
StandaloneAdapter.classify_route() — always reachable:
def classify_route(self, rel: Path, nav_paths: frozenset[str]) -> RouteStatus:
"""Always REACHABLE — no nav to compare against."""
return "REACHABLE"
The contrast is stark. DocusaurusAdapter implements a four-rule priority
cascade because Docusaurus has explicit navigation contracts (sidebars.ts,
docusaurus.config.*). StandaloneAdapter returns a constant because with no
engine there is no navigation contract — every file that exists is reachable.
get_link_scheme_bypasses() — Engine-specific URI schemes
Rule R21 (Protocol Sovereignty) mandates that the Core never hardcodes engine names in validation logic. Engine-specific URI schemes are declared by the adapter and queried by the Core.
DocusaurusAdapter.get_link_scheme_bypasses():
def get_link_scheme_bypasses(self) -> frozenset[str]:
# Docusaurus uses pathname:/// links to reference static/ files that
# bypass the React router. The leading / is a URI convention artifact,
# not a server-absolute path — suppress Z105 for these links.
return frozenset({"pathname"})
StandaloneAdapter.get_link_scheme_bypasses():
def get_link_scheme_bypasses(self) -> frozenset[str]:
"""Standalone projects have no engine-specific link-scheme bypass."""
return frozenset()
When a link like pathname:///assets/brand.html is encountered in a Docusaurus
project, the Core checks adapter.get_link_scheme_bypasses(). It finds
"pathname" in the returned set and skips the Z105 (absolute path) check.
In a Standalone project, the same link triggers Z105 — correctly, because
pathname:/// is a Docusaurus-specific escape hatch with no meaning in a generic
Markdown project.