Skip to main content

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.

CheckWhat triggers it
LinksMissing file, dead anchor, path traversal, absolute path, broken i18n
Orphansapi.md exists on disk but is absent from the nav
SnippetsPython block with a SyntaxError (missing colon)
Placeholdersapi.md has only 18 words and a bare task marker
Assetsassets/unused.png is on disk but never referenced
Custom rulesZZ-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.md siblings, never in a

    docs/it/ subtree

  • Path symmetry../../assets/brand/brand-kit.zip resolves identically from

    both page.md and page.it.md

  • Build artifact exclusionexcluded_build_artifacts lets Zenzic validate

    links 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).

FileWhat it triggers
traversal.mdPathTraversal: ../../etc/passwd escapes docs/
attack.mdPathTraversal + seven fake credential patterns (all Shield families)
absolute.mdAbsolute paths (/assets/logo.png, /etc/passwd)
fenced.mdFake 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.md and fenced.md are 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 mode
  • fail_under enforces 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.


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.