From ee1dbc90e57a6a9354ab06fdb5657ae82220cc87 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Sun, 19 Apr 2026 19:14:44 +0200 Subject: [PATCH 01/13] feat(core): release v0.6.1 'Obsidian Glass' stable Consolidates the v0.6.1 series, establishing Zenzic as a truly engine-agnostic documentation linter. This stable release bridges the gap between MkDocs and Zensical, while providing enterprise-grade support for Docusaurus v3. Key Features: - Zensical Transparent Proxy: native fallback to mkdocs.yml when zensical.toml is absent. - Docusaurus v3 Multi-versioning: support for versioned_docs/ and versions.json. - Path Resolution: @site/ alias support for Docusaurus project-relative links. - Offline Mode: --offline flag to force flat .html URL structure (ignoring directory slugs). - Sentinel Banner: transparent UI notifications for proxy and offline states. Stability & Compliance: - Fixed Docusaurus routeBasePath default to match official engine behavior (/docs/). - Full REUSE 3.3 compliance and metadata synchronization (CITATION.cff, pyproject.toml). - Bilingual (EN/IT) changelog and readme parity. - Resolved all linting violations via ruff hardening. Adheres to Zenzic Pillars: Lint the Source, No Subprocesses, Pure Functions First. --- .gitignore | 1 + CHANGELOG.it.md | 33 ++++- CHANGELOG.md | 31 ++++- CITATION.cff | 4 +- README.it.md | 6 +- README.md | 6 +- RELEASE.md | 173 +++--------------------- pyproject.toml | 4 +- src/zenzic/__init__.py | 2 +- src/zenzic/cli.py | 15 ++ src/zenzic/core/adapters/_base.py | 2 + src/zenzic/core/adapters/_docusaurus.py | 93 +++++++++++-- src/zenzic/core/adapters/_factory.py | 15 ++ src/zenzic/core/adapters/_mkdocs.py | 6 +- src/zenzic/core/adapters/_zensical.py | 112 ++++++++++++--- src/zenzic/core/resolver.py | 6 + src/zenzic/models/config.py | 4 + tests/test_blue_i18n_edge.py | 6 +- tests/test_blue_vsm_edge.py | 22 +-- tests/test_docusaurus_adapter.py | 14 +- tests/test_protocol_evolution.py | 2 +- tests/test_vanilla_mode.py | 19 +-- uv.lock | 2 +- zenzic.toml | 1 + 24 files changed, 357 insertions(+), 222 deletions(-) diff --git a/.gitignore b/.gitignore index 5c8fb2b..d6f28bf 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ dist/ # Zenzic: Drafts drafts/ +Draft/ *.egg MANIFEST .installed.cfg diff --git a/CHANGELOG.it.md b/CHANGELOG.it.md index ac72242..5ebfde0 100644 --- a/CHANGELOG.it.md +++ b/CHANGELOG.it.md @@ -11,6 +11,37 @@ Le versioni seguono il [Semantic Versioning](https://semver.org/). ## [Non rilasciato] +## [0.6.1] — 2026-04-19 — Obsidian Glass (Stable) + +### Aggiunto + +- **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. +- **Supporto multi-versione Docusaurus v3.** `DocusaurusAdapter` ora identifica + `versions.json`, `versioned_docs/` e le traduzioni versionate. +- **Proxy Trasparente Zensical.** Se viene dichiarato `engine = "zensical"` ma + `zensical.toml` è assente, l'adapter crea automaticamente un ponte con il tuo + `mkdocs.yml` esistente. +- **Ghost Routing consapevole delle versioni.** I percorsi della documentazione + versionata sono automaticamente classificati come `REACHABLE`. +- **Risoluzione Alias @site/.** Aggiunto il supporto per l'alias di percorso `@site/` + in `DocusaurusAdapter`, permettendo la corretta risoluzione dei link relativi al progetto. +- **Notifiche nel Banner Sentinel.** Nuovi messaggi di stato per l'attivazione della + **Modalità Offline** e della **Modalità Proxy**. + +### Corretto + +- **Passaggio di Stabilità Finale.** Risolte 14 violazioni di pre-commit (ruff format/lint) + e sincronizzati i README bilingue. +- **Integrità dei Metadati.** Corretto l'allineamento delle stringhe di versione in + `CITATION.cff` e `pyproject.toml`. +- **Default routeBasePath Docusaurus.** Ripristinato `docs` come prefisso URL predefinito + per i progetti Docusaurus per corrispondere al comportamento ufficiale dell'engine. + +- **Parità Documentale Bilingue.** Copertura completa della documentazione EN/IT per + tutte le feature della v0.6.1 nelle guide Architettura, Motori e Comandi. + ## [0.6.1rc2] — 2026-04-16 — Obsidian Bastion (Hardened) ### SICUREZZA: Risultati Operation Obsidian Stress @@ -97,7 +128,7 @@ Le versioni seguono il [Semantic Versioning](https://semver.org/). `check_nav_contract`, e tutte le funzioni dello scanner. Nessun default `None` retrocompatibile. -## [0.6.0a2] — 2026-04-13 — Obsidian Glass +## [0.6.0a2] — 2026-04-13 — Obsidian Glass (Alpha 2) ### Aggiunto diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad9bda..d160d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,35 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.6.1] — 2026-04-19 — Obsidian Glass (Stable) + +### Added + +- **`--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. +- **Docusaurus v3 Multi-version support.** The `DocusaurusAdapter` now identifies + `versions.json`, `versioned_docs/`, and versioned translations. +- **Zensical Transparent Proxy.** If `engine = "zensical"` is declared but `zensical.toml` + is missing, the adapter automatically bridges your existing `mkdocs.yml`. +- **Version-aware Ghost Routing.** Versioned documentation paths are automatically + classified as `REACHABLE`. +- **@site/ Alias Resolution.** Added support for the `@site/` path alias in + `DocusaurusAdapter`, enabling project-relative links to be resolved correctly. +- **Sentinel Banner Notifications.** New status messages for **Offline Mode** and + **Proxy Mode** activation. + +### Fixed + +- **Final Stability Pass.** Resolved 14 pre-commit violations (ruff format/lint) and + synchronized bilingual READMEs. +- **Metadata Integrity.** Corrected version string alignment in `CITATION.cff` and `pyproject.toml`. +- **Docusaurus routeBasePath default.** Restored `docs` as the default URL prefix for + Docusaurus projects to match official engine behavior. + +- **Bilingual Documentation Parity.** Full EN/IT documentation coverage for all + v0.6.1 features across the Architecture, Engine, and Command guides. + ## [0.6.1rc2] — 2026-04-16 — Obsidian Bastion (Hardened) ### SECURITY: Operation Obsidian Stress Findings @@ -91,7 +120,7 @@ Versions follow [Semantic Versioning](https://semver.org/). `check_nav_contract`, and all scanner functions. No backward-compatible `None` default. -## [0.6.0a2] — 2026-04-13 — Obsidian Glass +## [0.6.0a2] — 2026-04-13 — Obsidian Glass (Alpha 2) ### Added diff --git a/CITATION.cff b/CITATION.cff index 880f693..2451651 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -14,8 +14,8 @@ abstract: >- Markdown-based documentation. Zenzic introduces Universal Discovery, VCS-aware exclusion mapping, and the Sentinel Shield middleware to provide a deterministic Safe Harbor for complex documentation lifecycles. -version: 0.6.1rc2 -date-released: 2026-04-16 +version: 0.6.1 +date-released: 2026-04-19 url: "https://zenzic.dev" repository-code: "https://github.com/PythonWoods/zenzic" repository-artifact: "https://pypi.org/project/zenzic/" diff --git a/README.it.md b/README.it.md index 9727288..64791a2 100644 --- a/README.it.md +++ b/README.it.md @@ -44,7 +44,7 @@ SPDX-License-Identifier: Apache-2.0

```bash -╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1rc2 ─────────────────-────╮ +╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮ │ │ │ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │ │ │ @@ -82,7 +82,7 @@ dimostrabile, e la CLI è 100% subprocess-free. - **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. -> 🚀 **Ultima Release: v0.6.1rc2 "Obsidian Bastion"** — vedi [CHANGELOG.md](CHANGELOG.md) per i dettagli. +> 🚀 **Ultima Release: v0.6.1 "Obsidian Glass"** — vedi [CHANGELOG.md](CHANGELOG.md) per i dettagli. --- @@ -642,7 +642,7 @@ nox -s preflight # pipeline CI completa (lint + test + self-check) L'audit completo della Sentinella — banner, rilevamento engine e verdetto: ```bash -╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1rc2 ───────────────────────╮ +╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮ │ │ │ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │ │ │ diff --git a/README.md b/README.md index 1dab6e9..83de5ab 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ SPDX-License-Identifier: Apache-2.0

```bash -╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1rc2 ───────────────────────╮ +╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮ │ │ │ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │ │ │ @@ -80,7 +80,7 @@ engine identity must be provable, and the CLI is 100% subprocess-free. - **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. -> 🚀 **Latest Release: v0.6.1rc2 "Obsidian Bastion"** — see [CHANGELOG.md](CHANGELOG.md) for details. +> 🚀 **Latest Release: v0.6.1 "Obsidian Glass"** — see [CHANGELOG.md](CHANGELOG.md) for details. --- @@ -634,7 +634,7 @@ nox -s preflight # full CI pipeline (lint + test + self-check) The full Sentinel audit — banner, engine detection, and pass/fail verdict: ```bash -╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1rc2 ───────────────────────╮ +╭─────────────────────── 🛡 ZENZIC SENTINEL v0.6.1 ────────────────────────╮ │ │ │ docusaurus • 38 files (18 docs, 20 assets) • 0.9s │ │ │ diff --git a/RELEASE.md b/RELEASE.md index e0f8b69..fe1808b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,174 +1,43 @@ +# 🛡️ Zenzic v0.6.1 — Obsidian Glass -# Zenzic v0.6.1rc2 — Obsidian Bastion (Hardened) Release Protocol +## "The Engine-Agnostic Revolution" -**Prepared by:** S-1 (Auditor) -**Date:** 2026-04-16 -**Status:** RELEASE CANDIDATE 2 — Security audit completed -**Branch:** `main` -**Codename:** Obsidian Bastion (Hardened) — Post-Stress-Test Seal +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. -> **Tech Lead note:** RC2 follows Operation Obsidian Stress — a controlled -> siege by Red/Blue/Purple teams. The Red Team found 4 Shield bypass vectors -> (Unicode Cf, HTML entities, comment-interleaving, cross-line split). All -> have been sealed. The Purple Team identified 6 documentation drift items -> including a phantom `serve` command. All corrected. 1046 tests pass. +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. ---- - -## 1. Version Anchors - -| Location | Expected | Status | -| :--- | :--- | :---: | -| `src/zenzic/__init__.py` | `0.6.1rc2` | ✅ | -| `pyproject.toml` `[project]` | `0.6.1rc2` | ✅ | -| `pyproject.toml` `[tool.bumpversion]` | `0.6.1rc2` | ✅ | -| `CITATION.cff` | `0.6.1rc2` | ✅ | -| `CHANGELOG.md` top entry | `[0.6.1rc2]` | ✅ | -| `CHANGELOG.it.md` top entry | `[0.6.1rc2]` | ✅ | - -**Not tracked** (Clean Harbor): - -- `mkdocs.yml` — deleted (docs migrated to `zenzic-doc`) -- `uv.lock` — updated by `uv lock`, not by bumpversion - ---- - -## 2. The Adapter Gate (Core Logic) - -### 2a. Docusaurus v3 Adapter (new in v0.6.0a1) - -- [x] `DocusaurusAdapter` satisfies the `@runtime_checkable` `BaseAdapter` protocol -- [x] `baseUrl` and `routeBasePath` extraction via static parsing (zero Node.js — Pillar 2) -- [x] Ghost Route mapping for locale entry points (`/it/`, `/`) verified -- [x] `from_repo()` auto-discovers `docusaurus.config.ts` / `.js` -- [x] `classify_route()` marks `_`-prefixed files as `IGNORED` -- [x] Frontmatter `slug:` resolution (absolute and relative) -- [x] `.md` and `.mdx` source file handling -- [x] i18n locale tree discovery (`i18n/{locale}/docusaurus-plugin-content-docs/current/`) -- [x] Dynamic config detection (`async`, `import()`, `require()`) with graceful fallback -- [x] 65 dedicated tests across 12 test classes - -### 2b. Metadata-Driven Routing (new in v0.6.0a2) - -- [x] `RouteMetadata` dataclass and `get_route_info()` on `BaseAdapter` protocol -- [x] All 4 adapters implement the metadata API -- [x] `build_vsm()` prefers metadata path, falls back to legacy `map_url()` + `classify_route()` -- [x] Shield IO Middleware: `safe_read_line()` scans frontmatter through Shield before parsing - -### 2c. Adapter Inventory - -| Adapter | LOC | Status | -| :--- | :---: | :---: | -| MkDocs | 698 | ✅ | -| Docusaurus v3 | 589 | ✅ | -| Zensical | 324 | ✅ | -| Vanilla | 92 | ✅ | -| Factory + cache | 164 | ✅ | - ---- - -## 3. The Obsidian Bastion Gate (Layered Exclusion) - -- [x] `ExclusionManager` — 4-level hierarchy (L1 System → L2 VCS → L3 Config → L4 CLI) -- [x] L1 System Guardrails immutable (`.git/`, `node_modules/`, etc.) -- [x] L2 VCS Ignore Parser — Pure Python `.gitignore` interpreter with pre-compiled regex -- [x] L3 Config — `excluded_dirs` / `excluded_file_patterns` from `zenzic.toml` -- [x] L4 CLI — `--exclude-dir` / `--include-dir` repeatable flags -- [x] `exclusion_manager` parameter **mandatory** on all scanner/validator entry points -- [x] 57 dedicated tests (677 lines in `test_exclusion.py`) - ---- - -## 4. The Tabula Rasa Gate (Universal Discovery) - -- [x] **Every** `rglob()` call removed from the entire codebase -- [x] All file iteration via `walk_files()` / `iter_markdown_sources()` in `discovery.py` -- [x] 168 call sites updated across 13 test files -- [x] No `Optional[ExclusionManager]` — `TypeError` at call time if missing - ---- +### 🚀 Key Highlights -## 5. Security Hardening Gate +#### 1. Zensical Transparent Proxy (Legacy Bridge) -- [x] **F2-1:** Lines > 1 MiB truncated before Shield regex matching (ReDoS prevention) -- [x] **F4-1:** `_validate_docs_root()` rejects `docs_dir` escaping repo root (Exit Code 3) -- [x] **Adapter Cache:** Module-level dict keyed by `(engine, docs_root, repo_root)`, thread-safe -- [x] **Shield IO Middleware:** Frontmatter lines scanned before any parser processes them -- [x] **ZRT-006:** Unicode Cf character stripping in Shield normalizer (zero-width bypass) -- [x] **ZRT-006:** HTML entity decoding in Shield normalizer (`&#NNN;` bypass) -- [x] **ZRT-007:** HTML/MDX comment stripping in Shield normalizer (interleaving bypass) -- [x] **ZRT-007:** 1-line lookback buffer `scan_lines_with_lookback()` (split-token bypass) -- [x] **Red Team:** 11 Blood Sentinel jailbreak vectors tested — all blocked -- [x] **Red Team:** DoS resilience verified (10MB lines, 5000 files, 50-level nesting) +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. ---- +#### 2. Docusaurus v3 Multi-Versioning -## 6. Clean Harbor Gate (Repo Hygiene) +Zenzic is now a first-class citizen for large-scale Docusaurus projects. We’ve implemented native support for `versions.json` and the `versioned_docs/` directory. Your versioned routes are now automatically tracked in the **Virtual Site Map (VSM)**, ensuring that links to older documentation are validated with the same rigor as your latest release. -- [x] `mkdocs.yml` — physically deleted -- [x] `overrides/` — physically deleted -- [x] `scripts/generate_docs_assets.py` — physically deleted -- [x] `scripts/generate_hero_specimen.py` — physically deleted -- [x] `scripts/generate_social.py` — physically deleted -- [x] `.github/workflows/deploy-docs.yml` — physically deleted -- [x] `.github/workflows/zenzic.yml` — physically deleted -- [x] `docs/` fully migrated to `zenzic-doc` repository -- [x] `noxfile.py` — doc sessions removed -- [x] MkDocs plugin relocated to `zenzic.integrations.mkdocs` +#### 3. Global Offline Mode (`--offline`) ---- +Distributed documentation on USB drives? Local intranets without directory-index support? The new `--offline` flag forces all adapters to resolve Markdown sources to flat `.html` files (e.g., `intro.md` → `/intro.html`). Ensure your documentation remains navigable even in air-gapped environments. -## 7. Architectural Purity Gate (Pillar 2) +#### 4. @site/ Alias Resolution -- [x] `zenzic serve` — removed entirely -- [x] Zero `subprocess.run()`, `os.system()`, or shell calls in codebase -- [x] Docusaurus config parsed as text, not via Node.js -- [x] `.gitignore` interpreted in Pure Python, not via `git check-ignore` -- [x] Core free of engine-specific imports +For Docusaurus users, we’ve added support for the `@site/` path alias. Zenzic now correctly resolves project-relative links like `[logo](@site/static/img/logo.png)` without requiring complex exclusion rules. ---- +### 🛠️ Migration & Call to Action -## 8. Quality Gates +If you are currently using MkDocs and considering a move to a more modern, TOML-based or MDX-powered architecture, **Zenzic v0.6.1 is your safety net**. -- [x] `pytest` — 1046 tests passing, 0 failed -- [x] `ruff check src/` → 0 violations -- [x] `reuse lint` → compliant -- [x] `pip install -e .` → `zenzic --help` outputs usage -- [x] `uv run zenzic --version` → `Zenzic v0.6.1rc2` +1. Install Zenzic: `uv tool install zenzic` +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. --- -## 9. Docusaurus Validation (zenzic-doc) +### 🇮🇹 Engineered with Precision -- [x] `zenzic check all --engine docusaurus` → exit code 0 -- [x] Zero CONFLICT routes -- [x] `zenzic-doc` has `release-docs.yml` workflow for Cloudflare Pages deploy -- [x] `release-docs.yml` has `deployments: write` permission (fixed in `fix/deploy-permissions`) - ---- - -## 10. Performance Benchmark - -| Scenario | Result | -| :--- | :--- | -| 5,000 files, 100 VCS patterns | 626 ms | -| RSS memory delta | 0 MB | - ---- - -## 11. RC2 Gate Decision - -- [x] All gates (§§ 2–9) verified -- [x] Benchmark § 10 within acceptable thresholds -- [x] Operation Obsidian Stress completed — 4 Shield bypasses sealed -- [x] Documentation Reality Sync — 6 drift items corrected -- [x] CI pipeline green on `main` - -**Decision:** ✅ RC2 approved — `v0.6.1rc2` tagged and published to PyPI - ---- +Zenzic is developed by **PythonWoods**, based in Italy, and committed to the craft of high-performance, deterministic Python engineering. -*"Il Bastione non si fida dell'assenza di attacchi — si fida della resistenza verificata."* -— Senior Tech Lead +[**Read the Full Documentation →**](https://zenzic.dev) diff --git a/pyproject.toml b/pyproject.toml index d8bcee6..18d78cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "hatchling.build" [project] name = "zenzic" -version = "0.6.1rc2" +version = "0.6.1" description = "Engineering-grade, engine-agnostic linter and security shield for Markdown documentation" readme = "README.md" requires-python = ">=3.11" @@ -183,7 +183,7 @@ pytest_add_cli_args = ["--import-mode=prepend"] # ─── Version bumping ─────────────────────────────────────────────────────────── [tool.bumpversion] -current_version = "0.6.1rc2" +current_version = "0.6.1" commit = true tag = true tag_name = "v{new_version}" diff --git a/src/zenzic/__init__.py b/src/zenzic/__init__.py index 7028f02..9e57e45 100644 --- a/src/zenzic/__init__.py +++ b/src/zenzic/__init__.py @@ -2,4 +2,4 @@ # SPDX-License-Identifier: Apache-2.0 """Zenzic — engine-agnostic linter and security shield for Markdown documentation.""" -__version__ = "0.6.1rc2" +__version__ = "0.6.1" diff --git a/src/zenzic/cli.py b/src/zenzic/cli.py index f1a9233..a6ba300 100644 --- a/src/zenzic/cli.py +++ b/src/zenzic/cli.py @@ -261,12 +261,17 @@ def check_links( show_info: bool = typer.Option( False, "--show-info", help="Show info-level findings (e.g. circular links) in the report." ), + offline: bool = typer.Option( + False, "--offline", help="Force flat URL resolution for offline builds." + ), ) -> None: """Check for broken internal links. Pass --strict to also validate external URLs.""" from zenzic import __version__ repo_root = find_repo_root() config, _ = ZenzicConfig.load(repo_root) + if offline: + config.build_context.offline_mode = True docs_root = (repo_root / config.docs_dir).resolve() exclusion_mgr = _build_exclusion_manager(config, repo_root, docs_root) @@ -348,6 +353,9 @@ def check_orphans( show_info: bool = typer.Option( False, "--show-info", help="Show info-level findings (e.g. circular links) in the report." ), + offline: bool = typer.Option( + False, "--offline", help="Force flat URL resolution for offline builds." + ), ) -> None: """Detect .md files not listed in the nav.""" from zenzic import __version__ @@ -357,6 +365,8 @@ def check_orphans( if not loaded_from_file: _print_no_config_hint() config = _apply_engine_override(config, engine) + if offline: + config.build_context.offline_mode = True docs_root = (repo_root / config.docs_dir).resolve() exclusion_mgr = _build_exclusion_manager(config, repo_root, docs_root) @@ -1122,6 +1132,9 @@ def check_all( show_info: bool = typer.Option( False, "--show-info", help="Show info-level findings (e.g. circular links) in the report." ), + offline: bool = typer.Option( + False, "--offline", help="Force flat URL resolution for offline builds." + ), ) -> None: """Run all checks: links, orphans, snippets, placeholders, assets, references. @@ -1134,6 +1147,8 @@ def check_all( if not loaded_from_file and not quiet: _print_no_config_hint() config = _apply_engine_override(config, engine) + if offline: + config.build_context.offline_mode = True # ── Target mode (single file OR custom directory) ────────────────────────── _single_file: Path | None = None diff --git a/src/zenzic/core/adapters/_base.py b/src/zenzic/core/adapters/_base.py index 6d7e17e..ed4e73e 100644 --- a/src/zenzic/core/adapters/_base.py +++ b/src/zenzic/core/adapters/_base.py @@ -52,6 +52,7 @@ class RouteMetadata: pages auto-created by ``reconfigure_material`` or Docusaurus's i18n entry points). Proxy routes are ``REACHABLE`` but should not be checked for content integrity. + version: Optional version identifier for multi-version setups (e.g. Docusaurus versioned_docs). """ canonical_url: str @@ -59,6 +60,7 @@ class RouteMetadata: slug: str | None = None route_base_path: str = "/" is_proxy: bool = False + version: str | None = None # ── BaseAdapter Protocol ───────────────────────────────────────────────────── diff --git a/src/zenzic/core/adapters/_docusaurus.py b/src/zenzic/core/adapters/_docusaurus.py index 717cd36..5d4d62e 100644 --- a/src/zenzic/core/adapters/_docusaurus.py +++ b/src/zenzic/core/adapters/_docusaurus.py @@ -254,13 +254,16 @@ def __init__( docs_root: Path, base_url: str = "/", route_base_path: str | None = None, + versions: list[str] | None = None, ) -> None: self._docs_root = docs_root + self._context = context self._base_url = base_url.rstrip("/") or "" # Docusaurus default routeBasePath is 'docs', but when docs are at # the site root it is ''. None means "not set in config" → use # no prefix (docs are already relative to docs_root). self._route_base_path = route_base_path + self._versions: tuple[str, ...] = tuple(versions or []) # Locale configuration from BuildContext (zenzic.toml). self._locale_dirs: frozenset[str] = frozenset(context.locales) @@ -425,17 +428,45 @@ def map_url(self, rel: Path) -> str: stem = stem.with_suffix("") parts = list(stem.parts) - if not parts: - return "/" + + locale = None + if parts and parts[0] in self._locale_dirs: + locale = parts.pop(0) + + version = None + if parts and parts[0] == "_version_": + parts.pop(0) + if parts: + version = parts.pop(0) # index files collapse to their parent directory - if parts[-1] == "index": + if parts and parts[-1] == "index": parts = parts[:-1] - if not parts: - return "/" + url_parts = [] + if locale: + url_parts.append(locale) + + rbp = self._route_base_path if self._route_base_path is not None else "docs" + if rbp: + # Note: root routeBasePath is empty string + url_parts.append(rbp) + + if version: + url_parts.append(version) - return "/" + "/".join(parts) + "/" + url_parts.extend(parts) + + use_dir = True + if getattr(self._context, "offline_mode", False): + use_dir = False + + if not url_parts: + return "/" if use_dir else "/index.html" + + if use_dir: + return "/" + "/".join(url_parts) + "/" + return "/" + "/".join(url_parts) + ".html" def classify_route(self, rel: Path, nav_paths: frozenset[str]) -> RouteStatus: """Classify a Docusaurus route. @@ -462,11 +493,18 @@ def classify_route(self, rel: Path, nav_paths: frozenset[str]) -> RouteStatus: ``RouteStatus`` literal (never ``'CONFLICT'``). """ # Rule 1: Private/meta files starting with _ - if any(part.startswith("_") for part in rel.parts): + # Exemption: _version_ sentinel prefix is injected by ZenzicAdapter — not a real user dir. + non_sentinel_parts = [p for p in rel.parts if p != "_version_"] + if any(part.startswith("_") for part in non_sentinel_parts): return "IGNORED" # Draft files: check is deferred to frontmatter parsing (future). + # Version Ghost Routes: all files under a known versioned_docs tree are REACHABLE. + # The path sentinel prefix is _version_//... + if len(rel.parts) >= 2 and rel.parts[0] == "_version_": + return "REACHABLE" + # Rule 2: No explicit nav → auto-generated sidebar → all REACHABLE if not nav_paths: return "REACHABLE" @@ -518,12 +556,18 @@ def get_route_info(self, rel: Path) -> RouteMetadata: if locale_dir in self._locale_dirs: is_proxy = True + # Extract version from sentinel path prefix (_version_/