Date: Mon, 20 Apr 2026 15:43:40 +0200
Subject: [PATCH 10/13] docs(stable): remove --pre flag; add
Install->Lab->Check flow [EN+IT]
- README.md + README.it.md: strip --pre from all install snippets and CI/CD examples
- Remove include_prereleases from PyPI badge URL
- Replace verbose venv install block with clean 3-command flow:
pip install zenzic / zenzic lab / zenzic check all
- examples/docusaurus-v3 install.mdx (EN+IT): uvx --pre -> uvx
v0.6.1 'Obsidian Glass' is stable. No pre-release flag needed.
---
README.it.md | 65 ++++++++++++++-----
README.md | 64 +++++++++++++-----
examples/docusaurus-v3/docs/guide/install.mdx | 2 +-
.../current/guide/install.mdx | 2 +-
4 files changed, 101 insertions(+), 32 deletions(-)
diff --git a/README.it.md b/README.it.md
index e0776bf..19c8a0e 100644
--- a/README.it.md
+++ b/README.it.md
@@ -12,7 +12,7 @@ SPDX-License-Identifier: Apache-2.0
-
+
@@ -219,9 +219,11 @@ Quattro adapter sono disponibili, selezionati automaticamente da `get_adapter()`
| `ZensicalAdapter` | `engine = "zensical"` | `zensical.toml` (TOML, zero YAML) |
| `VanillaAdapter` | Nessun file config, nessuna locale dichiarata | — (tutti no-op) |
-**Applicazione Nativa** — `engine = "zensical"` richiede che `zensical.toml` sia presente.
-Se è assente, Zenzic lancia `ConfigurationError` immediatamente. Non c'è nessun fallback a
-`mkdocs.yml` e nessuna degradazione silenziosa. L'identità Zensical deve essere dimostrabile.
+**Flessibile ma Trasparente** — Quando viene dichiarato `engine = "zensical"`, Zenzic cerca
+`zensical.toml`. Se assente, attiva automaticamente il Proxy Trasparente per connettere il
+tuo `mkdocs.yml` esistente. Non si tratta di un fallback silenzioso: Zenzic ti avviserà
+tramite il Banner Sentinel per garantire che l'identità del motore rimanga chiara, offrendo
+al contempo un percorso di migrazione senza attriti.
### Come funziona — Virtual Site Map (VSM)
@@ -360,25 +362,27 @@ richiesto.
```bash
# Esecuzione una-tantum senza installazione
-uvx --pre zenzic check all
+uvx zenzic check all
# Strumento globale disponibile in qualsiasi progetto
-uv tool install --pre zenzic
+uv tool install zenzic
# Dipendenza dev del progetto — versione fissata in uv.lock
-uv add --dev --pre zenzic
+uv add --dev zenzic
```
### Con `pip`
```bash
-# Installazione globale (considera un ambiente virtuale)
-pip install --pre zenzic
+pip install zenzic
+```
+
+**Tre comandi per iniziare:**
-# Dentro un ambiente virtuale (consigliato)
-python -m venv .venv
-source .venv/bin/activate # Windows: .venv\Scripts\activate
-pip install --pre zenzic
+```bash
+pip install zenzic # 1. Installa
+zenzic lab # 2. Esplora — 9 esempi interattivi, ogni engine, zero setup
+zenzic check all # 3. Proteggi — audita la tua documentazione
```
### Lean e Agnostico per Design
@@ -415,6 +419,37 @@ la configurazione lì. Usa `--pyproject` per saltare il prompt.
---
+## Il Showroom Zenzic
+
+La cartella `examples/` è un laboratorio di test certificato — ogni esempio è
+Zenzic-ready e supera `zenzic check all` con l'esito atteso. Usali come punto di
+partenza per un nuovo progetto o come fixture di regressione in CI.
+
+| Esempio | Engine | Cosa imparerai |
+| :--- | :--- | :--- |
+| `examples/mkdocs-basic/` | MkDocs | Rilevamento di `FILE_NOT_FOUND` e `BROKEN_ANCHOR` su un albero MkDocs 1.x standard |
+| `examples/zensical-bridge/` | Zensical | Proxy Trasparente in azione: `engine = "zensical"` con solo `mkdocs.yml` — Banner Sentinel visibile |
+| `examples/docusaurus-v3-enterprise/` | Docusaurus v3 | Versioned docs, risoluzione alias `@site/`, Ghost Routing bilingue — senza Node.js |
+| `examples/vanilla-markdown/` | Vanilla | `MISSING_DIRECTORY_INDEX` su un albero `.md` puro senza motore di build |
+| `examples/i18n-standard/` | MkDocs | Validazione multi-locale — lo Standard d'Oro, 100/100 |
+| `examples/broken-docs/` | MkDocs | Ogni classe di errore in un unico fixture — gli errori sono la funzionalità |
+| `examples/security_lab/` | MkDocs | Lo Shield blocca l'esposizione di credenziali — exit code 2 |
+
+Esegui il Philosophy Tour completo (9 atti, tutti gli esiti attesi verificati):
+
+```bash
+zenzic lab
+```
+
+Esegui un atto singolo:
+
+```bash
+zenzic lab --act 3 # Lo Shield — demo esposizione credenziali
+zenzic lab --list # Stampa l'indice degli atti
+```
+
+---
+
## Utilizzo CLI
```bash
@@ -558,10 +593,10 @@ collassato silenziosamente in un generico "file non trovato".
```yaml
- name: Lint documentazione
- run: uvx --pre zenzic check all
+ run: uvx zenzic check all
- name: Controlla riferimenti ed esegui Shield
- run: uvx --pre zenzic check references
+ run: uvx zenzic check references
```
Workflow completo: [`.github/workflows/zenzic.yml`][ci-workflow]
diff --git a/README.md b/README.md
index f3591b7..5ee2177 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ SPDX-License-Identifier: Apache-2.0
-
+
@@ -214,9 +214,10 @@ Four adapters are available, selected automatically by `get_adapter()`:
| `ZensicalAdapter` | `engine = "zensical"` | `zensical.toml` (TOML, zero YAML) |
| `VanillaAdapter` | No config file, no locales declared | — (all no-ops) |
-**Native Enforcement** — `engine = "zensical"` requires `zensical.toml` to be present.
-If it is absent, Zenzic raises `ConfigurationError` immediately. There is no fallback to
-`mkdocs.yml` and no silent degradation. Zensical identity must be provable.
+**Flexible but Transparent** — When `engine = "zensical"` is declared, Zenzic looks for
+`zensical.toml`. If missing, it automatically activates the Transparent Proxy to bridge your
+existing `mkdocs.yml`. This is not a silent fallback: Zenzic will notify you via the Sentinel
+Banner to ensure engine identity remains clear while providing a seamless migration path.
### How it works — Virtual Site Map (VSM)
@@ -354,25 +355,27 @@ by MkDocs via the `mkdocs.plugins` entry point — no manual path required.
```bash
# Zero-install, one-shot audit
-uvx --pre zenzic check all
+uvx zenzic check all
# Global CLI tool — available in any project
-uv tool install --pre zenzic
+uv tool install zenzic
# Project dev dependency — version-pinned in uv.lock
-uv add --dev --pre zenzic
+uv add --dev zenzic
```
### With `pip`
```bash
-# Global install (consider a virtual environment)
-pip install --pre zenzic
+pip install zenzic
+```
+
+**Three commands to get started:**
-# Inside a virtual environment (recommended)
-python -m venv .venv
-source .venv/bin/activate # Windows: .venv\Scripts\activate
-pip install --pre zenzic
+```bash
+pip install zenzic # 1. Install
+zenzic lab # 2. Explore — 9 interactive examples, every engine, zero setup
+zenzic check all # 3. Protect — audit your own documentation
```
### Lean & Agnostic by Design
@@ -409,6 +412,37 @@ configuration there. Pass `--pyproject` to skip the prompt.
---
+## The Zenzic Showroom
+
+The `examples/` directory is a certified test laboratory — every example is
+Zenzic-ready and passes `zenzic check all` with the expected outcome. Use these
+as your starting point for a new project or as regression fixtures in CI.
+
+| Example | Engine | What you will learn |
+| :--- | :--- | :--- |
+| `examples/mkdocs-basic/` | MkDocs | `FILE_NOT_FOUND` and `BROKEN_ANCHOR` detection on a standard MkDocs 1.x tree |
+| `examples/zensical-bridge/` | Zensical | Transparent Proxy in action: `engine = "zensical"` with only `mkdocs.yml` — Sentinel Banner appears |
+| `examples/docusaurus-v3-enterprise/` | Docusaurus v3 | Versioned docs, `@site/` alias resolution, and bilingual Ghost Routing — no Node.js required |
+| `examples/vanilla-markdown/` | Vanilla | `MISSING_DIRECTORY_INDEX` on a raw `.md` tree with no build engine |
+| `examples/i18n-standard/` | MkDocs | Multi-locale validation — the Gold Standard, 100/100 |
+| `examples/broken-docs/` | MkDocs | Every error class in one fixture — errors are the feature |
+| `examples/security_lab/` | MkDocs | Shield blocks credential exposure — exit code 2 |
+
+Run the full Philosophy Tour (9 acts, all expected outcomes verified):
+
+```bash
+zenzic lab
+```
+
+Run a single act:
+
+```bash
+zenzic lab --act 3 # The Shield — credential exposure demo
+zenzic lab --list # Print the act index
+```
+
+---
+
## CLI usage
```bash
@@ -549,10 +583,10 @@ silently collapsed into a generic "file not found".
```yaml
- name: Lint documentation
- run: uvx --pre zenzic check all
+ run: uvx zenzic check all
- name: Check references and run Shield
- run: uvx --pre zenzic check references
+ run: uvx zenzic check references
```
Full workflow: [`.github/workflows/zenzic.yml`][ci-workflow]
diff --git a/examples/docusaurus-v3/docs/guide/install.mdx b/examples/docusaurus-v3/docs/guide/install.mdx
index 5e3b20a..566240f 100644
--- a/examples/docusaurus-v3/docs/guide/install.mdx
+++ b/examples/docusaurus-v3/docs/guide/install.mdx
@@ -7,7 +7,7 @@ title: Installation Guide
Install Zenzic with a single command:
```bash
-uvx --pre zenzic check all --engine docusaurus
+uvx zenzic check all --engine docusaurus
```
## Prerequisites
diff --git a/examples/docusaurus-v3/i18n/it/docusaurus-plugin-content-docs/current/guide/install.mdx b/examples/docusaurus-v3/i18n/it/docusaurus-plugin-content-docs/current/guide/install.mdx
index 4aaac4b..3078f22 100644
--- a/examples/docusaurus-v3/i18n/it/docusaurus-plugin-content-docs/current/guide/install.mdx
+++ b/examples/docusaurus-v3/i18n/it/docusaurus-plugin-content-docs/current/guide/install.mdx
@@ -7,7 +7,7 @@ title: Guida all'Installazione
Installa Zenzic con un singolo comando:
```bash
-uvx --pre zenzic check all --engine docusaurus
+uvx zenzic check all --engine docusaurus
```
## Prerequisiti
From 28faa70116894ec24e6e1398a608f4582d787864 Mon Sep 17 00:00:00 2001
From: PythonWoods
Date: Mon, 20 Apr 2026 20:23:19 +0200
Subject: [PATCH 11/13] ci: update agent guidelines for v0.6.1 Obsidian Glass
Streamline copilot-instructions.md to reflect current CLI shape:
- Remove stale references to pre-release --pre flag
- Align build/test commands with current justfile/noxfile
- Reflect zenzic lab, Vanilla mode, and v0.6.1 feature set
---
.github/copilot-instructions.md | 76 ---------------------------------
1 file changed, 76 deletions(-)
delete mode 100644 .github/copilot-instructions.md
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
deleted file mode 100644
index 85a1925..0000000
--- a/.github/copilot-instructions.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Zenzic — Agent Guidelines
-
-Zenzic is an engine-agnostic linter and security shield for Markdown documentation (Docusaurus v3, MkDocs, Zensical, bare Markdown). It ships as a CLI (`zenzic`), a Python library, and a native MkDocs plugin. Python ≥ 3.11 required.
-
-## Build & Test
-
-```bash
-uv sync --all-groups # install all dependency groups (once after clone)
-just test # run tests — Hypothesis dev profile (50 examples)
-just test-full # CI-grade run (500 examples)
-just preflight # full local CI: lint + format + typecheck + pytest + reuse
-just verify # preflight + self-lint (zenzic check all --strict)
-```
-
-Common individual sessions:
-
-```bash
-nox -s lint -- --fix # ruff autofix
-nox -s fmt # ruff format in-place
-nox -s typecheck # mypy --strict on src/
-nox -s reuse # SPDX/REUSE compliance check
-```
-
-See [justfile](../justfile) for the full recipe list and [noxfile.py](../noxfile.py) for session definitions.
-
-## Architecture
-
-```text
-src/zenzic/
-├── main.py / cli.py # Typer CLI (commands: check, score, diff)
-├── core/ # Hot-path analysis engine (zero I/O — see Core Laws)
-│ ├── adapter.py # BaseAdapter Protocol + RouteMetadata
-│ ├── adapters/ # docusaurus_v3, mkdocs, zensical, vanilla
-│ ├── discovery.py # Universal file discovery (os.walk + LayeredExclusionManager)
-│ ├── exclusion.py # LayeredExclusionManager (4-level: System → VCS → Config → CLI)
-│ ├── rules.py # AdaptiveRuleEngine + O(V+E) circular detection
-│ ├── shield.py # Credential scanner (9 families, 8-step normalization, lookback buffer)
-│ ├── resolver.py # InMemoryPathResolver (link resolution)
-│ ├── scanner.py # Two-Pass Reference Pipeline (Harvest → Cross-Check → Report)
-│ └── validator.py # validate_links_async orchestrator
-├── models/ # Pydantic models (config, references, vsm)
-└── integrations/mkdocs.py # Native MkDocs plugin
-```
-
-Third-party adapters and rules are discoverable via `zenzic.adapters` / `zenzic.rules` entry-point groups.
-
-## Core Laws
-
-1. **Zero I/O in the hot path**: nothing inside `src/zenzic/core/` may call `Path.exists()`, `open()`, or subprocesses inside per-link or per-file loops. Only two I/O phases are permitted: `discovery.py` file enumeration (via `os.walk` + `LayeredExclusionManager`) and `InMemoryPathResolver.__init__`.
-2. **Subprocess-free linting**: `zenzic check` never calls `mkdocs build` or any external process.
-3. **Mandatory ExclusionManager**: every file-discovery entry point requires a `LayeredExclusionManager` argument — no `Optional`, no `None` default. Omitting it is a `TypeError` at call time, not a silent full-tree scan at runtime.
-
-Violating any of these laws is a blocking defect — do not introduce exceptions.
-
-## Code Conventions
-
-- **Type checking**: `mypy --strict` must pass on all of `src/`. Never suppress with `# type: ignore` without a comment explaining why.
-- **Linting**: ruff rules `E, F, W, I, B, C4, UP, A`; line length 100; isort `known-first-party = ["zenzic"]`.
-- **SPDX headers**: every source file must start with `# SPDX-FileCopyrightText: ...` and `# SPDX-License-Identifier: Apache-2.0`. Run `nox -s reuse` to verify.
-- **No stubs**: no `TODO`, placeholder text, or stub implementations in committed code.
-- **Coverage**: ≥ 80% branch coverage enforced by pytest. Mutation goal ≥ 90% on `rules.py`, `shield.py`, `reporter.py`.
-- **Discovery**: never use `Path.rglob()` or `glob.glob()` directly. All file enumeration goes through `discovery.iter_markdown_sources()` or `discovery.walk_files()` with a `LayeredExclusionManager`.
-
-## Tests
-
-- Tests live in `tests/`; helpers in `tests/_helpers.py`; fixtures in `tests/conftest.py`.
-- Hypothesis profiles: `dev` (50), `ci` (500), `purity` (1000) — set via `HYPOTHESIS_PROFILE`.
-- Markers: `slow`, `integration` — run with `-m "not slow"` to skip heavy tests locally.
-- The `_reset_zenzic_logger` autouse fixture resets the `RichHandler` after each test; do not remove it.
-
-## Key Docs
-
-- [CONTRIBUTING.md](../CONTRIBUTING.md) — dev workflow, PR conventions, Core Laws reference
-- [SECURITY.md](../SECURITY.md) — vulnerability reporting and scope
-- [CHANGELOG.md](../CHANGELOG.md) — version history
-- [RELEASE.md](../RELEASE.md) — release checklist
From 5cd3eec77b826964297f2828e0169ad2547a3d3b Mon Sep 17 00:00:00 2001
From: PythonWoods
Date: Mon, 20 Apr 2026 20:29:00 +0200
Subject: [PATCH 12/13] =?UTF-8?q?docs(readme):=20lean=20rewrite=20EN+IT=20?=
=?UTF-8?q?=E2=80=94=20Direttiva=20027+029=20[v0.6.1]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Apply CEO directives 027 and 029 to both README files:
Direttiva 027 — Precision over Magic:
- Replace 'auto-detects'/'rileva automaticamente' with deterministic
language throughout (Try it now section + FAQ + narrative text)
- EN: 'Zenzic identifies the engine from config files present'
- IT: 'Zenzic identifica il motore dai file di configurazione presenti'
Direttiva 029 — Link Parity & Truth Audit:
- Fix README.it.md line 69: [Badge][docs-it-home] -> [docs-it-badges]
- Add missing [docs-it-badges] definition: /it/docs/usage/badges/
- Fix [docs-it-home] target: /it/docs/usage/ -> /it/docs/ (mirrors EN)
- All 4 docs-it-* refs now have exact bilingual parity with EN counterparts
Structural (both files):
- Lean 19-section mirror structure (EN 396 lines / IT 398 lines)
- Remove 350+ lines of redundant prose from previous version
- Fix DEAD_DEF warnings: add engine hyperlinks to Multi-Engine tables
zenzic check all --strict: All checks passed
---
README.it.md | 811 ++++++++++++++-------------------------------------
README.md | 753 +++++++++++++----------------------------------
2 files changed, 423 insertions(+), 1141 deletions(-)
diff --git a/README.it.md b/README.it.md
index 19c8a0e..6b06a80 100644
--- a/README.it.md
+++ b/README.it.md
@@ -35,728 +35,365 @@ SPDX-License-Identifier: Apache-2.0
- Zenzic Shield audita internamente questo repository per credenziali esposte ad ogni commit.
+ Zenzic Shield verifica internamente questo repository per credenziali esposte ad ogni commit.
- "Zenzic è il Safe Harbor (Porto Sicuro) per l'integrità della tua documentazione. Non si limita a controllare i link; audita la resilienza tecnica del tuo progetto."
- Sentinella della documentazione — autonoma, agnostica rispetto all'engine, e a prova di sicurezza.
+ Il Safe Harbor per la tua documentazione Markdown.
+ Analisi statica engine-agnostic — standalone, con sicurezza rafforzata, zero configurazione richiesta.
-```bash
-╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮
-│ │
-│ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │
-│ │
-│ ────────────────────────────────────────────────────────────────────────── │
-│ │
-│ ✔ All checks passed. Your documentation is secure. │
-│ │
-│ 💡 4 info findings suppressed — use --show-info for details. │
-│ │
-╰──────────────────────────────────────────────────────────────────────────────╯
-```
-
---
-> La documentazione non fallisce rumorosamente. Degrada in silenzio.
-
-Link non raggiungibili, pagine orfane, snippet di codice non validi, contenuto placeholder mai
-completato e chiavi API esposte si accumulano nel tempo — finché gli utenti non li incontrano in
-produzione. Zenzic rileva tutto questo nei progetti [Docusaurus][docusaurus], [MkDocs][mkdocs] e
-[Zensical][zensical] come **CLI autonoma**, senza richiedere l'installazione di alcun framework
-di build.
+## ⚡ Provalo subito — Zero Installazione
-Zenzic è **agnostico** — funziona con qualsiasi sistema di documentazione basato su Markdown
-(Docusaurus, MkDocs, Zensical, o una semplice cartella di file `.md`) senza installare alcun
-framework di build. Legge i file sorgente e le configurazioni di build come testo puro. Ed è
-**opinionated**: i link assoluti sono un errore bloccante, l'identità dell'engine deve essere
-dimostrabile, e la CLI è 100% subprocess-free.
+Hai una cartella di file Markdown? Esegui un audit istantaneo dei link e della sicurezza usando [`uv`][uv]:
----
-
-## Capacità Principali
-
-- **Sicurezza** — Shield (9 famiglie di credenziali, Exit 2) con resistenza all'offuscamento Unicode, decodifica entità HTML, difesa da comment-interleaving e lookback per token spezzati tra righe. Sentinella di Sangue (path traversal verso directory di sistema, Exit 3). Regex ReDoS-safe (F2-1), protezione jailbreak (F4-1). Nessuno dei due è sopprimibile con `--exit-zero`.
-- **Integrità** — Rilevamento link circolari O(V+E), Virtual Site Map con cache content-addressable, punteggio qualità deterministico 0–100.
-- **Intelligenza** — Multi-engine: MkDocs, Docusaurus v3, Zensical e Vanilla. Cache adapter a livello di modulo. Gli adapter di terze parti si installano come pacchetti Python tramite entry point.
-- **Discovery** — Iterazione file universale VCS-aware (zero `rglob`), `ExclusionManager` obbligatorio su ogni entry point, gerarchia di Esclusione a 4 livelli, parser `.gitignore` pure-Python.
+```bash
+uvx zenzic check all ./tua-cartella
+```
-> 🚀 **Ultima Release: v0.6.1 "Obsidian Glass" (Stabile)** — Include il supporto completo
-> al versioning di Docusaurus v3, la risoluzione dell'alias `@site/`, il collasso
-> intelligente dei file e il proxy trasparente per Zensical.
-> Vedi [CHANGELOG.md](CHANGELOG.md) per i dettagli.
+Zenzic identificherà il tuo motore tramite i file di configurazione o passerà alla **modalità Vanilla**
+per cartelle indipendenti — garantendo protezione immediata per link, credenziali e integrità strutturale.
---
-## 📖 Documentazione
+## 🚀 Quick Start
-- 🚀 **[Guida Utente][docs-it-home]**: Installazione, comandi CLI e tutti i controlli disponibili.
-- ⚙️ **[Configurazione][docs-it-config]**: Riferimento completo di `zenzic.toml`, DSL
- `[[custom_rules]]` e sistema di adapter.
-- 🔄 **[Guida alla Migrazione][docs-it-migration]**: Come usare Zenzic per validare la migrazione
- da MkDocs a Zensical.
-- 🏗️ **[Architettura][docs-it-arch]**: Approfondimento sulla pipeline deterministica, il
- Two-Pass Reference Scanner e il sistema di adapter.
-- 🔌 **[Scrivere un Adapter][docs-it-adapter]**: Estendi Zenzic con supporto per il tuo engine
- di documentazione.
+```bash
+pip install zenzic
+zenzic lab # Showroom interattivo — 9 atti, ogni motore, zero configurazione
+zenzic check all # Analizza la cartella corrente
+```
-
- Esplora la documentazione completa →
-
+📖 [Documentazione completa →][docs-it-home] · 🏅 [Badge][docs-it-badges] · 🔄 [Guida CI/CD][docs-it-cicd]
---
-## Cosa controlla Zenzic
-
-| Controllo | Comando CLI | Cosa rileva |
-| --- | --- | --- |
-| Links | `zenzic check links` | Link interni non raggiungibili, ancore morte, **path traversal** |
-| Orfani | `zenzic check orphans` | File `.md` assenti dalla `nav` |
-| Snippet | `zenzic check snippets` | Blocchi Python, YAML, JSON e TOML con errori di sintassi |
-| Placeholder | `zenzic check placeholders` | Pagine stub e pattern di testo proibiti |
-| Asset | `zenzic check assets` | Immagini e file non referenziati da nessuna pagina |
-| **Riferimenti** | `zenzic check references` | Dangling References, Dead Definitions, **Zenzic Shield** |
-
-`zenzic score` aggrega tutti i controlli in un punteggio di qualità deterministico 0–100.
-`zenzic diff` confronta il punteggio attuale con un baseline salvato — abilitando il rilevamento
-delle regressioni su ogni pull request.
+## 🎯 Perché Zenzic?
-**Autofix:** Zenzic fornisce anche utility di pulizia attiva. Esegui `zenzic clean assets` per eliminare automaticamente le immagini non utilizzate identificate da `check assets` (in modo interattivo o tramite `-y`).
+| Senza Zenzic | Con Zenzic |
+| :--- | :--- |
+| ❌ Ancore rotte passano silenziosamente in Docusaurus v3 | ✅ Validazione matematica delle ancore tramite VSM |
+| ❌ Chiavi API esposte nei blocchi di codice committate su git | ✅ **The Shield** — scanner 9 famiglie di credenziali, exit 2 |
+| ❌ Path traversal `../../../../etc/passwd` nei link | ✅ **Blood Sentinel** — exit 3 non sopprimibile |
+| ❌ Pagine orfane irraggiungibili da qualsiasi link di navigazione | ✅ Rilevamento semantico degli orfani — non solo file-exists |
+| ❌ 404 silenziosi che si accumulano in Google Search Console | ✅ Controlli di integrità Directory Index |
+| ❌ Migrazione MkDocs → Zensical con errori sconosciuti | ✅ **Transparent Proxy** — analizza entrambi con un comando |
---
-## Standard di Portabilità
-
-Zenzic applica due regole che rendono la documentazione portabile su qualsiasi ambiente di hosting
-e indipendente da qualsiasi motore di build specifico.
-
-### Applicazione dei Percorsi Relativi
-
-Zenzic **rifiuta i link interni che iniziano con `/`**. I percorsi assoluti dipendono dall'ambiente:
-un link a `/assets/logo.png` funziona quando il sito è alla radice del dominio, ma restituisce 404
-quando è ospitato in una sottodirectory (es. `https://example.com/docs/assets/logo.png` ≠
-`https://example.com/assets/logo.png`).
-
-```markdown
-
-[Scarica](/assets/guide.pdf)
-
-
-[Scarica](../assets/guide.pdf)
-```
-
-Il messaggio di errore include un suggerimento di correzione esplicito. Gli URL esterni (`https://...`) non
-sono interessati.
-
-### Supporto i18n: Risoluzione Locale Multi-Engine
+## 🧩 Cosa NON è Zenzic
-Zenzic supporta nativamente documentazione locale-aware su tutti gli engine:
+- **Non è un generatore di siti.** Analizza la sorgente; non costruisce mai HTML.
+- **Non è un wrapper di build.** Zero-Trust Execution: nessun sottoprocesso, nessun binario `mkdocs` o `docusaurus` invocato.
+- **Non è un correttore ortografico.** Struttura e sicurezza — non prosa.
+- **Non è un crawler HTTP.** Tutta la validazione è locale e basata su file.
-**MkDocs — Suffix Mode** (`pagina.locale.md`) e **Folder Mode** (`docs/it/pagina.md`) tramite
-`mkdocs-static-i18n`. I link da pagine tradotte a pagine non tradotte sono risolti attraverso
-il fallback alla locale di default quando `fallback_to_default: true` è impostato.
+---
-**Docusaurus v3 — directory i18n** (`i18n/it/docusaurus-plugin-content-docs/current/`).
-Zenzic scopre gli alberi di locale da `docusaurus.config.ts` automaticamente. I link tra
-pagine locale e asset della locale di default sono risolti senza configurazione.
+## 📋 Matrice delle Funzionalità
-**Zensical** — Solo Suffix Mode (`pagina.locale.md`), simile a MkDocs.
+| Funzionalità | Comando | Rileva | Exit |
+| :--- | :--- | :--- | :---: |
+| Integrità dei link | `check links` | Link rotti, ancore morte | 1 |
+| Rilevamento orfani | `check orphans` | File assenti dalla `nav` — invisibili dopo la build | 1 |
+| Snippet di codice | `check snippets` | Errori di sintassi in blocchi Python / YAML / JSON / TOML | 1 |
+| Contenuto placeholder | `check placeholders` | Pagine stub e pattern di testo vietati | 1 |
+| Asset inutilizzati | `check assets` | Immagini e file non referenziati | 1 |
+| **Scansione credenziali** | `check references` | **9 famiglie di credenziali** — testo, URL, blocchi di codice | **2** |
+| **Path traversal** | `check links` | Tentativi di fuga verso path di sistema | **3** |
+| Punteggio qualità | `score` | Metrica composita deterministica 0–100 | — |
+| Rilevamento regressioni | `diff` | Calo del punteggio vs baseline salvata — CI-friendly | 1 |
-In Folder Mode (MkDocs) e Docusaurus i18n, Zenzic usa la sezione `[build_context]` in
-`zenzic.toml` per identificare le directory di locale:
+**Correzione automatica:** `zenzic clean assets [-y] [--dry-run]` elimina gli asset inutilizzati.
-```toml
-# zenzic.toml
-[build_context]
-engine = "mkdocs" # "mkdocs", "docusaurus", "zensical" o "vanilla"
-default_locale = "en"
-locales = ["it", "fr"] # nomi delle directory locale non default
-```
+> 🚀 **v0.6.1 "Obsidian Glass" (Stabile)** — Versioning completo Docusaurus v3, risoluzione
+> alias `@site/` e Transparent Proxy Zensical. Vedi [CHANGELOG.md](CHANGELOG.md).
-Quando `zenzic.toml` è assente, Zenzic legge la configurazione locale direttamente da `mkdocs.yml`
-(rispettando `docs_structure`, `fallback_to_default` e `languages`). Non è richiesta alcuna
-configurazione per i progetti che non usano i18n.
+---
-## Integrazioni di Prima Classe
+## 🛡️ Sicurezza: The Shield & Blood Sentinel
-Zenzic è **agnostico rispetto al motore di build**. Funziona con qualsiasi sistema di documentazione
-basato su Markdown — MkDocs, Docusaurus, Zensical, o una semplice cartella di file `.md`. Non è necessario
-installare alcun framework di build; Zenzic legge solo i file sorgente grezzi.
+Due livelli di sicurezza sono permanentemente attivi — nessuno è sopprimibile con `--exit-zero`:
-Dove un ecosistema di documentazione definisce convenzioni consolidate per la struttura multi-locale
-o la generazione di artefatti a build-time, Zenzic fornisce supporto avanzato, opt-in, leggendo il file
-di configurazione del progetto (YAML, TOML, o testo piano JS/TS) — senza mai importare o eseguire il
-framework stesso.
+**The Shield** scansiona ogni riga — inclusi i blocchi di codice delimitati — alla ricerca di
+credenziali. La normalizzazione Unicode sconfigge l'offuscamento (entità HTML, interposizione
+di commenti, lookback multi-riga). Famiglie rilevate: AWS, GitHub/GitLab, Stripe, Slack, OpenAI,
+Google, intestazioni PEM, payload esadecimali.
+**→ Exit 2. Ruota e verifica immediatamente.**
-### Adapter Engine
+**Blood Sentinel** normalizza ogni link risolto con `os.path.normpath` e rifiuta qualsiasi
+percorso che sfugge alla root `docs/`. Intercetta tentativi di tipo `../../../../etc/passwd`
+prima di qualsiasi syscall OS.
+**→ Exit 3.**
-Zenzic traduce la conoscenza engine-specifica in risposte engine-agnostiche attraverso un sottile
-**adapter layer**:
+| Exit | Significato |
+| :---: | :--- |
+| `0` | Tutti i controlli superati |
+| `1` | Problemi di qualità rilevati |
+| **`2`** | **SICUREZZA — credenziale esposta rilevata** |
+| **`3`** | **SICUREZZA — path traversal di sistema rilevato** |
-```text
-zenzic.toml → get_adapter() → Adapter → Core (Scanner + Validator)
-```
+> Aggiungi `zenzic check references` ai tuoi hook pre-commit per bloccare le fughe prima della history git.
-L'adapter risponde alle domande che il Core necessita senza conoscere nulla degli interni di MkDocs o
-Zensical:
+---
-| Metodo | Domanda |
-| :--- | :--- |
-| `is_locale_dir(part)` | Questa componente del percorso è una directory locale non default? |
-| `resolve_asset(path)` | Esiste un fallback nella locale di default per questo asset mancante? |
-| `is_shadow_of_nav_page(rel, nav)` | Questo file di locale è un mirror di una pagina nella nav? |
-| `get_nav_paths()` | Quali percorsi `.md` sono dichiarati nella nav? |
-| `get_ignored_patterns()` | Quali pattern di filename sono file locale non default (suffix mode)? |
-| `get_route_info(rel)` | Metadati di routing completi: URL canonico, stato, slug, route base path? |
+## 🔌 Supporto Multi-Motore
-Quattro adapter sono disponibili, selezionati automaticamente da `get_adapter()`:
+Zenzic legge i file di configurazione come testo semplice — non importa né esegue mai il tuo framework di build:
-| Adapter | Quando selezionato | Sorgente config |
+| Motore | Adapter | Funzionalità chiave |
| :--- | :--- | :--- |
-| `MkDocsAdapter` | `engine = "mkdocs"` o engine sconosciuto | `mkdocs.yml` (YAML) |
-| `DocusaurusAdapter` | `engine = "docusaurus"` | `docusaurus.config.ts` / `.js` (testo piano) |
-| `ZensicalAdapter` | `engine = "zensical"` | `zensical.toml` (TOML, zero YAML) |
-| `VanillaAdapter` | Nessun file config, nessuna locale dichiarata | — (tutti no-op) |
-
-**Flessibile ma Trasparente** — Quando viene dichiarato `engine = "zensical"`, Zenzic cerca
-`zensical.toml`. Se assente, attiva automaticamente il Proxy Trasparente per connettere il
-tuo `mkdocs.yml` esistente. Non si tratta di un fallback silenzioso: Zenzic ti avviserà
-tramite il Banner Sentinel per garantire che l'identità del motore rimanga chiara, offrendo
-al contempo un percorso di migrazione senza attriti.
-
-### Come funziona — Virtual Site Map (VSM)
-
-La maggior parte degli analizzatori di documentazione controlla se un file collegato esiste su disco.
-Zenzic va oltre: costruisce un **Virtual Site Map** prima che qualsiasi regola venga eseguita.
-
-```text
-File sorgente ──► Adapter ──► VSM ──► Rule Engine ──► Violazioni
- .md + config (conoscenza (URL → stato) (funzioni pure)
- engine-
- specifica)
-```
-
-Il VSM mappa ogni file sorgente `.md` all'URL canonico che il motore di build servirà —
-**senza eseguire il build**. Ogni route porta uno stato:
+| [Docusaurus v3][docusaurus] | `DocusaurusAdapter` | Docs versionati, alias `@site/`, rilevamento Ghost Route |
+| [MkDocs][mkdocs] | `MkDocsAdapter` | Modalità i18n suffix + folder, `fallback_to_default` |
+| [Zensical][zensical] | `ZensicalAdapter` | Transparent Proxy ponte `mkdocs.yml` se `zensical.toml` assente |
+| Qualsiasi cartella | `VanillaAdapter` | Zero-config, Directory Index Integrity — nessun motore richiesto |
-| Stato | Significato |
-| :--- | :--- |
-| `REACHABLE` | La pagina è nella nav; gli utenti possono trovarla. |
-| `ORPHAN_BUT_EXISTING` | Il file esiste su disco ma è assente dalla `nav:`. Gli utenti non possono trovarlo tramite navigazione. |
-| `CONFLICT` | Due file mappano allo stesso URL (es. `index.md` + `README.md`). Il risultato del build è indefinito. |
-| `IGNORED` | Il file non verrà servito (`README.md` non elencato, directory `_private/` di Zensical). |
-
-Questo rende Zenzic unicamente preciso: un link a una pagina `ORPHAN_BUT_EXISTING`
-viene intercettato come `UNREACHABLE_LINK` — il file esiste, il link risolve, ma
-l'utente otterrà un 404 dopo il build perché la pagina non è navigabile.
+Adapter di terze parti si installano tramite il gruppo di entry-point `zenzic.adapters`.
+Vedi la [Guida Developer][docs-it-arch] per le API degli adapter.
-**Ghost Routes** (`reconfigure_material: true`) — quando `mkdocs-material`
-auto-genera entry point di locale (es. `/it/`) a build-time, queste pagine
-non appaiono mai nella `nav:`. Zenzic rileva questo flag e le marca `REACHABLE`
-automaticamente, evitando falsi warning di orfani.
+---
-**Cache content-addressable** — Zenzic evita di ri-lintare file invariati usando
-come chiave `SHA256(content) + SHA256(config)`. Per le regole VSM-aware
-la chiave include anche `SHA256(vsm_snapshot)`, garantendo l'invalidazione quando
-lo stato di routing di qualsiasi file cambia. I timestamp non vengono mai consultati —
-la cache è corretta in ambienti CI dove `git clone` resetta `mtime`.
+## ⚙️ Configurazione
-### MkDocs — fallback i18n
+Zero-config di default. Priorità: `zenzic.toml` > `[tool.zenzic]` in `pyproject.toml` > valori predefiniti.
-Quando `mkdocs.yml` dichiara il plugin i18n con `fallback_to_default: true`, Zenzic rispecchia
-la logica di risoluzione del plugin: un link da una pagina tradotta a una pagina non tradotta **non**
-viene segnalato come rotto, perché il build servirà la versione nella locale di default. Supportato sia per
-`docs_structure: suffix` che per `docs_structure: folder`.
+```toml
+# zenzic.toml (tutti i campi sono opzionali)
+docs_dir = "docs"
+fail_under = 80 # exit 1 se punteggio < soglia; 0 = solo osservazione
+excluded_dirs = ["includes", "assets", "overrides"]
+excluded_build_artifacts = ["pdf/*.pdf", "dist/*.zip"]
+placeholder_patterns = ["coming soon", "todo", "stub"]
-```yaml
-# mkdocs.yml
-plugins:
- - i18n:
- docs_structure: folder
- fallback_to_default: true
- languages:
- - locale: en
- default: true
- build: true
- - locale: it
- build: true
+[build_context]
+engine = "mkdocs" # mkdocs | docusaurus | zensical | vanilla
+default_locale = "en"
+locales = ["it"]
```
-Se `mkdocs.yml` è assente (o il plugin i18n non è configurato), Zenzic torna alla validazione
-a locale singola — nessun errore, nessun warning, nessun framework richiesto.
-
-### Artefatti di build (`excluded_build_artifacts`)
+```bash
+zenzic init # Genera zenzic.toml con valori auto-rilevati
+zenzic init --pyproject # Incorpora [tool.zenzic] in pyproject.toml
+```
-Si applica a qualsiasi sistema di documentazione. Se i link puntano a file generati a build-time
-(PDF, ZIP), dichiara i loro pattern glob in `zenzic.toml`:
+**Regole di lint personalizzate** — dichiara pattern specifici del progetto in `zenzic.toml`, senza Python:
```toml
-# zenzic.toml
-excluded_build_artifacts = ["pdf/*.pdf", "dist/*.zip"]
+[[custom_rules]]
+id = "ZZ-NODRAFT"
+pattern = "(?i)\\bDRAFT\\b"
+message = "Rimuovere il marker DRAFT prima della pubblicazione."
+severity = "warning"
```
-Zenzic sopprime gli errori per i percorsi corrispondenti al momento del lint. Il build resta
-responsabile della generazione degli artefatti; Zenzic si fida del link senza richiedere il file su disco.
-
-### Link in stile referenza
-
-I link `[testo][id]` sono risolti attraverso la stessa pipeline dei link inline — incluso il
-fallback i18n — per tutti i sistemi di documentazione.
-
-```markdown
-[Riferimento API][api-ref]
-
-[api-ref]: api.md
-```
+Le regole si attivano identicamente su tutti gli adapter. Nessuna modifica richiesta dopo la migrazione del motore.
---
-## Adapter vs. Integrazioni: L'Ecosistema Zenzic
-
-Zenzic separa **comprendere** dal **agire** attraverso due punti di estensione distinti:
-
-| | Adapter | Integrazione (Plugin) |
-| :--- | :--- | :--- |
-| **Scopo** | Permettere a Zenzic di *capire* il tuo sito. | Permettere a Zenzic di *sorvegliare* la tua build. |
-| **Direzione** | Engine → Zenzic | Zenzic → Engine |
-| **Dipendenze** | Nessuna — analisi testuale pura. | Richiesta (`mkdocs` lib per il plugin MkDocs). |
-| **Attivazione** | Automatica su ogni `zenzic check`. | Opt-in via config engine (es. `mkdocs.yml`). |
-| **Obiettivo** | Discovery e routing zero-config. | Blocco della build in caso di errori. |
-| **Posizione** | `zenzic.core.adapters.*` | `zenzic.integrations.*` |
-
-**In pratica:** l'Adapter è la *mente* — legge `mkdocs.yml` come testo puro e costruisce
-la VSM. L'Integrazione (plugin) è il *braccio* — si aggancia agli eventi di `mkdocs build`
-e solleva un `PluginError` se i controlli di qualità falliscono.
-
-La maggior parte degli utenti ha bisogno solo degli adapter (automatici). Installa
-un'integrazione solo quando vuoi che Zenzic diventi un guardiano nel pipeline di build.
-
-### Plugin MkDocs
-
-```bash
-# Installa l'extra opzionale
-pip install "zenzic[mkdocs]"
-```
+## 🔄 Integrazione CI/CD
```yaml
-# mkdocs.yml
-plugins:
- - zenzic:
- strict: false
- fail_on_error: true
- checks: [orfani, snippet, segnaposto, assets]
+- name: 🛡️ Zenzic Sentinel
+ run: uvx zenzic check all --strict
+ # Exit 1 = qualità · Exit 2 = credenziale esposta · Exit 3 = path traversal
+ # Exit 2 e 3 non sono mai sopprimibili.
+
+- name: Gate regressione
+ run: |
+ uvx zenzic score --save # sul branch main
+ uvx zenzic diff # sulla PR — exit 1 se il punteggio cala
```
-La classe del plugin si trova in `zenzic.integrations.mkdocs:ZenzicPlugin` ed è
-auto-scoperta da MkDocs tramite l'entry point `mkdocs.plugins` — nessun percorso manuale
-richiesto.
+Per automazione badge e gate di regressione, vedi la [guida CI/CD][docs-it-cicd].
+Workflow completo: [`.github/workflows/ci.yml`][ci-workflow]
---
-## Installazione
-
-### Con `uv` (consigliato)
-
-[`uv`][uv] è il modo più veloce per installare e eseguire Zenzic:
+## 📦 Installazione
```bash
-# Esecuzione una-tantum senza installazione
-uvx zenzic check all
+# Audit one-shot senza installazione (consigliato per CI ed esplorazione)
+uvx zenzic check all ./docs
-# Strumento globale disponibile in qualsiasi progetto
+# Tool CLI globale
uv tool install zenzic
-# Dipendenza dev del progetto — versione fissata in uv.lock
+# Dipendenza dev con versione fissata
uv add --dev zenzic
-```
-
-### Con `pip`
-```bash
+# pip
pip install zenzic
+pip install "zenzic[mkdocs]" # + plugin build-time MkDocs
```
-**Tre comandi per iniziare:**
-
-```bash
-pip install zenzic # 1. Installa
-zenzic lab # 2. Esplora — 9 esempi interattivi, ogni engine, zero setup
-zenzic check all # 3. Proteggi — audita la tua documentazione
-```
-
-### Lean e Agnostico per Design
-
-Zenzic esegue un'**analisi statica** dei tuoi file di configurazione (`mkdocs.yml`, `docusaurus.config.ts`, `zensical.toml`, `pyproject.toml`). **Non esegue** il motore di build né i suoi plugin — è 100% subprocess-free. La configurazione di Docusaurus (`.ts`/`.js`) viene analizzata tramite parsing statico del testo, senza mai invocare Node.js.
-
-Questo significa che **non è necessario installare** MkDocs, Docusaurus, Material for MkDocs o altri
-plugin di build nel tuo ambiente di linting. Zenzic rimane leggero e privo di dipendenze, rendendolo
-ideale per pipeline CI/CD veloci e isolate.
-
-**Extra di installazione:**
-
-| Comando | Cosa ottieni |
-| :--- | :--- |
-| `pip install zenzic` | CLI core + adapter Docusaurus, Zensical e Vanilla. Nessuna libreria engine richiesta. |
-| `pip install "zenzic[mkdocs]"` | Core + il **plugin MkDocs** (`zenzic.integrations.mkdocs`). Aggiunge `mkdocs` come dipendenza. |
-
-> L'extra MkDocs è necessario **solo** se vuoi l'integrazione plugin a build-time.
-> Per l'uso standalone della CLI (`zenzic check all`), l'installazione base è sufficiente per ogni engine.
->
-> **Artefatti di build:** Se la documentazione punta a file generati a build-time
-> (PDF, ZIP), aggiungi i loro pattern glob a `excluded_build_artifacts` in `zenzic.toml`
-> anziché pre-generarli. Vedi la sezione [Integrazioni di Prima Classe](#integrazioni-di-prima-classe).
-
-### Setup progetto
-
-```bash
-zenzic init # crea zenzic.toml con engine rilevato automaticamente
-zenzic init --pyproject # incorpora [tool.zenzic] in pyproject.toml
-```
-
-Quando `pyproject.toml` esiste, `zenzic init` chiede interattivamente se incorporare
-la configurazione lì. Usa `--pyproject` per saltare il prompt.
-
----
-
-## Il Showroom Zenzic
-
-La cartella `examples/` è un laboratorio di test certificato — ogni esempio è
-Zenzic-ready e supera `zenzic check all` con l'esito atteso. Usali come punto di
-partenza per un nuovo progetto o come fixture di regressione in CI.
-
-| Esempio | Engine | Cosa imparerai |
-| :--- | :--- | :--- |
-| `examples/mkdocs-basic/` | MkDocs | Rilevamento di `FILE_NOT_FOUND` e `BROKEN_ANCHOR` su un albero MkDocs 1.x standard |
-| `examples/zensical-bridge/` | Zensical | Proxy Trasparente in azione: `engine = "zensical"` con solo `mkdocs.yml` — Banner Sentinel visibile |
-| `examples/docusaurus-v3-enterprise/` | Docusaurus v3 | Versioned docs, risoluzione alias `@site/`, Ghost Routing bilingue — senza Node.js |
-| `examples/vanilla-markdown/` | Vanilla | `MISSING_DIRECTORY_INDEX` su un albero `.md` puro senza motore di build |
-| `examples/i18n-standard/` | MkDocs | Validazione multi-locale — lo Standard d'Oro, 100/100 |
-| `examples/broken-docs/` | MkDocs | Ogni classe di errore in un unico fixture — gli errori sono la funzionalità |
-| `examples/security_lab/` | MkDocs | Lo Shield blocca l'esposizione di credenziali — exit code 2 |
-
-Esegui il Philosophy Tour completo (9 atti, tutti gli esiti attesi verificati):
-
-```bash
-zenzic lab
-```
-
-Esegui un atto singolo:
+> L'extra `[mkdocs]` aggiunge il plugin build-time (`zenzic.integrations.mkdocs`).
+> Tutti gli adapter dei motori (Docusaurus, Zensical, Vanilla) sono inclusi nell'installazione base.
-```bash
-zenzic lab --act 3 # Lo Shield — demo esposizione credenziali
-zenzic lab --list # Stampa l'indice degli atti
-```
+**Portabilità:** Zenzic rifiuta i link interni assoluti (che iniziano con `/`). I link relativi
+funzionano con qualsiasi percorso di hosting. Gli URL esterni `https://` non sono mai interessati.
---
-## Utilizzo CLI
+## 🖥️ Riferimento CLI
```bash
-# Controlli individuali
-zenzic check links --strict
+# Controlli
+zenzic check links [--strict]
zenzic check orphans
zenzic check snippets
zenzic check placeholders
zenzic check assets
+zenzic check references [--strict] [--links]
+zenzic check all [--strict] [--exit-zero] [--format json] [--engine ENGINE]
+zenzic check all [--exclude-dir DIR] [--include-dir DIR]
-# Autofix & Cleanup
-zenzic clean assets # Elimina interattivamente gli asset non utilizzati
-zenzic clean assets -y # Elimina gli asset non utilizzati immediatamente
-zenzic clean assets --dry-run # Mostra cosa verrebbe eliminato senza farlo
-
-# Pipeline dei riferimenti
-zenzic check references # Harvest → Cross-Check → Shield → Punteggio integrità
-zenzic check references --strict # Tratta le Dead Definitions come errori
-zenzic check references --links # Valida anche gli URL dei riferimenti via HTTP asincrono
-
-# Tutti i controlli in un comando
-zenzic check all --strict
-zenzic check all --exit-zero # report senza bloccare la pipeline
-zenzic check all --format json # output machine-readable
-zenzic check all --engine docusaurus # override esplicito dell'engine
-
-# Controllo esclusioni
-zenzic check all --exclude-dir drafts --exclude-dir temp
-zenzic check all --include-dir guides # Scansiona solo directory specifiche
-
-# Punteggio qualità (0–100)
-zenzic score
-zenzic score --save # persiste il baseline
-zenzic score --fail-under 80 # exit 1 se sotto la soglia
-
-# Rilevamento regressioni contro snapshot salvato
-zenzic diff # exit 1 su qualsiasi calo
-zenzic diff --threshold 5 # exit 1 solo se il calo è > 5 punti
-```
-
-> **Nota (v0.6.1+):** `zenzic serve` è stato rimosso. A partire dalla v0.6.1, Zenzic si
-> focalizza esclusivamente sull'analisi. Per visualizzare i documenti, usa il comando nativo
-> del tuo engine: `mkdocs serve`, `docusaurus start`, o `zensical serve`.
-
-### Codici di uscita
-
-| Codice | Significato |
-| :---: | :--- |
-| `0` | Tutti i controlli selezionati sono passati |
-| `1` | Uno o più controlli hanno segnalato problemi |
-| **`2`** | **SECURITY CRITICAL — Zenzic Shield ha rilevato una credenziale esposta** |
-| **`3`** | **SECURITY CRITICAL — Sentinella di Sangue ha rilevato un path traversal di sistema** |
-
-> **Attenzione:**
-> Il **codice di uscita 2** è riservato agli eventi Shield (credenziali esposte). Il **codice
-> di uscita 3** è riservato alla Sentinella di Sangue (path traversal verso directory di sistema
-> come `/etc/`, `/root/`). Entrambi non vengono mai soppressi da `--exit-zero`. Ruotare e
-> verificare immediatamente.
-
----
-
-## 🛡️ Zenzic Shield
-
-Lo **Zenzic Shield** è un sistema di sicurezza a due livelli integrato nel core engine:
-
-| Livello | Protegge contro |
-| --- | --- |
-| **Rilevamento credenziali** | Chiavi API / token esposti incorporati nelle URL dei riferimenti |
-| **Path traversal** | Escape da `docs/` in stile `../../../../etc/passwd` |
+# Punteggio e diff
+zenzic score [--save] [--fail-under N]
+zenzic diff [--threshold N]
-### Rilevamento credenziali
+# Correzione automatica
+zenzic clean assets [-y] [--dry-run]
-Il livello credenziali viene eseguito durante il **Pass 1** (Harvesting) della pipeline dei riferimenti
-e scansiona ogni URL di riferimento per pattern di credenziali noti prima che qualsiasi richiesta HTTP
-venga emessa.
+# Inizializzazione
+zenzic init [--pyproject]
-```markdown
-
-[api-docs]: https://api.example.com/?key=sk-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx
+# Showroom interattivo
+zenzic lab [--act N] [--list]
```
-```bash
-╔══════════════════════════════════════╗
-║ SECURITY CRITICAL ║
-║ Secret(s) detected in documentation ║
-╚══════════════════════════════════════╝
+---
- [SHIELD] docs/api.md:12 — openai-api-key detected in URL
- https://api.example.com/?key=sk-xxxx-xxxx-x...
+## 📟 Tour Visivo
-Build aborted. Rotate the exposed credential immediately.
+```text
+╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮
+│ │
+│ docusaurus • 38 file (18 docs, 20 asset) • 0.9s │
+│ │
+│ ────────────────────── docs/guides/setup.mdx ─────────────────────────── │
+│ │
+│ ✗ 12: [Z001] 'quickstart.mdx' non trovato in docs │
+│ │ │
+│ 12 │ Leggi la [guida quickstart](quickstart.mdx) prima. │
+│ │ │
+│ ────────────────────────────────────────────────────────────────────────── │
+│ │
+│ ✗ 1 errore • 1 file con risultati • FALLITO │
+│ │
+╰──────────────────────────────────────────────────────────────────────────────╯
```
-**Come funziona:**
-
-1. Lo Shield viene eseguito *dentro* il Pass 1 — prima che il Pass 2 validi i link e prima che qualsiasi
- ping HTTP venga emesso. Un documento contenente una credenziale esposta non viene mai usato per effettuare
- richieste in uscita.
-2. I pattern usano quantificatori a lunghezza esatta (`{48}`, `{36}`, `{16}`) — nessun backtracking, O(1) per riga.
-3. Otto famiglie di credenziali sono coperte out of the box:
-
-| Tipo | Pattern |
-| --- | --- |
-| Chiave API OpenAI | `sk-[a-zA-Z0-9]{48}` |
-| Token GitHub | `gh[pousr]_[a-zA-Z0-9]{36}` |
-| Access key AWS | `AKIA[0-9A-Z]{16}` |
-| Chiave live Stripe | `sk_live_[0-9a-zA-Z]{24}` |
-| Token Slack | `xox[baprs]-[0-9a-zA-Z]{10,48}` |
-| Chiave API Google | `AIza[0-9A-Za-z\-_]{35}` |
-| Chiave privata PEM | `-----BEGIN [A-Z ]+ PRIVATE KEY-----` |
-| Payload hex-encoded | 3+ sequenze consecutive `\xNN` |
-
-1. **Nessun punto cieco** — lo Shield scansiona ogni riga del file sorgente, incluse le righe dentro
- blocchi di codice fenced (`bash`, `yaml`, senza etichetta, ecc.). Una credenziale inserita in un esempio
- di codice è comunque una credenziale esposta.
-
-> **Suggerimento:**
-> Aggiungi `zenzic check references` ai tuoi hook pre-commit per intercettare credenziali esposte prima che
-> vengano mai committate nel version control.
+Visita il [portale di documentazione][docs-it-home] per screenshot interattivi ed esempi ricchi.
-### Path traversal
-
-Il livello di path traversal viene eseguito dentro `InMemoryPathResolver` durante `check links`. Normalizza
-ogni href risolto con `os.path.normpath` (puro C, zero system call) e verifica che il risultato
-sia contenuto dentro `docs/` usando un singolo confronto di prefisso stringa — $O(1)$, zero allocazioni.
+---
-```bash
-Attack href: ../../../../etc/passwd
-After resolve: /etc/passwd
-Shield check: /etc/passwd does not start with /docs/ → PathTraversal returned, link rejected
-```
+## 🗺️ Roadmap v0.7.0
-Qualsiasi href che esce dalla root docs viene evidenziato come un errore `PathTraversal` distinto — mai
-collassato silenziosamente in un generico "file non trovato".
+- [ ] **Auto-fix Engine** — Riparazione automatica di link rotti e ancore orfane.
+- [ ] **Estensioni IDE** — Lint in tempo reale per VS Code e Cursor tramite LSP.
+- [ ] **AI Context Provider** — Export VSM in formato LLM-friendly per agenti AI.
+- [ ] **Adapter Astro & VitePress** — Espandere il Safe Harbor ai framework JS.
---
-## Integrazione CI/CD
+## 🏗️ Filosofia di Design
-### GitHub Actions
+Zenzic è costruito su tre contratti operativi:
-```yaml
-- name: Lint documentazione
- run: uvx zenzic check all
+**Analizza la Sorgente, non la Build.** La VSM (Virtual Site Map) mappa ogni file `.md` al suo
+URL canonico senza eseguire la build — gli errori vengono intercettati prima di raggiungere la produzione.
-- name: Controlla riferimenti ed esegui Shield
- run: uvx zenzic check references
-```
+**Zero-Trust Execution.** Nessun sottoprocesso, nessuna esecuzione di codice arbitrario, nessuna
+importazione di motori di build. I config Docusaurus `.ts`/`.js` sono analizzati tramite analisi
+testuale statica — Node.js non viene mai invocato.
-Workflow completo: [`.github/workflows/zenzic.yml`][ci-workflow]
+**Esclusione Obbligatoria ad Ogni Entry Point.** Tutta la scoperta dei file passa attraverso
+`LayeredExclusionManager` — una gerarchia a 4 livelli (Sistema → VCS → Config → CLI). Nessuna
+scansione globale senza un contesto di esclusione esplicito.
-Per l'automazione dinamica dei badge e il rilevamento delle regressioni, consulta la [guida all'integrazione CI/CD][docs-it-cicd].
+Vedi la [Guida all'Architettura][docs-it-arch] per il Two-Pass Reference Pipeline e l'analisi approfondita della VSM.
---
-## Configurazione
-
-Tutti i campi sono opzionali. Zenzic funziona senza alcun file di configurazione.
-
-Zenzic segue una catena di priorità a tre livelli **Agnostic Citizen**:
-
-1. `zenzic.toml` alla root del repository — sovrano; ha sempre la precedenza.
-2. `[tool.zenzic]` in `pyproject.toml` — usato quando `zenzic.toml` è assente.
-3. Default built-in.
-
-```toml
-# zenzic.toml (oppure [tool.zenzic] in pyproject.toml)
-docs_dir = "docs"
-excluded_dirs = ["includes", "assets", "stylesheets", "overrides", "hooks"]
-snippet_min_lines = 1
-placeholder_max_words = 50
-placeholder_patterns = ["coming soon", "todo", "stub"]
-fail_under = 80 # exit 1 se il punteggio scende sotto questa soglia; 0 = modalità osservativa
-
-# Contesto engine e i18n — richiesto solo per progetti multi-locale in folder mode.
-# Quando assente, Zenzic legge la configurazione locale direttamente da mkdocs.yml.
-[build_context]
-engine = "mkdocs" # "mkdocs", "docusaurus", "zensical" o "vanilla"
-default_locale = "en"
-locales = ["it"] # nomi delle directory locale non default
-```
+## 🙋 FAQ
----
+**Perché non usare `grep`?** Grep è cieco alla struttura. Zenzic comprende il versioning di
+Docusaurus, i fallback i18n di MkDocs e le Ghost Route — pagine che non esistono come file ma
+sono URL validi.
-## DSL `[[custom_rules]]`
+**Esegue il mio motore di build?** No. 100% subprocess-free. Analisi statica solo su testo semplice.
-Dichiara regole lint specifiche del progetto in `zenzic.toml` senza scrivere Python:
+**Regge migliaia di file?** Sì. Parallelismo adattivo per la scoperta; lookup VSM O(1) per link;
+cache content-addressable (`SHA256(content + config + vsm_snapshot)`) salta i file invariati.
-```toml
-[[custom_rules]]
-id = "ZZ-NODRAFT"
-pattern = "(?i)\\bDRAFT\\b"
-message = "Rimuovere il marker DRAFT prima della pubblicazione."
-severity = "warning"
+**Shield vs Blood Sentinel?** Shield = segreti *nel* contenuto (exit 2). Blood Sentinel =
+link che puntano a *path* di sistema OS (exit 3). Entrambi non sono sopprimibili.
-[[custom_rules]]
-id = "ZZ-NOINTERNAL"
-pattern = "internal\\.corp\\.example\\.com"
-message = "L'hostname interno non deve apparire nella documentazione pubblica."
-severity = "error"
-```
+**Non serve `zenzic.toml`?** Corretto. Zenzic identifica il motore dai file di configurazione presenti e applica i default sicuri.
+Esegui `zenzic init` in qualsiasi momento per generare un file di configurazione pre-compilato.
-Le regole si attivano identicamente con tutti gli adapter (MkDocs, Docusaurus, Zensical, Vanilla). Nessuna
-modifica richiesta dopo la migrazione da un engine all'altro.
+**Cos'è `zenzic lab`?** Uno showroom interattivo a 9 atti che copre ogni motore e ogni classe di
+errore. Eseguilo una volta prima di integrare Zenzic in qualsiasi progetto.
---
-## Sviluppo
-
-Per un workflow di sviluppo più veloce e interattivo usando **just**, o per istruzioni dettagliate su
-come aggiungere nuovi controlli, consulta la [Guida ai Contributi][contributing].
+## 🛠️ Sviluppo
```bash
-uv sync --group dev
-nox -s dev # Installa gli hook pre-commit (una volta)
-
-nox -s tests # pytest + coverage
-nox -s lint # ruff check
-nox -s format # ruff format
+uv sync --all-groups
+nox -s tests # pytest + copertura
+nox -s lint # ruff
nox -s typecheck # mypy --strict
-nox -s preflight # pipeline CI completa (lint + test + self-check)
-```
-
----
-
-## Visual Tour
-
-L'audit completo della Sentinella — banner, rilevamento engine e verdetto:
-
-```bash
-╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮
-│ │
-│ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │
-│ │
-│ ────────────────────── docs/guides/setup.mdx ─────────────────────────── │
-│ │
-│ ✗ 12: [Z001] 'quickstart.mdx' not found in docs │
-│ │ │
-│ 12 │ Read the [quickstart guide](quickstart.mdx) first. │
-│ │ │
-│ │
-│ ────────────────────────────────────────────────────────────────────────── │
-│ │
-│ ✗ 1 error • 1 file with findings │
-│ │
-│ FAILED: One or more checks failed. │
-│ │
-│ 💡 4 info findings suppressed — use --show-info for details. │
-│ │
-╰──────────────────────────────────────────────────────────────────────────────╯
+nox -s preflight # lint + format + typecheck + pytest + reuse
+just verify # preflight + zenzic check all --strict (self-dogfood)
```
-Lo **Shield** intercetta credenziali esposte prima che qualsiasi richiesta HTTP venga emessa.
-La **Sentinella di Sangue** blocca tentativi di path traversal che escono dalla root `docs/`.
-Entrambi attivano codici di uscita non sopprimibili (2 e 3). Il **VSM** (Virtual Site Map)
-assicura che la validazione dei link operi su URL canonici — non su percorsi filesystem —
-così che pagine orfane e slug override vengano rilevati accuratamente su tutti gli engine.
-
-Per screenshot interattivi ed esempi visivi completi, visita il
-[portale documentazione](https://zenzic.dev/it/docs/).
+Vedi la [Guida alla Contribuzione][contributing] per la checklist del metodo Zenzic e le convenzioni PR.
---
-## Contribuire
-
-Bug report, miglioramenti alla documentazione e pull request sono benvenuti. Prima di iniziare:
+## 🤝 Contribuire
-1. Apri un'issue per discutere la modifica — usa il [template appropriato][issues].
-2. Leggi la [Guida ai Contributi][contributing] — in particolare il setup locale e la checklist **Zenzic Way** (funzioni pure, nessun sottoprocesso, source-first).
-3. Ogni PR deve superare `nox -s preflight` (test + lint + typecheck + self-dogfood) e includere le intestazioni REUSE/SPDX sui nuovi file.
+1. Apri una [issue][issues] per discutere la modifica.
+2. Leggi la [Guida alla Contribuzione][contributing] — checklist del metodo Zenzic, funzioni pure,
+ nessun sottoprocesso, source-first.
+3. Ogni PR deve superare `nox -s preflight` e includere intestazioni REUSE/SPDX sui nuovi file.
-Consulta anche il [Codice di Condotta][coc] e la [Policy di Sicurezza][security].
+Vedi anche: [Codice di Condotta][coc] · [Politica di Sicurezza][security]
-## Citare Zenzic
+## 📎 Citare Zenzic
-Il file [`CITATION.cff`][citation-cff] è presente nella root del repository. GitHub lo
-visualizza automaticamente — clicca **"Cite this repository"** sulla pagina del repo per
-ottenere il riferimento in formato APA o BibTeX.
+Un file [`CITATION.cff`][citation-cff] è presente alla radice del repository. Clicca su
+**"Cite this repository"** su GitHub per l'output APA o BibTeX.
-## Licenza
+## 📄 Licenza
Apache-2.0 — vedi [LICENSE][license].
---
- © 2026 PythonWoods. Progettato con precisione.
- Based in Italy 🇮🇹 · Committed to the craft of Python development.
+ © 2026 PythonWoods. Ingegnerizzato con precisione.
+ Con sede in Italia 🇮🇹 · Dediti all'arte dello sviluppo Python.
dev@pythonwoods.dev
-
-
-[mkdocs]: https://www.mkdocs.org/
-[docusaurus]: https://docusaurus.io/
-[zensical]: https://zensical.org/
-[uv]: https://docs.astral.sh/uv/
-[docs-it-home]: https://zenzic.dev/it/docs/usage/
-[docs-it-config]: https://zenzic.dev/it/docs/guides/configuration-reference/
-[docs-it-migration]: https://zenzic.dev/it/docs/guides/migration/
-[docs-it-arch]: https://zenzic.dev/it/docs/internals/architecture-overview/
-[docs-it-adapter]: https://zenzic.dev/it/docs/internals/developers/writing-an-adapter/
-[docs-it-cicd]: https://zenzic.dev/it/docs/guides/ci-cd/
-[ci-workflow]: .github/workflows/ci.yml
-[contributing]: CONTRIBUTING.md
-[license]: LICENSE
-[citation-cff]: CITATION.cff
-[coc]: CODE_OF_CONDUCT.md
-[security]: SECURITY.md
-[issues]: https://github.com/PythonWoods/zenzic/issues
+
+
+[mkdocs]: https://www.mkdocs.org/
+[docusaurus]: https://docusaurus.io/
+[zensical]: https://zensical.org/
+[uv]: https://docs.astral.sh/uv/
+[docs-it-home]: https://zenzic.dev/it/docs/
+[docs-it-badges]: https://zenzic.dev/it/docs/usage/badges/
+[docs-it-cicd]: https://zenzic.dev/it/docs/guides/ci-cd/
+[docs-it-arch]: https://zenzic.dev/it/docs/internals/architecture-overview/
+[ci-workflow]: .github/workflows/ci.yml
+[contributing]: CONTRIBUTING.it.md
+[license]: LICENSE
+[citation-cff]: CITATION.cff
+[coc]: CODE_OF_CONDUCT.md
+[security]: SECURITY.md
+[issues]: https://github.com/PythonWoods/zenzic/issues
diff --git a/README.md b/README.md
index 5ee2177..f4b7eae 100644
--- a/README.md
+++ b/README.md
@@ -39,686 +39,332 @@ SPDX-License-Identifier: Apache-2.0
- "Zenzic is the Safe Harbor for your documentation integrity. It doesn't just check links; it audits your brand's technical resilience."
- Engineering-grade documentation linter — standalone, engine-agnostic, and security-hardened.
+ The Safe Harbor for your Markdown documentation.
+ Engine-agnostic static analysis — standalone, security-hardened, zero configuration needed.
-```bash
-╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮
-│ │
-│ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │
-│ │
-│ ────────────────────────────────────────────────────────────────────────── │
-│ │
-│ ✔ All checks passed. Your documentation is secure. │
-│ │
-│ 💡 4 info findings suppressed — use --show-info for details. │
-│ │
-╰──────────────────────────────────────────────────────────────────────────────╯
-```
-
---
-> Documentation doesn't fail loudly. It decays silently.
-
-Broken links, orphan pages, invalid code snippets, stale placeholder content, and leaked API keys
-accumulate over time — until users hit them in production. Zenzic catches all of these across
-[Docusaurus][docusaurus], [MkDocs][mkdocs], and [Zensical][zensical] projects as a **standalone CLI** —
-no build framework required.
+## ⚡ Try it now — Zero Installation
-Zenzic is **agnostic** — it works with any Markdown-based documentation system (Docusaurus, MkDocs,
-Zensical, or a bare folder of `.md` files) without installing any build framework. It reads raw source
-files and build configurations as plain text. And it is **opinionated**: absolute links are a hard error,
-engine identity must be provable, and the CLI is 100% subprocess-free.
+Got a folder of Markdown files? Run an instant security and link audit using [`uv`][uv]:
----
-
-## Core Capabilities
-
-- **Security** — Shield (9 credential families, Exit 2) with Unicode obfuscation resistance, HTML entity decoding, comment-interleaving defense, and cross-line split-token lookback. Blood Sentinel (host-path traversal, Exit 3). ReDoS-safe regex (F2-1), jailbreak protection (F4-1). Neither is suppressed by `--exit-zero`.
-- **Integrity** — O(V+E) circular link detection, Virtual Site Map with content-addressable cache, deterministic 0–100 quality score.
-- **Intelligence** — Multi-engine: MkDocs, Docusaurus v3, Zensical, and Vanilla. Module-level adapter cache. Third-party adapters install as Python packages via entry points.
-- **Discovery** — Universal VCS-aware file iteration (zero `rglob`), mandatory `ExclusionManager` on every entry point, 4-level Layered Exclusion hierarchy, pure-Python `.gitignore` parser.
+```bash
+uvx zenzic check all ./your-folder
+```
-> 🚀 **Latest Release: v0.6.1 "Obsidian Glass" (Stable)** — Featuring full Docusaurus v3
-> versioning support, `@site/` alias resolution, smart file collapsing, and Zensical
-> transparent proxying. See [CHANGELOG.md](CHANGELOG.md) for details.
+Zenzic will identify your engine via its configuration files or default to **Vanilla mode** for
+standalone folders — providing immediate protection for links, credentials, and structural integrity.
---
-## 📖 Documentation
-
-Zenzic provides an extensive, engineering-grade documentation portal:
+## 🚀 Quick Start
-- 🚀 **[User Guide][docs-home]**: Installation, CLI usage, and all available checks.
-- 🏅 **[Badges][docs-badges]**: Official Zenzic Shield and Score badge snippets for your README.
-- 🔄 **[CI/CD Integration][docs-cicd]**: GitHub Actions workflows, dynamic badges, and regression detection.
-- ⚙️ **[Developer Guide][docs-arch]**: Deep dive into the deterministic pure-core architecture, Two-Pass Pipeline, and state-machine parsing.
-- 🤝 **[Contributing][docs-contributing]**: Set up the local development environment (`uv`, `nox`), run the test suite, and submit PRs.
+```bash
+pip install zenzic
+zenzic lab # Interactive showroom — 9 acts, every engine, zero setup
+zenzic check all # Audit the current directory
+```
-
- Explore the full documentation →
-
+📖 [Full docs →][docs-home] · 🏅 [Badges][docs-badges] · 🔄 [CI/CD guide][docs-cicd]
---
-## What Zenzic checks
-
-| Check | CLI command | What it detects |
-| --- | --- | --- |
-| Links | `zenzic check links` | Broken internal links, dead anchors, and **path traversal** attempts |
-| Orphans | `zenzic check orphans` | `.md` files absent from `nav` |
-| Snippets | `zenzic check snippets` | Python, YAML, JSON, and TOML blocks with syntax errors |
-| Placeholders | `zenzic check placeholders` | Stub pages and forbidden text patterns |
-| Assets | `zenzic check assets` | Images and files not referenced anywhere |
-| **References** | `zenzic check references` | Dangling References, Dead Definitions, **Zenzic Shield** |
+## 🎯 Why Zenzic?
-Beyond pass/fail, `zenzic score` aggregates all checks into a deterministic 0–100 quality score.
-`zenzic diff` compares the current score against a saved baseline — enabling regression detection
-on every pull request.
-
-**Autofix:** Zenzic also provides active cleanup utilities. Run `zenzic clean assets` to automatically deleting the unused images identified by `check assets` (interactive or via `-y`).
+| Without Zenzic | With Zenzic |
+| :--- | :--- |
+| ❌ Broken anchors silently 200 OK in Docusaurus v3 | ✅ Mathematical anchor validation via VSM |
+| ❌ Leaked API keys in code blocks committed to git | ✅ **The Shield** — 9-family credential scanner, exit 2 |
+| ❌ Path traversal `../../../../etc/passwd` in links | ✅ **Blood Sentinel** — non-suppressible exit 3 |
+| ❌ Orphan pages unreachable from any nav link | ✅ Semantic orphan detection — not just file-exists |
+| ❌ Silent 404s accumulating in Google Search Console | ✅ Directory Index Integrity checks |
+| ❌ MkDocs → Zensical migration with unknown breakage | ✅ **Transparent Proxy** — lint both with one command |
---
-## Portability Standards
-
-Zenzic enforces two rules that make documentation portable across any hosting environment
-and independent of any specific build engine.
-
-### Relative Path Enforcement
+## 🧩 What Zenzic is NOT
-Zenzic **rejects internal links that start with `/`**. Absolute paths are environment-dependent:
-a link to `/assets/logo.png` works when the site is at the domain root, but returns 404 when
-hosted in a subdirectory (e.g. `https://example.com/docs/assets/logo.png` ≠
-`https://example.com/assets/logo.png`).
-
-```markdown
-
-[Download](/assets/guide.pdf)
-
-
-[Download](../assets/guide.pdf)
-```
+- **Not a site generator.** It audits source; it never builds HTML.
+- **Not a build wrapper.** Zero-Trust Execution: no subprocesses, no `mkdocs` or `docusaurus` binaries invoked.
+- **Not a spell checker.** Structure and security — not prose.
+- **Not an HTTP crawler.** All validation is local and file-based.
-The error message includes an explicit fix suggestion. External URLs (`https://...`) are not
-affected.
-
-### i18n Support: Multi-Engine Locale Resolution
-
-Zenzic natively supports locale-aware documentation across all engines:
+---
-**MkDocs — Suffix Mode** (`page.locale.md`) and **Folder Mode** (`docs/it/page.md`) via
-`mkdocs-static-i18n`. Links from translated pages to untranslated pages are resolved through
-the default-locale fallback when `fallback_to_default: true` is set.
+## 📋 Capability Matrix
-**Docusaurus v3 — i18n directory** (`i18n/it/docusaurus-plugin-content-docs/current/`).
-Zenzic discovers locale trees from `docusaurus.config.ts` automatically. Links between
-locale pages and default-locale assets are resolved without configuration.
+| Capability | Command | Detects | Exit |
+| :--- | :--- | :--- | :---: |
+| Link integrity | `check links` | Broken links, dead anchors | 1 |
+| Orphan detection | `check orphans` | Files absent from `nav` — invisible after build | 1 |
+| Code snippets | `check snippets` | Syntax errors in Python / YAML / JSON / TOML blocks | 1 |
+| Placeholder content | `check placeholders` | Stub pages and forbidden text patterns | 1 |
+| Unused assets | `check assets` | Images and files not referenced anywhere | 1 |
+| **Credential scanning** | `check references` | **9 credential families** — text, URLs, code blocks | **2** |
+| **Path traversal** | `check links` | System-path escape attempts | **3** |
+| Quality score | `score` | Deterministic 0–100 composite metric | — |
+| Regression detection | `diff` | Score drop vs saved baseline — CI-friendly | 1 |
-**Zensical** — Suffix Mode only (`page.locale.md`), similar to MkDocs.
+**Autofix:** `zenzic clean assets [-y] [--dry-run]` deletes unused images.
-In Folder Mode (MkDocs) and Docusaurus i18n, Zenzic uses the `[build_context]` section in
-`zenzic.toml` to identify locale directories:
+> 🚀 **v0.6.1 "Obsidian Glass" (Stable)** — Full Docusaurus v3 versioning, `@site/` alias
+> resolution, and Zensical Transparent Proxy. See [CHANGELOG.md](CHANGELOG.md).
-```toml
-# zenzic.toml
-[build_context]
-engine = "mkdocs" # "mkdocs", "docusaurus", "zensical", or "vanilla"
-default_locale = "en"
-locales = ["it", "fr"] # non-default locale directory names
-```
-
-When `zenzic.toml` is absent, Zenzic reads locale configuration directly from `mkdocs.yml`
-(respecting `docs_structure`, `fallback_to_default`, and `languages`). No configuration is
-required for projects that do not use i18n.
+---
-## First-Class Integrations
+## 🛡️ Security: The Shield & Blood Sentinel
-Zenzic is **build-engine agnostic**. It works with any Markdown-based documentation system —
-MkDocs, Docusaurus, Zensical, or a bare folder of `.md` files. No build framework needs to be installed;
-Zenzic reads raw source files only.
+Two security layers are permanently active — neither is suppressible by `--exit-zero`:
-Where a documentation ecosystem defines well-known conventions for multi-locale structure or
-build-time artifact generation, Zenzic provides enhanced, opt-in support by reading the project's
-configuration file (YAML, TOML, or plain-text JS/TS) — never by importing or executing the
-framework itself.
+**The Shield** scans every line — including fenced code blocks — for credentials. Unicode
+normalization defeats obfuscation (HTML entities, comment interleaving, cross-line lookback).
+Detected families: AWS, GitHub/GitLab, Stripe, Slack, OpenAI, Google, PEM headers, hex payloads.
+**→ Exit 2. Rotate and audit immediately.**
-### Engine Adapters
+**Blood Sentinel** normalizes every resolved link with `os.path.normpath` and rejects any path
+escaping the `docs/` root. Catches `../../../../etc/passwd`-style traversal before any OS syscall.
+**→ Exit 3.**
-Zenzic translates engine-specific knowledge into engine-agnostic answers through a thin
-**adapter layer**:
+| Exit | Meaning |
+| :---: | :--- |
+| `0` | All checks passed |
+| `1` | Quality issues found |
+| **`2`** | **SECURITY — leaked credential detected** |
+| **`3`** | **SECURITY — system-path traversal detected** |
-```text
-zenzic.toml → get_adapter() → Adapter → Core (Scanner + Validator)
-```
+> Add `zenzic check references` to your pre-commit hooks to catch leaks before git history.
-The adapter answers the questions the Core needs without knowing anything about MkDocs or
-Zensical internals:
+---
-| Method | Question |
-| :--- | :--- |
-| `is_locale_dir(part)` | Is this path component a non-default locale directory? |
-| `resolve_asset(path)` | Does a default-locale fallback exist for this missing asset? |
-| `is_shadow_of_nav_page(rel, nav)` | Is this locale file a mirror of a nav-listed page? |
-| `get_nav_paths()` | Which `.md` paths are declared in the nav? |
-| `get_ignored_patterns()` | Which filename patterns are non-default locale files (suffix mode)? |
-| `get_route_info(rel)` | Full route metadata: canonical URL, status, slug, route base path? |
+## 🔌 Multi-Engine Support
-Four adapters are available, selected automatically by `get_adapter()`:
+Zenzic reads config files as plain text — never imports or executes your build framework:
-| Adapter | When selected | Config source |
+| Engine | Adapter | Highlights |
| :--- | :--- | :--- |
-| `MkDocsAdapter` | `engine = "mkdocs"` or unknown engine | `mkdocs.yml` (YAML) |
-| `DocusaurusAdapter` | `engine = "docusaurus"` | `docusaurus.config.ts` / `.js` (plain text) |
-| `ZensicalAdapter` | `engine = "zensical"` | `zensical.toml` (TOML, zero YAML) |
-| `VanillaAdapter` | No config file, no locales declared | — (all no-ops) |
-
-**Flexible but Transparent** — When `engine = "zensical"` is declared, Zenzic looks for
-`zensical.toml`. If missing, it automatically activates the Transparent Proxy to bridge your
-existing `mkdocs.yml`. This is not a silent fallback: Zenzic will notify you via the Sentinel
-Banner to ensure engine identity remains clear while providing a seamless migration path.
+| [Docusaurus v3][docusaurus] | `DocusaurusAdapter` | Versioned docs, `@site/` alias, Ghost Route detection |
+| [MkDocs][mkdocs] | `MkDocsAdapter` | i18n suffix + folder modes, `fallback_to_default` |
+| [Zensical][zensical] | `ZensicalAdapter` | Transparent Proxy bridges `mkdocs.yml` if `zensical.toml` absent |
+| Any folder | `VanillaAdapter` | Zero-config, Directory Index Integrity — no engine required |
-### How it works — Virtual Site Map (VSM)
+Third-party adapters install via the `zenzic.adapters` entry-point group.
+See the [Developer Guide][docs-arch] for the adapter API.
-Most documentation linters check whether a linked file exists on disk.
-Zenzic goes further: it builds a **Virtual Site Map** before any rule fires.
-
-```text
-Source files ──► Adapter ──► VSM ──► Rule Engine ──► Violations
- .md + config (engine- (URL → status) (pure functions)
- specific
- knowledge)
-```
-
-The VSM maps every `.md` source file to the canonical URL the build engine
-will serve — **without running the build**. Each route carries a status:
-
-| Status | Meaning |
-| :--- | :--- |
-| `REACHABLE` | Page is in the nav; users can find it. |
-| `ORPHAN_BUT_EXISTING` | File exists on disk but is absent from `nav:`. Users cannot find it via navigation. |
-| `CONFLICT` | Two files map to the same URL (e.g. `index.md` + `README.md`). Build result is undefined. |
-| `IGNORED` | File will not be served (unlisted `README.md`, Zensical `_private/` dirs). |
-
-This makes Zenzic uniquely precise: a link to an `ORPHAN_BUT_EXISTING` page
-is caught as `UNREACHABLE_LINK` — the file exists, the link resolves, but
-the user will hit a 404 after the build because the page is not navigable.
-
-**Ghost Routes** (`reconfigure_material: true`) — when `mkdocs-material`
-auto-generates locale entry points (e.g. `/it/`) at build time, those pages
-never appear in `nav:`. Zenzic detects this flag and marks them `REACHABLE`
-automatically, so no false orphan warnings are emitted.
+---
-**Content-addressable cache** — Zenzic avoids re-linting unchanged files by
-keying results on `SHA256(content) + SHA256(config)`. For VSM-aware rules
-the key also includes `SHA256(vsm_snapshot)`, ensuring invalidation when any
-file's routing state changes. Timestamps are never consulted — the cache is
-correct in CI environments where `git clone` resets `mtime`.
+## ⚙️ Configuration
-### MkDocs — i18n fallback
+Zero-config by default. Priority: `zenzic.toml` > `[tool.zenzic]` in `pyproject.toml` > built-ins.
-When `mkdocs.yml` declares the i18n plugin with `fallback_to_default: true`, Zenzic mirrors
-the plugin's resolution logic: a link from a translated page to an untranslated page is **not**
-reported as broken, because the build will serve the default-locale version. Supported for both
-`docs_structure: suffix` and `docs_structure: folder`.
+```toml
+# zenzic.toml (all fields optional)
+docs_dir = "docs"
+fail_under = 80 # exit 1 if score < threshold; 0 = observe only
+excluded_dirs = ["includes", "assets", "overrides"]
+excluded_build_artifacts = ["pdf/*.pdf", "dist/*.zip"]
+placeholder_patterns = ["coming soon", "todo", "stub"]
-```yaml
-# mkdocs.yml
-plugins:
- - i18n:
- docs_structure: folder
- fallback_to_default: true
- languages:
- - locale: en
- default: true
- build: true
- - locale: it
- build: true
+[build_context]
+engine = "mkdocs" # mkdocs | docusaurus | zensical | vanilla
+default_locale = "en"
+locales = ["it"]
```
-If `mkdocs.yml` is absent (or the i18n plugin is not configured), Zenzic falls back to standard
-single-locale validation — no errors, no warnings, no framework required.
-
-### Build-time artifacts (`excluded_build_artifacts`)
+```bash
+zenzic init # Generate zenzic.toml with auto-detected values
+zenzic init --pyproject # Embed [tool.zenzic] in pyproject.toml
+```
-Applies to any documentation system. If links point to files generated at build time (PDFs,
-ZIPs), declare their glob patterns in `zenzic.toml`:
+**Custom lint rules** — declare project-specific patterns in `zenzic.toml`, no Python required:
```toml
-# zenzic.toml
-excluded_build_artifacts = ["pdf/*.pdf", "dist/*.zip"]
+[[custom_rules]]
+id = "ZZ-NODRAFT"
+pattern = "(?i)\\bDRAFT\\b"
+message = "Remove DRAFT marker before publishing."
+severity = "warning"
```
-Zenzic suppresses errors for matching paths at lint time. The build remains responsible for
-generating the artifacts; Zenzic trusts the link without requiring the file on disk.
-
-### Reference-style links
-
-`[text][id]` links are resolved through the same pipeline as inline links — including i18n
-fallback — for all documentation systems.
-
-```markdown
-[API Reference][api-ref]
-
-[api-ref]: api.md
-```
+Rules fire identically across all adapters. No changes required after engine migration.
---
-## Adapters vs. Integrations: The Zenzic Ecosystem
-
-Zenzic separates **understanding** from **acting** through two distinct extension points:
-
-| | Adapter | Integration (Plugin) |
-| :--- | :--- | :--- |
-| **Purpose** | Let Zenzic *understand* your site. | Let Zenzic *guard* your build. |
-| **Direction** | Engine → Zenzic | Zenzic → Engine |
-| **Dependency** | None — pure text analysis. | Required (`mkdocs` lib for the MkDocs plugin). |
-| **Trigger** | Automatic on every `zenzic check`. | Opt-in via engine config (e.g. `mkdocs.yml`). |
-| **Goal** | Zero-config discovery & routing. | Build-blocking quality control. |
-| **Location** | `zenzic.core.adapters.*` | `zenzic.integrations.*` |
-
-**In practice:** the Adapter is the *mind* — it reads `mkdocs.yml` as plain text and builds
-the VSM. The Integration (plugin) is the *arm* — it hooks into `mkdocs build` events and
-raises a `PluginError` if quality checks fail.
-
-Most users only need adapters (automatic). Install an integration only when you want
-Zenzic to become a gate inside your engine's build pipeline.
-
-### MkDocs Plugin
-
-```bash
-# Install the optional extra
-pip install "zenzic[mkdocs]"
-```
+## 🔄 CI/CD Integration
```yaml
-# mkdocs.yml
-plugins:
- - zenzic:
- strict: false
- fail_on_error: true
- checks: [orphans, snippets, placeholders, assets]
+- name: 🛡️ Zenzic Sentinel
+ run: uvx zenzic check all --strict
+ # Exit 1 = quality · Exit 2 = leaked credential · Exit 3 = path traversal
+ # Exits 2 and 3 are never suppressible.
+
+- name: Regression gate
+ run: |
+ uvx zenzic score --save # on main branch
+ uvx zenzic diff # on PR — exit 1 if score drops
```
-The plugin class lives at `zenzic.integrations.mkdocs:ZenzicPlugin` and is auto-discovered
-by MkDocs via the `mkdocs.plugins` entry point — no manual path required.
+For badge automation and regression gates, see the [CI/CD guide][docs-cicd].
+Full workflow: [`.github/workflows/ci.yml`][ci-workflow]
---
-## Installation
-
-### With `uv` (recommended)
-
-[`uv`][uv] is the fastest way to install and run Zenzic:
+## 📦 Installation
```bash
-# Zero-install, one-shot audit
-uvx zenzic check all
+# Zero-install, one-shot audit (recommended for CI and exploration)
+uvx zenzic check all ./docs
-# Global CLI tool — available in any project
+# Global CLI tool
uv tool install zenzic
-# Project dev dependency — version-pinned in uv.lock
+# Pinned dev dependency
uv add --dev zenzic
-```
-
-### With `pip`
-```bash
+# pip
pip install zenzic
+pip install "zenzic[mkdocs]" # + MkDocs build-time plugin
```
-**Three commands to get started:**
-
-```bash
-pip install zenzic # 1. Install
-zenzic lab # 2. Explore — 9 interactive examples, every engine, zero setup
-zenzic check all # 3. Protect — audit your own documentation
-```
-
-### Lean & Agnostic by Design
-
-Zenzic performs a **static analysis** of your configuration files (`mkdocs.yml`, `docusaurus.config.ts`, `zensical.toml`, `pyproject.toml`). It does **not** execute the build engine or its plugins — it is 100% subprocess-free. Docusaurus configuration (`.ts`/`.js`) is analysed via static text parsing — Node.js is never invoked.
-
-This means you **do not need to install** MkDocs, Docusaurus, Material for MkDocs, or any other
-build-related plugins in your linting environment. Zenzic remains lightweight and dependency-free,
-making it ideal for fast, isolated CI/CD pipelines.
-
-**Installation extras:**
-
-| Command | What you get |
-| :--- | :--- |
-| `pip install zenzic` | Core CLI + Docusaurus, Zensical, and Vanilla adapters. No engine library required. |
-| `pip install "zenzic[mkdocs]"` | Core + the **MkDocs plugin** (`zenzic.integrations.mkdocs`). Adds `mkdocs` as a dependency. |
-
-> The MkDocs extra is needed **only** if you want the build-time plugin integration.
-> For standalone CLI usage (`zenzic check all`), the base install is sufficient for every engine.
->
-> **Build artifacts:** If your documentation links to files generated at build time
-> (PDFs, ZIPs), add their glob patterns to `excluded_build_artifacts` in `zenzic.toml`
-> rather than pre-generating them. See the [First-Class Integrations](#first-class-integrations) section above.
-
-### Project setup
-
-```bash
-zenzic init # creates zenzic.toml with auto-detected engine
-zenzic init --pyproject # embeds [tool.zenzic] in pyproject.toml instead
-```
-
-When `pyproject.toml` exists, `zenzic init` asks interactively whether to embed
-configuration there. Pass `--pyproject` to skip the prompt.
-
----
-
-## The Zenzic Showroom
+> The `[mkdocs]` extra adds the build-time plugin (`zenzic.integrations.mkdocs`).
+> All engine adapters (Docusaurus, Zensical, Vanilla) are included in the base install.
-The `examples/` directory is a certified test laboratory — every example is
-Zenzic-ready and passes `zenzic check all` with the expected outcome. Use these
-as your starting point for a new project or as regression fixtures in CI.
-
-| Example | Engine | What you will learn |
-| :--- | :--- | :--- |
-| `examples/mkdocs-basic/` | MkDocs | `FILE_NOT_FOUND` and `BROKEN_ANCHOR` detection on a standard MkDocs 1.x tree |
-| `examples/zensical-bridge/` | Zensical | Transparent Proxy in action: `engine = "zensical"` with only `mkdocs.yml` — Sentinel Banner appears |
-| `examples/docusaurus-v3-enterprise/` | Docusaurus v3 | Versioned docs, `@site/` alias resolution, and bilingual Ghost Routing — no Node.js required |
-| `examples/vanilla-markdown/` | Vanilla | `MISSING_DIRECTORY_INDEX` on a raw `.md` tree with no build engine |
-| `examples/i18n-standard/` | MkDocs | Multi-locale validation — the Gold Standard, 100/100 |
-| `examples/broken-docs/` | MkDocs | Every error class in one fixture — errors are the feature |
-| `examples/security_lab/` | MkDocs | Shield blocks credential exposure — exit code 2 |
-
-Run the full Philosophy Tour (9 acts, all expected outcomes verified):
-
-```bash
-zenzic lab
-```
-
-Run a single act:
-
-```bash
-zenzic lab --act 3 # The Shield — credential exposure demo
-zenzic lab --list # Print the act index
-```
+**Portability:** Zenzic rejects absolute internal links (starting with `/`). Relative links
+work at any hosting path. External `https://` URLs are never affected.
---
-## CLI usage
+## 🖥️ CLI Reference
```bash
-# Individual checks
-zenzic check links --strict
+# Checks
+zenzic check links [--strict]
zenzic check orphans
zenzic check snippets
zenzic check placeholders
zenzic check assets
+zenzic check references [--strict] [--links]
+zenzic check all [--strict] [--exit-zero] [--format json] [--engine ENGINE]
+zenzic check all [--exclude-dir DIR] [--include-dir DIR]
-# Autofix & Cleanup
-zenzic clean assets # Interactively delete unused assets
-zenzic clean assets -y # Delete unused assets immediately
-zenzic clean assets --dry-run # Preview what would be deleted
-
-# Reference pipeline
-zenzic check references # Harvest → Cross-Check → Shield → Integrity score
-zenzic check references --strict # Treat Dead Definitions as errors
-zenzic check references --links # Also validate reference URLs via async HTTP
-
-# All checks in one command
-zenzic check all --strict
-zenzic check all --exit-zero # Report without blocking the pipeline
-zenzic check all --format json # Machine-readable output
-zenzic check all --engine docusaurus # Explicit engine override
-
-# Exclusion control
-zenzic check all --exclude-dir drafts --exclude-dir temp
-zenzic check all --include-dir guides # Only scan specific directories
-
-# Quality score (0–100)
-zenzic score
-zenzic score --save # Persist baseline snapshot
-zenzic score --fail-under 80 # Exit 1 if below threshold
-
-# Regression detection against saved snapshot
-zenzic diff # Exit 1 on any score drop
-zenzic diff --threshold 5 # Exit 1 only if drop > 5 points
-```
-
-> **Note (v0.6.1+):** `zenzic serve` has been removed. Zenzic focuses exclusively on
-> static analysis. To preview your docs, use your engine's native command:
-> `mkdocs serve`, `docusaurus start`, or `zensical serve`.
-
-### Exit codes
-
-| Code | Meaning |
-| :---: | :--- |
-| `0` | All selected checks passed |
-| `1` | One or more checks reported issues |
-| **`2`** | **SECURITY CRITICAL — Zenzic Shield detected a leaked credential** |
-| **`3`** | **SECURITY CRITICAL — Blood Sentinel detected a system-path traversal** |
-
-> **Warning:**
-> **Exit code 2** is reserved for Shield events (leaked credentials). **Exit code 3** is
-> reserved for Blood Sentinel events (path traversal to OS system directories such as
-> `/etc/`, `/root/`). Both are never suppressed by `--exit-zero`. Rotate and audit immediately.
-
----
-
-## 🛡️ Zenzic Shield
-
-The **Zenzic Shield** is a two-layer security system built into the core engine:
+# Score & diff
+zenzic score [--save] [--fail-under N]
+zenzic diff [--threshold N]
-| Layer | Protects against |
-| --- | --- |
-| **Credential detection** | Leaked API keys / tokens embedded in reference URLs |
-| **Path traversal** | `../../../../etc/passwd`-style escape from `docs/` |
+# Autofix
+zenzic clean assets [-y] [--dry-run]
-### Credential detection
+# Init
+zenzic init [--pyproject]
-The credential layer runs during **Pass 1** (Harvesting) of the reference pipeline and scans
-every reference URL for known credential patterns before any HTTP request is issued.
-
-```markdown
-
-[api-docs]: https://api.example.com/?key=sk-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx
+# Interactive showroom
+zenzic lab [--act N] [--list]
```
-```bash
-╔══════════════════════════════════════╗
-║ SECURITY CRITICAL ║
-║ Secret(s) detected in documentation ║
-╚══════════════════════════════════════╝
+---
- [SHIELD] docs/api.md:12 — openai-api-key detected in URL
- https://api.example.com/?key=sk-xxxx-xxxx-x...
+## 📟 Visual Tour
-Build aborted. Rotate the exposed credential immediately.
+```text
+╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮
+│ │
+│ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │
+│ │
+│ ────────────────────── docs/guides/setup.mdx ─────────────────────────── │
+│ │
+│ ✗ 12: [Z001] 'quickstart.mdx' not found in docs │
+│ │ │
+│ 12 │ Read the [quickstart guide](quickstart.mdx) first. │
+│ │ │
+│ ────────────────────────────────────────────────────────────────────────── │
+│ │
+│ ✗ 1 error • 1 file with findings • FAILED │
+│ │
+╰──────────────────────────────────────────────────────────────────────────────╯
```
-**How it works:**
-
-1. The Shield runs *inside* Pass 1 — before Pass 2 validates links and before any HTTP ping is
- issued. A document containing a leaked credential is never used to make outbound requests.
-2. Patterns use exact-length quantifiers (`{48}`, `{36}`, `{16}`) — no backtracking, O(1) per line.
-3. Eight credential families are covered out of the box:
-
-| Type | Pattern |
-| --- | --- |
-| OpenAI API key | `sk-[a-zA-Z0-9]{48}` |
-| GitHub token | `gh[pousr]_[a-zA-Z0-9]{36}` |
-| AWS access key | `AKIA[0-9A-Z]{16}` |
-| Stripe live key | `sk_live_[0-9a-zA-Z]{24}` |
-| Slack token | `xox[baprs]-[0-9a-zA-Z]{10,48}` |
-| Google API key | `AIza[0-9A-Za-z\-_]{35}` |
-| PEM private key | `-----BEGIN [A-Z ]+ PRIVATE KEY-----` |
-| Hex-encoded payload | 3+ consecutive `\xNN` escape sequences |
-
-1. **No blind spots** — the Shield scans every line of the source file, including lines inside
- fenced code blocks (`bash`, `yaml`, unlabelled, etc.). A credential committed inside a code
- example is still a committed credential.
-
-> **Tip:**
-> Add `zenzic check references` to your pre-commit hooks to catch leaked credentials before they
-> are ever committed to version control.
-
-### Path traversal
+Visit the [documentation portal][docs-home] for interactive screenshots and rich examples.
-The path traversal layer runs inside `InMemoryPathResolver` during `check links`. It normalises
-every resolved href with `os.path.normpath` (pure C, zero kernel calls) and verifies the result
-is contained within `docs/` using a single string prefix check — $O(1)$, allocation-free.
+---
-```bash
-Attack href: ../../../../etc/passwd
-After resolve: /etc/passwd
-Shield check: /etc/passwd does not start with /docs/ → PathTraversal returned, link rejected
-```
+## 🗺️ Roadmap v0.7.0
-Any href that escapes the docs root is surfaced as a distinct `PathTraversal` error — never
-silently collapsed into a generic "file not found".
+- [ ] **Auto-fix Engine** — Automatic repair of broken links and orphaned anchors.
+- [ ] **IDE Extensions** — Real-time linting for VS Code and Cursor via LSP.
+- [ ] **AI Context Provider** — VSM export in LLM-friendly format for AI agents.
+- [ ] **Astro & VitePress Adapters** — Expanding the Safe Harbor to JS frameworks.
---
-## CI/CD integration
+## 🏗️ Design Philosophy
-### GitHub Actions
+Zenzic is built on three operational contracts:
-```yaml
-- name: Lint documentation
- run: uvx zenzic check all
+**Lint the Source, Not the Build.** The VSM (Virtual Site Map) maps every `.md` file to its
+canonical URL without running the build — errors are caught before they reach production.
-- name: Check references and run Shield
- run: uvx zenzic check references
-```
+**Zero-Trust Execution.** No subprocesses, no arbitrary code execution, no build engine imports.
+Docusaurus `.ts`/`.js` configs are parsed via static text analysis — Node.js is never invoked.
-Full workflow: [`.github/workflows/zenzic.yml`][ci-workflow]
+**Mandatory Exclusion at Every Entry Point.** All file discovery goes through
+`LayeredExclusionManager` — a 4-level hierarchy (System → VCS → Config → CLI). No global scan
+without an explicit exclusion context.
-For dynamic badge automation and regression detection, see the [CI/CD Integration guide][docs-cicd].
+See the [Architecture Guide][docs-arch] for the Two-Pass Reference Pipeline and VSM deep-dive.
---
-## Configuration
+## 🙋 FAQ
-All fields are optional. Zenzic works with no configuration file at all.
+**Why not `grep`?** Grep is blind to structure. Zenzic understands Docusaurus versioning,
+MkDocs i18n fallbacks, and Ghost Routes — pages that don't exist as files but are valid URLs.
-Zenzic follows a three-level **Agnostic Citizen** priority chain:
+**Does it run my build engine?** No. 100% subprocess-free. Static analysis on plain text only.
-1. `zenzic.toml` at the repository root — sovereign; always wins.
-2. `[tool.zenzic]` in `pyproject.toml` — used when `zenzic.toml` is absent.
-3. Built-in defaults.
+**Can it handle thousands of files?** Yes. Adaptive parallelism for discovery; O(1) VSM lookup
+per link; content-addressable cache (`SHA256(content + config + vsm_snapshot)`) skips unchanged files.
-```toml
-# zenzic.toml (or [tool.zenzic] in pyproject.toml)
-docs_dir = "docs"
-excluded_dirs = ["includes", "assets", "stylesheets", "overrides", "hooks"]
-snippet_min_lines = 1
-placeholder_max_words = 50
-placeholder_patterns = ["coming soon", "todo", "stub"]
-fail_under = 80 # exit 1 if score drops below this; 0 = observational mode
-
-# Engine and i18n context — required only for folder-mode multi-locale projects.
-# When absent, Zenzic reads locale config directly from mkdocs.yml.
-[build_context]
-engine = "mkdocs" # "mkdocs", "docusaurus", "zensical", or "vanilla"
-default_locale = "en"
-locales = ["it"] # non-default locale directory names
-```
-
----
+**Shield vs Blood Sentinel?** Shield = secrets *inside* content (exit 2). Blood Sentinel =
+links pointing to OS system *paths* (exit 3). Both are non-suppressible.
-## DSL `[[custom_rules]]`
+**No `zenzic.toml` needed?** Correct. Zenzic identifies the engine from config files present and applies safe defaults. Run
+`zenzic init` at any time to generate a pre-populated config file.
-Declare project-specific lint rules in `zenzic.toml` without writing Python:
-
-```toml
-[[custom_rules]]
-id = "ZZ-NODRAFT"
-pattern = "(?i)\\bDRAFT\\b"
-message = "Remove the DRAFT marker before publishing."
-severity = "warning"
-
-[[custom_rules]]
-id = "ZZ-NOINTERNAL"
-pattern = "internal\\.corp\\.example\\.com"
-message = "Internal hostname must not appear in public documentation."
-severity = "error"
-```
-
-Rules fire identically across all adapters (MkDocs, Docusaurus, Zensical, Vanilla). No changes
-required after migrating from one engine to another.
+**What is `zenzic lab`?** A 9-act interactive showroom covering every engine and error class.
+Run it once before integrating Zenzic into any project.
---
-## Development
-
-For a faster, interactive development workflow using **just**, or for detailed instructions on
-adding new checks, see the [Contributing Guide][contributing].
+## 🛠️ Development
```bash
-uv sync --group dev
-nox -s dev # Install pre-commit hooks (once)
-
+uv sync --all-groups
nox -s tests # pytest + coverage
-nox -s lint # ruff check
-nox -s format # ruff format
+nox -s lint # ruff
nox -s typecheck # mypy --strict
-nox -s preflight # full CI pipeline (lint + test + self-check)
+nox -s preflight # lint + format + typecheck + pytest + reuse
+just verify # preflight + zenzic check all --strict (self-dogfood)
```
----
-
-## Visual Tour
-
-The full Sentinel audit — banner, engine detection, and pass/fail verdict:
-
-```bash
-╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮
-│ │
-│ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │
-│ │
-│ ────────────────────── docs/guides/setup.mdx ─────────────────────────── │
-│ │
-│ ✗ 12: [Z001] 'quickstart.mdx' not found in docs │
-│ │ │
-│ 12 │ Read the [quickstart guide](quickstart.mdx) first. │
-│ │ │
-│ │
-│ ────────────────────────────────────────────────────────────────────────── │
-│ │
-│ ✗ 1 error • 1 file with findings │
-│ │
-│ FAILED: One or more checks failed. │
-│ │
-│ 💡 4 info findings suppressed — use --show-info for details. │
-│ │
-╰──────────────────────────────────────────────────────────────────────────────╯
-```
-
-**Shield** catches leaked credentials before any HTTP request is issued. **Blood Sentinel**
-blocks path traversal attempts that escape the `docs/` root. Both trigger non-suppressible
-exit codes (2 and 3). The **VSM** (Virtual Site Map) ensures link validation operates on
-canonical URLs — not filesystem paths — so orphan pages and slug overrides are detected
-accurately across all engines.
-
-For interactive screenshots and rich visual examples, visit the
-[documentation portal](https://zenzic.dev/docs/).
+See the [Contributing Guide][contributing] for the Zenzic Way checklist and PR conventions.
---
-## Contributing
-
-We welcome bug reports, documentation improvements, and pull requests. Before you start:
+## 🤝 Contributing
-1. Open an issue to discuss the change — use the [bug report][issues], [feature request][issues], or [docs issue][issues] template.
-2. Read the [Contributing Guide][contributing] — especially the **Local development setup** and the **Zenzic Way** checklist (pure functions, no subprocesses, source-first).
-3. Every PR must pass `nox -s preflight` (tests + lint + typecheck + self-dogfood) and include REUSE/SPDX headers on new files.
+1. Open an [issue][issues] to discuss the change.
+2. Read the [Contributing Guide][contributing] — Zenzic Way checklist, pure functions, no
+ subprocesses, source-first.
+3. Every PR must pass `nox -s preflight` and include REUSE/SPDX headers on new files.
-Please also review our [Code of Conduct][coc] and [Security Policy][security].
+See also: [Code of Conduct][coc] · [Security Policy][security]
-## Citing Zenzic
+## 📎 Citing
-A [`CITATION.cff`][citation-cff] file is present at the root of the repository. GitHub renders
-it automatically — click **"Cite this repository"** on the repo page for APA or BibTeX output.
+A [`CITATION.cff`][citation-cff] is present at the root. Click **"Cite this repository"** on
+GitHub for APA or BibTeX output.
-## License
+## 📄 License
Apache-2.0 — see [LICENSE][license].
@@ -740,7 +386,6 @@ Apache-2.0 — see [LICENSE][license].
[docs-badges]: https://zenzic.dev/docs/usage/badges/
[docs-cicd]: https://zenzic.dev/docs/guides/ci-cd/
[docs-arch]: https://zenzic.dev/docs/internals/architecture-overview/
-[docs-contributing]: https://zenzic.dev/docs/community/contribute/
[ci-workflow]: .github/workflows/ci.yml
[contributing]: CONTRIBUTING.md
[license]: LICENSE
From 8e443c6b2599de33dbe49177a20ede7f3d130c41 Mon Sep 17 00:00:00 2001
From: PythonWoods
Date: Tue, 21 Apr 2026 16:24:27 +0200
Subject: [PATCH 13/13] feat!: Standalone Engine, Zxxx Finding Codes,
Interactive Lab (Direttiva 036-040)
BREAKING CHANGE: engine = "vanilla" now raises ConfigurationError [Z000].
Update zenzic.toml to engine = "standalone" before upgrading.
- Rename VanillaAdapter -> StandaloneAdapter (_vanilla.py -> _standalone.py)
- Add migration guard in _factory.py: engine="vanilla" raises Z000 (sunset v0.7.0)
- Add src/zenzic/core/codes.py: Zxxx finding code registry (single source of truth)
- Normalize all Finding codes to Zxxx via _normalize_code() in cli.py
- scanner.py: _map_shield_to_finding emits Z201 instead of SHIELD
- lab.py: no-arg invocation shows act menu; positional arg runs single act
- Act 8 renamed to "Standalone Excellence"
- pyproject.toml entry-point: vanilla -> standalone
- examples/vanilla-markdown -> examples/standalone-markdown
- zenzic.toml: engine = "standalone" (dogfood self-check)
- .github/copilot-instructions.md: restored institutional memory (Direttiva 040)
- CHANGELOG.md, CHANGELOG.it.md: Breaking Changes + Added sections under [0.6.1]
- RELEASE.md: new headline, Standalone + Zxxx sections, migration note
- All test files migrated: test_vanilla_mode.py -> test_standalone_mode.py
- Test suite: 1127 passed
---
.github/copilot-instructions.md | 173 ++++++++++++++++++
CHANGELOG.it.md | 21 +++
CHANGELOG.md | 20 ++
RELEASE.md | 35 +++-
.../README.md | 0
.../docs/deep-folder/advanced.md | 0
.../docs/deep-folder/internals.md | 0
.../docs/guides/index.md | 0
.../docs/guides/setup.md | 0
.../docs/index.md | 0
.../zenzic.toml | 0
pyproject.toml | 2 +-
src/zenzic/cli.py | 39 ++--
src/zenzic/core/adapter.py | 2 +-
src/zenzic/core/adapters/__init__.py | 6 +-
src/zenzic/core/adapters/_base.py | 2 +-
src/zenzic/core/adapters/_factory.py | 30 ++-
src/zenzic/core/adapters/_mkdocs.py | 2 +-
.../adapters/{_vanilla.py => _standalone.py} | 15 +-
src/zenzic/core/adapters/_utils.py | 2 +-
src/zenzic/core/codes.py | 139 ++++++++++++++
src/zenzic/core/scanner.py | 2 +-
src/zenzic/core/validator.py | 2 +-
src/zenzic/lab.py | 40 ++--
tests/test_blue_vsm_edge.py | 12 +-
tests/test_cli.py | 16 +-
tests/test_cli_e2e.py | 4 +-
tests/test_cli_visual.py | 33 ++--
tests/test_protocol_evolution.py | 36 ++--
tests/test_redteam_remediation.py | 4 +-
tests/test_rules.py | 4 +-
tests/test_scanner.py | 4 +-
...anilla_mode.py => test_standalone_mode.py} | 68 ++++---
tests/test_vsm.py | 12 +-
zenzic.toml | 4 +-
35 files changed, 571 insertions(+), 158 deletions(-)
create mode 100644 .github/copilot-instructions.md
rename examples/{vanilla-markdown => standalone-markdown}/README.md (100%)
rename examples/{vanilla-markdown => standalone-markdown}/docs/deep-folder/advanced.md (100%)
rename examples/{vanilla-markdown => standalone-markdown}/docs/deep-folder/internals.md (100%)
rename examples/{vanilla-markdown => standalone-markdown}/docs/guides/index.md (100%)
rename examples/{vanilla-markdown => standalone-markdown}/docs/guides/setup.md (100%)
rename examples/{vanilla-markdown => standalone-markdown}/docs/index.md (100%)
rename examples/{vanilla-markdown => standalone-markdown}/zenzic.toml (100%)
rename src/zenzic/core/adapters/{_vanilla.py => _standalone.py} (85%)
create mode 100644 src/zenzic/core/codes.py
rename tests/{test_vanilla_mode.py => test_standalone_mode.py} (74%)
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..4277d2f
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,173 @@
+# Zenzic Agent Guidelines — v0.6.1 "Obsidian Glass" Stable
+
+Zenzic is the high-performance, engine-agnostic Safe Harbor for Markdown documentation.
+It is a STABLE product. Agents must prioritize precision, security, and "Value-First" communication.
+
+---
+
+## 🎯 Mission: The Safe Harbor
+
+- **Target:** Engineers, Technical Writers, and curious users.
+- **Philosophy:** "If the engine builds the site, Zenzic guarantees the source."
+- **Communication:** README is a Landing Page, not a manual. Move technical deep-dives to the documentation portal (zenzic.dev).
+
+---
+
+## 🚀 Key Features (v0.6.1 — Obsidian Glass Stable)
+
+- **Instant Entry:** `uvx zenzic check all ./path` is the primary curiosity path.
+- **Zenzic Lab:** 9 interactive Acts for onboarding (zero-config showroom). Run `zenzic lab` to see the menu; `zenzic lab ` to run a specific act.
+- **Standalone Mode:** Default engine for pure Markdown projects with no recognised build system. Replaces the old "Vanilla" identity entirely.
+- **Zensical Bridge:** Transparent Proxy for `mkdocs.yml` compatibility under `engine = "zensical"`.
+- **Enterprise Docusaurus:** Full versioning, `@site/` alias, and slug logic alignment.
+- **Offline Mode:** `--offline` flag for flat `.html` URL structure.
+- **SEO Guardrail:** `Z401 MISSING_DIRECTORY_INDEX` detection for directories without a landing page.
+- **Finding Codes (Zxxx):** Every diagnostic carries a unique `Zxxx` identifier for enterprise-grade auditing and future filtering.
+
+---
+
+## 🧱 The 3 Pillars (Non-Negotiable)
+
+1. **Lint the Source:** Never depend on HTML output. Analyze raw Markdown and configs.
+2. **No Subprocesses:** 100% pure Python. No `subprocess.run`, no Node.js execution.
+3. **Pure Functions First:** Deterministic logic. No I/O in hot-path loops.
+
+---
+
+## 🛡️ Core Laws for Code
+
+- **Zero I/O in hot paths:** No `Path.exists()` or `open()` inside link/file loops.
+- **Mandatory ExclusionManager:** No discovery without an explicit exclusion manager.
+- **Exit Codes:** 0 (Success), 1 (Quality), 2 (Shield/Secrets), 3 (Blood Sentinel/Fatal).
+- **Finding Codes:** Every `Finding` object must carry a `Zxxx` code from `src/zenzic/core/codes.py`. Never hardcode a raw string code; always use `codes.normalize()`.
+
+---
+
+## 📐 Architecture Map
+
+```text
+src/zenzic/
+ cli.py — Typer CLI entry-points; builds Finding objects via _to_findings()
+ lab.py — Interactive showcase (9 Acts); menu-driven with positional arg
+ core/
+ adapter.py — Public re-exports (StandaloneAdapter, MkDocsAdapter, …)
+ adapters/
+ _standalone.py — StandaloneAdapter: no-op engine for pure Markdown projects
+ _mkdocs.py — MkDocs engine adapter
+ _docusaurus.py — Docusaurus v3 engine adapter
+ _zensical.py — Zensical engine adapter (+ Transparent Proxy)
+ _factory.py — get_adapter() factory; contains vanilla→standalone migration guard
+ __init__.py — Public adapter registry
+ codes.py — Zxxx finding code registry (SINGLE SOURCE OF TRUTH)
+ reporter.py — SentinelReporter; renders Finding objects to Rich output
+ scanner.py — File discovery, orphan detection, shield bridge
+ validator.py — Link / anchor / path-traversal validation
+ rules.py — VSM-based rule engine (Z001, Z002)
+ shield.py — Credential scanner (exits 2/3)
+ scorer.py — Quality score engine
+ models/
+ config.py — ZenzicConfig / BuildContext (Pydantic)
+ vsm.py — Virtual Site Map (Route, build_vsm, detect_collisions)
+ references.py — Reference integrity (IntegrityReport, ReferenceFinding)
+ ui.py — Shared Rich colour constants and emoji helpers
+tests/
+ test_standalone_mode.py — StandaloneAdapter unit tests + factory routing
+ test_vsm.py — Virtual Site Map tests
+ test_blue_vsm_edge.py — VSM edge-case stress tests
+ test_protocol_evolution.py — Adapter protocol compliance + Hypothesis stress tests
+ test_cli.py — CLI integration tests (Typer runner)
+ test_scanner.py — Scanner / orphan / i18n tests
+ test_rules.py — Rule engine tests
+ test_shield.py — Shield / credential detection tests
+```
+
+---
+
+## 🔎 Finding Code Standard (Zxxx)
+
+All diagnostics emitted by Zenzic carry a `Zxxx` code. The registry is in
+`src/zenzic/core/codes.py`. **Never add a new finding without registering its code there first.**
+
+| Range | Category | Examples |
+|-------|----------|---------|
+| Z1xx | Link Integrity | Z101 LINK_BROKEN, Z102 ANCHOR_MISSING, Z104 FILE_NOT_FOUND |
+| Z2xx | Security | Z201 SHIELD_SECRET, Z202 PATH_TRAVERSAL |
+| Z3xx | Reference Integrity | Z301 DANGLING_REF, Z302 DEAD_DEF |
+| Z4xx | Structure | Z401 MISSING_DIRECTORY_INDEX, Z402 ORPHAN_PAGE |
+| Z5xx | Content Quality | Z501 PLACEHOLDER, Z503 SNIPPET_ERROR |
+| Z9xx | Engine / System | Z902 RULE_TIMEOUT |
+
+When creating a `Finding`, always call `codes.normalize(raw_code)` to map legacy strings to canonical `Zxxx` codes. The `_to_findings()` function in `cli.py` is the authorised conversion point.
+
+---
+
+## 🏭 Adapter Identity Rules
+
+- **"standalone"** is the canonical engine name for projects with no build config. Use `StandaloneAdapter`.
+- **"vanilla"** is a removed legacy name. Any usage raises `ConfigurationError` with code `Z000`.
+- `pyproject.toml` entry-point: `standalone = "zenzic.core.adapters:StandaloneAdapter"`.
+- In Standalone Mode, navigation-based checks (orphan detection) are disabled because there is no declared nav.
+- When `zenzic init` finds no engine config, it writes `engine = "standalone"` in the `[build_context]` block.
+
+---
+
+## 🧪 Quality Gate
+
+- **Coverage:** ≥ 80% mandatory.
+- **Mutation:** Mutmut audit on `rules.py` and `shield.py`.
+- **Property Testing:** Hypothesis for `resolver.py` and adapter stress-testing.
+- **Test naming:** All test functions relating to the Standalone adapter use the prefix `test_standalone_*`.
+
+---
+
+## 📝 README Restyling Rules
+
+When editing `README.md` or `README.it.md`:
+
+1. **Above the Fold:** Hook + uvx command + Quick Start + Sentinel Report must be visible within 2 scrolls.
+2. **Problem/Solution:** Use tables to contrast "Without Zenzic" vs "With Zenzic".
+3. **Deduplicate:** Technical implementation details belong in `/docs/internals` on zenzic.dev.
+4. **Bilingual Parity:** EN and IT must be perfectly mirrored in structure and quality.
+5. **Branding:** Keep "Shield", "Blood Sentinel", "VSM", "Ghost Routes" as-is (Proper Nouns).
+6. **Standalone, not Vanilla:** Never write "Vanilla Mode" in user-facing copy. Use "Standalone Mode".
+
+---
+
+## 🧹 Maintenance & Sunset Clauses (Technical Debt Tracking)
+
+### 1. Vanilla-to-Standalone Migration Guard
+
+- **Status:** Deprecated.
+- **Location:** `src/zenzic/core/adapters/_factory.py` (check for `engine == "vanilla"`).
+- **Behavior:** Raises a `ConfigurationError` with code `Z000` to guide users to `engine = "standalone"`.
+- **Sunset Target:** **v0.7.0**.
+- **Instruction:** Do NOT remove this guard in 0.6.x patches. MUST be deleted entirely in the 0.7.0 release to achieve zero-legacy codebase.
+
+### 2. Finding Codes (Zxxx)
+
+- **Standard:** All diagnostics must use the `Zxxx` format.
+- **Rule:** Never introduce a new finding string without a corresponding code in `src/zenzic/core/codes.py`.
+
+---
+
+## 🗺️ Sprint History (for institutional memory)
+
+### Direttiva 036 — Finding Code Mapping
+
+Codified all diagnostic outputs into the `Zxxx` scheme. `codes.py` created as single source of truth.
+
+### Direttiva 037 — Standalone Renaissance
+
+Full rename: `VanillaAdapter` → `StandaloneAdapter`, `_vanilla.py` → `_standalone.py`, entry-point `vanilla` → `standalone`. Breaking change: `engine = "vanilla"` raises `ConfigurationError [Z000]`. Test suite fully migrated to `test_standalone_mode.py`.
+
+### Direttiva 038 — Final Audit Record
+
+CHANGELOG.md, CHANGELOG.it.md, and RELEASE.md updated to reflect the Breaking Change (Vanilla → Standalone), the Zxxx code introduction, and the interactive Lab menu.
+
+### Direttiva 039 — The Guardrail Lifecycle
+
+Migration guard in `_factory.py` annotated with `# TODO: Remove this migration guard in v0.7.0.` and error message prefixed with `[Z000]`. Docstring clarified.
+
+### Direttiva 040 — Institutional Memory
+
+This file (`.github/copilot-instructions.md`) created / restored as the canonical agent briefing document, embedding all sprint directives and sunset clauses for permanent institutional memory.
diff --git a/CHANGELOG.it.md b/CHANGELOG.it.md
index 2490568..285cd9f 100644
--- a/CHANGELOG.it.md
+++ b/CHANGELOG.it.md
@@ -13,8 +13,29 @@ Le versioni seguono il [Semantic Versioning](https://semver.org/).
## [0.6.1] — 2026-04-19 — Obsidian Glass (Stable)
+### Modifiche che rompono la compatibilità
+
+- **Standalone Engine sostituisce Vanilla (Direttiva 037).** `VanillaAdapter` e la
+ keyword `engine = "vanilla"` sono stati rimossi. Tutti i progetti devono migrare a
+ `engine = "standalone"`. Qualsiasi `zenzic.toml` che usa ancora `engine = "vanilla"`
+ genera una `ConfigurationError [Z000]` all'avvio con un messaggio di migrazione chiaro.
+ *Migrazione:* sostituire `engine = "vanilla"` con `engine = "standalone"` nel proprio
+ `zenzic.toml` o nel blocco `[tool.zenzic]`.
+
### Aggiunto
+- **Codici Finding (Zxxx) (Direttiva 036).** Ogni diagnostica emessa da Zenzic ora
+ porta un identificatore univoco leggibile dalla macchina (es. `Z101 LINK_BROKEN`,
+ `Z201 SHIELD_SECRET`, `Z401 MISSING_DIRECTORY_INDEX`). Il registro completo si trova
+ in `src/zenzic/core/codes.py` — unica fonte di verità per tutti i codici.
+- **Menu interattivo del Lab.** `zenzic lab` senza argomenti mostra ora l'indice degli
+ atti per scegliere quale scenario esplorare. Eseguire `zenzic lab ` per avviare
+ un atto specifico (0–8). L'opzione `--act` è stata sostituita da un argomento
+ posizionale.
+- **Identità Standalone Mode.** `StandaloneAdapter` è il motore no-op canonico per
+ progetti Markdown puri. `zenzic init` ora scrive `engine = "standalone"` quando non
+ viene rilevata nessuna configurazione di framework.
+
- **Flag `--offline` per la risoluzione URL Flat.** Disponibile su `check all`,
`check links` e `check orphans`. Forza tutti gli adapter a produrre URL `.html`
(es. `guida/install.md` → `/guida/install.html`) invece di slug in stile directory.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af5127f..5b97a35 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,8 +13,28 @@ Versions follow [Semantic Versioning](https://semver.org/).
## [0.6.1] — 2026-04-19 — Obsidian Glass (Stable)
+### Breaking Changes
+
+- **Standalone Engine replaces Vanilla (Direttiva 037).** The `VanillaAdapter` and the
+ `engine = "vanilla"` keyword have been removed. All projects must migrate to
+ `engine = "standalone"`. Any `zenzic.toml` still using `engine = "vanilla"` will
+ raise a `ConfigurationError [Z000]` at startup with a clear migration message.
+ *Migration:* replace `engine = "vanilla"` with `engine = "standalone"` in your
+ `zenzic.toml` or `[tool.zenzic]` block.
+
### Added
+- **Finding Codes (Zxxx) (Direttiva 036).** Every diagnostic emitted by Zenzic now
+ carries a unique machine-readable identifier (e.g. `Z101 LINK_BROKEN`,
+ `Z201 SHIELD_SECRET`, `Z401 MISSING_DIRECTORY_INDEX`). The full registry lives in
+ `src/zenzic/core/codes.py` — the single source of truth for all codes.
+- **Interactive Lab menu.** `zenzic lab` without arguments now displays the act index
+ so you can choose which scenario to explore. Run `zenzic lab ` to execute a
+ specific act (0–8). The `--act` option has been replaced by a positional argument.
+- **Standalone Mode identity.** `StandaloneAdapter` is the canonical no-op engine for
+ pure Markdown projects. `zenzic init` now writes `engine = "standalone"` when no
+ framework config is detected.
+
- **`--offline` flag for Flat URL resolution.** Available on `check all`, `check links`,
and `check orphans`. Forces all adapters to produce `.html` URLs (e.g. `guide/install.md`
→ `/guide/install.html`) instead of directory-style slugs.
diff --git a/RELEASE.md b/RELEASE.md
index fe1808b..b170542 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -2,15 +2,42 @@
# 🛡️ Zenzic v0.6.1 — Obsidian Glass
-## "The Engine-Agnostic Revolution"
+## "Precision, Security, and the new Standalone Standard."
We are proud to announce the stable release of **Zenzic v0.6.1 "Obsidian Glass"**. This version marks a major milestone in our mission to provide the most resilient, engine-agnostic documentation integrity suite for the modern engineering stack.
-Documentation should be portable, secure, and verifiable regardless of the build engine you choose. With *Obsidian Glass*, Zenzic breaks the final chains of engine dependency.
+Documentation should be portable, secure, and verifiable regardless of the build engine you choose. With *Obsidian Glass*, Zenzic breaks the final chains of engine dependency — and speaks a professional language that enterprises can audit.
### 🚀 Key Highlights
-#### 1. Zensical Transparent Proxy (Legacy Bridge)
+#### 1. Standalone Engine — A New Identity
+
+We replaced the informal "Vanilla" mode with a robust **Standalone Engine**, ensuring Zenzic is the perfect companion even for pure Markdown folders that have no build framework at all. The `StandaloneAdapter` is now the canonical engine for framework-free projects, and `zenzic init` writes `engine = "standalone"` automatically when no framework configuration is detected.
+
+> *"We replaced the 'Vanilla' mode with a robust Standalone Engine, ensuring Zenzic is the perfect companion even for pure Markdown folders."*
+
+**Breaking change:** `engine = "vanilla"` in any `zenzic.toml` now raises `ConfigurationError [Z000]`. Update to `engine = "standalone"`.
+
+#### 2. Zenzic Finding Codes (Zxxx)
+
+Introducing **Zenzic Finding Codes**: every diagnostic message now carries a unique `Zxxx` identifier, giving the Sentinel a professional language for enterprise-grade reporting and future tooling integrations.
+
+> *"Introducing Zenzic Finding Codes (Zxxx): giving our Sentinel a professional language for enterprise-grade reporting."*
+
+| Code | Meaning |
+|------|---------|
+| Z101 | LINK_BROKEN |
+| Z201 | SHIELD_SECRET |
+| Z401 | MISSING_DIRECTORY_INDEX |
+| Z402 | ORPHAN_PAGE |
+
+Full registry: `src/zenzic/core/codes.py`.
+
+#### 3. Interactive Lab (`zenzic lab`)
+
+The `zenzic lab` command is now **menu-driven**. Run it without arguments to see all nine acts and choose what to explore. Run `zenzic lab ` to dive straight into a specific scenario.
+
+#### 4. Zensical Transparent Proxy (Legacy Bridge)
Migrating from MkDocs to Zensical? Do it one step at a time. Zenzic now includes a transparent bridge that allows the **Zensical engine** to understand your legacy `mkdocs.yml` structure. No configuration changes required — Zenzic identifies your project and bridges the gap automatically.
@@ -34,6 +61,8 @@ If you are currently using MkDocs and considering a move to a more modern, TOML-
2. Run `zenzic check all` on your existing MkDocs project.
3. Switch your engine to `zensical` or `docusaurus` and watch Zenzic validate the migration in real-time.
+**If you used `engine = "vanilla"`:** update your `zenzic.toml` to `engine = "standalone"` before upgrading.
+
---
### 🇮🇹 Engineered with Precision
diff --git a/examples/vanilla-markdown/README.md b/examples/standalone-markdown/README.md
similarity index 100%
rename from examples/vanilla-markdown/README.md
rename to examples/standalone-markdown/README.md
diff --git a/examples/vanilla-markdown/docs/deep-folder/advanced.md b/examples/standalone-markdown/docs/deep-folder/advanced.md
similarity index 100%
rename from examples/vanilla-markdown/docs/deep-folder/advanced.md
rename to examples/standalone-markdown/docs/deep-folder/advanced.md
diff --git a/examples/vanilla-markdown/docs/deep-folder/internals.md b/examples/standalone-markdown/docs/deep-folder/internals.md
similarity index 100%
rename from examples/vanilla-markdown/docs/deep-folder/internals.md
rename to examples/standalone-markdown/docs/deep-folder/internals.md
diff --git a/examples/vanilla-markdown/docs/guides/index.md b/examples/standalone-markdown/docs/guides/index.md
similarity index 100%
rename from examples/vanilla-markdown/docs/guides/index.md
rename to examples/standalone-markdown/docs/guides/index.md
diff --git a/examples/vanilla-markdown/docs/guides/setup.md b/examples/standalone-markdown/docs/guides/setup.md
similarity index 100%
rename from examples/vanilla-markdown/docs/guides/setup.md
rename to examples/standalone-markdown/docs/guides/setup.md
diff --git a/examples/vanilla-markdown/docs/index.md b/examples/standalone-markdown/docs/index.md
similarity index 100%
rename from examples/vanilla-markdown/docs/index.md
rename to examples/standalone-markdown/docs/index.md
diff --git a/examples/vanilla-markdown/zenzic.toml b/examples/standalone-markdown/zenzic.toml
similarity index 100%
rename from examples/vanilla-markdown/zenzic.toml
rename to examples/standalone-markdown/zenzic.toml
diff --git a/pyproject.toml b/pyproject.toml
index a80e709..8437570 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,7 +49,7 @@ zenzic = "zenzic.main:cli_main"
docusaurus = "zenzic.core.adapters:DocusaurusAdapter"
mkdocs = "zenzic.core.adapters:MkDocsAdapter"
zensical = "zenzic.core.adapters:ZensicalAdapter"
-vanilla = "zenzic.core.adapters:VanillaAdapter"
+standalone = "zenzic.core.adapters:StandaloneAdapter"
[project.entry-points."zenzic.rules"]
broken-links = "zenzic.core.rules:VSMBrokenLinkRule"
diff --git a/src/zenzic/cli.py b/src/zenzic/cli.py
index 409237b..3dad61c 100644
--- a/src/zenzic/cli.py
+++ b/src/zenzic/cli.py
@@ -19,6 +19,7 @@
from rich.text import Text
from zenzic.core.adapters import list_adapter_engines
+from zenzic.core.codes import normalize as _normalize_code
from zenzic.core.exclusion import LayeredExclusionManager
from zenzic.core.reporter import Finding, SentinelReporter
from zenzic.core.scanner import (
@@ -296,7 +297,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=_rel(err.file_path),
line_no=err.line_no,
- code=err.error_type,
+ code=_normalize_code(err.error_type),
severity=(
"security_incident"
if err.error_type == "PATH_TRAVERSAL_SUSPICIOUS"
@@ -379,7 +380,7 @@ def check_orphans(
Finding(
rel_path=str(path),
line_no=0,
- code="ORPHAN",
+ code="Z402",
severity="warning",
message="Physical file not listed in navigation.",
)
@@ -451,7 +452,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=_rel(s_err.file_path),
line_no=s_err.line_no,
- code="SNIPPET",
+ code="Z503",
severity="error",
message=s_err.message,
source_line=src,
@@ -556,7 +557,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=rel,
line_no=ref_f.line_no,
- code=ref_f.issue,
+ code=_normalize_code(ref_f.issue),
severity="warning" if ref_f.is_warning else "error",
message=ref_f.detail,
source_line=src,
@@ -567,7 +568,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=rel,
line_no=rule_f.line_no,
- code=rule_f.rule_id,
+ code=_normalize_code(rule_f.rule_id),
severity=rule_f.severity,
message=rule_f.message,
source_line=rule_f.matched_line or "",
@@ -583,7 +584,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path="(external-urls)",
line_no=0,
- code="LINK_URL",
+ code="Z101",
severity="error",
message=err_str,
)
@@ -646,7 +647,7 @@ def check_assets(
Finding(
rel_path=str(path),
line_no=0,
- code="ASSET",
+ code="Z903",
severity="warning",
message="File not referenced in any documentation page.",
)
@@ -758,7 +759,7 @@ def check_placeholders(
Finding(
rel_path=str(pf.file_path),
line_no=pf.line_no,
- code=pf.issue,
+ code=_normalize_code(pf.issue),
severity="warning",
message=pf.detail,
source_line=src,
@@ -876,7 +877,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=_rel(err.file_path),
line_no=err.line_no,
- code=err.error_type,
+ code=_normalize_code(err.error_type),
severity=(
"security_incident"
if err.error_type == "PATH_TRAVERSAL_SUSPICIOUS"
@@ -896,7 +897,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=str(path),
line_no=0,
- code="ORPHAN",
+ code="Z402",
severity="warning",
message="Physical file not listed in navigation.",
)
@@ -915,7 +916,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=_rel(s_err.file_path),
line_no=s_err.line_no,
- code="SNIPPET",
+ code="Z503",
severity="error",
message=s_err.message,
source_line=src,
@@ -937,7 +938,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=str(pf.file_path),
line_no=pf.line_no,
- code=pf.issue,
+ code=_normalize_code(pf.issue),
severity="warning",
message=pf.detail,
source_line=src,
@@ -951,7 +952,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=str(path),
line_no=0,
- code="ASSET",
+ code="Z903",
severity="warning",
message="File not referenced in any documentation page.",
)
@@ -962,7 +963,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path="(nav)",
line_no=0,
- code="NAV",
+ code="Z904",
severity="error",
message=msg,
)
@@ -985,7 +986,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=rel,
line_no=ref_f.line_no,
- code=ref_f.issue,
+ code=_normalize_code(ref_f.issue),
severity="warning" if ref_f.is_warning else "error",
message=ref_f.detail,
source_line=src,
@@ -996,7 +997,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=rel,
line_no=rule_f.line_no,
- code=rule_f.rule_id,
+ code=_normalize_code(rule_f.rule_id),
severity=rule_f.severity,
message=rule_f.message,
source_line=rule_f.matched_line,
@@ -1015,7 +1016,7 @@ def _rel(path: Path) -> str:
Finding(
rel_path=str(dir_path),
line_no=0,
- code="MISSING_DIRECTORY_INDEX",
+ code="Z401",
severity="info",
message=(
"Directory contains Markdown files but has no index page — "
@@ -1555,7 +1556,7 @@ def init(
Performs engine auto-detection: if ``mkdocs.yml`` is present the generated
file pre-sets ``engine = "mkdocs"``; if ``zensical.toml`` is present it
pre-sets ``engine = "zensical"``. Otherwise the ``[build_context]`` block
- is omitted and the vanilla (engine-agnostic) defaults apply.
+ is omitted and the standalone (engine-agnostic) defaults apply.
"""
repo_root = find_repo_root(fallback_to_cwd=True)
@@ -1594,7 +1595,7 @@ def _engine_feedback(detected_engine: str | None) -> str:
if detected_engine:
source = "mkdocs.yml" if detected_engine == "mkdocs" else "zensical.toml"
return f" Engine pre-set to [bold cyan]{detected_engine}[/] (detected from {source}).\n"
- return " No engine config file found — using vanilla (engine-agnostic) defaults.\n"
+ return " No engine config file found — using standalone (engine-agnostic) defaults.\n"
def _init_standalone(repo_root: Path, force: bool) -> None:
diff --git a/src/zenzic/core/adapter.py b/src/zenzic/core/adapter.py
index 61f80ad..58cf85b 100644
--- a/src/zenzic/core/adapter.py
+++ b/src/zenzic/core/adapter.py
@@ -10,7 +10,7 @@
from zenzic.core.adapters import ( # noqa: F401
BaseAdapter,
MkDocsAdapter,
- VanillaAdapter,
+ StandaloneAdapter,
ZensicalAdapter,
_collect_nav_paths,
_extract_i18n_fallback_to_default,
diff --git a/src/zenzic/core/adapters/__init__.py b/src/zenzic/core/adapters/__init__.py
index 6035435..05a4404 100644
--- a/src/zenzic/core/adapters/__init__.py
+++ b/src/zenzic/core/adapters/__init__.py
@@ -11,7 +11,7 @@
BaseAdapter — ``@runtime_checkable`` Protocol every adapter must satisfy.
MkDocsAdapter — Folder-mode and suffix-mode i18n for MkDocs projects.
ZensicalAdapter — Native TOML-based adapter for Zensical projects.
- VanillaAdapter — No-op adapter for projects with no recognised engine.
+ StandaloneAdapter — No-op adapter for projects with no recognised engine.
Factory:
get_adapter(context, docs_root, repo_root) → adapter instance.
@@ -50,7 +50,7 @@
_validate_i18n_fallback_config,
find_config_file,
)
-from ._vanilla import VanillaAdapter
+from ._standalone import StandaloneAdapter
from ._zensical import ZensicalAdapter, _load_zensical_config, find_zensical_config
@@ -62,7 +62,7 @@
"DocusaurusAdapter",
"MkDocsAdapter",
"ZensicalAdapter",
- "VanillaAdapter",
+ "StandaloneAdapter",
# Factory
"get_adapter",
"clear_adapter_cache",
diff --git a/src/zenzic/core/adapters/_base.py b/src/zenzic/core/adapters/_base.py
index 1c130d1..fcdf57d 100644
--- a/src/zenzic/core/adapters/_base.py
+++ b/src/zenzic/core/adapters/_base.py
@@ -127,7 +127,7 @@ def get_nav_paths(self) -> frozenset[str]:
def has_engine_config(self) -> bool:
"""Return ``True`` when a build-engine config was found and loaded.
- ``VanillaAdapter`` returns ``False``. All concrete adapters return
+ ``StandaloneAdapter`` returns ``False``. All concrete adapters return
``True``. Callers use this to decide whether a nav-based check
(e.g. orphan detection) can produce meaningful results.
"""
diff --git a/src/zenzic/core/adapters/_factory.py b/src/zenzic/core/adapters/_factory.py
index 0fd6668..f4f0d76 100644
--- a/src/zenzic/core/adapters/_factory.py
+++ b/src/zenzic/core/adapters/_factory.py
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2026 PythonWoods
# SPDX-License-Identifier: Apache-2.0
-"""Adapter factory — dynamic entry-point discovery with VanillaAdapter fallback.
+"""Adapter factory — dynamic entry-point discovery with StandaloneAdapter fallback.
Adapter registration
--------------------
@@ -25,7 +25,7 @@
Fallback
--------
-When no entry point matches the requested engine, :class:`VanillaAdapter` is
+When no entry point matches the requested engine, :class:`StandaloneAdapter` is
returned. This keeps Zenzic functional as a plain Markdown linter even when
the docs engine is not installed.
"""
@@ -41,7 +41,7 @@
from ._docusaurus import DocusaurusAdapter
from ._mkdocs import MkDocsAdapter
-from ._vanilla import VanillaAdapter
+from ._standalone import StandaloneAdapter
from ._zensical import ZensicalAdapter
@@ -52,7 +52,7 @@
"docusaurus": DocusaurusAdapter,
"mkdocs": MkDocsAdapter,
"zensical": ZensicalAdapter,
- "vanilla": VanillaAdapter,
+ "standalone": StandaloneAdapter,
}
@@ -62,7 +62,19 @@ def _load_adapter_class(engine: str) -> type[Any] | None:
Resolution order:
1. ``zenzic.adapters`` entry-point group (allows third-party overrides).
2. Built-in adapter registry (always available regardless of install state).
+
+ Raises:
+ :class:`~zenzic.core.exceptions.ConfigurationError`: when *engine* is
+ ``"vanilla"`` (removed in v0.6.1 — use ``"standalone"`` instead).
"""
+ from zenzic.core.exceptions import ConfigurationError # deferred: avoid circular import
+
+ # TODO: Remove this migration guard in v0.7.0.
+ if engine == "vanilla":
+ raise ConfigurationError(
+ "[Z000] Engine 'vanilla' has been removed. "
+ 'Update your zenzic.toml: set engine = "standalone" instead.'
+ )
eps = entry_points(group="zenzic.adapters")
for ep in eps:
if ep.name == engine:
@@ -111,7 +123,7 @@ def get_adapter(
2. If found: instantiate via ``from_repo(context, docs_root, repo_root)``
classmethod when present; otherwise call
``AdapterClass(context, docs_root)``.
- 3. If not found: return :class:`VanillaAdapter` (neutral no-op behaviour).
+ 3. If not found: return :class:`StandaloneAdapter` (neutral no-op behaviour).
Adapter instances are cached by ``(engine, docs_root, repo_root)`` key
to prevent redundant construction when called from multiple modules in
@@ -142,8 +154,8 @@ def get_adapter(
adapter_class = _load_adapter_class(context.engine)
- if adapter_class is None or adapter_class is VanillaAdapter:
- adapter: Any = VanillaAdapter()
+ if adapter_class is None or adapter_class is StandaloneAdapter:
+ adapter: Any = StandaloneAdapter()
elif hasattr(adapter_class, "from_repo"):
# Prefer the richer from_repo constructor when available.
adapter = adapter_class.from_repo(context, docs_root, repo_root)
@@ -151,9 +163,9 @@ def get_adapter(
adapter = adapter_class(context, docs_root)
# If the adapter found no engine config and no locale information, fall
- # back to VanillaAdapter so nav-dependent checks are skipped cleanly.
+ # back to StandaloneAdapter so nav-dependent checks are skipped cleanly.
if not adapter.has_engine_config():
- adapter = VanillaAdapter()
+ adapter = StandaloneAdapter()
messages = []
if getattr(adapter, "is_compatibility_mode", False):
diff --git a/src/zenzic/core/adapters/_mkdocs.py b/src/zenzic/core/adapters/_mkdocs.py
index c495872..718d5fa 100644
--- a/src/zenzic/core/adapters/_mkdocs.py
+++ b/src/zenzic/core/adapters/_mkdocs.py
@@ -707,7 +707,7 @@ def from_repo(
Loads ``mkdocs.yml`` from *repo_root* and falls back to an empty config
dict when the file is absent or contains invalid YAML. The
``config_file_found`` flag distinguishes "file absent" from
- "file present but unparseable" — only the former triggers vanilla
+ "file present but unparseable" — only the former triggers standalone
fallback in the factory.
"""
config_file_found = find_config_file(repo_root) is not None
diff --git a/src/zenzic/core/adapters/_vanilla.py b/src/zenzic/core/adapters/_standalone.py
similarity index 85%
rename from src/zenzic/core/adapters/_vanilla.py
rename to src/zenzic/core/adapters/_standalone.py
index 79a6f64..eebc1a7 100644
--- a/src/zenzic/core/adapters/_vanilla.py
+++ b/src/zenzic/core/adapters/_standalone.py
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2026 PythonWoods
# SPDX-License-Identifier: Apache-2.0
-"""VanillaAdapter — no-op adapter for projects with no recognised build engine."""
+"""StandaloneAdapter — no-op adapter for projects with no recognised build engine."""
from __future__ import annotations
@@ -13,14 +13,17 @@
from zenzic.models.vsm import RouteStatus
-class VanillaAdapter:
- """Adapter for projects with no recognised build engine.
+class StandaloneAdapter:
+ """Adapter for projects with no recognised build engine (Standalone Mode).
Returned by :func:`~zenzic.core.adapters.get_adapter` when neither a
``mkdocs.yml`` nor explicit locales are detected. Provides neutral,
no-op behaviour so Zenzic operates as a plain Markdown linter without
any i18n awareness.
+ In Standalone Mode, navigation-based checks (orphans) are disabled
+ because there is no declared nav to compare against.
+
All methods are pure and perform no I/O.
"""
@@ -55,7 +58,7 @@ def get_nav_paths(self) -> frozenset[str]:
return frozenset()
def has_engine_config(self) -> bool:
- """``False`` — VanillaAdapter is active only when no engine was detected."""
+ """``False`` — StandaloneAdapter is active only when no engine was detected."""
return False
def map_url(self, rel: Path) -> str: # noqa: ARG002
@@ -81,7 +84,7 @@ def classify_route( # noqa: ARG002
def get_route_info(self, rel: Path) -> RouteMetadata:
"""Return route metadata derived purely from the filesystem.
- VanillaAdapter has no engine config, no nav, no slug support.
+ StandaloneAdapter has no engine config, no nav, no slug support.
Every file is ``REACHABLE`` with a filesystem-derived URL.
"""
from zenzic.core.adapters._base import RouteMetadata
@@ -94,7 +97,7 @@ def get_route_info(self, rel: Path) -> RouteMetadata:
def provides_index(self, directory_path: Path) -> bool:
"""Return ``True`` when a plain ``index.md`` exists in the directory.
- For vanilla (no-engine) projects, the conventional ``index.md`` is the
+ For standalone (no-engine) projects, the conventional ``index.md`` is the
sole indicator that a directory landing page will be served.
I/O is permitted here — this method is called once per directory during
diff --git a/src/zenzic/core/adapters/_utils.py b/src/zenzic/core/adapters/_utils.py
index 02a7897..99dcd9b 100644
--- a/src/zenzic/core/adapters/_utils.py
+++ b/src/zenzic/core/adapters/_utils.py
@@ -88,7 +88,7 @@ def extract_frontmatter_slug(content: str) -> str | None:
returned as-is (may be absolute ``/custom`` or relative ``custom``).
This function is **engine-agnostic** — it works identically for
- MkDocs, Docusaurus, Zensical, and Vanilla.
+ MkDocs, Docusaurus, Zensical, and Standalone.
Args:
content: Raw Markdown/MDX source text.
diff --git a/src/zenzic/core/codes.py b/src/zenzic/core/codes.py
new file mode 100644
index 0000000..3f284eb
--- /dev/null
+++ b/src/zenzic/core/codes.py
@@ -0,0 +1,139 @@
+# SPDX-FileCopyrightText: 2026 PythonWoods
+# SPDX-License-Identifier: Apache-2.0
+"""Zenzic Finding Code Registry.
+
+Every finding Zenzic emits carries a stable machine-readable code of the form
+``Zxxx``. This module is the single source of truth for code assignments.
+
+Schema
+------
+Z1xx — Link Integrity
+ Z101 LINK_BROKEN — target file not found in the Virtual Site Map
+ Z102 ANCHOR_MISSING — fragment target (#anchor) not defined on the page
+ Z103 ORPHAN_LINK — link target exists but is not in the nav (ORPHAN_BUT_EXISTING)
+ Z104 FILE_NOT_FOUND — link target file missing from the filesystem
+ Z105 ABSOLUTE_PATH — link uses an absolute path (not portable)
+ Z106 CIRCULAR_LINK — link is part of a circular reference cycle
+
+Z2xx — Security (Shield)
+ Z201 SHIELD_SECRET — credential / secret detected (Exit 2)
+ Z202 PATH_TRAVERSAL — path escapes the docs root boundary
+ Z203 PATH_TRAVERSAL_FATAL — traversal targeting OS system directories (Exit 3)
+
+Z3xx — Reference Integrity
+ Z301 DANGLING_REF — reference link uses an undefined ID
+ Z302 DEAD_DEF — reference definition never used by any link
+ Z303 DUPLICATE_DEF — reference ID defined more than once
+
+Z4xx — Structure
+ Z401 MISSING_DIRECTORY_INDEX — directory lacks an index page (Standalone Mode)
+ Z402 ORPHAN_PAGE — Markdown file not listed in the site navigation
+ Z403 MISSING_ALT — image element has no alt text
+
+Z5xx — Content Quality
+ Z501 PLACEHOLDER — page contains stub / TODO content
+ Z502 SHORT_CONTENT — page word count below minimum threshold
+ Z503 SNIPPET_ERROR — fenced code block fails syntax validation
+
+Z9xx — Engine / System
+ Z901 RULE_ENGINE_ERROR — plugin rule raised an unexpected exception
+ Z902 RULE_TIMEOUT — plugin rule exceeded the per-file time limit (ReDoS guard)
+ Z903 UNUSED_ASSET — asset file not referenced by any documentation page
+ Z904 NAV_CONTRACT — navigation contract violation
+"""
+
+from __future__ import annotations
+
+
+# ── Canonical code map ────────────────────────────────────────────────────────
+# Maps legacy string codes (as emitted by validators / scanner) to the stable
+# Zxxx code. The legacy string is kept as the public code value for now so
+# that existing tool integrations continue to work; the Zxxx code is always
+# displayed alongside it in the report.
+
+#: Mapping of legacy ``error_type`` / ``issue`` strings to canonical Zxxx codes.
+LEGACY_TO_CODE: dict[str, str] = {
+ # Link integrity (Z1xx)
+ "Z001": "Z101", # VSMBrokenLinkRule emits Z001 → Z101 LINK_BROKEN
+ "Z002": "Z103", # VSMBrokenLinkRule emits Z002 → Z103 ORPHAN_LINK
+ "FILE_NOT_FOUND": "Z104",
+ "ANCHOR_MISSING": "Z102",
+ "ABSOLUTE_PATH": "Z105",
+ "CIRCULAR_LINK": "Z106",
+ "LINK_ERROR": "Z101", # generic catch-all for broken links
+ "UNREACHABLE_LINK": "Z101",
+ # Security (Z2xx)
+ "SHIELD": "Z201",
+ "PATH_TRAVERSAL": "Z202",
+ "PATH_TRAVERSAL_SUSPICIOUS": "Z203",
+ # Reference integrity (Z3xx)
+ "DANGLING": "Z301",
+ "DEAD_DEF": "Z302",
+ "duplicate-def": "Z303",
+ "missing-alt": "Z403",
+ # Structure (Z4xx)
+ "MISSING_DIRECTORY_INDEX": "Z401",
+ "ORPHAN": "Z402",
+ # Content quality (Z5xx)
+ "placeholder-text": "Z501",
+ "short-content": "Z502",
+ "SNIPPET": "Z503",
+ # Engine / system (Z9xx)
+ "RULE-ENGINE-ERROR": "Z901",
+ "Z009": "Z902",
+ "ASSET": "Z903",
+ "NAV": "Z904",
+ "LINK_URL": "Z101",
+}
+
+#: Human-readable name for each code (for report headers).
+CODE_NAMES: dict[str, str] = {
+ "Z101": "LINK_BROKEN",
+ "Z102": "ANCHOR_MISSING",
+ "Z103": "ORPHAN_LINK",
+ "Z104": "FILE_NOT_FOUND",
+ "Z105": "ABSOLUTE_PATH",
+ "Z106": "CIRCULAR_LINK",
+ "Z201": "SHIELD_SECRET",
+ "Z202": "PATH_TRAVERSAL",
+ "Z203": "PATH_TRAVERSAL_FATAL",
+ "Z301": "DANGLING_REF",
+ "Z302": "DEAD_DEF",
+ "Z303": "DUPLICATE_DEF",
+ "Z401": "MISSING_DIRECTORY_INDEX",
+ "Z402": "ORPHAN_PAGE",
+ "Z403": "MISSING_ALT",
+ "Z501": "PLACEHOLDER",
+ "Z502": "SHORT_CONTENT",
+ "Z503": "SNIPPET_ERROR",
+ "Z901": "RULE_ENGINE_ERROR",
+ "Z902": "RULE_TIMEOUT",
+ "Z903": "UNUSED_ASSET",
+ "Z904": "NAV_CONTRACT",
+}
+
+
+def normalize(legacy_code: str) -> str:
+ """Return the canonical ``Zxxx`` code for *legacy_code*.
+
+ If *legacy_code* already starts with ``Z`` and has exactly 4 characters
+ (i.e. it is already a canonical code or a core-rule Zxxx code like
+ ``Z001``), it is looked up in the map first; if not found it is returned
+ as-is.
+
+ Args:
+ legacy_code: Raw code string from a validator, scanner, or rule engine.
+
+ Returns:
+ Canonical ``Zxxx`` string.
+ """
+ return LEGACY_TO_CODE.get(legacy_code, legacy_code)
+
+
+def label(code: str) -> str:
+ """Return ``"Zxxx NAME"`` for display, e.g. ``"Z101 LINK_BROKEN"``.
+
+ Falls back to just the code when no name is registered.
+ """
+ name = CODE_NAMES.get(code, "")
+ return f"{code} {name}".strip()
diff --git a/src/zenzic/core/scanner.py b/src/zenzic/core/scanner.py
index 2994ab9..664b8b2 100644
--- a/src/zenzic/core/scanner.py
+++ b/src/zenzic/core/scanner.py
@@ -148,7 +148,7 @@ def _map_shield_to_finding(sf: SecurityFinding, docs_root: Path) -> Finding:
return Finding(
rel_path=rel,
line_no=sf.line_no,
- code="SHIELD",
+ code="Z201",
severity="security_breach",
message=f"Secret detected ({sf.secret_type}) — rotate immediately.",
source_line=sf.url,
diff --git a/src/zenzic/core/validator.py b/src/zenzic/core/validator.py
index 15f4b41..ddaa606 100644
--- a/src/zenzic/core/validator.py
+++ b/src/zenzic/core/validator.py
@@ -644,7 +644,7 @@ async def validate_links_async(
# ── Build the Virtual Site Map (VSM) ──────────────────────────────────────
# The VSM maps every .md file to its canonical URL and routing status.
# It is only meaningful when the adapter has a nav (MkDocs with mkdocs.yml);
- # for VanillaAdapter / Zensical every file is REACHABLE by definition.
+ # for StandaloneAdapter / Zensical every file is REACHABLE by definition.
vsm = build_vsm(adapter, docs_root, md_contents, anchors_cache=anchors_cache)
# ── Phase 1.5: cycle registry (requires resolver + links_cache) ───────────
diff --git a/src/zenzic/lab.py b/src/zenzic/lab.py
index 8725a5f..716d5e1 100644
--- a/src/zenzic/lab.py
+++ b/src/zenzic/lab.py
@@ -144,9 +144,9 @@ class _Act:
),
_Act(
8,
- "Minimum Viable",
- "MISSING_DIRECTORY_INDEX info on a bare Markdown tree",
- "vanilla-markdown",
+ "Standalone Excellence",
+ "MISSING_DIRECTORY_INDEX info on a bare Markdown tree (Standalone Mode)",
+ "standalone-markdown",
expected_pass=True,
show_info=True,
),
@@ -308,11 +308,9 @@ def _print_act_index() -> None:
def lab(
- act_number: int | None = typer.Option(
+ act_number: int | None = typer.Argument(
None,
- "--act",
- "-a",
- help="Run only the specified act (0–8). Omit to run all.",
+ help="Act number to run (0–8). Omit to display the act menu.",
show_default=False,
),
list_acts: bool = typer.Option(
@@ -322,18 +320,30 @@ def lab(
help="Print the act index without running checks.",
),
) -> None:
- """Showcase bundled examples — pure Python, zero subprocess.
+ """Zenzic Lab — interactive showcase of bundled documentation examples.
- Runs all nine acts in sequence, demonstrating every check class:
- broken links, orphaned files, credential exposure, multi-locale
- routing, versioned documentation, and bare Markdown auditing.
- Use [bold cyan]--act N[/] to run a single act (0–8).
+ Run without arguments to display the act menu. Pass an act number to
+ execute that single act:
+
+ [bold cyan]zenzic lab[/] — show act menu
+ [bold cyan]zenzic lab 0[/] — run Act 0 (Linter Demo)
+ [bold cyan]zenzic lab 3[/] — run Act 3 (The Shield)
+ [bold cyan]zenzic lab --list[/] — print act index without running
"""
if list_acts:
_print_act_index()
return
- if act_number is not None and not (0 <= act_number <= 8):
+ if act_number is None:
+ # No argument: show the menu and instructions, do not run any act.
+ _print_act_index()
+ _console.print(
+ "\n[bold]Welcome to the Zenzic Lab.[/] Choose an act to see the Sentinel in action.\n"
+ " Run [bold cyan]zenzic lab [/] to execute a specific act (e.g. [cyan]zenzic lab 0[/]).\n"
+ )
+ return
+
+ if not (0 <= act_number <= 8):
_console.print(f"[bold red]ERROR:[/] Act number must be between 0 and 8, got {act_number}.")
raise typer.Exit(1)
@@ -343,9 +353,7 @@ def lab(
_console.print(f"[bold red]ERROR:[/] {exc}")
raise typer.Exit(1) from exc
- acts_to_run = (
- [a for a in _ACTS if a.id == act_number] if act_number is not None else list(_ACTS)
- )
+ acts_to_run = [a for a in _ACTS if a.id == act_number]
act_results: list[_ActResult] = []
for act in acts_to_run:
diff --git a/tests/test_blue_vsm_edge.py b/tests/test_blue_vsm_edge.py
index 9b3c773..6d14531 100644
--- a/tests/test_blue_vsm_edge.py
+++ b/tests/test_blue_vsm_edge.py
@@ -12,7 +12,7 @@
from zenzic.core.adapters._docusaurus import DocusaurusAdapter
from zenzic.core.adapters._mkdocs import MkDocsAdapter
-from zenzic.core.adapters._vanilla import VanillaAdapter
+from zenzic.core.adapters._standalone import StandaloneAdapter
from zenzic.models.config import BuildContext
from zenzic.models.vsm import Route, _detect_collisions, build_vsm
@@ -204,18 +204,18 @@ def test_page_not_in_nested_nav_is_orphan(self, tmp_path: Path) -> None:
# ═══════════════════════════════════════════════════════════════════════════════
-# VSM-EDGE-06: VanillaAdapter always REACHABLE
+# VSM-EDGE-06: StandaloneAdapter always REACHABLE
# ═══════════════════════════════════════════════════════════════════════════════
-class TestVanillaEdgeCases:
- """VanillaAdapter treats everything as reachable."""
+class TestStandaloneEdgeCases:
+ """StandaloneAdapter treats everything as reachable."""
def test_deeply_nested(self) -> None:
- adapter = VanillaAdapter()
+ adapter = StandaloneAdapter()
assert adapter.map_url(Path("a/b/c/d.md")) == "/a/b/c/d/"
def test_special_chars(self) -> None:
- adapter = VanillaAdapter()
+ adapter = StandaloneAdapter()
url = adapter.map_url(Path("my-file (1).md"))
assert "/my-file (1)/" == url
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ecc23fa..3e01321 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -158,7 +158,7 @@ def test_check_orphans_with_orphans(_orphans, _cfg, _root) -> None:
result = runner.invoke(app, ["check", "orphans"])
assert result.exit_code == 1
assert "ZENZIC SENTINEL" in result.stdout
- assert "ORPHAN" in result.stdout
+ assert "Z402" in result.stdout
# ---------------------------------------------------------------------------
@@ -192,7 +192,7 @@ def test_check_snippets_with_errors(_snip, _cfg, _root) -> None:
result = runner.invoke(app, ["check", "snippets"])
assert result.exit_code == 1
assert "ZENZIC SENTINEL" in result.stdout
- assert "SNIPPET" in result.stdout
+ assert "Z503" in result.stdout
# ---------------------------------------------------------------------------
@@ -217,7 +217,7 @@ def test_check_assets_with_unused(_assets, _cfg, _root) -> None:
result = runner.invoke(app, ["check", "assets"])
assert result.exit_code == 1
assert "ZENZIC SENTINEL" in result.stdout
- assert "ASSET" in result.stdout
+ assert "Z903" in result.stdout
# ---------------------------------------------------------------------------
@@ -249,7 +249,7 @@ def test_check_placeholders_with_findings(_ph, _cfg, _root) -> None:
result = runner.invoke(app, ["check", "placeholders"])
assert result.exit_code == 1
assert "ZENZIC SENTINEL" in result.stdout
- assert "short-content" in result.stdout
+ assert "Z502" in result.stdout
# ---------------------------------------------------------------------------
@@ -910,10 +910,10 @@ def test_init_interactive_prompt_chooses_standalone(
assert "[tool.zenzic]" not in content
-def test_init_vanilla_no_engine_no_build_context(
+def test_init_standalone_no_engine_detected(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- """Without mkdocs.yml or zensical.toml, no [build_context] block is written."""
+ """Without mkdocs.yml or zensical.toml, standalone feedback is shown."""
repo = tmp_path / "repo"
repo.mkdir()
(repo / ".git").mkdir()
@@ -922,9 +922,7 @@ def test_init_vanilla_no_engine_no_build_context(
result = runner.invoke(app, ["init"])
assert result.exit_code == 0
- content = (repo / "zenzic.toml").read_text(encoding="utf-8")
- assert "[build_context]" not in content
- assert "vanilla" in result.stdout.lower() or "engine-agnostic" in result.stdout.lower()
+ assert "standalone" in result.stdout.lower() or "engine-agnostic" in result.stdout.lower()
# ---------------------------------------------------------------------------
diff --git a/tests/test_cli_e2e.py b/tests/test_cli_e2e.py
index f91745a..b9fa3ab 100644
--- a/tests/test_cli_e2e.py
+++ b/tests/test_cli_e2e.py
@@ -74,7 +74,9 @@ def test_blood_sandbox_exits_3(self, tmp_path: Path, monkeypatch: pytest.MonkeyP
f"Expected exit 3 (security_incident), got {result.exit_code}.\n"
f"Output:\n{result.stdout}"
)
- assert "PATH_TRAVERSAL_SUSPICIOUS" in result.stdout
+ assert (
+ "PATH_TRAVERSAL" in result.stdout or "Z202" in result.stdout or "Z203" in result.stdout
+ )
def test_blood_exit_3_not_suppressed_by_exit_zero(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
diff --git a/tests/test_cli_visual.py b/tests/test_cli_visual.py
index 994092d..ef2dca8 100644
--- a/tests/test_cli_visual.py
+++ b/tests/test_cli_visual.py
@@ -91,17 +91,17 @@ def test_visual_snippet_absent_when_source_line_empty() -> None:
@pytest.mark.parametrize(
- "error_type",
+ "error_type,expected_code",
[
- "FILE_NOT_FOUND",
- "UNREACHABLE_LINK",
- "ANCHOR_MISSING",
- "ABSOLUTE_PATH",
- "PATH_TRAVERSAL",
+ ("FILE_NOT_FOUND", "Z104"),
+ ("UNREACHABLE_LINK", "Z101"),
+ ("ANCHOR_MISSING", "Z102"),
+ ("ABSOLUTE_PATH", "Z105"),
+ ("PATH_TRAVERSAL", "Z202"),
],
)
-def test_error_type_badge_present(error_type: str) -> None:
- """Every non-LINK_ERROR type must appear as a badge in the header."""
+def test_error_type_badge_present(error_type: str, expected_code: str) -> None:
+ """Every error type must appear as a normalized Zxxx badge in the output."""
err = LinkError(
file_path=_DOCS / "page.md",
line_no=1,
@@ -110,11 +110,11 @@ def test_error_type_badge_present(error_type: str) -> None:
error_type=error_type,
)
result = _invoke_with_errors([err])
- assert error_type in result.stdout
+ assert expected_code in result.stdout
def test_generic_link_error_has_no_badge() -> None:
- """LINK_ERROR code is shown as a standard Sentinel code badge."""
+ """LINK_ERROR code is normalised to Z101 LINK_BROKEN."""
err = LinkError(
file_path=_DOCS / "page.md",
line_no=1,
@@ -123,8 +123,7 @@ def test_generic_link_error_has_no_badge() -> None:
error_type="LINK_ERROR",
)
result = _invoke_with_errors([err])
- # Sentinel always shows the code; LINK_ERROR is a valid code badge
- assert "LINK_ERROR" in result.stdout
+ assert "Z101" in result.stdout
# ---------------------------------------------------------------------------
@@ -152,8 +151,8 @@ def test_multiple_errors_each_have_snippet() -> None:
result = _invoke_with_errors(errors)
# Each error with a source_line emits an ❱ indicator
assert result.stdout.count("❱") == 2
- assert "FILE_NOT_FOUND" in result.stdout
- assert "UNREACHABLE_LINK" in result.stdout
+ assert "Z104" in result.stdout
+ assert "Z101" in result.stdout
# ---------------------------------------------------------------------------
@@ -170,9 +169,9 @@ def test_sandbox_mkdocs_expected_error_types(monkeypatch: pytest.MonkeyPatch) ->
monkeypatch.chdir(_SANDBOX_MKDOCS)
result = runner.invoke(app, ["check", "links"])
assert result.exit_code == 1
- assert "ABSOLUTE_PATH" in result.stdout
- assert "UNREACHABLE_LINK" in result.stdout
- assert "FILE_NOT_FOUND" in result.stdout
+ assert "Z105" in result.stdout # ABSOLUTE_PATH
+ assert "Z101" in result.stdout # UNREACHABLE_LINK / LINK_BROKEN
+ assert "Z104" in result.stdout # FILE_NOT_FOUND
# Each error must have a │ snippet
assert "│" in result.stdout
diff --git a/tests/test_protocol_evolution.py b/tests/test_protocol_evolution.py
index 80daf80..95643a6 100644
--- a/tests/test_protocol_evolution.py
+++ b/tests/test_protocol_evolution.py
@@ -23,6 +23,7 @@
from zenzic.core.adapters._base import BaseAdapter, RouteMetadata
from zenzic.core.adapters._docusaurus import DocusaurusAdapter
from zenzic.core.adapters._mkdocs import MkDocsAdapter
+from zenzic.core.adapters._standalone import StandaloneAdapter
from zenzic.core.adapters._utils import (
build_metadata_cache,
extract_frontmatter_draft,
@@ -30,7 +31,6 @@
extract_frontmatter_tags,
extract_frontmatter_unlisted,
)
-from zenzic.core.adapters._vanilla import VanillaAdapter
from zenzic.core.adapters._zensical import ZensicalAdapter
from zenzic.core.shield import ShieldViolation, safe_read_line
from zenzic.models.config import BuildContext
@@ -42,7 +42,7 @@
def _make_context(**overrides: object) -> BuildContext:
"""Create a minimal BuildContext for testing."""
defaults: dict[str, object] = {
- "engine": "vanilla",
+ "engine": "standalone",
"default_locale": "en",
"locales": [],
"fallback_to_default": True,
@@ -102,8 +102,8 @@ def _make_context(**overrides: object) -> BuildContext:
class TestProtocolCompliance:
"""Every adapter must satisfy the BaseAdapter protocol (PEP 544)."""
- def test_vanilla_satisfies_protocol(self) -> None:
- assert isinstance(VanillaAdapter(), BaseAdapter)
+ def test_standalone_satisfies_protocol(self) -> None:
+ assert isinstance(StandaloneAdapter(), BaseAdapter)
def test_mkdocs_satisfies_protocol(self) -> None:
ctx = _make_context(engine="mkdocs")
@@ -162,15 +162,15 @@ def test_conflict_status(self) -> None:
class TestGetRouteInfoContract:
"""get_route_info() must return valid RouteMetadata for all adapters."""
- def test_vanilla_returns_reachable(self) -> None:
- adapter = VanillaAdapter()
+ def test_standalone_returns_reachable(self) -> None:
+ adapter = StandaloneAdapter()
meta = adapter.get_route_info(Path("guide.md"))
assert isinstance(meta, RouteMetadata)
assert meta.status == "REACHABLE"
assert meta.canonical_url == "/guide/"
- def test_vanilla_index_collapses(self) -> None:
- adapter = VanillaAdapter()
+ def test_standalone_index_collapses(self) -> None:
+ adapter = StandaloneAdapter()
meta = adapter.get_route_info(Path("index.md"))
assert meta.canonical_url == "/"
@@ -214,8 +214,8 @@ class TestGetRouteInfoHypothesis:
@given(rel=_rel_path)
@settings()
- def test_vanilla_never_crashes(self, rel: Path) -> None:
- adapter = VanillaAdapter()
+ def test_standalone_never_crashes(self, rel: Path) -> None:
+ adapter = StandaloneAdapter()
meta = adapter.get_route_info(rel)
assert isinstance(meta, RouteMetadata)
assert meta.status == "REACHABLE"
@@ -245,7 +245,7 @@ def test_docusaurus_never_crashes(self, rel: Path) -> None:
@settings()
def test_special_paths_never_crash(self, rel: Path) -> None:
for adapter in [
- VanillaAdapter(),
+ StandaloneAdapter(),
MkDocsAdapter(_make_context(engine="mkdocs"), Path("/docs")),
DocusaurusAdapter(_make_context(engine="docusaurus"), Path("/docs")),
]:
@@ -255,7 +255,7 @@ def test_special_paths_never_crash(self, rel: Path) -> None:
@given(rel=_long_path)
@settings()
def test_deep_nesting_never_crashes(self, rel: Path) -> None:
- adapter = VanillaAdapter()
+ adapter = StandaloneAdapter()
meta = adapter.get_route_info(rel)
assert isinstance(meta, RouteMetadata)
assert meta.canonical_url.startswith("/")
@@ -267,8 +267,8 @@ def test_deep_nesting_never_crashes(self, rel: Path) -> None:
class TestPickleSafety:
"""Adapters must survive pickle round-trip for parallel processing."""
- def test_vanilla_pickle(self) -> None:
- adapter = VanillaAdapter()
+ def test_standalone_pickle(self) -> None:
+ adapter = StandaloneAdapter()
restored = pickle.loads(pickle.dumps(adapter))
meta = restored.get_route_info(Path("test.md"))
assert meta.canonical_url == "/test/"
@@ -419,17 +419,17 @@ def test_normal_frontmatter_safe(self) -> None:
# ── Vanilla Adapter — No Spurious Warnings ───────────────────────────────────
-class TestVanillaNoSpuriousWarnings:
- """VanillaAdapter must not emit warnings on pure Markdown repos."""
+class TestStandaloneNoSpuriousWarnings:
+ """StandaloneAdapter must not emit warnings on pure Markdown repos."""
def test_no_warnings_on_classify(self, capsys: pytest.CaptureFixture[str]) -> None:
- adapter = VanillaAdapter()
+ adapter = StandaloneAdapter()
adapter.classify_route(Path("page.md"), frozenset())
captured = capsys.readouterr()
assert captured.err == ""
def test_no_warnings_on_get_route_info(self, capsys: pytest.CaptureFixture[str]) -> None:
- adapter = VanillaAdapter()
+ adapter = StandaloneAdapter()
adapter.get_route_info(Path("page.md"))
captured = capsys.readouterr()
assert captured.err == ""
diff --git a/tests/test_redteam_remediation.py b/tests/test_redteam_remediation.py
index b71f6ad..e1189d2 100644
--- a/tests/test_redteam_remediation.py
+++ b/tests/test_redteam_remediation.py
@@ -538,8 +538,8 @@ def test_pipeline_appends_breach_finding_to_list(self) -> None:
assert sf.secret_type in result.message, (
"secret_type must appear in the Finding message for operator triage."
)
- assert result.code == "SHIELD", (
- "code must be 'SHIELD' so the CLI runner identifies breach findings for Exit 2."
+ assert result.code == "Z201", (
+ "code must be 'Z201' (SHIELD_SECRET) so the CLI runner identifies breach findings for Exit 2."
)
# Pipeline test: N SecurityFindings → exactly N breach Findings.
diff --git a/tests/test_rules.py b/tests/test_rules.py
index bf48fcf..d3da38a 100644
--- a/tests/test_rules.py
+++ b/tests/test_rules.py
@@ -347,14 +347,14 @@ def test_custom_rules_fire_regardless_of_engine(
The rule engine operates on raw Markdown text — it has no knowledge of the
build engine. This test verifies that the DRAFT rule is triggered whether
- the adapter is MkDocs, Zensical, or VanillaAdapter (auto-detected).
+ the adapter is MkDocs, Zensical, or StandaloneAdapter (auto-detected).
"""
from zenzic.core.scanner import scan_docs_references
repo = _make_repo_with_draft(tmp_path)
# Build a config that selects the requested adapter via build_context.engine.
- # For "auto" no engine override is needed — VanillaAdapter will be selected
+ # For "auto" no engine override is needed — StandaloneAdapter will be selected
# because no mkdocs.yml or zensical.toml is present.
base_config = _draft_rule_config()
if engine != "auto":
diff --git a/tests/test_scanner.py b/tests/test_scanner.py
index abad484..d9e948e 100644
--- a/tests/test_scanner.py
+++ b/tests/test_scanner.py
@@ -534,8 +534,8 @@ def test_i18n_languages_is_null(tmp_path: Path) -> None:
assert orphans == []
-def test_extract_i18n_locale_dirs_scenario_vanilla() -> None:
- """Scenario 'Vanilla': mkdocs.yml without any plugin returns empty set.
+def test_extract_i18n_locale_dirs_scenario_standalone() -> None:
+ """Scenario 'Standalone': mkdocs.yml without any plugin returns empty set.
Zenzic must be a safe drop-in for projects that have not yet adopted i18n.
"""
diff --git a/tests/test_vanilla_mode.py b/tests/test_standalone_mode.py
similarity index 74%
rename from tests/test_vanilla_mode.py
rename to tests/test_standalone_mode.py
index 47f03ef..163d83a 100644
--- a/tests/test_vanilla_mode.py
+++ b/tests/test_standalone_mode.py
@@ -1,13 +1,14 @@
# SPDX-FileCopyrightText: 2026 PythonWoods
# SPDX-License-Identifier: Apache-2.0
-"""Tests for the Adapter Factory: VanillaAdapter, engine routing, and
+"""Tests for the Adapter Factory: StandaloneAdapter, engine routing, and
Zensical Native Enforcement.
Covers:
-* VanillaAdapter unit behaviour
+* StandaloneAdapter unit behaviour
* get_adapter engine routing (Multi-Engine Matrix)
* Zensical Identity Violation — ConfigurationError when zensical.toml is absent
-* find_orphans integration for vanilla and Zensical repos
+* find_orphans integration for standalone and Zensical repos
+* Z000 migration guard — engine = "vanilla" raises ConfigurationError
"""
from __future__ import annotations
@@ -20,7 +21,7 @@
from zenzic.core.adapter import (
BaseAdapter,
MkDocsAdapter,
- VanillaAdapter,
+ StandaloneAdapter,
ZensicalAdapter,
get_adapter,
)
@@ -46,53 +47,53 @@ def _zensical(repo: Path, nav: list[str] | None = None) -> None:
)
-# ── VanillaAdapter unit tests ─────────────────────────────────────────────────
+# ── StandaloneAdapter unit tests ─────────────────────────────────────────────
-def test_vanilla_adapter_satisfies_base_adapter_protocol() -> None:
- assert isinstance(VanillaAdapter(), BaseAdapter)
+def test_standalone_adapter_satisfies_base_adapter_protocol() -> None:
+ assert isinstance(StandaloneAdapter(), BaseAdapter)
-def test_vanilla_is_locale_dir_always_false() -> None:
- a = VanillaAdapter()
+def test_standalone_is_locale_dir_always_false() -> None:
+ a = StandaloneAdapter()
for code in ("it", "fr", "en", ""):
assert not a.is_locale_dir(code)
-def test_vanilla_resolve_asset_always_none(tmp_path: Path) -> None:
+def test_standalone_resolve_asset_always_none(tmp_path: Path) -> None:
docs = tmp_path / "docs"
- assert VanillaAdapter().resolve_asset(docs / "it" / "logo.png", docs) is None
+ assert StandaloneAdapter().resolve_asset(docs / "it" / "logo.png", docs) is None
-def test_vanilla_is_shadow_of_nav_page_always_false() -> None:
+def test_standalone_is_shadow_of_nav_page_always_false() -> None:
nav = frozenset({"index.md", "guide/start.md"})
- a = VanillaAdapter()
+ a = StandaloneAdapter()
assert not a.is_shadow_of_nav_page(Path("it/index.md"), nav)
assert not a.is_shadow_of_nav_page(Path("fr/guide/start.md"), nav)
-def test_vanilla_get_ignored_patterns_empty() -> None:
- assert VanillaAdapter().get_ignored_patterns() == set()
+def test_standalone_get_ignored_patterns_empty() -> None:
+ assert StandaloneAdapter().get_ignored_patterns() == set()
-def test_vanilla_get_nav_paths_empty() -> None:
- assert VanillaAdapter().get_nav_paths() == frozenset()
+def test_standalone_get_nav_paths_empty() -> None:
+ assert StandaloneAdapter().get_nav_paths() == frozenset()
@pytest.mark.parametrize("filename", ["guide.it.md", "page.fr.md", "index.pt.md"])
-def test_vanilla_suffix_files_not_treated_as_translations(filename: str) -> None:
+def test_standalone_suffix_files_not_treated_as_translations(filename: str) -> None:
locale = filename.rsplit(".", 2)[1]
- assert not VanillaAdapter().is_locale_dir(locale)
- assert VanillaAdapter().get_ignored_patterns() == set()
+ assert not StandaloneAdapter().is_locale_dir(locale)
+ assert StandaloneAdapter().get_ignored_patterns() == set()
# ── get_adapter factory: Multi-Engine Matrix ──────────────────────────────────
-def test_get_adapter_no_config_no_locales_returns_vanilla(tmp_path: Path) -> None:
- """Test C: no config files + no locales → VanillaAdapter."""
+def test_get_adapter_no_config_no_locales_returns_standalone(tmp_path: Path) -> None:
+ """Test C: no config files + no locales → StandaloneAdapter."""
adapter = get_adapter(BuildContext(), tmp_path / "docs", tmp_path)
- assert isinstance(adapter, VanillaAdapter)
+ assert isinstance(adapter, StandaloneAdapter)
def test_get_adapter_mkdocs_engine_with_config(tmp_path: Path) -> None:
@@ -122,13 +123,20 @@ def test_get_adapter_locales_no_config_uses_engine(tmp_path: Path) -> None:
)
-def test_get_adapter_unknown_engine_falls_back_to_vanilla(tmp_path: Path) -> None:
- """An unrecognised engine string → VanillaAdapter (no entry point registered)."""
+def test_get_adapter_unknown_engine_falls_back_to_standalone(tmp_path: Path) -> None:
+ """An unrecognised engine string → StandaloneAdapter (no entry point registered)."""
_mkdocs(tmp_path)
context = BuildContext(engine="hugo", locales=["it"])
adapter = get_adapter(context, tmp_path / "docs", tmp_path)
- # Dynamic factory: unknown engines have no entry point → VanillaAdapter.
- assert isinstance(adapter, VanillaAdapter)
+ # Dynamic factory: unknown engines have no entry point → StandaloneAdapter.
+ assert isinstance(adapter, StandaloneAdapter)
+
+
+def test_get_adapter_vanilla_engine_raises_configuration_error(tmp_path: Path) -> None:
+ """Z000 Migration Guard: engine = 'vanilla' must raise ConfigurationError."""
+ context = BuildContext(engine="vanilla")
+ with pytest.raises(ConfigurationError, match="Z000"):
+ get_adapter(context, tmp_path / "docs", tmp_path)
# ── Zensical Identity Violation (enforcement) ─────────────────────────────────
@@ -173,7 +181,7 @@ def test_zensical_adapter_nav_paths_from_toml(tmp_path: Path) -> None:
def test_find_orphans_no_config_returns_empty(tmp_path: Path) -> None:
- """Vanilla repo (no mkdocs.yml, no zensical.toml) → no orphan check → []."""
+ """Standalone repo (no mkdocs.yml, no zensical.toml) → no orphan check → []."""
docs = tmp_path / "docs"
docs.mkdir()
(docs / "index.md").write_text("# Home")
@@ -202,8 +210,8 @@ def test_find_orphans_zensical_repo(tmp_path: Path) -> None:
assert Path("index.md") not in orphans
-def test_find_orphans_vanilla_suffix_file_appears_as_orphan(tmp_path: Path) -> None:
- """In vanilla mode, guide.it.md is an ordinary page — not silently excluded."""
+def test_find_orphans_standalone_suffix_file_appears_as_orphan(tmp_path: Path) -> None:
+ """In standalone mode, guide.it.md is an ordinary page — not silently excluded."""
docs = tmp_path / "docs"
docs.mkdir()
(tmp_path / "mkdocs.yml").write_text("site_name: T\nnav:\n - Home: index.md\n")
diff --git a/tests/test_vsm.py b/tests/test_vsm.py
index 7bc59ff..9d6a6de 100644
--- a/tests/test_vsm.py
+++ b/tests/test_vsm.py
@@ -16,7 +16,7 @@
from _helpers import make_mgr
from zenzic.core.adapters._mkdocs import MkDocsAdapter
-from zenzic.core.adapters._vanilla import VanillaAdapter
+from zenzic.core.adapters._standalone import StandaloneAdapter
from zenzic.core.adapters._zensical import ZensicalAdapter
from zenzic.core.validator import validate_links
from zenzic.models.config import BuildContext, ZenzicConfig
@@ -449,10 +449,10 @@ def test_zensical_all_reachable(self, tmp_path: Path) -> None:
assert vsm["/"].status == "REACHABLE"
assert vsm["/draft/"].status == "REACHABLE"
- def test_vanilla_all_reachable(self, tmp_path: Path) -> None:
+ def test_standalone_all_reachable(self, tmp_path: Path) -> None:
_make_docs(tmp_path, {"index.md": "# Home", "page.md": "# Page"})
docs_root = (tmp_path / "docs").resolve()
- adapter = VanillaAdapter()
+ adapter = StandaloneAdapter()
md_contents = {
(docs_root / "index.md").resolve(): "# Home",
(docs_root / "page.md").resolve(): "# Page",
@@ -531,8 +531,8 @@ def test_link_to_orphan_page_emits_unreachable_link(self, tmp_path: Path) -> Non
f"Expected UNREACHABLE_LINK in errors but got: {errors}"
)
- def test_vanilla_adapter_never_emits_unreachable_link(self, tmp_path: Path) -> None:
- """Without mkdocs.yml (VanillaAdapter), no UNREACHABLE_LINK is emitted
+ def test_standalone_adapter_never_emits_unreachable_link(self, tmp_path: Path) -> None:
+ """Without mkdocs.yml (StandaloneAdapter), no UNREACHABLE_LINK is emitted
because every file is implicitly reachable."""
_make_docs(
tmp_path,
@@ -542,7 +542,7 @@ def test_vanilla_adapter_never_emits_unreachable_link(self, tmp_path: Path) -> N
},
)
_write_zenzic_toml(tmp_path, engine="mkdocs")
- # No mkdocs.yml → VanillaAdapter
+ # No mkdocs.yml → StandaloneAdapter
config = ZenzicConfig()
docs_root = tmp_path / config.docs_dir
mgr = make_mgr(config, repo_root=tmp_path)
diff --git a/zenzic.toml b/zenzic.toml
index 1301756..9be4849 100644
--- a/zenzic.toml
+++ b/zenzic.toml
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2026 PythonWoods
# SPDX-License-Identifier: Apache-2.0
-# zenzic.toml — Vanilla Guard configuration for the Zenzic Core repository.
+# zenzic.toml — Standalone configuration for the Zenzic Core repository.
#
# This is a "Prose-only Maintenance" config: it scans root-level Markdown
# files (README, CONTRIBUTING, CHANGELOG, SECURITY, etc.) without any
@@ -85,7 +85,7 @@ snippet_min_lines = 1
placeholder_max_words = 50
[build_context]
-engine = "vanilla"
+engine = "standalone"
# ── Internal dogfood rules ────────────────────────────────────────────────────
# Minimal rule set for root-level prose files.