diff --git a/.gitignore b/.gitignore index c5baff1..f92a243 100644 --- a/.gitignore +++ b/.gitignore @@ -94,7 +94,7 @@ dmypy.json .cache/ zenzic_report/ # Optional HTML report exports from zenzic .zenzic_cache/ # Reserved for a future linter cache -# Local score snapshot — commit intentionally on main to share a team baseline +# Zenzic quality scores — derived local metadata, never committed .zenzic-score.json # ──────────────────────────────────────────────────────────────────────────── diff --git a/.zenzic-score.json b/.zenzic-score.json deleted file mode 100644 index cd33ba3..0000000 --- a/.zenzic-score.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "project": "zenzic", - "score": 100, - "threshold": 80, - "status": "success", - "timestamp": "2026-03-24T15:38:21+00:00", - "categories": [ - { - "name": "links", - "weight": 0.35, - "issues": 0, - "category_score": 1.0, - "contribution": 0.35 - }, - { - "name": "orphans", - "weight": 0.2, - "issues": 0, - "category_score": 1.0, - "contribution": 0.2 - }, - { - "name": "snippets", - "weight": 0.2, - "issues": 0, - "category_score": 1.0, - "contribution": 0.2 - }, - { - "name": "placeholders", - "weight": 0.15, - "issues": 0, - "category_score": 1.0, - "contribution": 0.15 - }, - { - "name": "assets", - "weight": 0.1, - "issues": 0, - "category_score": 1.0, - "contribution": 0.1 - } - ] -} diff --git a/CHANGELOG.it.md b/CHANGELOG.it.md index 11936ba..fcdb077 100644 --- a/CHANGELOG.it.md +++ b/CHANGELOG.it.md @@ -12,6 +12,56 @@ Le versioni seguono il [Versionamento Semantico][semver]. --- +## [0.4.0-rc5] — 2026-04-01 — Sync Sprint: Zensical v0.0.31+ e Parallelismo API + +> **Sprint 10.** `ZensicalAdapter` viene sincronizzato con lo schema ufficiale +> Zensical v0.0.31+ (`[project].nav`). Il parsing nav ora supporta tutte le varianti +> (stringa, pagina con titolo, sezione annidata). La classificazione route diventa +> nav-aware (`ORPHAN_BUT_EXISTING` quando una pagina esiste ma non e elencata nella nav +> esplicita). `map_url()` rispetta `use_directory_urls = false`. La documentazione viene +> allineata in EN/IT e nasce `examples/zensical-basic/` come riferimento canonico. + +### Aggiunto + +- **Nuovo esempio `examples/zensical-basic/`** — progetto minimo completo con: + `zensical.toml` in formato v0.0.31+ (`[project]`), `zenzic.toml` con + `engine = "zensical"`, nav annidata e link relativi puliti. + +- **Sezioni documentali sul parallelismo (rc5)** — aggiunte in + `docs/architecture.md` e `docs/usage/advanced.md`: modello shared-nothing con + `ProcessPoolExecutor`, soglia pratica di convenienza (~200 file), vincoli tecnici + sulla picklability delle regole custom. + +- **Sezioni di coesistenza adapter (EN/IT)** — aggiornati + `docs/configuration/adapters-config.md` e `docs/it/configuration/adapters-config.md` + con la logica quando coesistono `mkdocs.yml` e `zensical.toml`: prevale sempre + `build_context.engine` (nessun auto-switch silenzioso). + +### Modificato + +- **`ZensicalAdapter`** — inizializzazione arricchita con stato nav pre-calcolato: + `_nav_paths`, `_has_explicit_nav`, `_use_directory_urls`. + +- **`get_nav_paths()`** — ora legge correttamente da `[project].nav` e supporta nav + ricorsiva (stringhe, dict titolo->pagina, dict titolo->lista annidata). + +- **`classify_route()`** — ora restituisce `ORPHAN_BUT_EXISTING` quando e presente + una nav esplicita e il file non compare nella nav. + +- **`tests/sandboxes/zensical/zensical.toml`** — migrato al formato `[project]` + con nav esplicita. + +- **Guide migrazione EN/IT** — aggiornati gli snippet `zensical.toml` in + `docs/guide/migration.md` e `docs/it/guide/migration.md` al formato v0.0.31+. + +### Corretto + +- **Compatibilita schema nav Zensical** — rimossa la dipendenza dal formato legacy + `[nav].nav` con coppie `{title, file}`; ora aderiamo allo schema ufficiale + `[project].nav`. + +--- + ## [0.4.0-rc4] — 2026-03-31 — Virtual Site Map, UNREACHABLE_LINK e Rilevamento Collisioni di Routing > **Sprint 8.** Zenzic acquisisce l'emulazione del motore di build: la Virtual Site Map (VSM) diff --git a/CHANGELOG.md b/CHANGELOG.md index e282dc5..3e264ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,89 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.4.0-rc4] — 2026-04-01 — Ghost Route Support, VSM Rule Engine & Content-Addressable Cache + +## [0.4.0-rc5] — 2026-04-01 — The Sync Sprint: Zensical v0.0.31+ & Parallel API + +> **Sprint 10.** `ZensicalAdapter` is fully synchronised with Zensical v0.0.31+. +> The legacy `[site]`/`[nav].nav` schema is replaced by the canonical `[project].nav` +> format. Navigation parsing now supports all three entry forms (plain string, titled page, +> nested section) recursively. `classify_route()` gains nav-aware orphan detection. +> `map_url()` honours `use_directory_urls = false`. The parallel scan API is stabilised +> and documented with explicit pickling requirements for custom rules. +> `examples/zensical-basic/` is introduced as the canonical Zensical reference project. +> All Zensical TOML snippets in `docs/` are updated to the v0.0.31+ schema. + +### Added + +- **`examples/zensical-basic/`** — new canonical example project for `engine = "zensical"`. + Contains a `zensical.toml` using `[project].nav` with all three nav entry forms, a + `zenzic.toml` with `engine = "zensical"`, and a complete `docs/` tree with clean + relative links. `zenzic check all` on this example exits 0. + +- **`_extract_nav_paths()`** (`zenzic.core.adapters._zensical`) — new pure helper that + recursively extracts `.md` file paths from a Zensical nav list. Handles plain strings + (`"page.md"`), titled pages (`{"Title" = "page.md"}`), and nested sections + (`{"Section" = [...]}`). External URLs are silently skipped. + +- **Nav-aware `classify_route()`** — when an explicit `[project].nav` is declared in + `zensical.toml`, files absent from the nav list are now classified + `ORPHAN_BUT_EXISTING` instead of `REACHABLE`. Zensical serves every file (filesystem + routing), but the sidebar is the user-visible navigation: files outside the nav are + effectively invisible. + +- **`use_directory_urls` support** in `ZensicalAdapter.map_url()` — reads + `[project].use_directory_urls` from `zensical.toml`. When `false`, files are mapped + to flat `.html` URLs (`/page.html`) instead of directory URLs (`/page/`). Default + remains `true`, matching Zensical's own default. + +- **Parallelism section in `docs/usage/advanced.md`** — documents + `scan_docs_references_parallel`, the performance crossover point (~200 files), + the absence of a `--parallel` CLI flag in rc5, and the **pickling requirements** for + custom `BaseRule` subclasses. + +- **Parallelism section in `docs/architecture.md`** — describes the shared-nothing + `ProcessPoolExecutor` model, the immutability contract on workers, and the performance + threshold with honest numbers. + +- **Engine coexistence section in `docs/configuration/adapters-config.md`** (EN + IT) — + documents behaviour when both `mkdocs.yml` and `zensical.toml` are present. Clarifies + that `build_context.engine` is always authoritative; no auto-detection occurs. + +- **`ZensicalAdapter` nav format reference in `docs/configuration/adapters-config.md`** + (EN + IT) — full TOML examples of all three nav entry forms, route classification rules + with and without explicit nav, and `use_directory_urls` documentation. + +### Changed + +- **`ZensicalAdapter.__init__`** — pre-computes `_nav_paths`, `_has_explicit_nav`, and + `_use_directory_urls` at construction time from `[project]` in `zensical.toml`. + `get_nav_paths()` is now an O(1) attribute read. + +- **`ZensicalAdapter` docstring** — updated from legacy `[nav].nav = [{title, file}]` + schema to the v0.0.31+ `[project].nav = [...]` format. + +- **`tests/sandboxes/zensical/zensical.toml`** — migrated from flat key schema to + `[project]` scope. Added a three-entry `nav` list (`index.md`, `features.md`, + `api.md`) to exercise orphan detection in integration tests. + +- **`docs/guide/migration.md`** and **`docs/it/guide/migration.md`** — Phase 3 example + `zensical.toml` updated from legacy `[site]`/`[nav].nav` to `[project].nav` format. + +### Fixed + +- **`ZensicalAdapter.get_nav_paths()`** previously read `[nav].nav` using `{title, file}` + key format — a schema that was never part of the official Zensical spec. Fixed to read + `[project].nav` using the actual v0.0.31+ format. + +- **`ZensicalAdapter.classify_route()`** previously returned `REACHABLE` for all + non-private files regardless of nav declaration. Fixed to return `ORPHAN_BUT_EXISTING` + when an explicit nav is declared and the file is absent from it. + --- ## [0.4.0-rc4] — 2026-04-01 — Ghost Route Support, VSM Rule Engine & Content-Addressable Cache - +> > **Sprint 9.** The Virtual Site Map (VSM) becomes the single source of truth for the Rule > Engine. MkDocs Material Ghost Routes are resolved without false orphan warnings. > The `VSMBrokenLinkRule` validates links against routing state rather than the filesystem. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea52853..2117b7c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,13 @@ Thank you for your interest in contributing to Zenzic! Zenzic is a documentation quality tool — a linter and strict build wrapper for MkDocs sites. Contributions that improve detection accuracy, add new check types, or improve CI/CD integration are especially welcome. +## Mission + +Zenzic is not just a linter. It is a long-term safety layer for documentation teams that +depend on open, auditable source files. We preserve validation continuity across engine +changes (MkDocs 1.x, Zensical, and future adapters) so projects keep control over their +data and quality process regardless of ecosystem churn. + --- ## Quick start diff --git a/README.it.md b/README.it.md index 4693e44..1587f59 100644 --- a/README.it.md +++ b/README.it.md @@ -42,6 +42,20 @@ fallback, nessuna supposizione. --- +## Novita RC5 (v0.4.0-rc5) + +- **Sync Zensical v0.0.31+**: `ZensicalAdapter` legge ora la nav da `[project].nav` + (schema TOML ufficiale), incluse sezioni annidate. +- **Routing nav-aware**: con nav esplicita, i file presenti su disco ma assenti dalla nav + vengono classificati `ORPHAN_BUT_EXISTING`. +- **Parita URL**: `map_url()` rispetta `[project].use_directory_urls = false` + (`/pagina.html`) oltre al default directory-style (`/pagina/`). +- **Parallelismo API documentato**: modello shared-nothing con `ProcessPoolExecutor`, + note oneste sull'overhead e requisiti di picklability per regole custom. +- **Nuovo esempio canonico**: `examples/zensical-basic/` allineato agli snippet documentati. + +--- + ## 📖 Documentazione - 🚀 **[Guida Utente][docs-it-home]**: Installazione, comandi CLI e tutti i controlli disponibili. @@ -235,7 +249,7 @@ non segnalare mai i file tradotti come orfani. ## Changelog & Note di Rilascio - 📋 [CHANGELOG.md](CHANGELOG.md) — storico completo delle modifiche (inglese) -- 📋 [changelog.it.md](changelog.it.md) — storico delle modifiche in italiano +- 📋 [CHANGELOG.it.md](CHANGELOG.it.md) — storico delle modifiche in italiano - 🚀 [RELEASE.md](RELEASE.md) — manifesto di rilascio v0.4.0 (inglese) - 🚀 [RELEASE.it.md](RELEASE.it.md) — manifesto di rilascio v0.4.0 (italiano) diff --git a/README.md b/README.md index d8f9a7b..c76ad34 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,21 @@ absolute links are a hard error, and if you declare `engine = "zensical"` you mu --- +## RC5 Highlights (v0.4.0-rc5) + +- **Zensical v0.0.31+ sync**: `ZensicalAdapter` now reads navigation from + `[project].nav` (official TOML schema), including nested sections. +- **Nav-aware routing**: with explicit nav, files present on disk but absent from nav are + classified as `ORPHAN_BUT_EXISTING`. +- **URL mode parity**: `map_url()` now respects `[project].use_directory_urls = false` + (`/page.html`) and default directory URLs (`/page/`). +- **Parallel scan API documented**: shared-nothing `ProcessPoolExecutor` model, + honest overhead notes, and picklability requirements for custom rules. +- **New canonical example**: `examples/zensical-basic/` mirrors the documented TOML + schema and migration flow. + +--- + ## 📖 Documentation Zenzic provides an extensive, engineering-grade documentation portal: diff --git a/docs/about/philosophy.md b/docs/about/philosophy.md index 88c5f6e..67ccd78 100644 --- a/docs/about/philosophy.md +++ b/docs/about/philosophy.md @@ -46,6 +46,17 @@ or any future generator — can run `zenzic check all` continuously against both simultaneously. A project that has not yet decided on a build engine can still validate its documentation quality today, using Vanilla mode with zero configuration. +As of early 2026, the MkDocs 2.0 direction publicly discussed by ecosystem maintainers includes +large compatibility breaks (plugin removal, theming model changes, config format shift). Zenzic's +position is intentionally conservative and user-protective: + +- MkDocs 1.x projects remain first-class citizens in Zenzic. +- Existing `mkdocs.yml` structures are validated as source contracts, not as runtime promises. +- Unknown or future YAML keys are treated with tolerant parsing, not hard failures. + +In practice, this means a team can defer migration decisions without losing quality gates. +Zenzic keeps the verification layer alive while the ecosystem around it evolves. + --- ## The frictionless sentinel diff --git a/docs/architecture.md b/docs/architecture.md index 92e3741..b5aa657 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -139,7 +139,45 @@ I/O operations, called at process start and end respectively by the CLI layer. --- -## Lifecycle overview +## Parallel scan (v0.4.0-rc5) + +The Three-Phase Pipeline is pure by design: `_scan_single_file` takes a file path and +returns an `IntegrityReport` with zero shared state. This makes it trivially +parallelisable. + +```mermaid +flowchart LR + classDef node fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#e2e8f0 + classDef worker fill:#0f172a,stroke:#10b981,stroke-width:2px,color:#e2e8f0 + classDef io fill:#0f172a,stroke:#4f46e5,stroke-width:2px,color:#e2e8f0 + + MAIN["Main process\nbuilds RuleEngine\nlists .md files"]:::node + W1["Worker 1\n_scan_single_file\n(page_A.md)"]:::worker + W2["Worker 2\n_scan_single_file\n(page_B.md)"]:::worker + WN["Worker N\n_scan_single_file\n(page_Z.md)"]:::worker + SORT["Sorted merge\nby file_path"]:::node + OUT["list[IntegrityReport]"]:::io + + MAIN -->|"pickle(config, engine)"| W1 & W2 & WN + W1 & W2 & WN --> SORT + SORT --> OUT +``` + +**Shared-nothing architecture:** `config` and `rule_engine` are serialised by `pickle` +when dispatched to each worker. Every worker operates on an independent copy — there is +no shared memory, no lock, no race condition. + +**Immutability contract:** workers must not mutate `config`. All scan functions honour +this contract. Rules that write to shared state (e.g. a counter in a class variable) +violate the Pure Functions Pillar and will produce non-deterministic results in parallel +mode. + +**Threshold for parallelism benefit:** process-spawn overhead is ~200–400 ms on a cold +Python interpreter. The crossover point where parallelism beats sequential scanning is +approximately 200 files on an 8-core machine. For smaller repos, use +`scan_docs_references` (sequential). + +--- ```mermaid flowchart LR diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css index ab62cf4..221149c 100644 --- a/docs/assets/stylesheets/extra.css +++ b/docs/assets/stylesheets/extra.css @@ -68,11 +68,20 @@ } } +/* On medium desktop widths, prioritise native header controls (language, search, source). */ +@media screen and (max-width: 76.234375em) { + .zz-home-nav { + display: none; + } +} + .zz-home-nav { display: flex; align-items: center; gap: 0.25rem; - margin: 0 auto; + margin-left: 1rem; + margin-right: auto; + min-width: 0; } .zz-home-nav__link { diff --git a/docs/configuration/adapters-config.md b/docs/configuration/adapters-config.md index 059e6d9..7bb3492 100644 --- a/docs/configuration/adapters-config.md +++ b/docs/configuration/adapters-config.md @@ -123,6 +123,71 @@ Install a third-party adapter or choose from the list above. --- +## Engine coexistence (`mkdocs.yml` + `zensical.toml` in the same repo) + +Some repositories carry both `mkdocs.yml` and `zensical.toml` during a transition — one +build test-running Zensical while the other keeps serving production on MkDocs. + +Zenzic does **not** auto-detect which file is present. The active adapter is always +determined by `build_context.engine` in `zenzic.toml`: if `engine = "mkdocs"` is +declared, Zenzic reads `mkdocs.yml` even when `zensical.toml` also exists, and vice versa. + +If `build_context` is absent from `zenzic.toml`, the default is `engine = "mkdocs"`, so +`MkDocsAdapter` is used regardless of whether `zensical.toml` is present. This is the +safe default — it never silently switches engines. + +!!! warning "Ambiguity is explicit, not silent" + Zenzic will not choose for you. When both config files exist and you have not set + `engine = "zensical"`, you are running with the MkDocs adapter. This is intentional: + **engine identity must be a deliberate declaration**, not an inference. + +```toml +# zenzic.toml — explicit engine declaration required +[build_context] +engine = "zensical" # ← this line is what activates ZensicalAdapter +``` + +--- + +## `ZensicalAdapter` — nav format reference + +`ZensicalAdapter` reads all navigation from `[project].nav` in `zensical.toml` +(Zensical v0.0.31+). Three nav entry forms are supported and freely mixable: + +```toml +[project] +site_name = "My Docs" +docs_dir = "docs" +nav = [ + "index.md", # plain string — title inferred from H1 + {"Guide" = "guide.md"}, # titled page + {"API" = [ # section with nested pages + "api/index.md", + {"Endpoints" = "api/endpoints.md"}, + ]}, + {"GitHub" = "https://github.com/x"}, # external link — skipped by nav extractor +] +``` + +**Route classification with explicit nav:** When `[project].nav` is present and non-empty, +any `.md` file that exists on disk but is absent from the nav list is classified +`ORPHAN_BUT_EXISTING` — the file is served by Zensical (filesystem routing) but is not +reachable via sidebar navigation. + +**Route classification without nav:** When `[project].nav` is absent or empty, every file +is classified `REACHABLE` — Zensical's default filesystem-based routing applies. + +**`use_directory_urls`:** Zensical defaults to directory-style URLs (`/page/`). Set +`use_directory_urls = false` under `[project]` to switch to flat URLs (`/page.html`). The +adapter reads this flag and adjusts `map_url()` accordingly. + +```toml +[project] +use_directory_urls = false # /page.html instead of /page/ +``` + +--- + ## Third-party adapters Third-party adapters (e.g. `zenzic-hugo-adapter`) are discovered automatically once installed as diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 5d43075..d1f186f 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -69,6 +69,23 @@ This makes Zenzic's output a portable quality certificate: if Zenzic says your documentation is structurally sound, that claim is true independently of which engine you use to render it tomorrow. +### MkDocs 2.0 resilience model + +If MkDocs 2.0 ships tomorrow with breaking changes, a Zenzic user still keeps a stable +quality gate for existing MkDocs 1.x sources. + +Why this holds technically: + +- `MkDocsAdapter` parses `mkdocs.yml` as static data and does not import MkDocs. +- Zenzic never executes plugin code; plugin sections are read as plain config. +- Unknown YAML tags and future keys are tolerated by a permissive loader. + +Result: your validation pipeline does not depend on the lifecycle of a single build +binary. You can keep linting MkDocs 1.x conventions while evaluating migration paths. + +For a concrete baseline, use the fixture at `examples/mkdocs-basic/`, which mirrors +official MkDocs 1.6 nav patterns (nested sections, titled entries, external links). + --- ## i18n: validating structure independently of rendering @@ -222,14 +239,13 @@ locales = ["it"] # if you have non-default locale dirs And create a minimal `zensical.toml` at the repository root: ```toml -# zensical.toml -[site] -name = "My Documentation" - -[nav] +# zensical.toml (Zensical v0.0.31+) +[project] +site_name = "My Documentation" +docs_dir = "docs" nav = [ - {title = "Home", file = "index.md"}, - {title = "Guide", file = "guide.md"}, + "index.md", + {"Guide" = "guide.md"}, ] ``` diff --git a/docs/it/about/philosophy.md b/docs/it/about/philosophy.md index 0a48758..bf5f1db 100644 --- a/docs/it/about/philosophy.md +++ b/docs/it/about/philosophy.md @@ -28,6 +28,21 @@ stesso punteggio deterministico. Questo paradigma garantisce che Zenzic rimanga definitivo della tua pipeline di deployment, indipendentemente dal generatore di siti che adotterai domani. +### Safe Harbor e resilienza a MkDocs 2.0 + +Il ruolo di Zenzic e essere il punto stabile quando il framework cambia. Se il futuro di +MkDocs 2.0 introduce rotture incompatibili, i progetti MkDocs 1.x non devono perdere il +proprio quality gate. + +Per questo Zenzic mantiene tre garanzie operative: + +- continua a validare `mkdocs.yml` come contratto sorgente, senza dipendere dal binario; +- non esegue plugin e non richiede il runtime del motore per produrre findings; +- tratta in modo tollerante chiavi e tag YAML sconosciuti, preservando la continuita + della validazione durante le transizioni. + +In sintesi: puoi rimandare una migrazione senza rimandare la qualita. + ### La Sentinella Silenziosa Un linter vale l'accuratezza con cui evita i falsi positivi. Quando gli strumenti sollevano allarmi ingiustificati, gli sviluppatori imparano a ignorarli. diff --git a/docs/it/configuration/adapters-config.md b/docs/it/configuration/adapters-config.md index f5c1871..36bc471 100644 --- a/docs/it/configuration/adapters-config.md +++ b/docs/it/configuration/adapters-config.md @@ -94,6 +94,62 @@ con codice 1. --- +## Coesistenza dei motori (`mkdocs.yml` + `zensical.toml` nella stessa repo) + +Alcune repository contengono sia `mkdocs.yml` che `zensical.toml` durante una transizione. + +Zenzic **non** rileva automaticamente quale file è presente. L'adapter attivo è sempre +determinato da `build_context.engine` in `zenzic.toml`: se è dichiarato `engine = "mkdocs"`, +Zenzic legge `mkdocs.yml` anche quando esiste anche `zensical.toml`, e viceversa. + +Se `build_context` è assente da `zenzic.toml`, il default è `engine = "mkdocs"`, quindi +`MkDocsAdapter` viene usato indipendentemente dalla presenza di `zensical.toml`. + +!!! warning "L'ambiguità è esplicita, non silenziosa" + Zenzic non sceglie per te. Quando entrambi i file di configurazione esistono e non hai + impostato `engine = "zensical"`, stai usando il MkDocs adapter. Questo è intenzionale: + **l'identità del motore deve essere una dichiarazione deliberata**, non un'inferenza. + +```toml +# zenzic.toml — dichiarazione esplicita del motore richiesta +[build_context] +engine = "zensical" # ← questa riga attiva ZensicalAdapter +``` + +--- + +## `ZensicalAdapter` — riferimento al formato nav + +`ZensicalAdapter` legge tutta la navigazione da `[project].nav` in `zensical.toml` +(Zensical v0.0.31+). Tre forme di voci nav sono supportate e liberamente mescolabili: + +```toml +[project] +site_name = "La Mia Documentazione" +docs_dir = "docs" +nav = [ + "index.md", # stringa semplice — titolo inferito dall'H1 + {"Guida" = "guide.md"}, # pagina con titolo + {"API" = [ # sezione con pagine annidate + "api/index.md", + {"Endpoint" = "api/endpoints.md"}, + ]}, + {"GitHub" = "https://github.com/x"}, # link esterno — ignorato dall'estrattore nav +] +``` + +**Classificazione delle route con nav esplicita:** Quando `[project].nav` è presente e +non vuota, qualsiasi file `.md` che esiste su disco ma è assente dalla nav viene +classificato `ORPHAN_BUT_EXISTING`. + +**Classificazione delle route senza nav:** Quando `[project].nav` è assente o vuota, +ogni file è classificato `REACHABLE` — si applica il routing filesystem-based di Zensical. + +**`use_directory_urls`:** Zensical usa URL in stile directory per default (`/pagina/`). +Impostare `use_directory_urls = false` in `[project]` per passare agli URL flat (`/pagina.html`). + +--- + ## Adapter di terze parti Gli adapter di terze parti si scoprono automaticamente una volta installati come pacchetti Python. diff --git a/docs/it/guide/migration.md b/docs/it/guide/migration.md index f4de94c..d105ac5 100644 --- a/docs/it/guide/migration.md +++ b/docs/it/guide/migration.md @@ -70,6 +70,24 @@ Questo rende l'output di Zenzic un **certificato di qualità portabile**: se Zen tua documentazione è strutturalmente corretta, questa affermazione è vera indipendentemente da quale motore userai per renderizzarla domani. +### Modello di resilienza per MkDocs 2.0 + +Se MkDocs 2.0 uscisse domani con breaking changes, un utente Zenzic manterrebbe comunque +un quality gate stabile sui sorgenti MkDocs 1.x esistenti. + +Motivi tecnici: + +- `MkDocsAdapter` interpreta `mkdocs.yml` come dato statico, senza importare MkDocs. +- Zenzic non esegue mai codice plugin; legge solo la configurazione. +- Tag YAML sconosciuti e chiavi future sono trattati in modo tollerante. + +Risultato: la pipeline di validazione non dipende dal ciclo di vita di un singolo +binario di build. Puoi continuare a validare lo standard MkDocs 1.x mentre valuti +la migrazione. + +Per un benchmark pratico usa il fixture `examples/mkdocs-basic/`, allineato ai pattern +nav ufficiali MkDocs 1.6 (sezioni annidate, entry con titolo, link esterni). + --- ## i18n: validare la struttura indipendentemente dal rendering @@ -225,14 +243,13 @@ locales = ["it"] E crea un `zensical.toml` minimale nella root del repository: ```toml -# zensical.toml -[site] -name = "La Mia Documentazione" - -[nav] +# zensical.toml (Zensical v0.0.31+) +[project] +site_name = "La Mia Documentazione" +docs_dir = "docs" nav = [ - {title = "Home", file = "index.md"}, - {title = "Guida", file = "guide.md"}, + "index.md", + {"Guida" = "guide.md"}, ] ``` diff --git a/docs/it/usage/index.md b/docs/it/usage/index.md index b4cbc7e..8a2849b 100644 --- a/docs/it/usage/index.md +++ b/docs/it/usage/index.md @@ -10,6 +10,17 @@ icon: lucide/play Zenzic legge direttamente dal filesystem e funziona con qualsiasi progetto basato su Markdown. Usalo in locale, come hook di pre-commit, nelle pipeline CI o per audit una-tantum. +## Esempi canonici + +Il repository include fixture mantenuti e allineati ai contratti documentati: + +- `examples/mkdocs-basic/` — baseline MkDocs 1.6 (nav annidata, link nav esterno, + tag YAML tolleranti come `!ENV` e `!relative`). +- `examples/i18n-standard/` — gold standard bilingue in strict mode. +- `examples/zensical-basic/` — baseline Zensical v0.0.31+ (`[project].nav`). +- `examples/broken-docs/` — errori intenzionali (exit 1). +- `examples/security_lab/` — fixture sicurezza (Shield exit 2). + !!! tip "Vuoi eseguirlo subito?" ```bash diff --git a/docs/usage/advanced.md b/docs/usage/advanced.md index b5173e8..43bbc72 100644 --- a/docs/usage/advanced.md +++ b/docs/usage/advanced.md @@ -204,19 +204,81 @@ for error in link_errors: `scan_docs_references_with_links` deduplicates external URLs across the entire docs tree before firing HTTP requests — 50 files linking to the same URL result in exactly one HEAD request. -### Parallel scan (large repos) +### Parallel scan (large repos) — v0.4.0-rc5 -For repositories with more than ~200 Markdown files, use `scan_docs_references_parallel`: +For repositories with more than ~200 Markdown files, use `scan_docs_references_parallel`. +It distributes each file to an independent worker process via `ProcessPoolExecutor`, +exploiting the pureness of the per-file scan function (`_scan_single_file`): ```python from pathlib import Path from zenzic.core.scanner import scan_docs_references_parallel +# workers=None lets ProcessPoolExecutor choose based on os.cpu_count() reports = scan_docs_references_parallel(Path("."), workers=4) ``` -Parallel mode uses `ProcessPoolExecutor`. External URL validation is not available in parallel -mode — use `scan_docs_references_with_links` for sequential scan with link validation. +**Honest performance contract:** + +| Repo size | Recommended mode | Reason | +| :--- | :--- | :--- | +| < 50 files | `scan_docs_references` (sequential) | Process-spawn overhead (~200 ms) exceeds the parallelism benefit | +| 50 – 200 files | Sequential or parallel | Benchmark your specific case; gains are marginal | +| 200+ files | `scan_docs_references_parallel` | Per-file CPU-bound regex work dominates; linear scaling | +| 5 000+ files | Parallel with `workers=cpu_count` | Proven 3–6× speedup in benchmarks on 8-core CI runners | + +!!! note "No `--parallel` flag" + The CLI does not expose a `--parallel` flag in rc5. Parallelism is available exclusively + via the Python API. A `--workers N` CLI flag is planned for v0.4.0 stable. + +**What parallelism does NOT affect:** + +- External URL validation (`--links`) — not available in parallel mode; use + `scan_docs_references_with_links` instead. +- The Shield — runs per-worker; findings are collected and returned normally. +- Output order — results are sorted by `file_path` after collection to guarantee deterministic + output regardless of worker scheduling. + +**Pickling requirements for custom rules (`BaseRule` subclasses):** + +`ProcessPoolExecutor` serialises the `RuleEngine` (including all registered rules) using +Python `pickle` before dispatching it to worker processes. This imposes one constraint +on custom rule classes: + +- **Rules must be picklable.** A rule defined at module level is always picklable. A rule + defined inside a function, method, or `lambda` is not. +- **Pre-compiled regex patterns** (`re.compile(...)`) are picklable. Store compiled patterns + as class attributes or `__post_init__` instance attributes — never as `lambda` closures. +- **No mutable global state.** Workers receive independent copies of the rule engine. Any + state mutation inside a rule (e.g. a counter) is local to that worker process and is + discarded on completion. Rules that need global state must return it as part of their + `RuleFinding` output — not by writing to a shared variable. + +```python +# ✓ picklable — defined at module level, uses re.compile +class NoDraftRule(BaseRule): + _pattern = re.compile(r"(?i)\bDRAFT\b") + + @property + def rule_id(self) -> str: + return "ZZ-NODRAFT" + + def check(self, file_path: Path, text: str) -> list[RuleFinding]: + findings = [] + for lineno, line in enumerate(text.splitlines(), start=1): + if self._pattern.search(line): + findings.append(RuleFinding(file_path, lineno, self.rule_id, "DRAFT marker found")) + return findings + +# ✗ not picklable — defined inside a function +def make_rule(): + class LocalRule(BaseRule): ... # pickle cannot serialise this + return LocalRule() +``` + +`CustomRule` entries declared in `[[custom_rules]]` inside `zenzic.toml` are frozen +dataclasses with a compiled regex — they are always picklable and work in parallel mode +out of the box. --- diff --git a/docs/usage/index.md b/docs/usage/index.md index 286edf4..54525fd 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -10,6 +10,17 @@ icon: lucide/play Zenzic reads directly from the filesystem and works with any Markdown-based project. Use it in local development, as a pre-commit hook, in CI pipelines, or for one-off audits. +## Canonical examples + +The repository ships maintained fixtures that mirror the documented contracts: + +- `examples/mkdocs-basic/` — MkDocs 1.6 baseline (nested nav, external nav link, + tolerant YAML tags like `!ENV` and `!relative`). +- `examples/i18n-standard/` — strict bilingual gold standard. +- `examples/zensical-basic/` — Zensical v0.0.31+ baseline (`[project].nav`). +- `examples/broken-docs/` — intentional failures (exit 1). +- `examples/security_lab/` — security fixture (Shield exit 2). + !!! tip "Just want to run it now?" ```bash diff --git a/examples/broken-docs/README.md b/examples/broken-docs/README.md index c308d8b..9cb54da 100644 --- a/examples/broken-docs/README.md +++ b/examples/broken-docs/README.md @@ -25,7 +25,7 @@ what failures look like and to serve as a regression fixture for the check engin ## Visual Snippets -Every `check links` finding in Zenzic rc4 includes a **Visual Snippet** — the exact +Every `check links` finding in Zenzic rc5 includes a **Visual Snippet** — the exact source line from your Markdown file, displayed below the error header with a `│` indicator. Run the command and watch the terminal: @@ -74,8 +74,9 @@ Expected exit code: **1** (check failures; no Shield events). Uses `engine = "mkdocs"` (via `[build_context]` in `zenzic.toml`). The `mkdocs.yml` intentionally omits `api.md` and `orphan-nav.md` from the nav to trigger the orphan and -UNREACHABLE_LINK checks. The `zensical.toml` provides an alternative native-engine config -where `_drafts/` triggers the private-directory UNREACHABLE_LINK rule. +UNREACHABLE_LINK checks. The `zensical.toml` mirrors the current v0.0.31+ schema and +provides an alternative native-engine config where `_drafts/` triggers the +private-directory UNREACHABLE_LINK rule. ## Ground truth diff --git a/examples/broken-docs/zensical.toml b/examples/broken-docs/zensical.toml index debbda1..1aef180 100644 --- a/examples/broken-docs/zensical.toml +++ b/examples/broken-docs/zensical.toml @@ -1,19 +1,17 @@ # SPDX-FileCopyrightText: 2026 PythonWoods # SPDX-License-Identifier: Apache-2.0 -# zensical.toml — matches the schema used by the main Zenzic project. -# api.md is intentionally absent from [nav] to demonstrate the orphan check. - -scope = "project" +# zensical.toml — Zensical v0.0.31+ schema. +# api.md is intentionally absent from [project].nav to demonstrate the orphan check. [project] site_name = "Broken Docs Example" description = "Intentionally broken documentation site — demonstrates all five Zenzic checks" +docs_dir = "docs" [project.theme] name = "material" -[nav] nav = [ - {title = "Home", file = "index.md"}, - {title = "Tutorial", file = "tutorial.md"} + "index.md", + {"Tutorial" = "tutorial.md"} ] diff --git a/examples/i18n-standard/docs/index.md b/examples/i18n-standard/docs/index.md index 758b74c..3c2f545 100644 --- a/examples/i18n-standard/docs/index.md +++ b/examples/i18n-standard/docs/index.md @@ -14,7 +14,7 @@ structured bilingual documentation project that scores **100/100** under `zenzic | **Zero absolute links** | Every internal link is relative (`../`, `./`) | | **Path symmetry** | A link `../assets/brand-kit.zip` resolves identically from `.md` and `.it.md` | | **Asset integrity** | All referenced assets exist on disk; no unreferenced files left dangling | -| **No placeholders** | Every page has real content, no incomplete markers | +| **No unfinished markers** | Every page has real content and publication-ready text | ## Explore the structure diff --git a/examples/mkdocs-basic/README.md b/examples/mkdocs-basic/README.md new file mode 100644 index 0000000..803fdcd --- /dev/null +++ b/examples/mkdocs-basic/README.md @@ -0,0 +1,25 @@ +# mkdocs-basic — MkDocs 1.6.1 Reference Fixture + +A clean, minimal MkDocs 1.x project used as a stable baseline for Zenzic. +This fixture intentionally models the official MkDocs nav patterns: + +- plain page entries +- titled page entries +- nested sections +- external nav links +- configuration tags like !ENV and !relative + +## Run + +```bash +cd examples/mkdocs-basic +zenzic check all +``` + +Expected exit code: 0. + +## Why this fixture exists + +- Validates that MkDocsAdapter remains compatible with MkDocs 1.6.x config shapes. +- Demonstrates that Zenzic parses mkdocs.yml statically without calling MkDocs. +- Provides a migration-safe baseline before moving projects to Zensical. diff --git a/examples/mkdocs-basic/docs/api.md b/examples/mkdocs-basic/docs/api.md new file mode 100644 index 0000000..6e8a35d --- /dev/null +++ b/examples/mkdocs-basic/docs/api.md @@ -0,0 +1,9 @@ +# API + +The API page is linked directly from nav with a titled entry. + +In this fixture the content remains intentionally concise, but long enough to +exercise quality thresholds and demonstrate that page-level checks +operate on source text independently from rendered templates or plugin runtime. + +Back to the [Guide](guide/index.md) or [Home](index.md). diff --git a/examples/mkdocs-basic/docs/guide/index.md b/examples/mkdocs-basic/docs/guide/index.md new file mode 100644 index 0000000..feae46e --- /dev/null +++ b/examples/mkdocs-basic/docs/guide/index.md @@ -0,0 +1,9 @@ +# Guide + +The guide section is defined as a nested nav block. + +This page validates that section containers and child pages are interpreted +consistently with MkDocs 1.x rules, including stable URL mapping and previous +next navigation derivation from declared nav order rather than filesystem order. + +Continue with [Setup](setup.md) or return [Home](../index.md). diff --git a/examples/mkdocs-basic/docs/guide/setup.md b/examples/mkdocs-basic/docs/guide/setup.md new file mode 100644 index 0000000..73d410c --- /dev/null +++ b/examples/mkdocs-basic/docs/guide/setup.md @@ -0,0 +1,13 @@ +# Setup + +Install dependencies and run checks: + +```bash +uv add --dev zenzic +zenzic check all +``` + +This page exists to validate nested path resolution and anchor-safe link checks. +It also demonstrates that code snippets are parsed in pure Python mode and can +be linted without executing commands, preserving deterministic and secure +analysis for contributors and CI systems. diff --git a/examples/mkdocs-basic/docs/index.md b/examples/mkdocs-basic/docs/index.md new file mode 100644 index 0000000..1c30372 --- /dev/null +++ b/examples/mkdocs-basic/docs/index.md @@ -0,0 +1,16 @@ +# MkDocs Basic + +This fixture represents a clean MkDocs 1.6-style documentation tree. + +It is intentionally small but complete enough to validate navigation parsing, +route classification, and source-only linting behavior with deterministic +results. The goal is to provide a reliable benchmark that can be executed in +CI without network assumptions or build-engine subprocess dependencies. + +## Contents + +- [Guide](guide/index.md) +- [API](api.md) + +The navigation includes nested sections and one external link in mkdocs.yml. +Zenzic parses all of it from source configuration without running mkdocs build. diff --git a/examples/mkdocs-basic/mkdocs.yml b/examples/mkdocs-basic/mkdocs.yml new file mode 100644 index 0000000..1b9e8bb --- /dev/null +++ b/examples/mkdocs-basic/mkdocs.yml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 + +site_name: !ENV [MKDOCS_SITE_NAME, MkDocs Basic Example] +site_url: https://example.com/docs/ + +plugins: + search: {} + i18n: + docs_structure: folder + fallback_to_default: true + reconfigure_material: true + languages: + - locale: en + default: true + name: English + - locale: it + name: Italiano + +markdown_extensions: + - pymdownx.snippets: + base_path: !relative $config_dir/includes + +nav: + - Home: index.md + - Guide: + - guide/index.md + - Setup: guide/setup.md + - API: api.md + - Issue Tracker: https://github.com/PythonWoods/zenzic/issues diff --git a/examples/mkdocs-basic/zenzic.toml b/examples/mkdocs-basic/zenzic.toml new file mode 100644 index 0000000..04a1bb7 --- /dev/null +++ b/examples/mkdocs-basic/zenzic.toml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# zenzic.toml — MkDocs 1.x baseline fixture. + +docs_dir = "docs" +fail_under = 90 + +[build_context] +engine = "mkdocs" diff --git a/examples/run_demo.sh b/examples/run_demo.sh index 6b20ba1..ef47fec 100755 --- a/examples/run_demo.sh +++ b/examples/run_demo.sh @@ -6,6 +6,7 @@ # # Three acts that cover the full spectrum of documentation integrity: # +# Act 0 — MkDocs Baseline : mkdocs-basic must pass as 1.x reference. # Act 1 — The Gold Standard : i18n-standard must pass with 100/100. # Act 2 — The Broken Docs : broken-docs must fail, showing every error class. # Act 3 — The Shield : security_lab must block traversal and absolute links. @@ -48,16 +49,28 @@ echo " brand-kit.zip ready (main repo)" echo " i18n-standard assets are ghost artifacts — excluded_build_artifacts handles them" +# ─── Act 0: MkDocs baseline ────────────────────────────────────────────────── + +print_header "Act 0 — MkDocs baseline (mkdocs-basic)" +echo " Expected: SUCCESS — clean MkDocs 1.6-style fixture." +echo "" + +if (cd "$REPO_ROOT/examples/mkdocs-basic" && uv run zenzic check all); then + print_result "mkdocs-basic check all" "PASS" +else + print_result "mkdocs-basic check all" "UNEXPECTED FAILURE" +fi + # ─── Act 1: Gold Standard ───────────────────────────────────────────────────── print_header "Act 1 — The Gold Standard (i18n-standard)" echo " Expected: SUCCESS — 100/100 with no errors." echo "" -if (cd "$REPO_ROOT/examples/i18n-standard" && uv run zenzic check links); then - print_result "i18n-standard check links" "PASS" +if (cd "$REPO_ROOT/examples/i18n-standard" && uv run zenzic check all --strict); then + print_result "i18n-standard check all --strict" "PASS" else - print_result "i18n-standard check links" "UNEXPECTED FAILURE" + print_result "i18n-standard check all --strict" "UNEXPECTED FAILURE" fi # ─── Act 2: Broken Docs ─────────────────────────────────────────────────────── @@ -75,13 +88,18 @@ fi # ─── Act 3: The Shield ──────────────────────────────────────────────────────── print_header "Act 3 — The Shield (security_lab)" -echo " Expected: FAILURE — path traversal and absolute link violations blocked." +echo " Expected: EXIT 2 — Shield blocks credential exposure (and reports link violations)." echo "" -if (cd "$REPO_ROOT/examples/security_lab" && uv run zenzic check links); then +if (cd "$REPO_ROOT/examples/security_lab" && uv run zenzic check all); then print_result "security_lab Shield" "UNEXPECTED PASS" else - print_result "security_lab Shield" "FAIL" + code=$? + if [ "$code" -eq 2 ]; then + print_result "security_lab Shield (exit 2)" "FAIL" + else + print_result "security_lab Shield" "UNEXPECTED EXIT $code" + fi fi # ─── Self-audit + score snapshot ────────────────────────────────────────────── diff --git a/examples/vanilla/docs/guides/setup.md b/examples/vanilla/docs/guides/setup.md index 63cdea8..57120aa 100644 --- a/examples/vanilla/docs/guides/setup.md +++ b/examples/vanilla/docs/guides/setup.md @@ -30,7 +30,7 @@ docs_dir = "docs" ``` That is the entire required configuration. Run `zenzic check all` — Zenzic will -validate links, snippets, placeholders, and assets across every `.md` file under +validate links, snippets, content quality, and assets across every `.md` file under `docs/`. ## Adding custom rules @@ -39,10 +39,10 @@ Extend enforcement without writing Python: ```toml [[custom_rules]] -id = "ZZ-NOFIXME" -pattern = "(?i)\\bFIXME\\b" -message = "FIXME markers must be resolved before publishing." -severity = "error" +id = "ZZ-NOHTML" +pattern = "<(?!--)[a-zA-Z]" +message = "Avoid raw HTML in Markdown — use native Markdown syntax instead." +severity = "warning" ``` ## Next steps diff --git a/examples/vanilla/docs/index.md b/examples/vanilla/docs/index.md index 34c8b32..6356d1a 100644 --- a/examples/vanilla/docs/index.md +++ b/examples/vanilla/docs/index.md @@ -6,7 +6,7 @@ This project has no build engine. There is no `mkdocs.yml`, no `zensical.toml`, no Hugo config, no Docusaurus config. Just Markdown files and a `zenzic.toml`. -Zenzic runs in **Vanilla mode**: links, snippets, placeholders, assets, and custom +Zenzic runs in **Vanilla mode**: links, snippets, content quality, assets, and custom rules are all checked. The orphan check is skipped — with no declared navigation, every file is reachable by definition. diff --git a/examples/zensical-basic/README.md b/examples/zensical-basic/README.md new file mode 100644 index 0000000..47d7bcd --- /dev/null +++ b/examples/zensical-basic/README.md @@ -0,0 +1,23 @@ +# Zensical Basic Example + +A minimal project showing Zenzic integration with the +[Zensical](https://zensical.org) build engine (v0.0.31+). + +## What it demonstrates + +| Feature | Where | +| :--- | :--- | +| `[project].nav` syntax (v0.0.31+) | `zensical.toml` | +| `engine = "zensical"` in `zenzic.toml` | `zenzic.toml` | +| Nested nav sections | `zensical.toml` nav | +| Orphan detection via `ORPHAN_BUT_EXISTING` | any file absent from nav | +| Clean relative links | `docs/**/*.md` | + +## Run + +```bash +cd examples/zensical-basic +zenzic check all +``` + +Expected result: `SUCCESS` with a score ≥ 90. diff --git a/examples/zensical-basic/docs/api/endpoints.md b/examples/zensical-basic/docs/api/endpoints.md new file mode 100644 index 0000000..c3988c7 --- /dev/null +++ b/examples/zensical-basic/docs/api/endpoints.md @@ -0,0 +1,21 @@ +# Endpoints + +This page is listed under `{"Endpoints" = "api/endpoints.md"}` in the nav section. + +## `GET /status` + +Returns the service health status. + +In this fixture, the endpoint payload is intentionally simple because the goal +is not API complexity but documentation consistency: valid JSON snippets, +portable relative links, and enough narrative content to pass quality checks +without introducing artificial boilerplate. The same pattern can be extended to +real endpoints with authentication, pagination, and versioning sections. + +**Response:** + +```json +{"status": "ok", "version": "1.0.0"} +``` + +Back to [API Reference](index.md) or the [home page](../index.md). diff --git a/examples/zensical-basic/docs/api/index.md b/examples/zensical-basic/docs/api/index.md new file mode 100644 index 0000000..575f9ad --- /dev/null +++ b/examples/zensical-basic/docs/api/index.md @@ -0,0 +1,25 @@ +# API Reference + +This section is declared as a nested nav section in `zensical.toml`: + +```toml +[project] +nav = [ + {"API" = [ + "api/index.md", + {"Endpoints" = "api/endpoints.md"}, + ]}, +] +``` + +## Overview + +- [Endpoints](endpoints.md) — full list of API operations. + +This reference page exists to verify that nested nav sections are interpreted +consistently by Zenzical and by Zenzic's `ZensicalAdapter`. The adapter must +extract `api/index.md` and `api/endpoints.md` from `[project].nav`, classify +both routes as reachable, and keep internal links fully portable with relative +paths only. This keeps the example realistic and lint-clean. + +Return to the [Guide](../guide.md) or the [home page](../index.md). diff --git a/examples/zensical-basic/docs/guide.md b/examples/zensical-basic/docs/guide.md new file mode 100644 index 0000000..f81d429 --- /dev/null +++ b/examples/zensical-basic/docs/guide.md @@ -0,0 +1,39 @@ +# Guide + +This page is listed in the nav under `{"Guide" = "guide.md"}`. + +## Getting started + +Install Zensical with `pip` or `uv`: + +```bash +pip install zensical +# or +uv add --dev zensical +``` + +Then create your `zensical.toml`: + +```toml +[project] +site_name = "My Docs" +docs_dir = "docs" +``` + +Run the dev server: + +```bash +zensical serve +``` + +## Navigation formats + +Zensical accepts three nav entry forms inside `[project].nav`: + +| Form | TOML syntax | Description | +| :--- | :--- | :--- | +| Plain string | `"page.md"` | Title inferred from first `#` heading | +| Titled page | `{"Title" = "page.md"}` | Explicit sidebar label | +| Section | `{"Section" = ["a.md", ...]}` | Collapsible group | + +See the [home page](index.md) or the [API Reference](api/index.md). diff --git a/examples/zensical-basic/docs/index.md b/examples/zensical-basic/docs/index.md new file mode 100644 index 0000000..72f3168 --- /dev/null +++ b/examples/zensical-basic/docs/index.md @@ -0,0 +1,17 @@ +# Welcome + +This is the home page of the **Zensical Basic Example** — a minimal project +used to demonstrate how to use Zenzic with the [Zensical][zensical] build +engine. + +[zensical]: https://zensical.org + +## What this example shows + +- A `zensical.toml` using the **`[project]` scope** (Zensical v0.0.31+). +- A `zenzic.toml` with `engine = "zensical"` activating `ZensicalAdapter`. +- A small but complete navigation tree to exercise orphan detection. +- Clean internal links using relative paths. + +Navigate to the [Guide](guide.md) or the [API Reference](api/index.md) to +explore the example. diff --git a/examples/zensical-basic/zensical.toml b/examples/zensical-basic/zensical.toml new file mode 100644 index 0000000..8a004ce --- /dev/null +++ b/examples/zensical-basic/zensical.toml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# +# zensical.toml — minimal Zensical v0.0.31+ project configuration. +# +# All settings live under the [project] scope. +# Navigation supports three entry forms, freely mixed: +# - plain string → title inferred from the first H1 +# - {"Title" = "file.md"} → titled page +# - {"Section" = [...]} → nested section +# +# Zenzic reads this file to: +# 1. Extract nav paths for ORPHAN_BUT_EXISTING detection. +# 2. Derive use_directory_urls for canonical URL computation. +# +# Target: zenzic check all → SUCCESS + +[project] +site_name = "Zensical Basic Example" +site_url = "https://example.com/" +docs_dir = "docs" + +nav = [ + "index.md", + {"Guide" = "guide.md"}, + {"API" = [ + "api/index.md", + {"Endpoints" = "api/endpoints.md"}, + ]}, +] diff --git a/examples/zensical-basic/zenzic.toml b/examples/zensical-basic/zenzic.toml new file mode 100644 index 0000000..40a2c90 --- /dev/null +++ b/examples/zensical-basic/zenzic.toml @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# +# zenzic.toml — Zenzic configuration for the zensical-basic example. +# +# Declares engine = "zensical", which activates ZensicalAdapter. +# ZensicalAdapter reads navigation from [project].nav in zensical.toml. +# +# Enforcement contract: zensical.toml MUST exist when engine = "zensical". +# Zenzic raises ConfigurationError immediately if it is absent. +# +# Target: zenzic check all → SUCCESS + +docs_dir = "docs" + +# Minimum quality score. Zenzic exits 1 if the measured score drops below +# this threshold. Set to 0 to disable the check. +fail_under = 90 + +[build_context] +engine = "zensical" diff --git a/mkdocs.yml b/mkdocs.yml index 18116a2..f6f4f1b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -142,7 +142,7 @@ extra_css: extra: build_date: !ENV [BUILD_DATE, "dev"] generator: false - version: "0.4.0rc4" + version: "0.4.0rc5" social: - icon: fontawesome/brands/github link: https://github.com/PythonWoods/zenzic diff --git a/overrides/partials/header.html b/overrides/partials/header.html index 37fdbb4..412d1fc 100644 --- a/overrides/partials/header.html +++ b/overrides/partials/header.html @@ -19,6 +19,26 @@ {% set label_docs = "Docs" %} {% endif %} +{# Language switcher sources: explicit extra.alternate or i18n plugin languages #} +{% set alternate = namespace(items=[]) %} +{% if config.extra.alternate %} + {% set alternate.items = config.extra.alternate %} +{% elif "i18n" in config.plugins %} + {% set i18n = config.plugins["i18n"] | attr("config") %} + {% if i18n and i18n.languages %} + {% for language in i18n.languages %} + {% if language.build | d(true) %} + {% if language.default | d(false) %} + {% set link = language.link | d("/") %} + {% else %} + {% set link = language.link | d("/" ~ language.locale ~ "/") %} + {% endif %} + {% set alternate.items = alternate.items + [{"name": language.name | d(language.locale), "link": link, "lang": language.locale}] %} + {% endif %} + {% endfor %} + {% endif %} +{% endif %} +