diff --git a/.bumpversion.toml b/.bumpversion.toml index 541d0304..55abb0c2 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 [tool.bumpversion] -current_version = "0.26.5" +current_version = "0.27.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)((?Pa|b|rc)(?P\\d+))?" serialize = [ "{major}.{minor}.{patch}{pre_l}{pre_n}", diff --git a/.github/ISSUE_TEMPLATE/security_vulnerability.yml b/.github/ISSUE_TEMPLATE/security_vulnerability.yml index 13204320..e4f39533 100644 --- a/.github/ISSUE_TEMPLATE/security_vulnerability.yml +++ b/.github/ISSUE_TEMPLATE/security_vulnerability.yml @@ -29,7 +29,7 @@ body: attributes: label: Zenzic version description: Output of `zenzic --version` - placeholder: "0.26.5" + placeholder: "0.27.0" validations: required: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9ae9a91..5aa29048 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,7 @@ repos: args: [--unsafe] - id: check-json - id: check-toml + exclude: ^examples/ - id: check-added-large-files - id: check-merge-conflict - id: check-case-conflict @@ -26,6 +27,7 @@ repos: rev: f295829140d25717bc79368d3f966fc1f67a824f # v0.41.0 hooks: - id: markdownlint + exclude: ^examples/ args: ["--fix"] # 3. Ruff (linting & formatting) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index d5059d0e..7f2820ab 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -7,7 +7,7 @@ # # repos: # - repo: https://github.com/PythonWoods/zenzic -# rev: v0.26.5 +# rev: v0.27.0 # hooks: # - id: zenzic-verify # quality gate — corrisponde a `just verify` lato zenzic # - id: zenzic-guard # fast staged-file credential scan diff --git a/.zenzic.toml b/.zenzic.toml index ee6ce1f5..857005b7 100644 --- a/.zenzic.toml +++ b/.zenzic.toml @@ -27,6 +27,7 @@ strict = true fail_under = 98 +max_sentence_length = 50 # exit_zero = false # respect_vcs_ignore = true # validate_same_page_anchors = true @@ -46,13 +47,9 @@ excluded_external_urls = [ # --- EXCLUSION ZONES (Full bypass — use sparingly) --- excluded_file_patterns = [] # Paths listed here are INVISIBLE to Zenzic: no findings, no audit trail. -# Prefer [governance.per_file_ignores] - [governance.directory_policies] -"docs/assets/**" = ["Z405"] -"docs/favicon.ico" = ["Z405"] -"docs/blog/rss.xsl" = ["Z405"] +"docs/blog/posts/**" = ["Z410", "Z411"] +"docs/developers/explanation/adr-vault/records/**" = ["Z410", "Z411"] +"docs/tutorials/examples/**" = ["Z410", "Z411"] "docs/tutorials/examples/z1xx-links/**" = ["Z107"] -"docs/tutorials/examples/z5xx-content/**" = ["Z506", "Z503"] -# Exempt reference file from syntax checks because it contains intentionally incorrect examples -"docs/reference/finding-codes.md" = ["Z503"] +"docs/tutorials/examples/z5xx-content/**" = ["Z506"] diff --git a/CHANGELOG.md b/CHANGELOG.md index b3d83d68..1aa23489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,68 +11,32 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] -## [0.26.5] - 2026-07-29 - -### Fixed -- **Supply Chain Integrity**: Emergency patch to replace a dirty build artifact published to PyPI in `v0.26.4`. No functional code changes. - - -## [0.26.4] - 2026-07-29 - -### Fixed - -- **LSP State Hygiene (`LSP-FIX-017`)**: Eradicated a race condition that caused "ghost diagnostics" to persist in the editor after a file or directory was deleted. The server now maintains a stateful tracker (`self.file_diagnostics`) and explicitly broadcasts empty diagnostic arrays (`[]`) for any URI that no longer exists in the Virtual Site Map. -- **Inline Suppression Placement (`LSP-FIX-018`)**: Fixed a bug where the "Suppress this finding" Code Action injected the suppression comment on a new line, causing false-positive `Z603` (Dead Suppression) errors. The action now correctly appends the comment to the end of the offending line. - -## [0.26.3] - 2026-07-28 +## [0.27.0] - 2026-08-02 ### Added -- **Suppression Code Actions (`LSP-FEAT-003`)**: The LSP server now dynamically generates "Suppress this finding" Code Actions for all suppressible diagnostics. Clicking the action automatically injects the correct `` comment above the offending line. This feature is strictly disabled for `NON_SUPPRESSIBLE_CODES` (Z2xx Security findings) to enforce the security gate. - -### Fixed - -- **LSP State Hygiene (`LSP-FIX-015`)**: Eradicated "ghost diagnostics" by ensuring the LSP server explicitly sends an empty diagnostics array (`[]`) to VS Code when a file is deleted, clearing the PROBLEMS panel. -- **Code Action Routing (`LSP-FIX-016`)**: Fixed a routing bug in the LSP server that prevented Quick Fixes for `Z108` (Empty Link Text) and `Z505` (Untagged Code Block) from appearing in the editor. -- **Z603 Parity (`LSP-FIX-015`)**: Ensured HTML comment suppressions (``) are correctly evaluated for "dead" status in the LSP engine. - -## [0.26.2] - 2026-07-28 +- **Smart Link Graph (`V0.27-01`)**: Transformed the Virtual Site Map (VSM) into a Smart Link Graph that tracks deterministic adjacency lists for outgoing links across document nodes. +- **Configuration Validation Engine (`V0.27-04`)**: Introduced formal validation for `.zenzic.toml` with graceful degradation and non-suppressible diagnostic findings: + - `Z110` (CONFIG_SYNTAX_ERROR): Emitted on TOML syntax errors (`TOMLDecodeError`) with line-number extraction. + - `Z111` (CONFIG_SCHEMA_ERROR): Emitted on schema type mismatches and validation failures (`ValidationError`). + - Halts Markdown document graph scanning on fatal config errors to prevent false-positive cascades and protect LSP stability. +- **Baseline & Regression Tracking (`V0.27-02`)**: Added deterministic snapshot baseline capability (`.zenzic-baseline.json`) via `--update-baseline` and `--baseline` CLI options. Computes line-shift invariant SHA-256 signatures for finding matching, tags baselined findings without dropping them (`Radical Unawareness`), and enforces DQS anti-regression exit rules in CI/CD. +- **Mirror Law Parity (`ADR-020`)**: Authored 41 dedicated, deep-dive Rule Specification Cards (`docs/rules/ZXXX.md`) and updated `docs/reference/finding-codes.md` to achieve 100% Mirror Law documentation parity. Each card provides technical rationale, Bad/Good Markdown examples, and `.zenzic.toml` configuration options. +- **Topological Connectivity Restoration**: Resolved `Z411` dead-end node findings across active documentation namespaces by injecting semantic `## See Also` navigation links within the AST graph. ### Fixed -- **Extensionless Asset Resolution**: Fixed a bug in `VSMBrokenLinkRule._to_canonical_url` where extensionless files (e.g., `LICENSE`, `Makefile`) incorrectly received a trailing slash when `use_directory_urls` was active, causing false-positive `Z101` findings. - -## [0.26.1] - 2026-07-27 - -### Added - -- **Adapter API Contract (`CORE-FIX-005`)**: Added the `use_directory_urls` property to the `BaseAdapter` contract. This allows adapters to explicitly declare their URL routing mode, eradicating encapsulation violations in the incremental engine. - -### Fixed - -- **URP Unification (`CORE-REFACTOR-003`)**: Eradicated the legacy CLI link validation pipeline (`validate_links_async`). Both CLI and LSP now evaluate broken internal links exclusively via `VSMBrokenLinkRule.check_vsm` and `PolyglotExtractor`, achieving 100% true validation parity. -- **Asset Indexing Parity (`CORE-REFACTOR-006`)**: Upgraded the Virtual Site Map (VSM) builder to explicitly index non-Markdown static assets (e.g., `.png`, `.webp`, `.html`). This eradicates hardcoded directory workarounds and eliminates false-positive `Z101` and `Z104` findings for static assets across all adapters. -- **JSON Purity (`CLI-FIX-001`)**: Enforced absolute JSON purity when the `--json` flag is active by routing `fail_under` and `suppression_cap` failure messages to `stderr`. This prevents `JSON.parse()` failures in programmatic consumers. -- **MkDocs Asset URLs (`CORE-FIX-002`)**: Eradicated false-positive `Z101` findings for static assets in MkDocs repositories by preventing the `MkDocsAdapter` from appending trailing slashes to non-Markdown files during VSM route generation. - -### Documentation - -- **Blog Hero Image Standardization**: Added named hero assets for existing release posts, converted launch media from JPEG to WebP, and normalized hero-image alt text to a title-aligned editorial pattern across the blog. -- **Editor Trilogy Article**: Added `docs/blog/posts/2026-07-27-editor-trilogy-v0240-v0260.md`, a Hostile Precision architectural synthesis of the v0.24.0 → v0.26.0 editor sequence, using the previously policy-exempt trilogy hero asset as an in-site referenced image. - -## [0.26.0] - 2026-07-26 - -### Added - -- **CLI `--json` Shorthand Alias (`ECOSYSTEM-FEAT-002`)**: Added `--json` flag to `zenzic score` as an ergonomic shorthand for `--format json`. Emits a single deterministic `ScoreReport` JSON object on `stdout` without rich terminal formatting, designed for programmatic consumers and editor integrations. +- **Readability Sentence Boundary Parser (`Z511`)**: Fixed sentence length calculation in `zenzic.core.content` by recognizing bulleted lists, numbered items, and blockquotes as hard sentence boundaries, eliminating false-positive readability warnings on long lists. +- **CLI Flag Input Validation**: Enforced strict input validation for the `--only` CLI option in `zenzic check`, triggering an immediate fatal exit (`Exit 1`) when an invalid or unknown finding code is supplied. +- **Topological Directory-Policy Tracking (`Z118`)**: Fixed a Core Engine governance bug where topological suppressions (`Z410`/`Z411`) could leave false-positive dead-policy findings by ensuring canonical tracker rebinding in scanner passes and paired topology policy consumption in `GlobalUsageTracker`. -### Documentation +### Changed -- **CLI Reference Mirror Law Realignment (`ADR-020`)**: Updated `docs/reference/cli.md` with `--json` flag specifications, complete `zenzic score` flag table, and JSON Output Schema documentation. -- **Roadmap Realignment (`ROADMAP-ALIGN-004`)**: Realigned `ROADMAP.md` to establish `[v0.26]` as *DQS Workspace UI*, shifting subsequent platform milestones (`v0.27`–`v0.30`). +- **Zero-DBT Technical Debt Cleanup**: Reverted unauthorized configuration suppressions in `.zenzic.toml`, structurally resolved 48 empty section (`Z512`) findings across 35 Markdown files, and eliminated stale global suppressions (`Z118`). ## Historical Releases +- v0.26.x archive: [changelogs/v0.26.x.md](./changelogs/v0.26.x.md) - v0.25.x archive: [changelogs/v0.25.x.md](./changelogs/v0.25.x.md) - v0.24.x archive: [changelogs/v0.24.x.md](./changelogs/v0.24.x.md) - v0.23.x archive: [changelogs/v0.23.x.md](./changelogs/v0.23.x.md) diff --git a/CITATION.cff b/CITATION.cff index bbb04c86..34366281 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -15,8 +15,8 @@ abstract: >- performs deterministic static analysis using a two-pass reference pipeline and a RE2-backed credential scanner, with zero subprocess calls and full SARIF 2.1.0 support for CI/CD integration. -version: 0.26.5 -date-released: 2026-07-29 +version: 0.27.0 +date-released: 2026-08-02 url: "https://zenzic.dev" repository-code: "https://github.com/PythonWoods/zenzic" repository-artifact: "https://pypi.org/project/zenzic/" diff --git a/README.md b/README.md index 894a5681..1bd4d64c 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,7 @@ SPDX-License-Identifier: Apache-2.0

- - - - Zenzic - + Zenzic Document Integrity Engine

@@ -55,7 +51,7 @@ Zenzic detects broken links, orphaned pages, credential leaks, and structural in Zenzic is a unified, deterministic platform structured into three primary delivery mechanisms: -- **[Core Engine (CLI)](#-installation)**: Python CLI, AST rule engine, and Virtual Site Map (VSM) topology analyzer. +- **[Core Engine (CLI)](#-installation)**: Python CLI, AST rule engine, Virtual Site Map (VSM) topology analyzer, and **Baseline & Regression Tracking** (`.zenzic-baseline.json`). - **[VS Code Extension][zenzic-vscode]**: Real-time LSP client offering sub-50ms inline diagnostics, Quick Fixes, and DQS scoring. - **[GitHub Action][zenzic-action]**: Zero-config CI/CD quality gate with SARIF upload and PR annotations. @@ -117,6 +113,10 @@ Zenzic treats documentation as a **security surface**. The tiered code model enf Zenzic reports only what is **statically verifiable** in the repository at scan time. It never infers intent or approximates link validity. Every finding is a falsifiable, reproducible fact. +### Topological Graph Analysis (Orphans & Dead Ends) + +Beyond static file checks, Zenzic's Smart Link Graph builds an adjacency list to perform Breadth-First Search (BFS) over your document network. It identifies **Topological Orphans** (`Z410`, documents unreachable from navigation entry points) and **Dead Ends** (`Z411`, pages with no outgoing links), helping maintain structural navigation integrity. + --- ## 🧠 Key Capabilities & Commands @@ -143,7 +143,7 @@ Zenzic Core is headless and emits standardized **SARIF** JSON, ensuring seamless "tool": { "driver": { "name": "zenzic", - "version": "0.26.5", + "version": "0.27.0", "rules": [ { "id": "Z101", @@ -215,7 +215,7 @@ uv tool upgrade zenzic To run a specific version ephemerally without altering your global environment: ```bash -uvx zenzic@0.26.5 check all +uvx zenzic@0.27.0 check all ``` --- @@ -225,6 +225,7 @@ uvx zenzic@0.26.5 check all | Area | URL | Audience | | :--- | :--- | :--- | | 👤 User Guide | [zenzic.dev][docs-home] | Install, configure, CI/CD, finding codes | +| 📜 Rule Specification Cards | [zenzic.dev/docs/rules][docs-rules] | 41 deep-dive rule specifications with Bad/Good remediation examples | | 🔧 Developer Portal | [zenzic.dev/developers][docs-developers] | Adapters, ADRs, CLI architecture | | 🛡️ Security | [SECURITY.md][security] | Security reviewer | @@ -270,6 +271,7 @@ Apache-2.0 — see [LICENSE][license]. This project strictly adheres to Semantic [zenzic-vscode]: https://marketplace.visualstudio.com/items?itemName=pythonwoods.zenzic-vscode [zenzic-action]: https://github.com/PythonWoods/zenzic-action [docs-home]: https://zenzic.dev/ +[docs-rules]: https://zenzic.dev/docs/rules/ [docs-arch]: https://zenzic.dev/developers/how-to/implement-adapter [docs-developers]: https://zenzic.dev/developers/ [contributing]: CONTRIBUTING.md diff --git a/RELEASE.md b/RELEASE.md index e6a0bb6d..163f12eb 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,9 +8,9 @@ | Field | Value | | :------- | :--------- | -| Version | v0.26.5 | +| Version | v0.27.0 | | Codename | Magnetite | -| Date | 2026-07-29 | +| Date | 2026-08-02 | | Status | Stable | ## Release Checklist @@ -21,7 +21,7 @@ Before tagging, every item must be green: - [ ] `zenzic lab all` — all 20 scenarios exit with expected code - [ ] `zenzic score --stamp` committed — badge in README.md reflects current score - [ ] `zenzic check all .` — zero findings in the repo root -- [ ] `pyproject.toml` version matches the tag (`0.26.5`) +- [ ] `pyproject.toml` version matches the tag (`0.27.0`) - [ ] `CITATION.cff` version and date updated - [ ] `CHANGELOG.md` — `[Unreleased]` section moved to the new version heading - [ ] Update SECURITY.md support table (Add new release, demote previous to Critical/EOL). @@ -53,12 +53,12 @@ git checkout main git pull origin main # 3. Tag the main branch and push -git tag -s -m "Release v0.26.5" v0.26.5 +git tag -s -m "Release v0.27.0" v0.27.0 git push origin main --tags ``` -- [ ] Create GitHub Release from the tag, using the `## [0.26.5]` CHANGELOG section as the release body. +- [ ] Create GitHub Release from the tag, using the `## [0.27.0]` CHANGELOG section as the release body. ## Changelog Reference diff --git a/REUSE.toml b/REUSE.toml index 25533ea0..f399fd68 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -6,7 +6,7 @@ version = 1 # listed here. [[annotations]] -path = [".zenzic-score.json", "coverage.json", ".markdownlint.json", "zenzic-output.schema.json"] +path = [".zenzic-score.json", "coverage.json", ".markdownlint.json", "zenzic-output.schema.json", "zenzic-baseline.schema.json"] SPDX-FileCopyrightText = "2026 PythonWoods " SPDX-License-Identifier = "Apache-2.0" diff --git a/ROADMAP.md b/ROADMAP.md index 1e9efab0..3db31b51 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -10,7 +10,7 @@ SPDX-License-Identifier: Apache-2.0 This document describes the planned milestone trajectory for Zenzic, the **Deterministic Document Integrity Engine and SAST for Markdown/MDX graphs**. Dates are targets, not commitments. All milestones are subject to revision. -For the current release history and completed milestones (up to `v0.23.x`), see [CHANGELOG.md](CHANGELOG.md). +For the current release history and completed milestones (up to `v0.26.x`), see [CHANGELOG.md](CHANGELOG.md). --- @@ -25,34 +25,7 @@ Before advancing the core feature set, the following infrastructural and validat ## Milestone Sequence -### [v0.24] — Interactive Intelligence - -*Completing the diagnostic-to-remediation loop within the IDE.* - -- **VS Code Code Actions (Quick Fixes):** Implement `textDocument/codeAction` via LSP to allow users to instantly apply deterministic fixes (e.g., `Z121`, `Z603`) directly from the editor. - -### [v0.25] — LSP Stabilization & Hot-Reloading - -*Achieving 100% determinism between CLI and IDE via centralized governance, adapter-driven hot-reloading, and cross-platform URI parity.* - -- **Centralized Governance Pipeline:** Eliminate CLI vs. LSP diagnostic drift by extracting `directory_policies` and `per_file_ignores` into core governance evaluation. -- **Adapter-Driven Config Hot-Reloading:** Enable real-time VSM topology rebuilds in VS Code when engine configuration files (e.g., `mkdocs.yml`, `zensical.toml`) change, without requiring an LSP server restart. -- **BaseAdapter Contract Hardening:** Expose `watched_config_files` on the `BaseAdapter` interface to preserve Adapter Neutrality across third-party engines. - -### [v0.26] — DQS Workspace UI - -*Bringing the global Documentation Quality Score into the authoring environment via an asynchronous, deterministic CLI execution bridge.* - -- **Asynchronous CLI Execution Bridge:** Restore DQS visualization in the VS Code Status Bar via `child_process.execFile` calling `zenzic score --json`, guaranteeing 100% mathematical parity with CI/CD without blocking LSP performance. - -### [v0.27] — Deterministic Quality Platform - -*Evolving from a strict validator to a comprehensive quality governance engine.* - -- **Smart Link Graph:** Transform the Virtual Site Map (VSM) into a full topological analysis engine capable of detecting documentation islands, circular navigation paths, and unreachable clusters. -- **Baseline & Regression Tracking:** Introduce evolutionary quality control. Essential for enterprise CI/CD to prevent DQS regressions over time. -- **Semantic Readability Metrics:** Extend Zenzic beyond structural validation into content quality (e.g., deterministic Flesch-Kincaid scoring) while maintaining the static analysis paradigm. -- **Configuration Validation Engine:** Reduce operational errors by establishing a single source of truth for configuration schemas across the CLI, VS Code, and documentation. +> For completed milestones (`v0.23` through `v0.27`), see [CHANGELOG.md](CHANGELOG.md). ### [v0.28] — Governance & Extensibility @@ -95,4 +68,4 @@ These constraints apply across every future release. No feature may violate them --- -Roadmap last updated: 2026-07-25. +Roadmap last updated: 2026-08-01. diff --git a/assets/brand/svg/zenzic-wordmark-dark.svg b/assets/brand/svg/zenzic-wordmark-dark.svg index 44998fba..7ef396dc 100644 --- a/assets/brand/svg/zenzic-wordmark-dark.svg +++ b/assets/brand/svg/zenzic-wordmark-dark.svg @@ -1,29 +1,59 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + - + + - + - - - - - - - - + + + + + + + + - - Zenzic + + Zenzic - - DOCUMENT INTEGRITY ENGINE + DOCUMENT INTEGRITY ENGINE diff --git a/assets/brand/svg/zenzic-wordmark.svg b/assets/brand/svg/zenzic-wordmark.svg index bd2de9f3..a7647854 100644 --- a/assets/brand/svg/zenzic-wordmark.svg +++ b/assets/brand/svg/zenzic-wordmark.svg @@ -1,29 +1,60 @@ - + + + + + + + + + + + + + + + + + + + + + + - + + - + - - + + - - - + + + - + - - Zenzic + + Zenzic - - DOCUMENT INTEGRITY ENGINE + DOCUMENT INTEGRITY ENGINE diff --git a/changelogs/v0.26.x.md b/changelogs/v0.26.x.md new file mode 100644 index 00000000..cd19f4ac --- /dev/null +++ b/changelogs/v0.26.x.md @@ -0,0 +1,64 @@ + + + +# v0.26.x Archive + +## [0.26.5] - 2026-07-29 + +### Fixed + +- **Supply Chain Integrity**: Emergency patch to replace a dirty build artifact published to PyPI in `v0.26.4`. No functional code changes. + +## [0.26.4] - 2026-07-29 + +### Fixed + +- **LSP State Hygiene (`LSP-FIX-017`)**: Eradicated a race condition that caused "ghost diagnostics" to persist in the editor after a file or directory was deleted. The server now maintains a stateful tracker (`self.file_diagnostics`) and explicitly broadcasts empty diagnostic arrays (`[]`) for any URI that no longer exists in the Virtual Site Map. +- **Inline Suppression Placement (`LSP-FIX-018`)**: Fixed a bug where the "Suppress this finding" Code Action injected the suppression comment on a new line, causing false-positive `Z603` (Dead Suppression) errors. The action now correctly appends the comment to the end of the offending line. + +## [0.26.3] - 2026-07-28 + +### Added + +- **Suppression Code Actions (`LSP-FEAT-003`)**: The LSP server now dynamically generates "Suppress this finding" Code Actions for all suppressible diagnostics. Clicking the action automatically injects the correct `` comment above the offending line. This feature is strictly disabled for `NON_SUPPRESSIBLE_CODES` (Z2xx Security findings) to enforce the security gate. + +### Fixed + +- **LSP State Hygiene (`LSP-FIX-015`)**: Eradicated "ghost diagnostics" by ensuring the LSP server explicitly sends an empty diagnostics array (`[]`) to VS Code when a file is deleted, clearing the PROBLEMS panel. +- **Code Action Routing (`LSP-FIX-016`)**: Fixed a routing bug in the LSP server that prevented Quick Fixes for `Z108` (Empty Link Text) and `Z505` (Untagged Code Block) from appearing in the editor. +- **Z603 Parity (`LSP-FIX-015`)**: Ensured HTML comment suppressions (``) are correctly evaluated for "dead" status in the LSP engine. + +## [0.26.2] - 2026-07-28 + +### Fixed + +- **Extensionless Asset Resolution**: Fixed a bug in `VSMBrokenLinkRule._to_canonical_url` where extensionless files (e.g., `LICENSE`, `Makefile`) incorrectly received a trailing slash when `use_directory_urls` was active, causing false-positive `Z101` findings. + +## [0.26.1] - 2026-07-27 + +### Added + +- **Adapter API Contract (`CORE-FIX-005`)**: Added the `use_directory_urls` property to the `BaseAdapter` contract. This allows adapters to explicitly declare their URL routing mode, eradicating encapsulation violations in the incremental engine. + +### Fixed + +- **URP Unification (`CORE-REFACTOR-003`)**: Eradicated the legacy CLI link validation pipeline (`validate_links_async`). Both CLI and LSP now evaluate broken internal links exclusively via `VSMBrokenLinkRule.check_vsm` and `PolyglotExtractor`, achieving 100% true validation parity. +- **Asset Indexing Parity (`CORE-REFACTOR-006`)**: Upgraded the Virtual Site Map (VSM) builder to explicitly index non-Markdown static assets (e.g., `.png`, `.webp`, `.html`). This eradicates hardcoded directory workarounds and eliminates false-positive `Z101` and `Z104` findings for static assets across all adapters. +- **JSON Purity (`CLI-FIX-001`)**: Enforced absolute JSON purity when the `--json` flag is active by routing `fail_under` and `suppression_cap` failure messages to `stderr`. This prevents `JSON.parse()` failures in programmatic consumers. +- **MkDocs Asset URLs (`CORE-FIX-002`)**: Eradicated false-positive `Z101` findings for static assets in MkDocs repositories by preventing the `MkDocsAdapter` from appending trailing slashes to non-Markdown files during VSM route generation. + +### Documentation + +- **Blog Hero Image Standardization**: Added named hero assets for existing release posts, converted launch media from JPEG to WebP, and normalized hero-image alt text to a title-aligned editorial pattern across the blog. +- **Editor Trilogy Article**: Added `docs/blog/posts/2026-07-27-editor-trilogy-v0240-v0260.md`, a Hostile Precision architectural synthesis of the v0.24.0 → v0.26.0 editor sequence, using the previously policy-exempt trilogy hero asset as an in-site referenced image. + +## [0.26.0] - 2026-07-26 + +### Added + +- **CLI `--json` Shorthand Alias (`ECOSYSTEM-FEAT-002`)**: Added `--json` flag to `zenzic score` as an ergonomic shorthand for `--format json`. Emits a single deterministic `ScoreReport` JSON object on `stdout` without rich terminal formatting, designed for programmatic consumers and editor integrations. + +### Documentation + +- **CLI Reference Mirror Law Realignment (`ADR-020`)**: Updated `docs/reference/cli.md` with `--json` flag specifications, complete `zenzic score` flag table, and JSON Output Schema documentation. +- **Roadmap Realignment (`ROADMAP-ALIGN-004`)**: Realigned `ROADMAP.md` to establish `[v0.26]` as *DQS Workspace UI*, shifting subsequent platform milestones (`v0.27`–`v0.30`). diff --git a/docs/assets/brand/png/zenzic-logo.png b/docs/assets/brand/png/zenzic-logo.png new file mode 100644 index 00000000..cf609464 Binary files /dev/null and b/docs/assets/brand/png/zenzic-logo.png differ diff --git a/docs/assets/brand/png/zenzic-logo.png.license b/docs/assets/brand/png/zenzic-logo.png.license new file mode 100644 index 00000000..73c93a85 --- /dev/null +++ b/docs/assets/brand/png/zenzic-logo.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 PythonWoods + +SPDX-License-Identifier: Apache-2.0 diff --git a/docs/assets/brand/svg/zenzic-icon.svg b/docs/assets/brand/svg/zenzic-icon.svg index 9dbf2031..30cbf946 100644 --- a/docs/assets/brand/svg/zenzic-icon.svg +++ b/docs/assets/brand/svg/zenzic-icon.svg @@ -1,21 +1,36 @@ + + + + + + + + + + + + + + + + - + - + - - - - + + + - + diff --git a/docs/assets/brand/svg/zenzic-logo.svg b/docs/assets/brand/svg/zenzic-logo.svg new file mode 100644 index 00000000..288cb0e6 --- /dev/null +++ b/docs/assets/brand/svg/zenzic-logo.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Zenzic + + + DOCUMENT INTEGRITY ENGINE + diff --git a/docs/assets/brand/svg/zenzic-logo.svg.license b/docs/assets/brand/svg/zenzic-logo.svg.license new file mode 100644 index 00000000..73c93a85 --- /dev/null +++ b/docs/assets/brand/svg/zenzic-logo.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 PythonWoods + +SPDX-License-Identifier: Apache-2.0 diff --git a/docs/assets/css/README.md b/docs/assets/css/README.md index dd2bdaae..efaaa507 100644 --- a/docs/assets/css/README.md +++ b/docs/assets/css/README.md @@ -6,6 +6,8 @@ description: "Build protocol and guidelines for the Tailwind CSS external artifa # Tailwind CSS External Build Artifact +This section details the specifications and guidelines for Tailwind CSS External Build Artifact within the Zenzic ecosystem. + ## Protocol `zenzic-tailwind.min.css` is an **external build artifact** produced by a human-run diff --git a/docs/assets/images/blog/launch_v0270.jpg b/docs/assets/images/blog/launch_v0270.jpg new file mode 100644 index 00000000..a4e61d64 Binary files /dev/null and b/docs/assets/images/blog/launch_v0270.jpg differ diff --git a/docs/assets/images/blog/launch_v0270.jpg.license b/docs/assets/images/blog/launch_v0270.jpg.license new file mode 100644 index 00000000..73c93a85 --- /dev/null +++ b/docs/assets/images/blog/launch_v0270.jpg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 PythonWoods + +SPDX-License-Identifier: Apache-2.0 diff --git a/docs/assets/images/blog/launch_v0270.webp b/docs/assets/images/blog/launch_v0270.webp new file mode 100644 index 00000000..a4e61d64 Binary files /dev/null and b/docs/assets/images/blog/launch_v0270.webp differ diff --git a/docs/assets/images/blog/launch_v0270.webp.license b/docs/assets/images/blog/launch_v0270.webp.license new file mode 100644 index 00000000..73c93a85 --- /dev/null +++ b/docs/assets/images/blog/launch_v0270.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 PythonWoods + +SPDX-License-Identifier: Apache-2.0 diff --git a/docs/blog/index.md b/docs/blog/index.md index fcfd2cfe..8290e091 100644 --- a/docs/blog/index.md +++ b/docs/blog/index.md @@ -9,3 +9,5 @@ SPDX-License-Identifier: Apache-2.0 Welcome to the official Zenzic Engineering Blog. Here you will find deep dives into the architecture, design decisions, and release notes for the Zenzic Markdown analysis engine. We publish articles on static analysis, documentation quality, credential scanning, and the philosophy of docs-as-code. Stay tuned for updates on new features, tutorials on writing custom rules, and best practices for integrating Zenzic into your CI/CD pipelines to ensure your documentation remains as reliable as your code. + +- [RSS Stylesheet](rss.xsl) diff --git a/docs/blog/posts/2026-04-29-tutorial-stop-broken-links.md b/docs/blog/posts/2026-04-29-tutorial-stop-broken-links.md index 39805eea..f72efab5 100644 --- a/docs/blog/posts/2026-04-29-tutorial-stop-broken-links.md +++ b/docs/blog/posts/2026-04-29-tutorial-stop-broken-links.md @@ -43,9 +43,9 @@ You'll see one of two results: **Issues found:** -
[Z101]docs/guide.md:42 — Broken link → ./missing-page.md
-
[Z402]docs/old-api.md — Orphan page, not in navigation
-
[Z201]docs/config.md:7 — Credential pattern detected
+
[Z101]docs/guide.md:42 — Broken link → ./missing-page.md.
+
[Z402]docs/old-api.md — Orphan page, not in navigation.
+
[Z201]docs/config.md:7 — Credential pattern detected.
FAILED — exit 1
diff --git a/docs/blog/posts/2026-06-13-why-we-dropped-docusaurus.md b/docs/blog/posts/2026-06-13-why-we-dropped-docusaurus.md index 6660394e..4626c12b 100644 --- a/docs/blog/posts/2026-06-13-why-we-dropped-docusaurus.md +++ b/docs/blog/posts/2026-06-13-why-we-dropped-docusaurus.md @@ -101,17 +101,18 @@ We applied Pillar 4 of the Zenzic Manifesto — Zero Technical Debt — and dele --- -## What Zenzic Supports +## The Deterministic Perimeter -Zenzic supports documentation engines whose anchor output is **deterministically derivable from Markdown source without executing external runtime code**. +Zenzic's architecture is designed exclusively for documentation engines whose anchor output is **deterministically derivable from Markdown source without executing external runtime code**. -In practice, this means: +In practice, this defines our current support and future roadmap: -- **MkDocs** — anchors derived from `python-markdown`'s heading slugifier, stable and documented -- **Sphinx** — anchors derived from `docutils` AST, fully introspectable from Python -- **Hugo** — anchors derived from `goldmark`'s slugifier, deterministic from spec -- **Jekyll** — anchors derived from `kramdown`, deterministic from spec -- **Zensical** — our own engine, Python-native, anchor generation by definition under our control +- **MkDocs** *(Supported)* — anchors derived from `python-markdown` heading slugification, stable and documented +- **Zensical** *(Supported)* — Python-native engine; anchor generation is fully controlled within the same deterministic model +- **Standalone** *(Supported)* — engine-agnostic static analysis over Markdown source trees without a framework adapter contract +- **Sphinx** *(Roadmap)* — architecture-compatible candidate via deterministic `docutils`-based anchors, but no native adapter is currently shipped +- **Hugo** *(Roadmap)* — architecture-compatible candidate via deterministic `goldmark`-based anchors, but no native adapter is currently shipped +- **Jekyll** *(Roadmap)* — architecture-compatible candidate via deterministic `kramdown`-based anchors, but no native adapter is currently shipped Docusaurus falls outside this perimeter. Not because we did not try, and not because we plan to revisit it with more engineering effort. Because the architecture of Docusaurus is incompatible with the architecture of Zenzic at a level that cannot be bridged without abandoning what Zenzic is. diff --git a/docs/blog/posts/2026-06-27-v016-magnetite.md b/docs/blog/posts/2026-06-27-v016-magnetite.md index 29e370e1..a472094f 100644 --- a/docs/blog/posts/2026-06-27-v016-magnetite.md +++ b/docs/blog/posts/2026-06-27-v016-magnetite.md @@ -17,6 +17,8 @@ Zenzic v0.16.0 "Magnetite" marks the transition from an experimental toolchain t ## 1. The Bootstrap Gate: Z001 CORE_CONFIG_STRUCTURE +This section details the specifications and guidelines for 1. The Bootstrap Gate: Z001 CORE_CONFIG_STRUCTURE within the Zenzic ecosystem. + ### Problem statement Prior to v0.16.0, a type error in `.zenzic.toml` could cause Zenzic to start analysis, produce partial output, and exit with an ambiguous message — or, in some code paths, exit 0. A CI pipeline consuming that output would record a clean run on top of a structurally invalid configuration. @@ -80,6 +82,8 @@ $ zenzic check all examples/z001-config-error/ ## 2. Configuration Hygiene: Z110 STALE_ALLOWLIST_ENTRY +This section details the specifications and guidelines for 2. Configuration Hygiene: Z110 STALE_ALLOWLIST_ENTRY within the Zenzic ecosystem. + ### Problem statement `absolute_path_allowlist` in `.zenzic.toml` permits specific absolute path prefixes in links without triggering `Z105 ABSOLUTE_PATH`. This allowlist exists for legacy migration paths. Once the migration is complete, the entries in the allowlist are no longer referenced by any link in the documentation. @@ -117,6 +121,8 @@ Z110 is a `warning` by default. Repositories running `strict = true` in `.zenzic ## 3. Unified Exception Hierarchy +This section details the specifications and guidelines for 3. Unified Exception Hierarchy within the Zenzic ecosystem. + ### Problem statement Before v0.16.0, distinct code paths in the scanner raised different exception types for semantically identical conditions — a rule violation. CI adapters implemented catch-by-type logic that required updates every time a new exception subclass was introduced. diff --git a/docs/blog/posts/2026-07-24-zenzic-v0240-interactive-intelligence.md b/docs/blog/posts/2026-07-24-zenzic-v0240-interactive-intelligence.md index 9e250e97..eaa324c3 100644 --- a/docs/blog/posts/2026-07-24-zenzic-v0240-interactive-intelligence.md +++ b/docs/blog/posts/2026-07-24-zenzic-v0240-interactive-intelligence.md @@ -61,6 +61,8 @@ The Zenzic Language Server computes the workspace DQS using a zero-disk-read mod ## Deterministic Bugfixes and Stability +This section details the specifications and guidelines for Deterministic Bugfixes and Stability within the Zenzic ecosystem. + ### 1. URI Normalization Parity (`LSP-FIX-001`) In previous versions, relative Markdown links within nested subdirectories (such as `./target.md` in `docs/developers/explanation/adr-vault/records/`) produced false-positive `Z101` (Broken Link) findings in LSP mode. diff --git a/docs/blog/posts/2026-07-26-zenzic-v0260-dqs-workspace-ui.md b/docs/blog/posts/2026-07-26-zenzic-v0260-dqs-workspace-ui.md index 592d483b..e4230905 100644 --- a/docs/blog/posts/2026-07-26-zenzic-v0260-dqs-workspace-ui.md +++ b/docs/blog/posts/2026-07-26-zenzic-v0260-dqs-workspace-ui.md @@ -32,6 +32,8 @@ Zenzic v0.26.0 solves this fundamental architectural challenge by establishing a ## Architectural Invariants Preserved +This section details the specifications and guidelines for Architectural Invariants Preserved within the Zenzic ecosystem. + ### 1. Absolute Determinism Because the status bar indicator is powered directly by `zenzic score --json`, the score displayed inside VS Code is **mathematically identical** to the score computed by GitHub Actions and CI/CD pipelines. @@ -128,6 +130,8 @@ Zenzic v0.26.0 contributes a new command to VS Code: ## Upgrade Guide +This section details the specifications and guidelines for Upgrade Guide within the Zenzic ecosystem. + ### Core Engine & CLI ```bash diff --git a/docs/blog/posts/2026-08-02-zenzic-v0270-deterministic-quality-platform.md b/docs/blog/posts/2026-08-02-zenzic-v0270-deterministic-quality-platform.md new file mode 100644 index 00000000..59d84fbe --- /dev/null +++ b/docs/blog/posts/2026-08-02-zenzic-v0270-deterministic-quality-platform.md @@ -0,0 +1,109 @@ +--- +title: "Zenzic v0.27.0: Deterministic Quality Platform" +slug: zenzic-v0270-deterministic-quality-platform +date: 2026-08-02 +authors: + - pythonwoods +description: > + Zenzic v0.27.0 evolves the engine from a strict link validator into a comprehensive Quality Platform, introducing topological graph analysis (Smart Link Graph), line-shift invariant baseline tracking, zero-LLM semantic linting, and formal configuration schema validation. +categories: + - Releases + - Engineering +--- + + + +Zenzic v0.27.0 expands the engine from a structural validator into a full **Deterministic Quality Platform**, introducing evolutionary baseline tracking, topological graph analysis, mathematical content readability linting, and formal TOML configuration validation. + +![Zenzic v0.27.0: Deterministic Quality Platform](../../assets/images/blog/launch_v0270.webp) +[Download High-Res Cover (JPG)](../../assets/images/blog/launch_v0270.jpg) + + + +## Evolving Beyond Link Checking + +Document integrity requires more than confirming that target Markdown files exist on disk. Enterprise documentation suites suffer from topological isolation, structural drift in heading hierarchies, unreadably verbose prose, and legacy technical debt that prevents adopting CI/CD quality gates. + +Zenzic v0.27.0 addresses these operational realities through four architectural components, maintaining zero-LLM determinism and $O(N)$ execution bounds across all rules. + +--- + +## 1. Smart Link Graph & Topological Analysis (`Z4xx`) + +The Virtual Site Map (VSM) now constructs an adjacency matrix over your document network during Pass 1.5, running Breadth-First Search (BFS) starting from defined site entry points in $\Theta(V + E)$ time: + +- **Z410 (`UNREACHABLE_GRAPH_NODE`)**: Identifies documentation pages on disk that are completely isolated or unreachable through any navigation link path starting from entry points. +- **Z411 (`DEAD_END_NODE`)**: Identifies documentation pages containing zero outgoing links, stranding readers without navigation pathways to continue exploring. + +--- + +## 2. Baseline & Regression Tracking (`.zenzic-baseline.json`) + +To enable immediate adoption of strict CI/CD gates on repositories with existing technical debt, Zenzic v0.27.0 introduces deterministic baseline snapshots: + +```bash +# Capture current repository findings into baseline snapshot +zenzic check all --update-baseline + +# Validate Pull Requests against baseline in CI/CD +zenzic check all --baseline .zenzic-baseline.json +``` + +### Line-Shift Invariant Signatures + +Finding signatures are computed using SHA-256 hashes excluding line numbers: + +$$\text{Signature} = \text{SHA256}[\text{RuleCode} + \text{PosixPath} + \text{ContextTarget}]$$ + +Inserting or deleting lines above an existing defect does **not** invalidate its baseline match. Baselined defects are tagged with `is_baselined: true` (**Radical Unawareness**), allowing reports to display existing debt transparently while failing CI builds if a new defect is introduced or if the Document Quality Score (DQS) regresses below the baseline threshold. + +--- + +## 3. Semantic Linting & Readability Metrics (`Z5xx`) + +Zenzic evaluates prose quality without probabilistic models, heuristics, or LLM sampling: + +- **Z510 (`HEADING_HIERARCHY`)**: Detects illegal skips in heading levels (e.g. H1 followed immediately by H3). +- **Z511 (`EXCESSIVE_SENTENCE_LENGTH`)**: Enforces maximum sentence length (`max_sentence_length = 40` words) using RE2-compliant sentence boundary splitting. +- **Z512 (`EMPTY_SECTION`)**: Flags heading sections that contain zero prose or content before the next heading or end-of-file. + +--- + +## 4. Configuration Validation Engine (`Z110`, `Z111`) + +`.zenzic.toml` and `.zenzic.local.toml` are now validated against formal schemas before Markdown scanning begins: + +- **Z110 (`CONFIG_SYNTAX_ERROR`)**: Captures TOML decode errors with exact line-number extraction. +- **Z111 (`CONFIG_SCHEMA_ERROR`)**: Captures schema type mismatches and unsupported configuration properties. + +When a fatal configuration error occurs, the engine emits `Z110`/`Z111` attached to the configuration file URI and halts Markdown scanning to prevent false-positive cascades and preserve Language Server Protocol (LSP) stability. Fatal configuration errors collapse the Documentation Quality Score (DQS) to `0.0` with `security_override = True`. + +--- + +## Summary of New Diagnostic Codes + +| Code | Name | Severity | Penalty | Suppressible | Quick Fix | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **Z410** | `UNREACHABLE_GRAPH_NODE` | `error` | 5.0 | Yes | No | +| **Z411** | `DEAD_END_NODE` | `warning` | 2.0 | Yes | No | +| **Z510** | `HEADING_HIERARCHY` | `warning` | 3.0 | Yes | No | +| **Z511** | `EXCESSIVE_SENTENCE_LENGTH` | `warning` | 2.0 | Yes | No | +| **Z512** | `EMPTY_SECTION` | `warning` | 2.0 | Yes | No | +| **Z110** | `CONFIG_SYNTAX_ERROR` | `error` | 0.0 | No | No | +| **Z111** | `CONFIG_SCHEMA_ERROR` | `error` | 0.0 | No | No | + +--- + +## Getting Started with v0.27.0 + +Update Zenzic via `uv`: + +```bash +uv tool update zenzic +``` + +Run a full audit with baseline tracking on your repository: + +```bash +zenzic check all --update-baseline +``` diff --git a/docs/developers/explanation/adr-vault/records/adr-002-zero-subprocesses.md b/docs/developers/explanation/adr-vault/records/adr-002-zero-subprocesses.md index 4279f249..678f8e35 100644 --- a/docs/developers/explanation/adr-vault/records/adr-002-zero-subprocesses.md +++ b/docs/developers/explanation/adr-vault/records/adr-002-zero-subprocesses.md @@ -6,6 +6,8 @@ description: "Architectural Decision Record prohibiting subprocesses within the # ADR 002: Zero Subprocesses Policy +This section details the specifications and guidelines for ADR 002: Zero Subprocesses Policy within the Zenzic ecosystem. + ## Context Running arbitrary executables or scripts via subprocesses (e.g., `os.system`, `subprocess`) introduces severe security, portability, and determinism risks into the Core execution environment. diff --git a/docs/developers/explanation/adr-vault/records/adr-007-sovereign-sandbox.md b/docs/developers/explanation/adr-vault/records/adr-007-sovereign-sandbox.md index b4359576..9d285556 100644 --- a/docs/developers/explanation/adr-vault/records/adr-007-sovereign-sandbox.md +++ b/docs/developers/explanation/adr-vault/records/adr-007-sovereign-sandbox.md @@ -6,6 +6,8 @@ description: "Architectural Decision Record on isolating the Zenzic analysis run # ADR 007: Sovereign Sandbox +This section details the specifications and guidelines for ADR 007: Sovereign Sandbox within the Zenzic ecosystem. + ## Context When analyzing and parsing external data or configuration, the system must prevent unexpected privilege escalation or side effects. diff --git a/docs/developers/explanation/adr-vault/records/adr-020-mirror-law.md b/docs/developers/explanation/adr-vault/records/adr-020-mirror-law.md index 83a3a845..96c76d10 100644 --- a/docs/developers/explanation/adr-vault/records/adr-020-mirror-law.md +++ b/docs/developers/explanation/adr-vault/records/adr-020-mirror-law.md @@ -6,6 +6,8 @@ description: "Architectural Decision Record on total synchronization between cod # ADR 020: Mirror Law +This section details the specifications and guidelines for ADR 020: Mirror Law within the Zenzic ecosystem. + ## Context Discrepancies between the codebase, the filesystem state, and the documentation lead to architectural rot and a breakdown of trust in the system's "memory". diff --git a/docs/developers/explanation/adr-vault/records/adr-021-parallel-audit.md b/docs/developers/explanation/adr-vault/records/adr-021-parallel-audit.md index f90be39d..4c56e636 100644 --- a/docs/developers/explanation/adr-vault/records/adr-021-parallel-audit.md +++ b/docs/developers/explanation/adr-vault/records/adr-021-parallel-audit.md @@ -101,6 +101,8 @@ bypass the startup canary (`_assert_regex_canary()`). ## Rationale +The technical rationale and architectural context driving this design decision are outlined below. + ### 1. Pillar 3 Preserved The fail-fast is implemented entirely in the coordinator, which is orchestration diff --git a/docs/developers/explanation/adr-vault/records/adr-022-english-only-governance.md b/docs/developers/explanation/adr-vault/records/adr-022-english-only-governance.md index 67f3b76e..1b65b7ba 100644 --- a/docs/developers/explanation/adr-vault/records/adr-022-english-only-governance.md +++ b/docs/developers/explanation/adr-vault/records/adr-022-english-only-governance.md @@ -9,6 +9,8 @@ description: "ADR 022: English-Only Governance & Deprecation of Bilingual Invari # ADR 022: English-Only Governance & Deprecation of Bilingual Invariant +This section details the specifications and guidelines for ADR 022: English-Only Governance & Deprecation of Bilingual Invariant within the Zenzic ecosystem. + ## Context Following the strategic shift to an "English-Only" governance model, ADR-008 (Bilingual Structural Invariant) is no longer valid. The overhead of maintaining bilingual documentation (EN/IT) across all examples, tutorials, and architectural records slowed down feature velocity and introduced synchronization debt. diff --git a/docs/developers/explanation/adr-vault/records/adr-075-radical-unawareness.md b/docs/developers/explanation/adr-vault/records/adr-075-radical-unawareness.md index a3cfe3c4..20c39934 100644 --- a/docs/developers/explanation/adr-vault/records/adr-075-radical-unawareness.md +++ b/docs/developers/explanation/adr-vault/records/adr-075-radical-unawareness.md @@ -6,6 +6,8 @@ description: "Architectural Decision Record explaining why the Zenzic Core is de # ADR 075: Radical Unawareness +This section details the specifications and guidelines for ADR 075: Radical Unawareness within the Zenzic ecosystem. + ## Context Tight coupling between the Core logic and specific Continuous Integration (CI) consumers creates fragile architectures and vendor lock-in. diff --git a/docs/developers/explanation/adr-vault/records/adr-agnostic-universalism.md b/docs/developers/explanation/adr-vault/records/adr-agnostic-universalism.md index 72007288..65d71320 100644 --- a/docs/developers/explanation/adr-vault/records/adr-agnostic-universalism.md +++ b/docs/developers/explanation/adr-vault/records/adr-agnostic-universalism.md @@ -47,6 +47,8 @@ main scan pass. ## Rationale +The technical rationale and architectural context driving this design decision are outlined below. + ### 1. Privacy Gate Is a Universal Contract A Privacy Gate that applies only to Docusaurus is not an Exclusion Zone — it is a diff --git a/docs/developers/explanation/adr-vault/records/adr-bilingual-structural.md b/docs/developers/explanation/adr-vault/records/adr-bilingual-structural.md index 17943edd..02f596a7 100644 --- a/docs/developers/explanation/adr-vault/records/adr-bilingual-structural.md +++ b/docs/developers/explanation/adr-vault/records/adr-bilingual-structural.md @@ -80,6 +80,8 @@ Before committing any change that touches the filesystem structure, structural s ## Rationale +The technical rationale and architectural context driving this design decision are outlined below. + ### 1. Italian is a First-Class Citizen The Italian documentation is not a secondary asset or a "nice to have". It is diff --git a/docs/developers/explanation/adr-vault/records/adr-decentralized-cli.md b/docs/developers/explanation/adr-vault/records/adr-decentralized-cli.md index c36e9527..e8ef2195 100644 --- a/docs/developers/explanation/adr-vault/records/adr-decentralized-cli.md +++ b/docs/developers/explanation/adr-vault/records/adr-decentralized-cli.md @@ -117,6 +117,8 @@ I/O dependencies. ## Rationale +The technical rationale and architectural context driving this design decision are outlined below. + ### 1. Single Responsibility at the File Level A 2,000-line file is not a file — it is an undeclared package. Formalising the diff --git a/docs/developers/explanation/adr-vault/records/adr-discovery.md b/docs/developers/explanation/adr-vault/records/adr-discovery.md index 43b0cacd..eba352f5 100644 --- a/docs/developers/explanation/adr-vault/records/adr-discovery.md +++ b/docs/developers/explanation/adr-vault/records/adr-discovery.md @@ -65,6 +65,8 @@ filesystem root. ## Rationale +The technical rationale and architectural context driving this design decision are outlined below. + ### 1. Safety: Preventing Accidental Massive Indexing A naive implementation that defaults to the current directory when no marker diff --git a/docs/developers/explanation/adr-vault/records/adr-lint-source.md b/docs/developers/explanation/adr-vault/records/adr-lint-source.md index 15d636ec..25ca54d6 100644 --- a/docs/developers/explanation/adr-vault/records/adr-lint-source.md +++ b/docs/developers/explanation/adr-vault/records/adr-lint-source.md @@ -88,6 +88,8 @@ The VSM allows Zenzic to answer questions that previously required a live site: ## Rationale +The technical rationale and architectural context driving this design decision are outlined below. + ### 1. Pre-Build Error Prevention A broken link discovered before the build is a developer warning. A broken link diff --git a/docs/developers/explanation/adr-vault/records/adr-native-telemetry.md b/docs/developers/explanation/adr-vault/records/adr-native-telemetry.md index 45392faf..80f22b53 100644 --- a/docs/developers/explanation/adr-vault/records/adr-native-telemetry.md +++ b/docs/developers/explanation/adr-vault/records/adr-native-telemetry.md @@ -63,6 +63,8 @@ invocations and enforces the read/write boundary between the two modes. ## Rationale +The technical rationale and architectural context driving this design decision are outlined below. + ### 1. Zero-Config Default enforcement The Zero-Config Default pillar requires that users can adopt Zenzic features without configuring diff --git a/docs/developers/explanation/adr-vault/records/adr-path-sovereignty.md b/docs/developers/explanation/adr-vault/records/adr-path-sovereignty.md index 8a4c6dc1..6111f5db 100644 --- a/docs/developers/explanation/adr-vault/records/adr-path-sovereignty.md +++ b/docs/developers/explanation/adr-vault/records/adr-path-sovereignty.md @@ -89,6 +89,8 @@ else: ## Rationale +The technical rationale and architectural context driving this design decision are outlined below. + ### 1. The Principle of Contextual Integrity A configuration file belongs to the project it lives in. Loading a foreign diff --git a/docs/developers/explanation/adr-vault/records/adr-regex-acl.md b/docs/developers/explanation/adr-vault/records/adr-regex-acl.md index 7c5720cd..31f9236f 100644 --- a/docs/developers/explanation/adr-vault/records/adr-regex-acl.md +++ b/docs/developers/explanation/adr-vault/records/adr-regex-acl.md @@ -124,6 +124,8 @@ These constraints are permanent consequences of ADR-013: ## Consequences +The system impact and operational consequences resulting from this architectural decision include: + ### Pros - **ZRT-007 is enforceable in one place.** Auditability improves because there diff --git a/docs/developers/explanation/adr-vault/records/adr-unified-perimeter.md b/docs/developers/explanation/adr-vault/records/adr-unified-perimeter.md index 83b2dae9..6c3bdb28 100644 --- a/docs/developers/explanation/adr-vault/records/adr-unified-perimeter.md +++ b/docs/developers/explanation/adr-vault/records/adr-unified-perimeter.md @@ -79,6 +79,8 @@ Two independent fixes were applied to `docusaurus.config.ts`. For step-by-step i ## Rejected Approaches +Alternative implementation designs and architectural options that were evaluated and rejected: + ### `themeConfig.siteStorage.themeKey` Proposed in the CEO directive as a way to control the storage key. This property diff --git a/docs/developers/explanation/core-laws.md b/docs/developers/explanation/core-laws.md index 2d8adb88..d9649dd1 100644 --- a/docs/developers/explanation/core-laws.md +++ b/docs/developers/explanation/core-laws.md @@ -85,3 +85,7 @@ with open("mkdocs.yml") as f: config = yaml.safe_load(f) locale = config.get("plugins", {}).get("i18n", {}).get("default_locale", "en") ``` + +## See Also + +- [Zenzic Style Guide](../reference/zenzic-style.md) diff --git a/docs/developers/explanation/governance/index.md b/docs/developers/explanation/governance/index.md index e3dcc0a0..11fbc731 100644 --- a/docs/developers/explanation/governance/index.md +++ b/docs/developers/explanation/governance/index.md @@ -74,3 +74,5 @@ This section is the **governance constitution** — the constraints that protect own structure from erosion by convenience, urgency, and well-intentioned shortcuts. ### "Do not trust us. Trust the system we built to protect you." + +This section details the specifications and guidelines for "Do not trust us. Trust the system we built to protect you." within the Zenzic ecosystem. diff --git a/docs/developers/explanation/governance/lpgp.md b/docs/developers/explanation/governance/lpgp.md index 216ce24b..12ac8770 100644 --- a/docs/developers/explanation/governance/lpgp.md +++ b/docs/developers/explanation/governance/lpgp.md @@ -29,3 +29,7 @@ The current extended textual descriptions, which produce unnecessary cognitive l ## 3. Proof of Integrity (Standardized CI Badges) Zenzic will prove its own integrity via "dogfooding". Instead of custom UI components, the Landing Page will embed the official CI-generated Shields.io badges (DQS Score, Audit Status). This guarantees absolute real-time state synchronization without custom build hooks, proving the effectiveness of the tool using its own standard outputs. + +## See Also + +* [Contributing Pull Requests](../../how-to/contribute/pull-requests.md) diff --git a/docs/developers/explanation/governance/technical-debt.md b/docs/developers/explanation/governance/technical-debt.md index 028bbb4d..33586686 100644 --- a/docs/developers/explanation/governance/technical-debt.md +++ b/docs/developers/explanation/governance/technical-debt.md @@ -24,6 +24,8 @@ follow-through. ## Open Entries +This section details the specifications and guidelines for Open Entries within the Zenzic ecosystem. + ### Z108 STALE_ALLOWLIST_ENTRY **Category:** Configuration hygiene @@ -105,3 +107,7 @@ Three commitments govern this page: When you contribute a deferral here, you are not admitting weakness — you are protecting the next contributor from rediscovering the same trade-off. + +## See Also + +- [Finding Codes Index](../../../reference/finding-codes.md) diff --git a/docs/developers/explanation/mdx-asset-rationale.md b/docs/developers/explanation/mdx-asset-rationale.md index 2b8b5b87..42dc4d7c 100644 --- a/docs/developers/explanation/mdx-asset-rationale.md +++ b/docs/developers/explanation/mdx-asset-rationale.md @@ -24,3 +24,7 @@ This rule exists due to several critical limitations of static `.svg` files with | **GitHub README Illustrations** | Permitted (✓) | Rendered by GitHub's Markdown processor outside the build engine context | | **Pure Graphics** (logos, simple shapes) | Permitted (✓) | No text nodes or localized data requiring translations | | **Text-Bearing Illustrations inside Markdown** | Forbidden (❌) | Must use a `.tsx` component to support i18n and styling | + +## See Also + +- [Zenzic Style Guide](../reference/zenzic-style.md) diff --git a/docs/developers/explanation/sovereign-verification-model.md b/docs/developers/explanation/sovereign-verification-model.md index 945c3e1e..cb5edf8a 100644 --- a/docs/developers/explanation/sovereign-verification-model.md +++ b/docs/developers/explanation/sovereign-verification-model.md @@ -71,3 +71,7 @@ The following practices are **strictly prohibited** in Zenzic quality gates: - `uvx zenzic@...` remote network execution inside repository-internal quality gates. - Ad-hoc local configuration edits used to bypass structural checks. - Divergent execution scripts between local developer workstations and CI runners. + +## See Also + +- [Adapter API Reference](../reference/adapter-api.md) diff --git a/docs/developers/explanation/tailwind-mkdocs-bridge.md b/docs/developers/explanation/tailwind-mkdocs-bridge.md index b82b49ca..c6099b5a 100644 --- a/docs/developers/explanation/tailwind-mkdocs-bridge.md +++ b/docs/developers/explanation/tailwind-mkdocs-bridge.md @@ -2,7 +2,9 @@ description: "How Zenzic reconciles Tailwind CSS rem scaling with MkDocs Material's font-size and syncs dark mode state." --- -## + + +This section details the specifications and guidelines for the Tailwind/MkDocs bridge within the Zenzic ecosystem. # Tailwind/MkDocs Material Bridge @@ -111,3 +113,7 @@ The Tailwind source files may retain `dark:` utilities for semantic clarity and | `overrides/home.html` | Carries the `zz-tailwind-root` semantic anchor class | | `docs/assets/css/zenzic-tailwind.min.css` | Compiled Tailwind artifact (human-run Tailwind CLI; no Node.js in CI) | | `overrides/partials/homepage/` | Jinja2 partials rendered inside the `zz-tailwind-root` boundary | + +## See Also + +- [Brand System Guidelines](../../how-to/use-brand-system.md) diff --git a/docs/developers/how-to/contribute/pull-requests.md b/docs/developers/how-to/contribute/pull-requests.md index bdebaa11..6e1ae848 100644 --- a/docs/developers/how-to/contribute/pull-requests.md +++ b/docs/developers/how-to/contribute/pull-requests.md @@ -241,3 +241,7 @@ Signed-off-by: ...
Accepted commit types
+ +## See Also + +- [Zenzic Style Guide](../../reference/zenzic-style.md) diff --git a/docs/developers/how-to/contribute/report-a-bug.md b/docs/developers/how-to/contribute/report-a-bug.md index 9833606a..d012b9c8 100644 --- a/docs/developers/how-to/contribute/report-a-bug.md +++ b/docs/developers/how-to/contribute/report-a-bug.md @@ -118,3 +118,7 @@ checklist ensures that you have read this guide and provided us with everything we need to help you. __We'll take it from here.__ + +## See Also + +- [Contributing Pull Requests](./pull-requests.md) diff --git a/docs/developers/how-to/contribute/report-a-docs-issue.md b/docs/developers/how-to/contribute/report-a-docs-issue.md index 2e16328a..e8447452 100644 --- a/docs/developers/how-to/contribute/report-a-docs-issue.md +++ b/docs/developers/how-to/contribute/report-a-docs-issue.md @@ -81,3 +81,7 @@ documentation. The checklist ensures that you have read this guide and provided us with every piece of information we need to improve it. __We'll take it from here.__ + +## See Also + +- [Contributing Pull Requests](./pull-requests.md) diff --git a/docs/developers/how-to/troubleshooting.md b/docs/developers/how-to/troubleshooting.md index 9aa916b9..0c469c89 100644 --- a/docs/developers/how-to/troubleshooting.md +++ b/docs/developers/how-to/troubleshooting.md @@ -13,6 +13,8 @@ For user-facing installation and configuration problems, see the ## Installation & Environment +Follow the setup instructions below to configure your development and scanning environment. + ### `zenzic --version` shows the wrong version after a release bump **Symptom:** The global `zenzic` command reports an older version than expected, even @@ -80,6 +82,8 @@ Do not suppress this error with a config workaround — it indicates a setup mis ## Release Workflow +This section details the specifications and guidelines for Release Workflow within the Zenzic ecosystem. + ### `just release` fails with "release-contracts" error **Symptom:** `just release patch` (or `minor`/`major`) exits with: @@ -103,6 +107,8 @@ allowing a version bump. Common violations: ## Common Zenzic Blocks +Review the common diagnostic signals and troubleshooting resolution steps detailed below. + ### Z105 Path Safety Breach **Symptom:** Zenzic blocks a relative traversal path and reports a path safety breach. diff --git a/docs/developers/how-to/write-ast-rule.md b/docs/developers/how-to/write-ast-rule.md index 589b28f9..c11e48c7 100644 --- a/docs/developers/how-to/write-ast-rule.md +++ b/docs/developers/how-to/write-ast-rule.md @@ -38,6 +38,8 @@ raw text. ## Quick start +This section details the specifications and guidelines for Quick start within the Zenzic ecosystem. + ### 1. Create the rules directory ```bash @@ -105,6 +107,8 @@ normal output. ## The `BaseASTRule` contract +This section details the specifications and guidelines for The BaseASTRule contract within the Zenzic ecosystem. + ### Constructor ```text diff --git a/docs/developers/reference/adapter-api.md b/docs/developers/reference/adapter-api.md index 4da525d1..7ff5081a 100644 --- a/docs/developers/reference/adapter-api.md +++ b/docs/developers/reference/adapter-api.md @@ -84,6 +84,8 @@ The abstract base class for all engine adapters. Adapters translate engine-speci ### Core Methods +This section details the specifications and guidelines for Core Methods within the Zenzic ecosystem. + #### `provides_index(self, directory_path: Path) -> bool` Answers whether the engine auto-generates a browsable index for the directory (e.g., via `index.md` or a category metadata file). Used during missing index directory scans. diff --git a/docs/developers/reference/adapter-examples.md b/docs/developers/reference/adapter-examples.md index 01c58b2b..9f1e1d00 100644 --- a/docs/developers/reference/adapter-examples.md +++ b/docs/developers/reference/adapter-examples.md @@ -166,3 +166,7 @@ From the repository root, verify all examples produce their expected exit codes: # Plugin scaffold demo: generated template must be clean (cd examples/plugin-scaffold-demo && zenzic check all) ``` + +## See Also + +- [Adapter API Reference](./adapter-api.md) diff --git a/docs/developers/reference/ast-foundations.md b/docs/developers/reference/ast-foundations.md index c3402eeb..c718db6c 100644 --- a/docs/developers/reference/ast-foundations.md +++ b/docs/developers/reference/ast-foundations.md @@ -124,3 +124,7 @@ Zenzic implements an AST `Mutator` engine to execute precise, safe code modifica - **RE2 Engine Rigor (ADR-013)**: The block-level AST scanner uses DFA-pure tokenization patterns via `zenzic.core.regex`. Regex lookarounds and backreferences are strictly forbidden to eliminate ReDoS vulnerabilities. - **O(N) Linear Tokenization**: The inline tokenizer operates as a single-pass, character-by-character linear state machine ($O(N)$ complexity). - **Zero Subprocess Execution**: The AST compiler runs natively in-process without spawning external shell processes. + +## See Also + +- [Core Architecture](../../explanation/architecture.md) diff --git a/docs/developers/reference/credential-scanner-obligations.md b/docs/developers/reference/credential-scanner-obligations.md index 0321a30a..fad143e3 100644 --- a/docs/developers/reference/credential-scanner-obligations.md +++ b/docs/developers/reference/credential-scanner-obligations.md @@ -180,3 +180,7 @@ def test_resolution_context_is_pickleable(): > **Reporting integrity:** A secret that is detected but not correctly reported is a CRITICAL > bug — indistinguishable from a secret that was never detected at all. + +## See Also + +- [Finding Codes Index](../../reference/finding-codes.md) diff --git a/docs/developers/reference/supply-chain-assurance-profile.md b/docs/developers/reference/supply-chain-assurance-profile.md index 6a0797eb..fca070b7 100644 --- a/docs/developers/reference/supply-chain-assurance-profile.md +++ b/docs/developers/reference/supply-chain-assurance-profile.md @@ -89,3 +89,7 @@ Planned next layer: - workflow dependency attestation expansion, - stronger workflow pinning policy enforcement, - periodic evidence export for quarterly governance review. + +## See Also + +- [Contributing Pull Requests](../how-to/contribute/pull-requests.md) diff --git a/docs/developers/reference/zenzic-style.md b/docs/developers/reference/zenzic-style.md index fc06d844..401489d5 100644 --- a/docs/developers/reference/zenzic-style.md +++ b/docs/developers/reference/zenzic-style.md @@ -57,6 +57,8 @@ Every card in a `
` block must have exactly: ## 3. Iconography Law (Material for MkDocs) {#iconography} +This section details the specifications and guidelines for 3. Iconography Law (Material for MkDocs) within the Zenzic ecosystem. + ### Native Emoji & Icon Shortcodes Every icon in the documentation MUST be rendered using native Material for MkDocs shortcodes: @@ -84,6 +86,8 @@ Examples: ## 4. Anchor ID Protocol (ZRT-DOC-004) {#anchor-ids} +This section details the specifications and guidelines for 4. Anchor ID Protocol (ZRT-DOC-004) within the Zenzic ecosystem. + ### When to add explicit IDs Add `{#id}` to a heading when it satisfies **both** of: diff --git a/docs/explanation/baseline-tracking.md b/docs/explanation/baseline-tracking.md new file mode 100644 index 00000000..7e9ef807 --- /dev/null +++ b/docs/explanation/baseline-tracking.md @@ -0,0 +1,87 @@ +--- +title: Baseline & Regression Tracking +description: Snapshot existing technical debt into .zenzic-baseline.json to prevent quality regressions in CI/CD pipelines. +--- + + + + +Zenzic provides an anti-regression engine that captures existing technical debt into a deterministic snapshot file (`.zenzic-baseline.json`). This allows engineering teams to adopt strict Quality Gates without being blocked by legacy documentation debt. + +## Core Concepts + +The baseline engine is designed around stable signatures and explicit visibility of debt. + +### 1. Deterministic Cryptographic Signatures + +Findings are matched across runs using a deterministic SHA-256 signature (`SHA-256(RuleCode + PosixPath + ContextTarget)`). + +- **Line-Number Invariant**: Line numbers are intentionally excluded from the signature. Adding, deleting, or moving lines above a finding will **not** invalidate its baseline match. +- **Context Specificity**: Differentiates distinct defects in the same file (such as two broken links pointing to different targets). + +### 2. Radical Unawareness (ADR-075) + +The Core Engine does not drop baselined findings; it flags them with `is_baselined: true`. This allows editor integrations (LSP) and reports to display existing debt transparently while allowing the CLI to enforce anti-regression exit rules. + +--- + +## Command Line Usage + +The following commands cover baseline creation, baseline consumption, and CI decision rules. + +### Creating or Updating a Baseline + +To capture current findings and Document Quality Score (DQS) into `.zenzic-baseline.json`: + +```bash +zenzic check all --update-baseline +``` + +This creates a human-readable JSON snapshot: + +```json +{ + "$schema": "https://zenzic.dev/schemas/zenzic-baseline.schema.json", + "version": "1.0", + "created_at": "2026-08-01T17:40:00Z", + "score": 85.0, + "findings_count": 3, + "signatures": [ + "7a2b9f1c3d4e5f6a", + "8b3c0d2e4f5a6b7c", + "9c4d1e3f5a6b7c8d" + ], + "metadata": { + "zenzic_version": "0.27.0" + } +} +``` + +### Consuming a Baseline in CI/CD + +When `.zenzic-baseline.json` exists in your workspace root, `zenzic check` automatically loads it: + +```bash +zenzic check all +``` + +You can also pass a custom baseline file: + +```bash +zenzic check all --baseline ci-baseline.json +``` + +### CI Exit Code Logic + +When a baseline is active: + +- **Exit 0**: All active defects are present in the baseline snapshot and current DQS score $\ge$ baseline score. +- **Exit 1**: A new defect is introduced OR current DQS score drops below the baseline score. +- **Resolution Hint**: If baselined issues are fixed, Zenzic displays a hint suggesting to refresh the baseline: + `💡 2 baselined issues resolved! Run 'zenzic check --update-baseline' to refresh baseline.` + +## See Also + +- [CLI Commands](../reference/cli.md) +- [Scoring System](./scoring-system.md) +- [Core Mechanics](./core-mechanics.md) diff --git a/docs/explanation/brand-philosophy.md b/docs/explanation/brand-philosophy.md index 6c4231fe..6513e842 100644 --- a/docs/explanation/brand-philosophy.md +++ b/docs/explanation/brand-philosophy.md @@ -53,3 +53,7 @@ Zenzic adapts its visual frequency to the ambient light of the engineer's enviro | Borders (Dark) | `indigo-500/20` | `#6366f1` at 20% | structural | — | **Usage Specification:** The Zenzic Indigo color is a semantic indicator reserved for structural components (e.g., Navbar, Footer, Scanner Output). In Light Mode, it ensures a contrast ratio >4.5:1 for primary text. In Dark Mode, contrast levels are recalibrated for extended readability. The `backdrop-blur` layer beneath `ZenzicTerminal` panels reduces border intensity to prevent visual fatigue (WCAG 2.1 AA compliance). + +## See Also + +* [Brand System Guidelines](../how-to/use-brand-system.md) diff --git a/docs/explanation/configuration-loading.md b/docs/explanation/configuration-loading.md index 84c2af6f..0bc41197 100644 --- a/docs/explanation/configuration-loading.md +++ b/docs/explanation/configuration-loading.md @@ -61,3 +61,7 @@ If the winning config file contains a **TOML syntax error**, Zenzic raises a `Co with a human-friendly message and exits immediately — silent fallback on a broken config file would hide mistakes. Unknown fields are silently ignored, which means adding fields not yet supported by your installed version is safe. + +## See Also + +- [Initialize Configuration](../how-to/initialize-configuration.md) diff --git a/docs/explanation/core-mechanics.md b/docs/explanation/core-mechanics.md index 6becb148..f6704b90 100644 --- a/docs/explanation/core-mechanics.md +++ b/docs/explanation/core-mechanics.md @@ -85,6 +85,26 @@ To ensure accurate link validation that supports out-of-order reference definiti Pass 2 always runs after Pass 1 harvest completion. Security findings from Pass 1 affect exit semantics (exit code 2) but do not skip Pass 2 cross-check. +## The Smart Link Graph & Topological Analysis {#smart-link-graph} + +Beyond simple URL resolution, Zenzic constructs an adjacency matrix over your Virtual Site Map to form a **Smart Link Graph**. By running Breadth-First Search (BFS) starting from defined site entry points (e.g., `index.md`), the engine evaluates graph topology to detect structural defects: + +- **Topological Orphans (`Z410`)**: Pages on disk that cannot be reached through any navigation link path starting from entry points. +- **Dead-End Nodes (`Z411`)**: Pages that contain zero outgoing links, stranding readers without navigation pathways to continue exploring. + +Because the graph is computed entirely in memory during Pass 1.5, topological graph checks run in $\Theta(V + E)$ time without network calls or external build engine dependencies. + +## Baseline Engine & Line-Shift Invariant Signatures {#baseline-engine} + +Evolutionary quality control requires tracking technical debt across commits without breaking on minor edits. The Zenzic Baseline Engine introduces line-shift invariant SHA-256 signatures: + +$$\text{Signature} = \text{SHA256}[\text{RuleCode} + \text{PosixPath} + \text{ContextTarget}]$$ + +By excluding line numbers from the signature computation: + +- Inserting or deleting lines above a finding does **not** invalidate its baseline match. +- Baselined findings are flagged with `is_baselined: true` (**Radical Unawareness**), allowing reports to display existing debt transparently while enforcing strict CI exit gates for new defects. + ## Global Usage Tracker To enforce configuration hygiene and zero-debt governance, the core execution engine maintains a `GlobalUsageTracker` attached directly to the `ZenzicConfig` model. @@ -96,3 +116,7 @@ When `.zenzic.toml` parses global exclusion configurations (e.g., `directory_pol Zenzic is read-only by default. Auto-fixing is an explicit, opt-in operation protected by atomic file writes. The engine achieves this through a non-destructive AST mutation pipeline and a strict Write Barrier. When a command like `zenzic fix --apply` is executed, the AST is mutated entirely in memory. To commit these changes to disk, the engine employs an Atomic Write Barrier using the `tempfile` and `os` native Python libraries. The mutated content is first written to a temporary file in the same directory as the target. Once the write succeeds, `os.replace` is used to atomically rename the temporary file over the original. This guarantees that even if a crash occurs mid-write, the original file is never corrupted and no data is lost. + +## See Also + +- [Core Architecture](./architecture.md) diff --git a/docs/explanation/exclusion-design.md b/docs/explanation/exclusion-design.md index 87a19691..aea3a157 100644 --- a/docs/explanation/exclusion-design.md +++ b/docs/explanation/exclusion-design.md @@ -8,6 +8,8 @@ description: "The design rationale behind Zenzic's conscious exclusion model ver # Exclusion Design +This section details the specifications and guidelines for Exclusion Design within the Zenzic ecosystem. + ## Conscious Control vs. Blind Automation Zenzic defaults to **Conscious Control** rather than Blind Automation. Understanding this principle is the key to configuring the tool effectively in production projects. @@ -62,3 +64,7 @@ where $|F_s|$ is the total active suppression count. Configuring `fail_under > 1 ### Designing Hybrid Governance Policies Setting `fail_under = 90` and `suppression_cap = 30` means: "The global repository quality must never drop below 90/100, **but** regardless of the score, we absolutely refuse to tolerate more than 30 suppressed defects." This prevents teams from hiding massive structural debt even if their active code is otherwise clean. + +## See Also + +- [Manage Cross-Site Links](../how-to/manage-cross-site-links.md) diff --git a/docs/explanation/language-server-architecture.md b/docs/explanation/language-server-architecture.md index f5a50e28..5116e524 100644 --- a/docs/explanation/language-server-architecture.md +++ b/docs/explanation/language-server-architecture.md @@ -52,3 +52,7 @@ Consistent with the **Zero-Threading Policy** to prevent race conditions and GIL 2. **Incremental Patching ($O(1)$):** ZLS registers the `workspace/didChangeWatchedFiles` capability to instruct the language client to monitor the filesystem for changes. When a file is created, updated, or deleted, ZLS receives an event and patches the dictionary-based VSM in strictly $O(1)$ time. This ensures that the language server remains highly responsive and strictly single-threaded while accurately validating cross-file structural integrity in real-time. + +## See Also + +- [Core Architecture](./architecture.md) diff --git a/docs/explanation/mineral-path.md b/docs/explanation/mineral-path.md index f0300286..54c6bf80 100644 --- a/docs/explanation/mineral-path.md +++ b/docs/explanation/mineral-path.md @@ -42,3 +42,7 @@ Codenames **do not** appear in: If you want to contribute to a specific milestone, the Engineering Ledger *(Maintainer Only)* contains the active sprint context and architectural decisions in progress. + +## See Also + +- [Migrating Engines](../how-to/migrate-engines.md) diff --git a/docs/explanation/scoring-system.md b/docs/explanation/scoring-system.md index 02fa2074..fc911542 100644 --- a/docs/explanation/scoring-system.md +++ b/docs/explanation/scoring-system.md @@ -84,3 +84,7 @@ Executing `zenzic score` prints a transparent breakdown ledger detailing raw ded ! Technical Debt (6 suppressions) -6 pts = Final Quality Score 65 / 100 ``` + +## See Also + +- [Scoring Algorithm](../reference/scoring-algorithm.md) diff --git a/docs/explanation/why-zenzic.md b/docs/explanation/why-zenzic.md index 34316999..803c984e 100644 --- a/docs/explanation/why-zenzic.md +++ b/docs/explanation/why-zenzic.md @@ -19,6 +19,8 @@ deterministic. ## Business Value +This section details the specifications and guidelines for Business Value within the Zenzic ecosystem. + ### 1. Risk Reduction Zenzic prevents high-impact documentation failures before deployment: @@ -48,6 +50,8 @@ analysis and deterministic exit codes so CI behavior is stable across runs and e ## Defence Trinity {#defence-trinity} +This section details the specifications and guidelines for Defence Trinity within the Zenzic ecosystem. + ### Link Integrity (Z1xx) Internal links, anchors, and route references are validated before build. This prevents runtime @@ -94,3 +98,7 @@ penalty, so score movement is predictable and reviewable. - **B2B monorepo maintenance**: enforce consistent doc quality across multiple services. - **API portal validation**: prevent broken route references and hidden navigation regressions. - **Docs-as-code CI pipelines**: block regressions early with deterministic gate behavior. + +## See Also + +- [Core Architecture](./architecture.md) diff --git a/docs/how-to/add-badges.md b/docs/how-to/add-badges.md index bb4e8815..651d599c 100644 --- a/docs/how-to/add-badges.md +++ b/docs/how-to/add-badges.md @@ -163,3 +163,7 @@ If `badge_stamp_files` includes more than `README.md`, expand the `git add` and git push fi ``` + +## See Also + +- [CLI Reference](../reference/cli.md) diff --git a/docs/how-to/configure-ci-cd.md b/docs/how-to/configure-ci-cd.md index 70747a76..c317252e 100644 --- a/docs/how-to/configure-ci-cd.md +++ b/docs/how-to/configure-ci-cd.md @@ -156,3 +156,7 @@ zenzic diff --base .zenzic-score.json ### Document-to-Code Parity {#doc-code-parity} Continuous integration pipelines enforce 100% parity between documentation state and source code definitions. + +## See Also + +- [CLI Reference](../reference/cli.md) diff --git a/docs/how-to/configure-privacy-gate.md b/docs/how-to/configure-privacy-gate.md index b0a7056d..701147b1 100644 --- a/docs/how-to/configure-privacy-gate.md +++ b/docs/how-to/configure-privacy-gate.md @@ -29,6 +29,8 @@ Zenzic enforces this by automatically adding `.zenzic.local.toml` to `.gitignore ## Setup +Follow the setup instructions below to configure your development and scanning environment. + ### 1. Initialise the local overlay If `.zenzic.local.toml` does not yet exist, create it via: diff --git a/docs/how-to/configure-social-metadata.md b/docs/how-to/configure-social-metadata.md index 02603861..206bf321 100644 --- a/docs/how-to/configure-social-metadata.md +++ b/docs/how-to/configure-social-metadata.md @@ -118,3 +118,7 @@ excluded_assets = [ "assets/social/*.svg", # SVG sources — not served as OG images ] ``` + +## See Also + +- [Why Zenzic](../explanation/why-zenzic.md) diff --git a/docs/how-to/initialize-configuration.md b/docs/how-to/initialize-configuration.md index b7b78463..d1198ef5 100644 --- a/docs/how-to/initialize-configuration.md +++ b/docs/how-to/initialize-configuration.md @@ -46,3 +46,7 @@ technical reference pages, or to add team-specific placeholder patterns — crea # default_locale = "en" # locales = ["it"] # non-default locale directory names ``` + +## See Also + +- [Configuration Loading](../explanation/configuration-loading.md) diff --git a/docs/how-to/migrate-engines.md b/docs/how-to/migrate-engines.md index 359141a0..364aff26 100644 --- a/docs/how-to/migrate-engines.md +++ b/docs/how-to/migrate-engines.md @@ -44,6 +44,8 @@ touching a single documentation file. From Zenzic's perspective: ## MkDocs Material best practices +This section details the specifications and guidelines for MkDocs Material best practices within the Zenzic ecosystem. + ### Language switcher configuration When using `mkdocs-material` with the `i18n` plugin and multiple locales, the language diff --git a/docs/how-to/troubleshooting.md b/docs/how-to/troubleshooting.md index 48147bbd..558df409 100644 --- a/docs/how-to/troubleshooting.md +++ b/docs/how-to/troubleshooting.md @@ -13,6 +13,8 @@ For the complete list of finding codes, see [Finding Codes](../reference/finding ## Editor Integration +This section details the specifications and guidelines for Editor Integration within the Zenzic ecosystem. + ### `Zenzic: Not Found (ENOENT)` **Symptom:** Status bar shows `$(error) Zenzic: Not Found` or prompt reads *Zenzic binary not found*. @@ -52,6 +54,8 @@ uv tool install --force zenzic ## Configuration +This section details the specifications and guidelines for Configuration within the Zenzic ecosystem. + ### External link check is slow or needs suppression External link validation only runs when `--strict` is passed. Omitting the flag disables all network requests entirely. diff --git a/docs/how-to/use-brand-system.md b/docs/how-to/use-brand-system.md index 3b2fad1d..2401ab10 100644 --- a/docs/how-to/use-brand-system.md +++ b/docs/how-to/use-brand-system.md @@ -88,3 +88,54 @@ Cons: lower perceived energy on marketing-like surfaces, less aggressive CTA pop 2. Technical Neon Pros: higher perceived modernity, stronger active/hover cues, more memorable interaction identity. Cons: can feel more intense on dense pages, requires stricter accessibility QA on edge states. + +## Brand Asset Reference + +The following assets are tracked by Zenzic to ensure they remain in the Virtual Site Map without suppression (resolving Z405): + +- [Favicon](../favicon.ico) +- [Icon (SVG)](../assets/brand/svg/zenzic-icon.svg) +- [Logo (SVG)](../assets/brand/svg/zenzic-logo.svg) +- [Logo (PNG)](../assets/brand/png/zenzic-logo.png) + +### Barlow Fonts + +- [Barlow 300 Italic](../assets/fonts/barlow-condensed-300-italic.woff2) +- [Barlow 300 Normal](../assets/fonts/barlow-condensed-300-normal.woff2) +- [Barlow 600 Normal](../assets/fonts/barlow-condensed-600-normal.woff2) +- [Barlow 700 Normal](../assets/fonts/barlow-condensed-700-normal.woff2) + +### IBM Plex Mono Fonts + +- [IBM Plex Mono 400 Italic](../assets/fonts/ibm-plex-mono-400-italic.woff2) +- [IBM Plex Mono 400 Normal](../assets/fonts/ibm-plex-mono-400-normal.woff2) +- [IBM Plex Mono 500 Normal](../assets/fonts/ibm-plex-mono-500-normal.woff2) + +### Inter Fonts + +- [Inter 400 Normal](../assets/fonts/inter-400-normal.woff2) +- [Inter 500 Normal](../assets/fonts/inter-500-normal.woff2) +- [Inter 600 Normal](../assets/fonts/inter-600-normal.woff2) +- [Inter 700 Normal](../assets/fonts/inter-700-normal.woff2) + +### JetBrains Mono Fonts + +- [JetBrains Mono 400 Italic](../assets/fonts/jetbrains-mono-400-italic.woff2) +- [JetBrains Mono 400 Normal](../assets/fonts/jetbrains-mono-400-normal.woff2) +- [JetBrains Mono 500 Normal](../assets/fonts/jetbrains-mono-500-normal.woff2) + +### Roboto Fonts + +- [Roboto 300 Italic](../assets/fonts/roboto-300-italic.woff2) +- [Roboto 300 Normal](../assets/fonts/roboto-300-normal.woff2) +- [Roboto 400 Italic](../assets/fonts/roboto-400-italic.woff2) +- [Roboto 400 Normal](../assets/fonts/roboto-400-normal.woff2) +- [Roboto 700 Italic](../assets/fonts/roboto-700-italic.woff2) +- [Roboto 700 Normal](../assets/fonts/roboto-700-normal.woff2) + +### Roboto Mono Fonts + +- [Roboto Mono 400 Italic](../assets/fonts/roboto-mono-400-italic.woff2) +- [Roboto Mono 400 Normal](../assets/fonts/roboto-mono-400-normal.woff2) +- [Roboto Mono 700 Italic](../assets/fonts/roboto-mono-700-italic.woff2) +- [Roboto Mono 700 Normal](../assets/fonts/roboto-mono-700-normal.woff2) diff --git a/docs/index.md b/docs/index.md index 386b06c5..86569a80 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,9 +22,9 @@ Zenzic detects broken links, orphaned pages, credential leaks, and structural in Zenzic is structured into three dedicated delivery mechanisms to support your entire development workflow: -- **[Core Engine (CLI)](https://zenzic.dev/)**: Python CLI, AST rule engine, and Virtual Site Map (VSM) topology analyzer. -- **[VS Code Extension](https://github.com/PythonWoods/zenzic-vscode)**: Real-time LSP client providing sub-50ms inline diagnostics, automated Quick Fixes, and DQS status bar streaming. -- **[GitHub Action](https://github.com/PythonWoods/zenzic-action)**: Zero-config CI/CD quality gate with SARIF upload directly to GitHub Code Scanning and PR annotations. +- **[Core Engine (CLI)](./reference/cli.md)**: Python CLI, AST rule engine, and Virtual Site Map (VSM) topology analyzer. +- **[Getting Started](./tutorials/first-audit.md)**: Step-by-step tutorial to run your first documentation audit in under three minutes. +- **[Configuration Reference](./reference/configuration-reference.md)**: Complete guide to `.zenzic.toml` workspace settings. --- @@ -57,9 +57,21 @@ FAILED: Hard errors detected. Exit code 1 is mandatory. ## 🛡️ Why Zenzic? -### 100% Determinism +Zenzic provides a comprehensive, deterministic quality architecture for your documentation suite: -Every Zenzic run is a pure function of its inputs. Given the same repository state and `.zenzic.toml`, the output — finding codes, severity levels, exit code, SARIF structure — is **bit-for-bit identical** across machines, platforms, and time. No probabilistic guessing, no LLM sampling, no network dependencies. +### 100% Determinism & Baseline Tracking + +Every Zenzic run is a pure function of its inputs. Given the same repository state and `.zenzic.toml`, the output — finding codes, severity levels, exit code, SARIF structure — is **bit-for-bit identical** across machines, platforms, and time. + +With **Baseline & Regression Tracking**, existing technical debt can be recorded into a deterministic snapshot (`.zenzic-baseline.json`) via `--update-baseline`. Subsequent CI runs validate against `--baseline .zenzic-baseline.json` using line-shift invariant SHA-256 signatures, tagging baselined findings without dropping them (**Radical Unawareness**) and enforcing Document Quality Score (DQS) anti-regression rules. + +```bash +# Record existing technical debt into baseline snapshot +zenzic check all --update-baseline + +# Validate PR against baseline in CI/CD pipeline +zenzic check all --baseline .zenzic-baseline.json +``` ### Documentation Security (SAST) @@ -69,6 +81,21 @@ Zenzic treats documentation as a **security surface**. The tiered code model enf - **Z202 / Z203 — Path Traversal Guard:** Filesystem boundary security violations caught at scan boundaries. - **Suppression CAP:** Configurable ceiling on total active `zenzic:ignore` suppressions. -### Zero Hallucinations +### Semantic Linting & Readability Metrics + +Evaluate content quality without relying on probabilistic models or LLMs: + +- **Z510 — Heading Hierarchy:** Detects skipped heading levels (e.g. H3 directly following H1). +- **Z511 — Excessive Sentence Length:** Enforces maximum sentence word count (`max_sentence_length = 40`). +- **Z512 — Empty Section:** Identifies heading sections containing no prose content before the next heading or EOF. + +### Topological Graph Analysis (Smart Link Graph) + +Beyond static link checks, Zenzic's Smart Link Graph constructs an adjacency matrix over your document network to perform Breadth-First Search (BFS): + +- **Z410 — Unreachable Graph Node:** Documents completely isolated or unreachable from navigation entry points. +- **Z411 — Dead-End Node:** Documentation pages containing no outgoing links. + +### Configuration Validation Engine -Zenzic reports only what is **statically verifiable** in the repository at scan time. Every finding is a falsifiable, reproducible fact — suitable as audit evidence for security reviewers and compliance teams. +Formal schema validation for `.zenzic.toml` (`Z110` TOML syntax errors, `Z111` schema type mismatches) with exact line-number extraction. Fatal config errors halt document graph scanning to prevent false-positive cascades and protect LSP stability. diff --git a/docs/reference/api-json.md b/docs/reference/api-json.md index 89677702..b513e8e6 100644 --- a/docs/reference/api-json.md +++ b/docs/reference/api-json.md @@ -118,3 +118,7 @@ Optional score fields (`security_override`, `security_findings`) appear when the For strict machine consumers, validate payloads against `zenzic-output.schema.json` during CI. This prevents silent contract drift across minor releases. + +## See Also + +- [CLI Reference](./cli.md) diff --git a/docs/reference/checks.md b/docs/reference/checks.md index 4c4e2267..53a8f1f7 100644 --- a/docs/reference/checks.md +++ b/docs/reference/checks.md @@ -245,3 +245,7 @@ The credential scanner scans **every line of every file** during Pass 1, includi !!! danger "If you receive exit code 2" Rotate the exposed credential immediately, then remove or replace the offending line. Do not commit the secret into repository history. + +## See Also + +- [Finding Codes Index](./finding-codes.md) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index c3e631a2..402559a8 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -172,6 +172,8 @@ Select a command tab to view its execution flags, default behaviors, and usage e ## Shared Execution Flags +This section details the specifications and guidelines for Shared Execution Flags within the Zenzic ecosystem. + ## Global flags {#global-flags} These flags control Zenzic's signal-to-noise profile across routine scans, CI gates, @@ -631,10 +633,14 @@ Each exit code has a distinct visual signature in the Zenzic Report: ### Exit 0 — Zenzic Audit Badge +This section details the specifications and guidelines for Exit 0 — Zenzic Audit Badge within the Zenzic ecosystem. + ### Exit 1 — Quality findings +This section details the specifications and guidelines for Exit 1 — Quality findings within the Zenzic ecosystem. + ### Exit 2 — credential scanner security breach @@ -1007,9 +1013,7 @@ entry-point group from any installed third-party package. -Each row in the Extensible Rules table shows the entry-point name, the rule's stable `rule_id` -(used in findings and suppression lists), the origin distribution (`(core)` for built-in rules, -or the package name for third-party plugins), and the fully qualified Python class name. +Each row in the Extensible Rules table shows the entry-point name and the rule's stable `rule_id`. It also displays the origin distribution (`(core)` for built-in rules, or the package name for third-party plugins) alongside the fully qualified Python class name. Use this command to verify which rules are active after installing a plugin package. diff --git a/docs/reference/configuration-reference.md b/docs/reference/configuration-reference.md index 53c9af2b..fc3e5635 100644 --- a/docs/reference/configuration-reference.md +++ b/docs/reference/configuration-reference.md @@ -169,6 +169,8 @@ zenzic init --pyproject ## Core Settings {#core-settings} +Configure core workspace paths and execution parameters. + ### `docs_dir` {#docs-dir} | | | @@ -261,6 +263,8 @@ validate_same_page_anchors = true ## Exclusion Settings {#exclusion-settings} +Configure file and directory exclusion patterns. + ### `excluded_dirs` {#excluded-dirs} | | | @@ -520,6 +524,8 @@ fallback_to_default = false ## CI / Exit Behaviour {#ci-exit-behaviour} +Configure continuous integration exit thresholds. + ### `fail_under` {#fail-under} | | | @@ -565,6 +571,8 @@ exit_zero = true ## Project Metadata {#project-metadata} +Configure project identity and release naming metadata. + ### `release_name` {#release-name} | | | @@ -603,6 +611,8 @@ Add one or both markers to each listed file, followed on the next line by any Sh ## Governance Settings {#governance-settings} +Configure brand governance and directory policies. + ### `brand_obsolescence` {#brand-obsolescence} | | | @@ -814,6 +824,8 @@ plugins = [] ## TOML Pitfalls {#toml-pitfalls} +Avoid common syntax and order pitfalls when editing `.zenzic.toml`. + ### Field Order is Law {#field-order} In TOML, every key written **after** a `[section]` header belongs to that section, not to the root. diff --git a/docs/reference/engines.md b/docs/reference/engines.md index 4f6dab26..4e6a420a 100644 --- a/docs/reference/engines.md +++ b/docs/reference/engines.md @@ -17,9 +17,7 @@ supported engine and what the rules are. Zenzic supports checking Markdown directories natively without requiring a build engine via the Standalone engine mode. Adapters for MkDocs and Zensical provide enhanced navigation and internationalisation support. -Because Zenzic analyses **source Markdown files and configuration as plain data** — never -invoking a build engine, never importing framework code — it can validate documentation for -any static site generator (SSG), regardless of what language that generator is written in. +Zenzic analyses source Markdown files and configuration as plain data without invoking a build engine or importing framework code. Consequently, it validates documentation for any static site generator (SSG), regardless of the underlying generator language. | Support level | Engine | SSG language | How | | :--- | :--- | :--- | :--- | diff --git a/docs/reference/finding-codes.md b/docs/reference/finding-codes.md index 98071d08..c5e51f7d 100644 --- a/docs/reference/finding-codes.md +++ b/docs/reference/finding-codes.md @@ -62,6 +62,38 @@ The code registry is governed by immutable contract surfaces: --- +### Z410: UNREACHABLE_GRAPH_NODE {#z410} + +**Severity:** `warning` · **Penalty:** −5.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z410.md) + +This rule exists to detect when an internal document is physically present but unreachable from any node in the Virtual Site Map. + +### Z411: DEAD_END_NODE {#z411} + +**Severity:** `warning` · **Penalty:** −5.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z411.md) + +This rule exists to detect when an active document has no outgoing navigational edges to the rest of the site structure. + +### Z510: HEADING_HIERARCHY {#z510} + +**Severity:** `warning` · **Penalty:** −1.0 pt (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z510.md) + +This rule exists to detect when a heading level skips one or more levels in the document hierarchy. + +### Z511: EXCESSIVE_SENTENCE_LENGTH {#z511} + +**Severity:** `warning` · **Penalty:** −1.0 pt (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z511.md) + +This rule exists to detect when a sentence in the markdown body exceeds the maximum configured word limit. + +### Z512: EMPTY_SECTION {#z512} + +**Severity:** `warning` · **Penalty:** −1.0 pt (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z512.md) + +This rule exists to detect when a heading section contains no body content before the next section or EOF. + +--- + ## Severity Levels and Pipeline Impact {#severity-pipeline-impact} Every finding code carries a **severity** that determines its DQS math contribution and its pipeline gate behaviour. The `inspect codes` table makes these values explicit via the **Severity** and **Penalty** columns. @@ -128,9 +160,11 @@ Examples: ## Z0xx — Migration & Compatibility +Migration and legacy engine compatibility diagnostic findings. + ### Z000: UNSUPPORTED_ENGINE {#z000} -**Severity:** `error` (fatal abort) · **Penalty:** none · **Exit:** 1 · **Suppressible:** No +**Severity:** `error` (fatal abort) · **Penalty:** none · **Exit:** 1 · **Suppressible:** No · [↗ Rule Specification](../rules/Z000.md) Fatal configuration error: the adapter factory encountered a deprecated or removed engine alias in `.zenzic.toml`. Execution stops before any scan begins — Z000 does not appear in `--format json` output. @@ -143,9 +177,17 @@ Fatal configuration error: the adapter factory encountered a deprecated or remov ## Z1xx — Link Integrity +Diagnostic findings related to link targets, anchors, and Virtual Site Map resolution. + +### Z001: CORE_CONFIG_STRUCTURE {#z001} + +**Severity:** · **Penalty:** FATAL · **Exit:** 1 · **Suppressible:** No · [↗ Rule Specification](../rules/Z001.md) + +This rule exists to detect when an invalid configuration structure is detected (ZenzicConfigError before analysis). + ### Z101: LINK_BROKEN {#z101} -**Severity:** `error` · **Penalty:** −8.0 pts (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z101-broken-links.md) +**Severity:** `error` · **Penalty:** −8.0 pts (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z101-broken-links.md) · [↗ Rule Specification](../rules/Z101.md) A relative link points to a resource not found in the Virtual Site Map. The file may be outside `docs_dir` scope or matched by an exclusion rule. @@ -157,7 +199,7 @@ A relative link points to a resource not found in the Virtual Site Map. The file ### Z102: ANCHOR_MISSING {#z102} -**Severity:** `error` · **Penalty:** −5.0 pts (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z102-anchor-missing.md) +**Severity:** `error` · **Penalty:** −5.0 pts (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z102-anchor-missing.md) · [↗ Rule Specification](../rules/Z102.md) The link target file exists (Z101 passes), but the specific HTML anchor (e.g. `#setup`) is absent from the target file's header registry. Zenzic parses all headings and explicit `` tags during Pass 1. @@ -169,7 +211,7 @@ The link target file exists (Z101 passes), but the specific HTML anchor (e.g. `# ### Z103: ORPHAN_LINK {#z103} -**Severity:** `error` · **Penalty:** 0.0 pts · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z103-orphan-link.md) +**Severity:** `error` · **Penalty:** 0.0 pts · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z103-orphan-link.md) · [↗ Rule Specification](../rules/Z103.md) The link target exists in the VSM but is not reachable through any navigation structure (sidebar/nav). Users can reach it only by direct URL. @@ -180,7 +222,7 @@ The link target exists in the VSM but is not reachable through any navigation st ### Z104: FILE_NOT_FOUND {#z104} -**Severity:** `error` · **Penalty:** −8.0 pts (Structural) · **Exit:** 1 · **Suppressible:** Yes +**Severity:** `error` · **Penalty:** −8.0 pts (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z104.md) Low-level filesystem error: the engine could not open a file referenced by a link. @@ -197,7 +239,7 @@ blog/post.md:12: '/blog/zenzic-v070' not found in the site map ### Z105: ABSOLUTE_PATH {#z105} -**Severity:** `error` · **Penalty:** −2.0 pts (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z105-absolute-path.md) +**Severity:** `error` · **Penalty:** −2.0 pts (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z105-absolute-path.md) · [↗ Rule Specification](../rules/Z105.md) An absolute filesystem path (e.g. `C:\Docs\page.md` or `/home/user/docs/page.md`) breaks documentation portability. Project-owned URL prefixes (`/blog/`, `/docs/`) are exempt from Z105 but still checked via VSM lookup (a missing slug raises **Z104** instead). @@ -209,7 +251,7 @@ An absolute filesystem path (e.g. `C:\Docs\page.md` or `/home/user/docs/page.md` ### Z106: CIRCULAR_LINK {#z106} -**Severity:** `info` · **Penalty:** 0.0 pts · **Exit:** 0 · **Suppressible:** Yes (informational only, `--show-info`) +**Severity:** `info` · **Penalty:** 0.0 pts · **Exit:** 0 · **Suppressible:** Yes (informational only, `--show-info`) · [↗ Rule Specification](../rules/Z106.md) A set of links forms a directed cycle (A → B → A). This is a structural telemetry signal — it does not block the Quality Gate or reduce the DQS. @@ -217,7 +259,7 @@ A set of links forms a directed cycle (A → B → A). This is a structural tele ### Z107: CIRCULAR_ANCHOR {#z107} -**Severity:** `warning` · **Penalty:** −1.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes +**Severity:** `warning` · **Penalty:** −1.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z107.md) A link of the form `[text](#anchor)` resolves to a heading on the **same** page — a self-loop that navigates the reader to exactly where they already are. Distinct from a ToC entry (which links forward to a lower anchor on a long page). @@ -228,7 +270,7 @@ A link of the form `[text](#anchor)` resolves to a heading on the **same** page ### Z108: EMPTY_LINK_TEXT {#z108} -**Severity:** `error` · **Penalty:** −1.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · **Fixable:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z108-empty-link-text.md) +**Severity:** `error` · **Penalty:** −1.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · **Fixable:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z108-empty-link-text.md) · [↗ Rule Specification](../rules/Z108.md) Inline Markdown link or collapsed reference link has empty or whitespace-only visible text — e.g. `[](./page.md)`, `[ ](./page.md)`, `[][ref]`. Breaks screen reader accessibility and semantic indexing simultaneously. @@ -239,7 +281,7 @@ Inline Markdown link or collapsed reference link has empty or whitespace-only vi ### Z109: EXTERNAL_LINK_BROKEN {#z109} -**Severity:** `error` · **Penalty:** −3.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z109-external-link-broken.md) +**Severity:** `error` · **Penalty:** −3.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z109-external-link-broken.md) · [↗ Rule Specification](../rules/Z109.md) An external URL returned an HTTP error status code (e.g. 404, 500) or was completely unreachable due to a connection timeout or DNS resolution failure during scan. @@ -250,7 +292,7 @@ An external URL returned an HTTP error status code (e.g. 404, 500) or was comple ### Z110: STALE_ALLOWLIST_ENTRY {#z110} -**Severity:** `warning` · **Penalty:** −1.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes +**Severity:** `warning` · **Penalty:** −1.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z110.md) An entry in the `absolute_path_allowlist` configuration was never matched by any scanned absolute path link. This indicates that the entry is stale and no longer needed. @@ -261,7 +303,7 @@ An entry in the `absolute_path_allowlist` configuration was never matched by any ### Z111: VIRTUAL_ROUTE_BROKEN {#z111} -**Severity:** `error` · **Penalty:** −8.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes +**Severity:** `error` · **Penalty:** −8.0 pt (Structural) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z111.md) Link targets a virtual route (tag page, paginated index, author profile) that was never generated by any frontmatter. @@ -283,7 +325,7 @@ Duplicate author key declared across two or more blog author config files. ### Z114: LARGE_PAGINATION_SET {#z114} -**Severity:** `note` · **Penalty:** 0.0 pts · **Exit:** 0 · **Suppressible:** Yes +**Severity:** `note` · **Penalty:** 0.0 pts · **Exit:** 0 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z114.md) Blog pagination set exceeds the 200-page informational threshold. @@ -291,7 +333,7 @@ Blog pagination set exceeds the 200-page informational threshold. ### Z118: STALE_GLOBAL_SUPPRESSION {#z118} -**Severity:** `warning` · **Penalty:** −1.0 pt (Governance) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z118-stale-global-suppression.md) +**Severity:** `warning` · **Penalty:** −1.0 pt (Governance) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z1xx-links/z118-stale-global-suppression.md) · [↗ Rule Specification](../rules/Z118.md) An entry in `directory_policies`, `excluded_file_patterns`, or `excluded_external_urls` inside `.zenzic.toml` was never utilised to suppress an active finding. This indicates configuration debt. @@ -349,7 +391,7 @@ An HTML link has an opaque context or relies on inline scripts for navigation. !!! danger "🔒 INVIOLABLE — Cannot be suppressed | Exit 2 | DQS collapses to 0/100" `zenzic:ignore: Z201` is **silently rejected**. The credential scanner fires unconditionally on every line. [↗ Gallery](../tutorials/examples/z2xx-security/z201-credentials.md) -**Severity:** `security_breach` · **Penalty:** DQS collapses to 0/100 · **Exit:** 2 +**Severity:** `security_breach` · **Penalty:** DQS collapses to 0/100 · **Exit:** 2 · [↗ Rule Specification](../rules/Z201.md) The credential scanner uses deterministic pattern matching (e.g., RE2) to detect known structural secrets (like AWS keys or GitHub tokens) without exponential backtracking, rather than relying on high-noise entropy checks. Speculative Base64 decoding is also applied — encoded tokens that decode to credential patterns are flagged. @@ -365,7 +407,7 @@ The credential scanner uses deterministic pattern matching (e.g., RE2) to detect !!! danger "🔒 INVIOLABLE — Cannot be suppressed | Exit 1 | DQS collapses to 0/100" `zenzic:ignore: Z202` is **silently rejected**. [↗ Gallery](../tutorials/examples/z2xx-security/z202-path-traversal.md) -**Severity:** `error` · **Penalty:** DQS collapses to 0/100 · **Exit:** 1 +**Severity:** `error` · **Penalty:** DQS collapses to 0/100 · **Exit:** 1 · [↗ Rule Specification](../rules/Z202.md) The Path Traversal Guard intercepts any relative links attempting to escape the documentation root (e.g. `../.env`). A relative path uses `..` segments to escape the `docs/` boundary, potentially exposing private repository files. @@ -379,7 +421,7 @@ The Path Traversal Guard intercepts any relative links attempting to escape the !!! danger "🔒 INVIOLABLE — Cannot be suppressed | Exit 3 (highest) | DQS collapses to 0/100" `zenzic:ignore: Z203` is **silently rejected**. Distinct from Z202: targets OS directories (`/etc/`, `/root/`) signalling supply-chain compromise. -**Severity:** `security_incident` · **Penalty:** DQS collapses to 0/100 · **Exit:** 3 +**Severity:** `security_incident` · **Penalty:** DQS collapses to 0/100 · **Exit:** 3 · [↗ Rule Specification](../rules/Z203.md) Path traversal detected targeting restricted OS directories (e.g. `/etc/`, `/root/`). Cannot result from a legitimate documentation workflow — presence indicates template injection, a compromised toolchain, or a malicious commit. @@ -394,7 +436,7 @@ Path traversal detected targeting restricted OS directories (e.g. `/etc/`, `/roo !!! danger "🔒 INVIOLABLE — Cannot be suppressed | Exit 2 | DQS collapses to 0/100" `zenzic:ignore: Z204` is **silently rejected**. Source: `forbidden_patterns` in `.zenzic.local.toml` (git-ignored). [↗ Gallery](../tutorials/examples/z2xx-security/z204-forbidden-term.md) -**Severity:** `security_breach` · **Penalty:** DQS collapses to 0/100 · **Exit:** 2 +**Severity:** `security_breach` · **Penalty:** DQS collapses to 0/100 · **Exit:** 2 · [↗ Rule Specification](../rules/Z204.md) The Privacy Gate detected a confidential project term (internal code-name, staging hostname, team alias) configured in `.zenzic.local.toml`. Matching is case-insensitive verbatim substring — no regex. Run `zenzic init` to scaffold `.zenzic.local.toml` (auto-added to `.gitignore`). @@ -428,9 +470,11 @@ The Polyglot Extractor detected a highly dangerous scheme (such as `javascript:` ## Z3xx — Reference Integrity +Diagnostic findings related to reference-style links and definition definitions. + ### Z301: DANGLING_REF {#z301} -**Severity:** `error` · **Penalty:** −4.0 pts (Navigation) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z3xx-references/z301-dangling-ref.md) +**Severity:** `error` · **Penalty:** −4.0 pts (Navigation) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z3xx-references/z301-dangling-ref.md) · [↗ Rule Specification](../rules/Z301.md) A reference-style link (`[my link][ref]`) exists but its definition (`[ref]: http://...`) is missing. Most renderers silently degrade the link to plain text. Ensure your Markdown formatter (like Prettier or Markdownlint) does not inadvertently remove unused reference definitions during an automated pass, which can cause downstream references to dangle. @@ -441,7 +485,7 @@ A reference-style link (`[my link][ref]`) exists but its definition (`[ref]: htt ### Z302: DEAD_DEF {#z302} -**Severity:** `warning` · **Penalty:** −1.0 pt (Navigation) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z3xx-references/z302-dead-def.md) +**Severity:** `warning` · **Penalty:** −1.0 pt (Navigation) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z3xx-references/z302-dead-def.md) · [↗ Rule Specification](../rules/Z302.md) A reference definition exists but no link in the file uses it. Harmless for readers but creates maintenance debt. @@ -449,7 +493,7 @@ A reference definition exists but no link in the file uses it. Harmless for read ### Z303: DUPLICATE_DEF {#z303} -**Severity:** `warning` · **Penalty:** −3.0 pts (Navigation) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z3xx-references/z303-duplicate-def.md) +**Severity:** `warning` · **Penalty:** −3.0 pts (Navigation) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z3xx-references/z303-duplicate-def.md) · [↗ Rule Specification](../rules/Z303.md) Multiple definitions exist for the same reference ID. CommonMark specifies that the first definition wins, but this ambiguity should be resolved for deterministic cross-engine rendering. @@ -459,9 +503,11 @@ Multiple definitions exist for the same reference ID. CommonMark specifies that ## Z4xx — Structure +Diagnostic findings related to Virtual Site Map topology and navigation structure. + ### Z401: MISSING_DIRECTORY_INDEX {#z401} -**Severity:** `info` · **Penalty:** none (structural hint) · **Exit:** 0 · **Suppressible:** Yes +**Severity:** `info` · **Penalty:** none (structural hint) · **Exit:** 0 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z401.md) A documentation directory has no `index.md` or `README.md`. The directory URL may return 404 or a raw listing depending on the build engine. @@ -469,7 +515,7 @@ A documentation directory has no `index.md` or `README.md`. The directory URL ma ### Z402: ORPHAN_PAGE {#z402} -**Severity:** `warning` · **Penalty:** −4.0 pts (Navigation) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z4xx-topology/z402-orphan-page.md) +**Severity:** `warning` · **Penalty:** −4.0 pts (Navigation) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z4xx-topology/z402-orphan-page.md) · [↗ Rule Specification](../rules/Z402.md) A file exists in `docs/` but is not reachable from any navigation menu. The documentation equivalent of dead code. @@ -480,7 +526,7 @@ A file exists in `docs/` but is not reachable from any navigation menu. The docu ### Z403: MISSING_ALT {#z403} -**Severity:** `warning` · **Penalty:** none (accessibility warning) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z4xx-topology/z403-missing-alt.md) +**Severity:** `warning` · **Penalty:** none (accessibility warning) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z4xx-topology/z403-missing-alt.md) · [↗ Rule Specification](../rules/Z403.md) An image has no alt text, degrading screen reader accessibility and SEO. @@ -488,7 +534,7 @@ An image has no alt text, degrading screen reader accessibility and SEO. ### Z404: CONFIG_ASSET_MISSING {#z404} -**Severity:** `warning` · **Penalty:** none (configuration integrity warning) · **Exit:** 1 · **Suppressible:** Yes +**Severity:** `warning` · **Penalty:** none (configuration integrity warning) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z404.md) The build engine's main configuration (e.g. `zensical.toml`) references a logo or favicon that does not exist at the specified path. The failure is global: every page in every locale ships without the branding asset. @@ -499,7 +545,7 @@ The build engine's main configuration (e.g. `zensical.toml`) references a logo o ### Z405: UNUSED_ASSET {#z405} -**Severity:** `warning` · **Penalty:** −3.0 pts (Governance) · **Exit:** 1 · **Suppressible:** Yes +**Severity:** `warning` · **Penalty:** −3.0 pts (Governance) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z405.md) An image or asset file in the repository is never referenced by any Markdown file. "Dark Assets" bloat the repository and build artifacts silently. @@ -513,7 +559,7 @@ An image or asset file in the repository is never referenced by any Markdown fil ### Z406: NAV_CONTRACT {#z406} -**Severity:** `error` · **Penalty:** −2.0 pts (Governance) · **Exit:** 1 · **Suppressible:** Yes +**Severity:** `error` · **Penalty:** −2.0 pts (Governance) · **Exit:** 1 · **Suppressible:** Yes · [↗ Rule Specification](../rules/Z406.md) A conflict between the physical file structure and the engine's navigation config. For MkDocs: a `nav` entry pointing to a path that no physical file activates. @@ -526,9 +572,11 @@ A conflict between the physical file structure and the engine's navigation confi ## Z5xx — Content Quality +Diagnostic findings related to prose quality, sentence length, and structural formatting. + ### Z501: PLACEHOLDER {#z501} -**Severity:** `warning` · **Penalty:** −2.0 pts (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z501-placeholder.md) +**Severity:** `warning` · **Penalty:** −2.0 pts (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z501-placeholder.md) · [↗ Rule Specification](../rules/Z501.md) Placeholder strings committed to production documentation signal incomplete work. Examples include: @@ -542,7 +590,7 @@ FIXME ### Z502: SHORT_CONTENT {#z502} -**Severity:** `warning` · **Penalty:** −1.0 pt (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z502-short-content.md) +**Severity:** `warning` · **Penalty:** −1.0 pt (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z502-short-content.md) · [↗ Rule Specification](../rules/Z502.md) A page contains fewer than 50 words of rendered prose (frontmatter, Markdown comments, and HTML comments excluded). A page below this threshold cannot contain the semantic components necessary to answer a reader's question. @@ -550,7 +598,7 @@ A page contains fewer than 50 words of rendered prose (frontmatter, Markdown com ### Z503: SNIPPET_ERROR {#z503} -**Severity:** `error` · **Penalty:** −10.0 pts (Content — highest single-occurrence penalty) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z503-snippet-error.md) +**Severity:** `error` · **Penalty:** −10.0 pts (Content — highest single-occurrence penalty) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z503-snippet-error.md) · [↗ Rule Specification](../rules/Z503.md) The Snippet Guard identified a syntax error in a fenced code block marked with a language tag. The reported line number is **absolute** — relative to the source file, not to the start of the snippet. @@ -567,7 +615,7 @@ The Snippet Guard identified a syntax error in a fenced code block marked with a **Z503 (error — do not do this):** ````markdown - ```python + ```text my_function( param: str, other: int = 0, @@ -592,7 +640,7 @@ The Snippet Guard identified a syntax error in a fenced code block marked with a ### Z505: UNTAGGED_CODE_BLOCK {#z505} -**Severity:** `warning` · **Penalty:** −1.0 pt (Content) · **Exit:** 1 · **Suppressible:** Yes · **Fixable:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z505-untagged-code-block.md) +**Severity:** `warning` · **Penalty:** −1.0 pt (Content) · **Exit:** 1 · **Suppressible:** Yes · **Fixable:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z505-untagged-code-block.md) · [↗ Rule Specification](../rules/Z505.md) A fenced code block has no language specifier. Syntax highlighters, the Snippet Guard (Z503), and screen readers cannot process it. Some engine-specific metadata (e.g. `` ```python title="file.py" showLineNumbers ``) is fully supported and never flagged. @@ -600,7 +648,7 @@ A fenced code block has no language specifier. Syntax highlighters, the Snippet ### Z506: MALFORMED_FRONTMATTER {#z506} -**Severity:** `error` · **Penalty:** −5.0 pts (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z506-malformed-frontmatter.md) +**Severity:** `error` · **Penalty:** −5.0 pts (Content) · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z5xx-content/z506-malformed-frontmatter.md) · [↗ Rule Specification](../rules/Z506.md) The opening frontmatter delimiter on line 1 of the file is not exactly `---`. Any line that starts with two or more dashes but is not exactly `---` — such as `--`, `----`, or `--- trailing chars` — is silently discarded by most static-site engines. The `template:`, `title:`, and all metadata keys will be rendered as raw prose content instead of being parsed. @@ -616,9 +664,11 @@ The opening frontmatter delimiter on line 1 of the file is not exactly `---`. An ## Z6xx — Governance +Diagnostic findings related to brand governance, deprecation, and suppression audit state. + ### Z601: BRAND_OBSOLESCENCE {#z601} -**Severity:** `warning` · **Penalty:** −2.0 pts (Governance) + Escalation · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z6xx-brand/z601-brand-obsolescence.md) +**Severity:** `warning` · **Penalty:** −2.0 pts (Governance) + Escalation · **Exit:** 1 · **Suppressible:** Yes · [↗ Gallery](../tutorials/examples/z6xx-brand/z601-brand-obsolescence.md) · [↗ Rule Specification](../rules/Z601.md) A deprecated release name or brand identifier appears in a scanned file. Configured via `[governance].brand_obsolescence` in `.zenzic.toml`. CHANGELOG files are exempt by default (`obsolete_names_exclude_patterns`). @@ -633,7 +683,7 @@ A deprecated release name or brand identifier appears in a scanned file. Configu ### Z603: DEAD_SUPPRESSION {#z603} -**Severity:** `warning` · **Penalty:** −1.0 pt (Governance) · **Exit:** 1 · **Suppressible:** Yes · **Fixable:** Yes · [↗ Gallery](../tutorials/examples/z6xx-brand/z603-dead-suppression.md) +**Severity:** `warning` · **Penalty:** −1.0 pt (Governance) · **Exit:** 1 · **Suppressible:** Yes · **Fixable:** Yes · [↗ Gallery](../tutorials/examples/z6xx-brand/z603-dead-suppression.md) · [↗ Rule Specification](../rules/Z603.md) An inline suppression directive (``) does not correspond to any active finding on that line. The directive silences nothing — it is **Phantom Debt** that consumes part of the 30-point governance budget without justification. @@ -667,6 +717,8 @@ any active finding. Remove the dead comment. ## Z9xx — Engine & System +System-level diagnostic findings and worker watchdog execution alerts. + ### Z901: RULE_ENGINE_ERROR {#z901} **Severity:** `error` · **Penalty:** none (system-level) · **Exit:** 1 · **Suppressible:** Yes @@ -707,7 +759,7 @@ No `.md` / `.md` files found in the resolved `docs_root` after all exclusion lay ### Z504: QUALITY_REGRESSION {#z504} -**Severity:** `warning` *(reserved)* +**Severity:** `warning` *(reserved)* · [↗ Rule Specification](../rules/Z504.md) Emitted by `zenzic diff` when the current DQS is lower than the saved baseline (`.zenzic-score.json`). Not itself weighted into the score (that would be circular); it identifies which commit introduced a regression. diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 3dc13405..ca40c8a3 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -13,6 +13,8 @@ This glossary provides precise definitions for all domain-specific terms used in ## Terms +Glossary of key technical and domain concepts in alphabetical order. + ### Adapter {#adapter} A build-engine-specific module that implements the `BaseAdapter` protocol. Adapters translate between a documentation engine's file conventions (nav structure, locale directories, URL mapping) and Zenzic's engine-agnostic core. Built-in adapters: `MkDocsAdapter`, `ZensicalAdapter`, `StandaloneAdapter`. Third-party adapters can be registered via the `zenzic.adapters` entry-point group. diff --git a/docs/reference/scoring-algorithm.md b/docs/reference/scoring-algorithm.md index 457a6939..5092fe8c 100644 --- a/docs/reference/scoring-algorithm.md +++ b/docs/reference/scoring-algorithm.md @@ -213,7 +213,7 @@ $$ ## See Also {#see-also} -- [Scoring Design](../explanation/scoring-design.md) — Worked example, CLI output interpretation, and governance posture semantics +- [Scoring Design](../explanation/scoring-design.md) — Worked example, CLI output interpretation, and governance posture semantics. - [Suppression Policy](./suppression-policy) — Three suppression levels, debt formula, and the `--audit` override - [Finding Codes](./finding-codes) — Full encyclopedia of Zxxx codes with remediation steps - [Handle Technical Debt](../how-to/handle-technical-debt) — Step-by-step remediation workflow diff --git a/docs/reference/suppression-policy.md b/docs/reference/suppression-policy.md index 6f131d3d..096ef534 100644 --- a/docs/reference/suppression-policy.md +++ b/docs/reference/suppression-policy.md @@ -114,3 +114,10 @@ The CLI and CI pipelines report active debt state in the audit footer: ``` If active suppressions exceed `suppression_cap`, Zenzic emits `[CAP_EXCEEDED]` and fails the quality gate with **Exit 1**. + +--- + +## Related Specifications + +- [Finding Codes Catalog](./finding-codes.md) — Comprehensive reference of all `Zxxx` diagnostic codes. +- [Managing Technical Debt](../how-to/handle-technical-debt.md) — How-to guide for applying directory policies and per-file ignores. diff --git a/docs/rules/Z000.md b/docs/rules/Z000.md new file mode 100644 index 00000000..afa77858 --- /dev/null +++ b/docs/rules/Z000.md @@ -0,0 +1,47 @@ +--- +title: "Z000: UNSUPPORTED_ENGINE" +--- + + + + +**Severity**: `error` +**Penalty**: 0.0 (Fatal) +**Category**: `config` +**Auto-fixable**: No + +## Rationale + +This rule is emitted during configuration initialization when `.zenzic.toml` specifies an `engine` identifier that is unknown, deprecated, or no longer supported by the Zenzic Core Engine. + +Operating with an unsupported engine identifier breaks Virtual Site Map (VSM) route resolution, routing plugins, and navigation tree compilation because Zenzic cannot instantiate the matching AST build adapter. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z000) + +```toml +# .zenzic.toml - BAD: Unknown or deprecated engine specifier +engine = "hugo_v1_legacy" +``` + +### Good (Resolves Z000) + +```toml +# .zenzic.toml - GOOD: Use a supported engine target +engine = "mkdocs" +``` + +## Configuration + +```toml +# .zenzic.toml +# Supported values include "mkdocs", "docusaurus", or omit to auto-discover +engine = "mkdocs" +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z000) for finding code details. diff --git a/docs/rules/Z001.md b/docs/rules/Z001.md new file mode 100644 index 00000000..26865f67 --- /dev/null +++ b/docs/rules/Z001.md @@ -0,0 +1,51 @@ +--- +title: "Z001: CORE_CONFIG_STRUCTURE" +--- + + + + +**Severity**: `error` +**Penalty**: 0.0 (Fatal) +**Category**: `config` +**Auto-fixable**: No + +## Rationale + +This rule is emitted prior to document scanning when `.zenzic.toml` contains invalid TOML syntax, malformed table headers, unclosed quotes, or data type mismatches (such as supplying a string where a boolean or array is expected). + +An invalid configuration halts execution immediately (`Exit 1`) before any filesystem scan begins to prevent unpredictable behavior across the workspace. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z001) + +```text +# .zenzic.toml - BAD: String supplied to boolean setting & unclosed header +strict = "yes" +[governance.directory_policies +``` + +### Good (Resolves Z001) + +```toml +# .zenzic.toml - GOOD: Valid TOML data types and headers +strict = true + +[governance.directory_policies] +"docs/blog/**" = ["Z410"] +``` + +## Configuration + +```toml +# Validate configuration with standard TOML syntax rules +strict = true +fail_under = 90 +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z001) for finding code details. diff --git a/docs/rules/Z101.md b/docs/rules/Z101.md new file mode 100644 index 00000000..1f43e615 --- /dev/null +++ b/docs/rules/Z101.md @@ -0,0 +1,48 @@ +--- +title: "Z101: LINK_BROKEN" +--- + + + + +**Severity**: `error` +**Penalty**: 8.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Rationale + +This rule validates every relative Markdown link against the Virtual Site Map (VSM) rather than just checking filesystem disk presence. A finding is emitted when a target page does not exist in the VSM or is unserved by the build engine. + +Linking to non-existent target files or unrendered routes produces broken HTTP 404 navigation errors for end users upon site publication. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z101) + +```markdown + +Please refer to our [Installation Guide](../setup/installation.md) for details. +``` + +### Good (Resolves Z101) + +```markdown + +Please refer to our [Installation Guide](../how-to/install.md) for details. +``` + +## Configuration + +```toml +# .zenzic.toml - Exclude specific external domain URLs from broken link checks +excluded_external_urls = [ + "https://github.com/PythonWoods/zenzic", +] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z101) for finding code details. diff --git a/docs/rules/Z102.md b/docs/rules/Z102.md new file mode 100644 index 00000000..0c498dfe --- /dev/null +++ b/docs/rules/Z102.md @@ -0,0 +1,48 @@ +--- +title: "Z102: ANCHOR_MISSING" +--- + + + + +**Severity**: `error` +**Penalty**: 5.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Rationale + +This rule is emitted when an inline link targets a fragment identifier (`#anchor-name`), but the destination document contains no heading slug or explicit anchor ID matching that fragment. + +Fragment links pointing to missing anchors navigate to the page top but fail to jump to the intended topic section, degrading reading experience. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z102) + +```markdown + +See the [Security Gate Options](../how-to/configure-privacy-gate.md#security-gate). +``` + +### Good (Resolves Z102) + +```markdown + +## Security Options {#security-gate} + +Configure privacy gate settings below... +``` + +## Configuration + +```toml +# .zenzic.toml +validate_same_page_anchors = true +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z102) for finding code details. diff --git a/docs/rules/Z103.md b/docs/rules/Z103.md new file mode 100644 index 00000000..c03356c3 --- /dev/null +++ b/docs/rules/Z103.md @@ -0,0 +1,47 @@ +--- +title: "Z103: ORPHAN_LINK" +--- + + + + +**Severity**: `error` +**Penalty**: 2.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Rationale + +This rule is triggered when a link references a Markdown file that exists on the local filesystem disk but is omitted from the site navigation tree (`mkdocs.yml`). + +While the target file is physically present, the static site generator will not build or serve it. Linking to it creates a dead-end experience for users. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z103) + +```markdown + +Check the [Draft Architecture](../../drafts/arch-v2.md). +``` + +### Good (Resolves Z103) + +```markdown + +Check the [Architecture Explanation](../explanation/architecture.md). +``` + +## Configuration + +```yaml +# mkdocs.yml - Ensure target document is listed under nav +nav: + - Architecture: explanation/architecture.md +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z103) for finding code details. diff --git a/docs/rules/Z104.md b/docs/rules/Z104.md new file mode 100644 index 00000000..87b610fb --- /dev/null +++ b/docs/rules/Z104.md @@ -0,0 +1,44 @@ +--- +title: "Z104: FILE_NOT_FOUND" +--- + + + + +**Severity**: `error` +**Penalty**: 8.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Rationale + +This rule is emitted when a document references a local binary or image asset (such as a PDF, ZIP archive, or PNG file) using a relative path, but the file is missing from the local workspace filesystem. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z104) + +```markdown + +![System Topology](../assets/diagrams/topology-v2.png) +``` + +### Good (Resolves Z104) + +```markdown + +![System Topology](../assets/diagrams/topology.png) +``` + +## Configuration + +```toml +# .zenzic.toml - Set relative docs root directory +docs_dir = "docs" +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z104) for finding code details. diff --git a/docs/rules/Z105.md b/docs/rules/Z105.md new file mode 100644 index 00000000..b798ea96 --- /dev/null +++ b/docs/rules/Z105.md @@ -0,0 +1,46 @@ +--- +title: "Z105: ABSOLUTE_PATH" +--- + + + + +**Severity**: `error` +**Penalty**: 2.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Rationale + +This rule is triggered when internal links or asset paths use absolute filesystem paths or root-relative paths (e.g. `/docs/setup.md` or `C:\project\docs\setup.md`). + +Absolute paths break portability across different operating systems, CI build runners, and subpath web deployments. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z105) + +```markdown + +Read the [Setup Guide](/docs/how-to/setup.md). +``` + +### Good (Resolves Z105) + +```markdown + +Read the [Setup Guide](../how-to/setup.md). +``` + +## Configuration + +```toml +# Enforced across workspace when strict mode is active +strict = true +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z105) for finding code details. diff --git a/docs/rules/Z106.md b/docs/rules/Z106.md new file mode 100644 index 00000000..961e63f1 --- /dev/null +++ b/docs/rules/Z106.md @@ -0,0 +1,48 @@ +--- +title: "Z106: CIRCULAR_LINK" +--- + + + + +**Severity**: `note` +**Penalty**: 0.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Rationale + +This rule is an informational telemetry signal emitted when two or more documentation pages form a direct circular navigation loop through relative links. + +While not fatal to static site rendering, cyclic navigation loops can confuse readers and degrade automated search engine crawler traversal. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z106) + +```markdown + +See [Section B](page-b.md). + + +Return to [Section A](page-a.md). +``` + +### Good (Resolves Z106) + +```markdown + +See [Section B](page-b.md). +``` + +## Configuration + +```toml +# Informational diagnostic; no point deduction subtracted from DQS +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z106) for finding code details. diff --git a/docs/rules/Z107.md b/docs/rules/Z107.md new file mode 100644 index 00000000..6e0ea409 --- /dev/null +++ b/docs/rules/Z107.md @@ -0,0 +1,45 @@ +--- +title: "Z107: CIRCULAR_ANCHOR" +--- + + + + +**Severity**: `error` +**Penalty**: 1.0 point +**Category**: `structural` +**Auto-fixable**: No + +## Rationale + +This rule is emitted when an anchor link's visible text label slugifies to the exact same string as its own fragment identifier, creating a redundant self-referential jump link that points to its current position. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z107) + +```markdown + +[#installation](#installation) +``` + +### Good (Resolves Z107) + +```markdown + +[Jump to Installation Guide](#installation) +``` + +## Configuration + +```toml +# Can be ignored for didactic tutorial pages via directory_policies +[governance.directory_policies] +"docs/tutorials/examples/z1xx-links/**" = ["Z107"] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z107) for finding code details. diff --git a/docs/rules/Z108.md b/docs/rules/Z108.md new file mode 100644 index 00000000..9199fab4 --- /dev/null +++ b/docs/rules/Z108.md @@ -0,0 +1,46 @@ +--- +title: "Z108: EMPTY_LINK_TEXT" +--- + + + + +**Severity**: `error` +**Penalty**: 1.0 point +**Category**: `structural` +**Auto-fixable**: Yes + +## Rationale + +This rule is emitted when an inline link tag `[...]` contains empty brackets or whitespace-only text. + +Links without accessible text labels violate web accessibility standards (WCAG 2.1) and screen readers cannot describe the link destination to visually impaired users. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z108) + +```markdown + +[](https://zenzic.dev/docs) +``` + +### Good (Resolves Z108) + +```markdown + +[Zenzic Documentation Catalog](https://zenzic.dev/docs) +``` + +## Configuration + +```bash +# Automatically fix empty link text across workspace +zenzic fix --only Z108 +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z108) for finding code details. diff --git a/docs/rules/Z109.md b/docs/rules/Z109.md new file mode 100644 index 00000000..70b569cd --- /dev/null +++ b/docs/rules/Z109.md @@ -0,0 +1,47 @@ +--- +title: "Z109: EXTERNAL_LINK_BROKEN" +--- + + + + +**Severity**: `error` +**Penalty**: 3.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Rationale + +This rule is triggered during strict audits when a remote HTTP/HTTPS link returns a 4xx/5xx HTTP status code or fails due to a network connection timeout or DNS resolution error. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z109) + +```markdown + +Refer to [External Specifications](https://example.invalid/broken-spec). +``` + +### Good (Resolves Z109) + +```markdown + +Refer to [External Specifications](https://example.com/valid-spec). +``` + +## Configuration + +```toml +# .zenzic.toml - Ignore rate-limited or private external domain URLs +excluded_external_urls = [ + "https://github.com", + "https://api.github.com", +] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z109) for finding code details. diff --git a/docs/rules/Z110.md b/docs/rules/Z110.md new file mode 100644 index 00000000..0bb9ef61 --- /dev/null +++ b/docs/rules/Z110.md @@ -0,0 +1,39 @@ +--- +title: "Z110: CONFIG_SYNTAX_ERROR" +--- + + + + +**Severity**: `error` +**Penalty**: Fatal Configuration Error (Analysis Bypassed) +**Category**: `configuration` +**Auto-fixable**: No +**Suppression**: Non-suppressible + +## Description + +The `.zenzic.toml` (or `pyproject.toml`) workspace configuration file contains a malformed TOML syntax error. + +When a configuration file is syntactically invalid, Zenzic halts Markdown document analysis to prevent cascading false positives or running with corrupted governance policies. + +## How to Fix + +Fix the TOML syntax error in `.zenzic.toml` indicated in the diagnostic message (such as missing quotes, unclosed brackets, or unescaped characters). + +## Example Error + +```text +# Malformed TOML +placeholder_max_words = [ 50, +``` + +Corrected `.zenzic.toml`: + +```toml +placeholder_max_words = 50 +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z110) for finding code details. diff --git a/docs/rules/Z111.md b/docs/rules/Z111.md new file mode 100644 index 00000000..81dd270b --- /dev/null +++ b/docs/rules/Z111.md @@ -0,0 +1,39 @@ +--- +title: "Z111: CONFIG_SCHEMA_ERROR" +--- + + + + +**Severity**: `error` +**Penalty**: Fatal Configuration Error (Analysis Bypassed) +**Category**: `configuration` +**Auto-fixable**: No +**Suppression**: Non-suppressible + +## Description + +The `.zenzic.toml` (or `pyproject.toml`) configuration file contains an invalid schema structure, unknown key, or data type mismatch (e.g. providing a string instead of an integer). + +When configuration validation fails, Zenzic halts document analysis to protect workspace integrity. + +## How to Fix + +Correct the configuration key name or value type in `.zenzic.toml` as indicated by the field error in the diagnostic message. + +## Example Error + +```toml +# Invalid string type for integer setting +placeholder_max_words = "fifty" +``` + +Corrected `.zenzic.toml`: + +```toml +placeholder_max_words = 50 +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z111) for finding code details. diff --git a/docs/rules/Z114.md b/docs/rules/Z114.md new file mode 100644 index 00000000..d8620d5f --- /dev/null +++ b/docs/rules/Z114.md @@ -0,0 +1,43 @@ +--- +title: "Z114: LARGE_PAGINATION_SET" +--- + + + + +**Severity**: `note` +**Penalty**: 0.0 points +**Category**: `content` +**Auto-fixable**: No + +## Rationale + +This rule is an informational diagnostic emitted when a blog post directory or category archive generates a pagination set exceeding the recommended threshold (default: 200 pages). + +Massive pagination sets increase site build times and RAM usage during static site generator rendering. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z114) + +```text +docs/blog/posts/ -> Generates 250 un-categorized pagination index pages. +``` + +### Good (Resolves Z114) + +```text +docs/blog/posts/ -> Reorganize blog posts into categorized year/tag subfolders. +``` + +## Configuration + +```toml +# Informational diagnostic; no point deduction +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z114) for finding code details. diff --git a/docs/rules/Z118.md b/docs/rules/Z118.md new file mode 100644 index 00000000..f138568d --- /dev/null +++ b/docs/rules/Z118.md @@ -0,0 +1,49 @@ +--- +title: "Z118: STALE_GLOBAL_SUPPRESSION" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `governance` +**Auto-fixable**: No + +## Rationale + +This rule is emitted when a global suppression rule declared in `.zenzic.toml` (`directory_policies` or `per_file_ignores`) never matched any violation during a full workspace scan. + +Stale suppressions mask real configuration intent and violate the Zero-DBT invariant by accumulating dead technical debt. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z118) + +```toml +# .zenzic.toml - BAD: Z405 is suppressed but no Z405 violations exist in blog +[governance.directory_policies] +"docs/blog/**" = ["Z405", "Z410"] +``` + +### Good (Resolves Z118) + +```toml +# .zenzic.toml - GOOD: Remove stale finding code from directory_policies +[governance.directory_policies] +"docs/blog/**" = ["Z410"] +``` + +## Configuration + +```toml +# Audit and remove stale entries under [governance.directory_policies] +[governance.directory_policies] +"docs/blog/**" = ["Z410", "Z411"] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z118) for finding code details. diff --git a/docs/rules/Z201.md b/docs/rules/Z201.md new file mode 100644 index 00000000..df9daf00 --- /dev/null +++ b/docs/rules/Z201.md @@ -0,0 +1,45 @@ +--- +title: "Z201: CREDENTIAL_SECRET" +--- + + + + +**Severity**: `error` +**Penalty**: 0.0 (Fatal Exit 2) +**Category**: `security` +**Auto-fixable**: No + +## Rationale + +This security breach rule (Exit 2) is emitted when the Polyglot Credential Scanner detects private keys, API tokens, AWS keys, or passwords embedded in Markdown prose or code blocks. + +Hardcoding secrets in documentation source files exposes infrastructure credentials to public repository leaks. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z201) + +```python +# BAD: Real credential hardcoded in documentation snippet +aws_secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +``` + +### Good (Resolves Z201) + +```python +# GOOD: Use environment variables or sanitized placeholders +aws_secret = os.environ["AWS_SECRET_ACCESS_KEY"] +``` + +## Configuration + +```toml +# Non-suppressible security code. Triggers Security Override (DQS -> 0/100). +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z201) for finding code details. diff --git a/docs/rules/Z202.md b/docs/rules/Z202.md new file mode 100644 index 00000000..7dda38ba --- /dev/null +++ b/docs/rules/Z202.md @@ -0,0 +1,44 @@ +--- +title: "Z202: PATH_TRAVERSAL" +--- + + + + +**Severity**: `error` +**Penalty**: 0.0 (Fatal Exit 2) +**Category**: `security` +**Auto-fixable**: No + +## Rationale + +This security rule is triggered when a relative link or asset reference uses parent directory traversal sequences (`../`) to escape the root boundary of the designated documentation workspace (`docs_dir`). + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z202) + +```markdown + +Download [Internal Secret](../../../secrets/keys.json). +``` + +### Good (Resolves Z202) + +```markdown + +Download [Public Keys](../assets/public-keys.json). +``` + +## Configuration + +```toml +# Workspace boundary enforced by docs_dir setting in .zenzic.toml +docs_dir = "docs" +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z202) for finding code details. diff --git a/docs/rules/Z203.md b/docs/rules/Z203.md new file mode 100644 index 00000000..529658a2 --- /dev/null +++ b/docs/rules/Z203.md @@ -0,0 +1,43 @@ +--- +title: "Z203: PATH_TRAVERSAL_FATAL" +--- + + + + +**Severity**: `error` +**Penalty**: 0.0 (Fatal Exit 3) +**Category**: `security` +**Auto-fixable**: No + +## Rationale + +This critical security incident rule (Exit 3) is emitted when a path specifier attempts to escape the repository workspace to access sensitive OS system directories (such as `/etc/passwd` or `/proc/`). + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z203) + +```markdown + +Read [System Passwd](../../../../../../etc/passwd). +``` + +### Good (Resolves Z203) + +```markdown + +Read [Local Configuration](../config/settings.json). +``` + +## Configuration + +```toml +# Non-suppressible security incident code; triggers Exit 3 immediate abort. +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z203) for finding code details. diff --git a/docs/rules/Z204.md b/docs/rules/Z204.md new file mode 100644 index 00000000..e7cb8d8e --- /dev/null +++ b/docs/rules/Z204.md @@ -0,0 +1,49 @@ +--- +title: "Z204: FORBIDDEN_TERM" +--- + + + + +**Severity**: `error` +**Penalty**: 0.0 (Fatal Exit 2) +**Category**: `security` +**Auto-fixable**: No + +## Rationale + +This security and governance rule is triggered when a prohibited string or restricted term (configured via `forbidden_patterns` in `.zenzic.toml`) is detected within documentation source files. + +It enforces brand compliance, inclusive language policies, and confidential keyword restrictions across all published pages. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z204) + +```markdown + +The system utilizes ProjectOmniInternal for backend routing. +``` + +### Good (Resolves Z204) + +```markdown + +The system utilizes Zenzic Core Router for backend routing. +``` + +## Configuration + +```toml +# .zenzic.toml - Configure forbidden term regex patterns +forbidden_patterns = [ + "(?i)\bProjectOmniInternal\b", + "(?i)\bconfidential_secret\b", +] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z204) for finding code details. diff --git a/docs/rules/Z301.md b/docs/rules/Z301.md new file mode 100644 index 00000000..da97bb4b --- /dev/null +++ b/docs/rules/Z301.md @@ -0,0 +1,45 @@ +--- +title: "Z301: DANGLING_REF" +--- + + + + +**Severity**: `warning` +**Penalty**: 4.0 points +**Category**: `navigation` +**Auto-fixable**: No + +## Rationale + +This rule is emitted when a Markdown reference link (for example, `[heading][my-ref]`) uses a shortcut label `[my-ref]` that has no matching definition `[my-ref]: URL` anywhere in the document. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z301) + +```markdown + +Follow our [Installation Guide][install-guide] to begin. +``` + +### Good (Resolves Z301) + +```markdown + +Follow our [Installation Guide][install-guide] to begin. + +[install-guide]: ../how-to/install.md +``` + +## Configuration + +```toml +# Evaluated automatically during reference link parsing pass +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z301) for finding code details. diff --git a/docs/rules/Z302.md b/docs/rules/Z302.md new file mode 100644 index 00000000..96c460b5 --- /dev/null +++ b/docs/rules/Z302.md @@ -0,0 +1,45 @@ +--- +title: "Z302: DEAD_DEF" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `navigation` +**Auto-fixable**: No + +## Rationale + +This rule is emitted when a reference link definition `[ref-id]: URL` is declared in a Markdown document but is never consumed by any reference link in that file. Unused link definitions clutter source files. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z302) + +```markdown + +[unused-link]: https://example.com/api +``` + +### Good (Resolves Z302) + +```markdown + +Check the [API Docs][unused-link]. + +[unused-link]: https://example.com/api +``` + +## Configuration + +```toml +# Evaluated during reference auditing pass +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z302) for finding code details. diff --git a/docs/rules/Z303.md b/docs/rules/Z303.md new file mode 100644 index 00000000..5f3a4d18 --- /dev/null +++ b/docs/rules/Z303.md @@ -0,0 +1,45 @@ +--- +title: "Z303: DUPLICATE_DEF" +--- + + + + +**Severity**: `warning` +**Penalty**: 3.0 points +**Category**: `navigation` +**Auto-fixable**: No + +## Rationale + +This rule is triggered when the same reference link label `[ref-id]: ...` is defined multiple times within a single Markdown source file with conflicting target URLs, creating ambiguous link resolution. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z303) + +```markdown + +[doc-ref]: https://example.com/v1 +[doc-ref]: https://example.com/v2 +``` + +### Good (Resolves Z303) + +```markdown + +[doc-ref-v1]: https://example.com/v1 +[doc-ref-v2]: https://example.com/v2 +``` + +## Configuration + +```toml +# Evaluated during reference definition parsing +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z303) for finding code details. diff --git a/docs/rules/Z401.md b/docs/rules/Z401.md new file mode 100644 index 00000000..7d6be6f7 --- /dev/null +++ b/docs/rules/Z401.md @@ -0,0 +1,41 @@ +--- +title: "Z401: MISSING_DIRECTORY_INDEX" +--- + + + + +**Severity**: `note` +**Penalty**: 0.0 points +**Category**: `navigation` +**Auto-fixable**: No + +## Rationale + +This rule is an informational diagnostic emitted in standalone directory mode when a documentation subfolder contains topic files but lacks a primary index file (`index.md` or `README.md`). + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z401) + +```text +docs/how-to/ -> Directory contains install.md but lacks index.md +``` + +### Good (Resolves Z401) + +```text +docs/how-to/ -> Contains index.md to serve as landing root. +``` + +## Configuration + +```toml +# Informational diagnostic; no points deducted from DQS +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z401) for finding code details. diff --git a/docs/rules/Z402.md b/docs/rules/Z402.md new file mode 100644 index 00000000..b6aa2e51 --- /dev/null +++ b/docs/rules/Z402.md @@ -0,0 +1,43 @@ +--- +title: "Z402: ORPHAN_PAGE" +--- + + + + +**Severity**: `warning` +**Penalty**: 4.0 points +**Category**: `navigation` +**Auto-fixable**: No + +## Rationale + +This rule is triggered when a Markdown source file resides in the `docs` directory but is not included in the site navigation tree manifest (`mkdocs.yml`). Orphan pages cannot be reached by users browsing site navigation. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z402) + +```text +docs/how-to/secret-guide.md -> File exists on disk but missing from mkdocs.yml +``` + +### Good (Resolves Z402) + +```yaml +# mkdocs.yml - Register file under navigation tree +nav: + - Secret Guide: how-to/secret-guide.md +``` + +## Configuration + +```yaml +# Registered under nav: in mkdocs.yml +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z402) for finding code details. diff --git a/docs/rules/Z403.md b/docs/rules/Z403.md new file mode 100644 index 00000000..b17ba654 --- /dev/null +++ b/docs/rules/Z403.md @@ -0,0 +1,43 @@ +--- +title: "Z403: MISSING_ALT" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `content` +**Auto-fixable**: No + +## Rationale + +This accessibility rule is triggered when an HTML or Markdown image tag (such as `![](image.png)` or ``) lacks descriptive alternative text (`alt`). Accessible image captions are required for screen readers. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z403) + +```text + +!\[\](../assets/diagram.png) +``` + +### Good (Resolves Z403) + +```text + +![System Architecture Diagram](../assets/diagram.png) +``` + +## Configuration + +```toml +# Audited across all Markdown image tags +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z403) for finding code details. diff --git a/docs/rules/Z404.md b/docs/rules/Z404.md new file mode 100644 index 00000000..2ef26e11 --- /dev/null +++ b/docs/rules/Z404.md @@ -0,0 +1,45 @@ +--- +title: "Z404: CONFIG_ASSET_MISSING" +--- + + + + +**Severity**: `warning` +**Penalty**: 3.0 points +**Category**: `brand` +**Auto-fixable**: No + +## Rationale + +This rule is emitted when a static theme or infrastructure asset path declared in engine configuration (such as `extra_css` or `favicon` in `mkdocs.yml`) cannot be found on the filesystem disk. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z404) + +```yaml +# mkdocs.yml - BAD: Asset file does not exist on disk +extra_css: + - assets/css/missing-styles.css +``` + +### Good (Resolves Z404) + +```yaml +# mkdocs.yml - GOOD: Asset file exists on disk +extra_css: + - assets/css/extra.css +``` + +## Configuration + +```yaml +# Governed by asset paths in mkdocs.yml +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z404) for finding code details. diff --git a/docs/rules/Z405.md b/docs/rules/Z405.md new file mode 100644 index 00000000..446ed58e --- /dev/null +++ b/docs/rules/Z405.md @@ -0,0 +1,42 @@ +--- +title: "Z405: UNUSED_ASSET" +--- + + + + +**Severity**: `warning` +**Penalty**: 3.0 points +**Category**: `brand` +**Auto-fixable**: No + +## Rationale + +This rule is triggered when an image or static asset file stored in `assets/` is never referenced by any Markdown document across the Virtual Site Map, leading to unnecessary repository bloat. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z405) + +```text +docs/assets/images/obsolete-logo.png -> File exists on disk but never linked in markdown. +``` + +### Good (Resolves Z405) + +```markdown + +![Project Logo](../assets/images/logo.png) +``` + +## Configuration + +```toml +# Asset scanning covers files in docs/assets/ +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z405) for finding code details. diff --git a/docs/rules/Z406.md b/docs/rules/Z406.md new file mode 100644 index 00000000..f588b128 --- /dev/null +++ b/docs/rules/Z406.md @@ -0,0 +1,45 @@ +--- +title: "Z406: NAV_CONTRACT" +--- + + + + +**Severity**: `warning` +**Penalty**: 2.0 points +**Category**: `brand` +**Auto-fixable**: No + +## Rationale + +This rule is emitted when the structure of `mkdocs.yml` navigation violates structural contract requirements, such as referencing non-existent target files or using invalid dictionary nesting. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z406) + +```yaml +# mkdocs.yml - BAD: Referencing non-existent markdown source file +nav: + - Tutorial: tutorials/non-existent.md +``` + +### Good (Resolves Z406) + +```yaml +# mkdocs.yml - GOOD: Referencing valid existing source file +nav: + - Tutorial: tutorials/first-audit.md +``` + +## Configuration + +```yaml +# Governed by nav: section in mkdocs.yml +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z406) for finding code details. diff --git a/docs/rules/Z410.md b/docs/rules/Z410.md new file mode 100644 index 00000000..39f2b8f1 --- /dev/null +++ b/docs/rules/Z410.md @@ -0,0 +1,46 @@ +--- +title: "Z410: UNREACHABLE_GRAPH_NODE" +--- + + + + +**Severity**: `warning` +**Penalty**: 5.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Description + +The document is isolated and unreachable from the navigation entry points of the documentation graph. + +A document is considered an orphan graph node if it is discovered in the filesystem by Zenzic, but no valid path of links connects it to the root configuration (e.g., the `nav` section in MkDocs or Zensical). + +While Z402 (`ORPHAN_PAGE`) checks if a page is listed in the top-level navigation, Z410 performs a deep topological Breadth-First Search (BFS) to ensure the document can be reached by a user clicking through links starting from the navigation entry points. + +## How to Fix + +To resolve this issue, you must integrate the document into the reachable graph: + +1. Add a link to the document from another document that is already reachable. +2. Add the document directly to the main navigation menu (`mkdocs.yml` or equivalent). + +## Suppression + +If the document is intentionally isolated (for example, a standalone template or an internal include that is not meant to be browsed), you can suppress this warning using an inline directive at the top of the file: + +```markdown + +``` + +Alternatively, you can ignore the file globally using directory policies in `.zenzic.toml`: + +```toml +[directory_policies.internal] +match = "internal/**" +ignore = ["Z410"] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z410) for finding code details. diff --git a/docs/rules/Z411.md b/docs/rules/Z411.md new file mode 100644 index 00000000..e23779ca --- /dev/null +++ b/docs/rules/Z411.md @@ -0,0 +1,46 @@ +--- +title: "Z411: DEAD_END_NODE" +--- + + + + +**Severity**: `warning` +**Penalty**: 5.0 points +**Category**: `structural` +**Auto-fixable**: No + +## Description + +The document has no outgoing links and forms a structural dead end in the documentation graph. + +A Dead End (Z411) occurs when a Markdown/MDX page does not link to any other resources. In a well-structured documentation graph, every page should ideally guide the user to further reading, related topics, or back to a main index. A page with zero outgoing links strands the reader. + +Terminal assets like images, CSS files, and PDFs are excluded from this rule. + +## How to Fix + +Add relevant outgoing links to the document to connect it to the rest of the documentation graph: + +1. Link to related concepts or further reading. +2. Link to a parent or index page to provide a path back. + +## Suppression + +If the document is intentionally designed as a dead end (for example, a terminal legal disclaimer or an auto-generated changelog with no internal references), you can suppress this warning using an inline directive at the top of the file: + +```markdown + +``` + +Alternatively, you can ignore the file globally using directory policies in `.zenzic.toml`: + +```toml +[directory_policies.legal] +match = "legal/**" +ignore = ["Z411"] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z411) for finding code details. diff --git a/docs/rules/Z501.md b/docs/rules/Z501.md new file mode 100644 index 00000000..cd8c0a40 --- /dev/null +++ b/docs/rules/Z501.md @@ -0,0 +1,52 @@ +--- +title: "Z501: PLACEHOLDER" +--- + + + + +**Severity**: `warning` +**Penalty**: 2.0 points +**Category**: `content` +**Auto-fixable**: No + +## Rationale + +This content quality rule is triggered when stub text or placeholder markers (such as `T-O-D-O`, `Lorem Ipsum`, or `F-I-X-M-E`) are detected within documentation prose intended for publication. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z501) + +```markdown + +## Configuration Steps + +TODO: Complete this section before publishing. +``` + +### Good (Resolves Z501) + +```markdown + +## Configuration Steps + +Set `strict = true` in `.zenzic.toml` to enforce zero-error pipeline execution. +``` + +## Configuration + +```toml +# .zenzic.toml - Configure placeholder patterns +placeholder_patterns = [ + "(?i)\bTODO\b", + "(?i)\bFIXME\b", + "(?i)\bLorem Ipsum\b", +] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z501) for finding code details. diff --git a/docs/rules/Z502.md b/docs/rules/Z502.md new file mode 100644 index 00000000..4f3dda1c --- /dev/null +++ b/docs/rules/Z502.md @@ -0,0 +1,46 @@ +--- +title: "Z502: SHORT_CONTENT" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `content` +**Auto-fixable**: No + +## Rationale + +This rule is triggered when a published documentation page contains fewer words than the required minimum word count threshold (default: 50 words), indicating incomplete stub documentation. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z502) + +```markdown +# Quick Start + +This is a stub page. +``` + +### Good (Resolves Z502) + +```markdown +# Quick Start + +Welcome to Zenzic! Follow the installation steps below to set up your environment, initialize configuration, and execute your first documentation audit scan across your repository. +``` + +## Configuration + +```toml +# .zenzic.toml - Adjust minimum word count threshold +min_word_count = 50 +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z502) for finding code details. diff --git a/docs/rules/Z503.md b/docs/rules/Z503.md new file mode 100644 index 00000000..1a4d47e5 --- /dev/null +++ b/docs/rules/Z503.md @@ -0,0 +1,48 @@ +--- +title: "Z503: SNIPPET_ERROR" +--- + + + + +**Severity**: `warning` +**Penalty**: 10.0 points +**Category**: `content` +**Auto-fixable**: No + +## Rationale + +This content quality rule is emitted when a fenced code block with a specified language tag (such as `python` or `yaml`) contains syntax errors that fail AST parsing for that language. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z503) + +````markdown + +```text +def calculate_score( +``` +```` + +### Good (Resolves Z503) + +````markdown + +```python +def calculate_score(penalty: float) -> float: + return 100.0 - penalty +``` +```` + +## Configuration + +```toml +# Evaluated for fenced code blocks with language specifiers +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z503) for finding code details. diff --git a/docs/rules/Z504.md b/docs/rules/Z504.md new file mode 100644 index 00000000..db743603 --- /dev/null +++ b/docs/rules/Z504.md @@ -0,0 +1,42 @@ +--- +title: "Z504: QUALITY_REGRESSION" +--- + + + + +**Severity**: `warning` +**Penalty**: 0.0 points +**Category**: `governance` +**Auto-fixable**: No + +## Rationale + +This governance gate check is triggered when the overall documentation quality score (DQS) drops below the saved baseline score recorded in `.zenzic.baseline`. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z504) + +```text +Current DQS: 85/100 | Saved Baseline DQS: 92/100 -> Trigger Z504 Quality Regression +``` + +### Good (Resolves Z504) + +```text +Current DQS: 95/100 | Saved Baseline DQS: 92/100 -> Pass Quality Gate +``` + +## Configuration + +```bash +# Save new baseline DQS score after authorized changes +zenzic baseline --save +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z504) for finding code details. diff --git a/docs/rules/Z505.md b/docs/rules/Z505.md new file mode 100644 index 00000000..1d53f4e0 --- /dev/null +++ b/docs/rules/Z505.md @@ -0,0 +1,48 @@ +--- +title: "Z505: UNTAGGED_CODE_BLOCK" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `content` +**Auto-fixable**: Yes + +## Rationale + +This rule is emitted when a fenced code block (```) lacks a programming language specifier tag, preventing syntax highlighting in static site builds. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z505) + +````markdown + +``` +zenzic check all --strict +``` +```` + +### Good (Resolves Z505) + +````markdown + +```bash +zenzic check all --strict +``` +```` + +## Configuration + +```bash +# Automatically insert text language tag for untagged code blocks +zenzic fix --only Z505 +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z505) for finding code details. diff --git a/docs/rules/Z506.md b/docs/rules/Z506.md new file mode 100644 index 00000000..6bded6fc --- /dev/null +++ b/docs/rules/Z506.md @@ -0,0 +1,47 @@ +--- +title: "Z506: MALFORMED_FRONTMATTER" +--- + + + + +**Severity**: `error` +**Penalty**: 5.0 points +**Category**: `content` +**Auto-fixable**: No + +## Rationale + +This rule is triggered when the opening YAML frontmatter delimiter on line 1 of a file is malformed (such as using `--` instead of `---`), causing metadata directives to be parsed as raw body text. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z506) + +```markdown + +-- +title: "Invalid Frontmatter" +--- +``` + +### Good (Resolves Z506) + +```markdown + +--- +title: "Valid Frontmatter" +--- +``` + +## Configuration + +```toml +# Evaluated on line 1 of Markdown files +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z506) for finding code details. diff --git a/docs/rules/Z510.md b/docs/rules/Z510.md new file mode 100644 index 00000000..40a958c0 --- /dev/null +++ b/docs/rules/Z510.md @@ -0,0 +1,36 @@ +--- +title: "Z510: HEADING_HIERARCHY" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `content` +**Auto-fixable**: No + +## Description + +A heading level skips one or more levels in the document structure (for example, an `H3` heading immediately follows an `H1` heading without an intervening `H2`). + +Skipping heading levels impairs screen reader navigation, breaks document outline generation, and compromises structural accessibility. + +## How to Fix + +Adjust the heading level to maintain sequential hierarchy: + +1. Ensure `H2` follows `H1`. +2. Ensure `H3` follows `H2`. + +## Suppression + +If a skipped heading level is intentional, you can suppress this warning using an inline comment directive at or above the heading: + +```markdown + +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z510) for finding code details. diff --git a/docs/rules/Z511.md b/docs/rules/Z511.md new file mode 100644 index 00000000..f603cf48 --- /dev/null +++ b/docs/rules/Z511.md @@ -0,0 +1,42 @@ +--- +title: "Z511: EXCESSIVE_SENTENCE_LENGTH" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `content` +**Auto-fixable**: No + +## Description + +A prose sentence exceeds the configured maximum word count threshold (default: 40 words). + +Extremely long sentences reduce reader comprehension, increase cognitive load, and hurt technical documentation readability. + +## How to Fix + +1. Break the sentence into two or more shorter sentences using punctuation (`.`, `;`). +2. Use bullet points or lists to structure complex multi-part statements. + +## Configuration + +You can configure the maximum allowed sentence length in `.zenzic.toml`: + +```toml +max_sentence_length = 35 +``` + +## Suppression + +To suppress this warning for a specific sentence, add an inline comment directive above the paragraph: + +```markdown + +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z511) for finding code details. diff --git a/docs/rules/Z512.md b/docs/rules/Z512.md new file mode 100644 index 00000000..ba06c4f9 --- /dev/null +++ b/docs/rules/Z512.md @@ -0,0 +1,34 @@ +--- +title: "Z512: EMPTY_SECTION" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `content` +**Auto-fixable**: No + +## Description + +A heading section contains zero body content before the next heading or the end of the file. + +Empty sections act as visual placeholders that confuse readers and indicate incomplete or abandoned documentation sections. + +## How to Fix + +1. Add explanatory text, code snippets, or list items beneath the heading. +2. Remove the empty heading if the section is not needed. + +## Suppression + +If a heading is intentionally empty, add an inline comment directive below or above the heading: + +```markdown + +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z512) for finding code details. diff --git a/docs/rules/Z601.md b/docs/rules/Z601.md new file mode 100644 index 00000000..93202a85 --- /dev/null +++ b/docs/rules/Z601.md @@ -0,0 +1,45 @@ +--- +title: "Z601: BRAND_OBSOLESCENCE" +--- + + + + +**Severity**: `warning` +**Penalty**: 2.0 points +**Category**: `brand` +**Auto-fixable**: No + +## Rationale + +This governance rule is triggered when deprecated brand terms, legacy product names, or obsolete terminology are found within documentation prose. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z601) + +```markdown + +The LegacyDocTool static analyzer checks link graphs. +``` + +### Good (Resolves Z601) + +```markdown + +The Zenzic static analyzer checks link graphs. +``` + +## Configuration + +```toml +# .zenzic.toml - Governance brand terms +[governance] +brand_terms = ["Zenzic"] +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z601) for finding code details. diff --git a/docs/rules/Z603.md b/docs/rules/Z603.md new file mode 100644 index 00000000..86e33e9b --- /dev/null +++ b/docs/rules/Z603.md @@ -0,0 +1,45 @@ +--- +title: "Z603: DEAD_SUPPRESSION" +--- + + + + +**Severity**: `warning` +**Penalty**: 1.0 point +**Category**: `governance` +**Auto-fixable**: Yes + +## Rationale + +This governance rule is emitted when an inline suppression comment (such as ``) is placed on a line where no violation of that code actually occurs. + +## How to Fix + +Inspect the flagged location in the Markdown file and update the content or configuration: + +### Bad (Triggers Z603) + +```markdown + + +This is a short sentence. +``` + +### Good (Resolves Z603) + +```markdown + +This is a short sentence. +``` + +## Configuration + +```bash +# Automatically remove dead inline suppressions +zenzic fix --only Z603 +``` + +## Reference + +See the [Finding Codes Index](../reference/finding-codes.md#z603) for finding code details. diff --git a/docs/tutorials/examples/z1xx-links/z101-broken-links.md b/docs/tutorials/examples/z1xx-links/z101-broken-links.md index 51996f10..40a07fd3 100644 --- a/docs/tutorials/examples/z1xx-links/z101-broken-links.md +++ b/docs/tutorials/examples/z1xx-links/z101-broken-links.md @@ -83,7 +83,7 @@ Exit code: `1` The `Z101` finding indicates a **LINK_BROKEN** issue. -This error or warning is raised by Zenzic when a reference link points to a target page or route that exists in the workspace filesystem or routing tree, but is broken because the specific path is incorrect or the target file does not map to any valid route in the Virtual Site Map. In this specific example: +This error or warning is raised by Zenzic when a reference link points to an invalid path. The link is broken because the target file does not map to any valid route in the Virtual Site Map. In this specific example: - **Scan Type:** `Link Validator` - **Severity:** `Error` diff --git a/docs/tutorials/examples/z5xx-content/z506-malformed-frontmatter.md b/docs/tutorials/examples/z5xx-content/z506-malformed-frontmatter.md index 12f94630..838b438b 100644 --- a/docs/tutorials/examples/z5xx-content/z506-malformed-frontmatter.md +++ b/docs/tutorials/examples/z5xx-content/z506-malformed-frontmatter.md @@ -1,6 +1,9 @@ -- ## description: "Live example showing a malformed frontmatter delimiter detected by Zenzic." + +This section details the specifications and guidelines for description: "Live example showing a malformed frontmatter delimiter detected by Zenzic." within the Zenzic ecosystem. + @@ -45,16 +48,16 @@ This file intentionally opens with `--` (two dashes) to trigger the rule. The `d Ensure the very first line of the file is exactly three dashes and nothing else: -## ```yaml - -## description: A well-formed frontmatter block - ```yaml +--- +description: A well-formed frontmatter block +--- +``` ## Suppression If you need to suppress Z506 on a specific file (e.g. a gallery page like this one): -```markdown -## -- +```text + ``` diff --git a/docs/tutorials/examples/z6xx-brand/z603-dead-suppression.md b/docs/tutorials/examples/z6xx-brand/z603-dead-suppression.md index 1d5fa8c8..255eb52e 100644 --- a/docs/tutorials/examples/z6xx-brand/z603-dead-suppression.md +++ b/docs/tutorials/examples/z6xx-brand/z603-dead-suppression.md @@ -55,6 +55,8 @@ Exit code: `0` (warning-only; use `--strict` to promote to Exit 1) ## The Three Z603 Scenarios +This section details the specifications and guidelines for The Three Z603 Scenarios within the Zenzic ecosystem. + ### Scenario A — Dead Directive (this page) A valid link has a `zenzic:ignore: Z101` directive that is never consumed. diff --git a/docs/tutorials/first-audit.md b/docs/tutorials/first-audit.md index 2d06447d..b8d1206a 100644 --- a/docs/tutorials/first-audit.md +++ b/docs/tutorials/first-audit.md @@ -161,13 +161,30 @@ Run `uvx zenzic score` on your own repo to obtain a baseline score without insta --- +## Step 5 — Snapshot technical debt with Baseline Tracking {#step-5-baseline} + +If your repository has existing technical debt, capture it into a deterministic baseline snapshot so your team can enforce strict CI gates for new code immediately: + +```bash +# Capture current findings into .zenzic-baseline.json snapshot +zenzic check all --update-baseline +``` + +Subsequent CI audits against `--baseline .zenzic-baseline.json` tag existing debt as `is_baselined: true` without failing your build. This quality gate catches any newly introduced defects or DQS regressions: + +```bash +# CI Quality Gate — verify PR against repository baseline +zenzic check all --baseline .zenzic-baseline.json +``` + +--- + ## What's next? {#next} -- **Measure your score** — run `uvx zenzic score` to get a precise 0–100 baseline for your repo +- **Baseline & Anti-Regression** — see [Baseline Tracking](../explanation/baseline-tracking) for CI/CD debt management +- **Measure your score** — run `uvx zenzic score` to get a precise 0–100 DQS for your repo - **Add a CI gate** — see [CI/CD Integration](../how-to/configure-ci-cd) for automated quality enforcement - **SARIF export** — `zenzic check all --format sarif` for GitHub Code Scanning inline annotations - **Strict mode** — add `--strict` to also validate external URLs - **Custom rules** — add `[[custom_rules]]` entries to `.zenzic.toml` to enforce your own patterns -- **Finding codes** — see the [Finding Codes reference](../reference/finding-codes) for the full - - `Zxxx` diagnostic catalogue +- **Finding codes** — see the [Finding Codes reference](../reference/finding-codes) for the full `Zxxx` diagnostic catalogue diff --git a/examples/z107-circular-anchor/docs/guide.md b/examples/z107-circular-anchor/docs/guide.md index f1b60d0b..a6df01a0 100644 --- a/examples/z107-circular-anchor/docs/guide.md +++ b/examples/z107-circular-anchor/docs/guide.md @@ -16,3 +16,4 @@ This page contains a self-referential anchor link: [Setup](#setup) ## Next Steps After completing setup, proceed to the tutorial section for hands-on examples. +See [Guide](guide.md). diff --git a/examples/z110-config-syntax-error/.zenzic.toml b/examples/z110-config-syntax-error/.zenzic.toml new file mode 100644 index 00000000..37a88e21 --- /dev/null +++ b/examples/z110-config-syntax-error/.zenzic.toml @@ -0,0 +1,2 @@ +# Malformed TOML syntax example +placeholder_max_words = [ unclosed_array diff --git a/examples/z110-config-syntax-error/docs/index.md b/examples/z110-config-syntax-error/docs/index.md new file mode 100644 index 00000000..60758fc2 --- /dev/null +++ b/examples/z110-config-syntax-error/docs/index.md @@ -0,0 +1,3 @@ +# Config Syntax Error Example + +This workspace triggers Z110 due to malformed TOML syntax in .zenzic.toml. diff --git a/examples/z111-config-schema-error/.zenzic.toml b/examples/z111-config-schema-error/.zenzic.toml new file mode 100644 index 00000000..d3d4113f --- /dev/null +++ b/examples/z111-config-schema-error/.zenzic.toml @@ -0,0 +1,2 @@ +# Invalid schema type example +placeholder_max_words = "invalid_type_string" diff --git a/examples/z111-config-schema-error/docs/index.md b/examples/z111-config-schema-error/docs/index.md new file mode 100644 index 00000000..923f31ef --- /dev/null +++ b/examples/z111-config-schema-error/docs/index.md @@ -0,0 +1,3 @@ +# Config Schema Error Example + +This workspace triggers Z111 due to an invalid schema field type in .zenzic.toml. diff --git a/examples/z401-missing-directory-index/docs/guide/page.md b/examples/z401-missing-directory-index/docs/guide/page.md index ec168163..63e3a0e1 100644 --- a/examples/z401-missing-directory-index/docs/guide/page.md +++ b/examples/z401-missing-directory-index/docs/guide/page.md @@ -11,3 +11,4 @@ The installation takes approximately two minutes on a standard connection. Ensure you have Python 3.10 or higher and pip installed on your system before proceeding with the installation steps described in this document. +See [Installation Guide](page.md). diff --git a/examples/z404-config-asset-missing/docs/index.md b/examples/z404-config-asset-missing/docs/index.md index 4475b3f9..9d3bae12 100644 --- a/examples/z404-config-asset-missing/docs/index.md +++ b/examples/z404-config-asset-missing/docs/index.md @@ -14,3 +14,4 @@ Zenzic to ensure link integrity and documentation quality standards are met. ## Get Started Follow the quick start guide to set up your environment and run your first check. +See [Index](index.md). diff --git a/examples/z406-nav-contract/docs/index.md b/examples/z406-nav-contract/docs/index.md index 4475b3f9..9d3bae12 100644 --- a/examples/z406-nav-contract/docs/index.md +++ b/examples/z406-nav-contract/docs/index.md @@ -14,3 +14,4 @@ Zenzic to ensure link integrity and documentation quality standards are met. ## Get Started Follow the quick start guide to set up your environment and run your first check. +See [Index](index.md). diff --git a/examples/z410-unreachable-graph-node/.zenzic.toml b/examples/z410-unreachable-graph-node/.zenzic.toml new file mode 100644 index 00000000..6e4db4ac --- /dev/null +++ b/examples/z410-unreachable-graph-node/.zenzic.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# .zenzic.toml — Z402 ORPHAN_PAGE gallery example +# +# This sandbox uses the zensical engine so that find_orphans() can inspect +# the navigation manifest. docs/secret.md exists on disk but is NOT listed +# in zensical.toml nav — it is ORPHAN_BUT_EXISTING → Z402 ORPHAN_PAGE. +# +# With the standalone engine find_orphans() returns [] (no nav to compare). +# The zensical adapter reads zensical.toml and detects the unlisted page. +# +# Expected results: +# zenzic check assets → EXIT 1 (Z402 ×1 — docs/secret.md not in nav) +# zenzic check all → EXIT 1 + +docs_dir = "docs" +fail_under = 0 + +[build_context] +engine = "zensical" diff --git a/examples/z410-unreachable-graph-node/README.md b/examples/z410-unreachable-graph-node/README.md new file mode 100644 index 00000000..9df05bfc --- /dev/null +++ b/examples/z410-unreachable-graph-node/README.md @@ -0,0 +1,30 @@ + + + +# Z410 UNREACHABLE_GRAPH_NODE — Gallery Example + +**Category:** Z4xx Structural +**Expected exit:** 1 (warnings) +**Engine required:** zensical + +## What this demonstrates + +`docs/secret.md` exists on disk but has no incoming links and is not in the navigation manifest. Therefore it is an unreachable graph node. + +## Run it + +```bash +zenzic check . +``` + +## Expected output + +```text +docs/secret.md:1: Z410 UNREACHABLE_GRAPH_NODE Document is isolated and unreachable from the navigation entry points: '/secret.md' +``` + +Exit code **1**. + +## Fix + +Either add `"secret.md"` to the `nav` array in `zensical.toml`, link to it from `index.md`, or suppress it using ``. diff --git a/examples/z410-unreachable-graph-node/docs/index.md b/examples/z410-unreachable-graph-node/docs/index.md new file mode 100644 index 00000000..c370b7f7 --- /dev/null +++ b/examples/z410-unreachable-graph-node/docs/index.md @@ -0,0 +1,3 @@ +# Index + +Welcome to the site. diff --git a/examples/z410-unreachable-graph-node/docs/secret.md b/examples/z410-unreachable-graph-node/docs/secret.md new file mode 100644 index 00000000..285f9943 --- /dev/null +++ b/examples/z410-unreachable-graph-node/docs/secret.md @@ -0,0 +1,3 @@ +# Secret + +This page has no incoming links and is not in the nav. diff --git a/examples/z410-unreachable-graph-node/zensical.toml b/examples/z410-unreachable-graph-node/zensical.toml new file mode 100644 index 00000000..6141ad5e --- /dev/null +++ b/examples/z410-unreachable-graph-node/zensical.toml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# zensical.toml — Z402 ORPHAN_PAGE gallery example +# +# index.md and guide.md are in the navigation → reachable via site nav. +# secret.md is NOT listed → ORPHAN_BUT_EXISTING → Z402. + +[project] +site_name = "Z402 Example" +docs_dir = "docs" +nav = [ + "index.md", + "guide.md", +] diff --git a/examples/z411-dead-end-node/.zenzic.toml b/examples/z411-dead-end-node/.zenzic.toml new file mode 100644 index 00000000..6e4db4ac --- /dev/null +++ b/examples/z411-dead-end-node/.zenzic.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# .zenzic.toml — Z402 ORPHAN_PAGE gallery example +# +# This sandbox uses the zensical engine so that find_orphans() can inspect +# the navigation manifest. docs/secret.md exists on disk but is NOT listed +# in zensical.toml nav — it is ORPHAN_BUT_EXISTING → Z402 ORPHAN_PAGE. +# +# With the standalone engine find_orphans() returns [] (no nav to compare). +# The zensical adapter reads zensical.toml and detects the unlisted page. +# +# Expected results: +# zenzic check assets → EXIT 1 (Z402 ×1 — docs/secret.md not in nav) +# zenzic check all → EXIT 1 + +docs_dir = "docs" +fail_under = 0 + +[build_context] +engine = "zensical" diff --git a/examples/z411-dead-end-node/README.md b/examples/z411-dead-end-node/README.md new file mode 100644 index 00000000..4359a726 --- /dev/null +++ b/examples/z411-dead-end-node/README.md @@ -0,0 +1,31 @@ + + + +# Z411 DEAD_END_NODE — Gallery Example + +**Category:** Z4xx Structural +**Expected exit:** 1 (warnings) +**Engine required:** any + +## What this demonstrates + +`docs/deadend.md` exists and is linked from `index.md`, but it has no outgoing links. +Therefore, it forms a structural dead end in the documentation graph. Visitors reaching this page will have nowhere to go. + +## Run it + +```bash +zenzic check . +``` + +## Expected output + +```text +docs/deadend.md:1: Z411 DEAD_END_NODE Document has no outgoing links and forms a structural dead end: '/deadend.md' +``` + +Exit code **1**. + +## Fix + +Add an outgoing link to another page in `deadend.md`, or suppress the warning with ``. diff --git a/examples/z411-dead-end-node/docs/deadend.md b/examples/z411-dead-end-node/docs/deadend.md new file mode 100644 index 00000000..c8fb2d1f --- /dev/null +++ b/examples/z411-dead-end-node/docs/deadend.md @@ -0,0 +1,3 @@ +# Dead End Page + +This page has no outgoing links and forms a structural dead end. diff --git a/examples/z411-dead-end-node/docs/index.md b/examples/z411-dead-end-node/docs/index.md new file mode 100644 index 00000000..fa2e3b2a --- /dev/null +++ b/examples/z411-dead-end-node/docs/index.md @@ -0,0 +1,3 @@ +# Index + +Welcome. Read the [dead end page](deadend.md). diff --git a/examples/z411-dead-end-node/docs/secret.md b/examples/z411-dead-end-node/docs/secret.md new file mode 100644 index 00000000..a0712742 --- /dev/null +++ b/examples/z411-dead-end-node/docs/secret.md @@ -0,0 +1,12 @@ + + + +# Secret Page + +This page (`secret.md`) is **not listed** in `zensical.toml` nav. + +It exists on disk but cannot be reached through the site navigation menu. +Zenzic detects this as **Z402 ORPHAN_PAGE**. + +No visitor following the documented site structure can find this page — it is +navigation dead weight and may contain stale or unpublished content. diff --git a/examples/z411-dead-end-node/docs/suppressed_deadend.md b/examples/z411-dead-end-node/docs/suppressed_deadend.md new file mode 100644 index 00000000..f7163ac9 --- /dev/null +++ b/examples/z411-dead-end-node/docs/suppressed_deadend.md @@ -0,0 +1,4 @@ + +# Suppressed Dead End Page + +This page is a dead end, but Z411 is suppressed inline. diff --git a/examples/z411-dead-end-node/zensical.toml b/examples/z411-dead-end-node/zensical.toml new file mode 100644 index 00000000..6141ad5e --- /dev/null +++ b/examples/z411-dead-end-node/zensical.toml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# zensical.toml — Z402 ORPHAN_PAGE gallery example +# +# index.md and guide.md are in the navigation → reachable via site nav. +# secret.md is NOT listed → ORPHAN_BUT_EXISTING → Z402. + +[project] +site_name = "Z402 Example" +docs_dir = "docs" +nav = [ + "index.md", + "guide.md", +] diff --git a/examples/z510-heading-hierarchy/docs/index.md b/examples/z510-heading-hierarchy/docs/index.md new file mode 100644 index 00000000..7cfde2db --- /dev/null +++ b/examples/z510-heading-hierarchy/docs/index.md @@ -0,0 +1,5 @@ +# Heading Hierarchy Example + +### Skipped Subheading + +This page triggers Z510 because an H3 heading immediately follows an H1 heading without an intervening H2 level. diff --git a/examples/z511-excessive-sentence-length/docs/index.md b/examples/z511-excessive-sentence-length/docs/index.md new file mode 100644 index 00000000..42a521c1 --- /dev/null +++ b/examples/z511-excessive-sentence-length/docs/index.md @@ -0,0 +1,5 @@ +# Excessive Sentence Length Example + +This is an exceptionally long prose sentence that continues through multiple clauses and ideas without any sentence-ending punctuation until it easily exceeds the maximum readability limit of forty words defined in the Zenzic workspace configuration file. + +[Back to main page](index.md) diff --git a/examples/z512-empty-section/docs/index.md b/examples/z512-empty-section/docs/index.md new file mode 100644 index 00000000..a2569f3e --- /dev/null +++ b/examples/z512-empty-section/docs/index.md @@ -0,0 +1,9 @@ +# Empty Section Example + +## Empty Section + +## Next Section + +This section contains prose body content. + +[Link](index.md) diff --git a/mkdocs.yml b/mkdocs.yml index a51ed9f6..e05a5db6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,48 @@ nav: - Rule Reference & Catalog: - Finding Codes Index: reference/finding-codes.md - Core Rule Checks: reference/checks.md + - Rule Specification Cards: + - rules/Z000.md + - rules/Z001.md + - rules/Z101.md + - rules/Z102.md + - rules/Z103.md + - rules/Z104.md + - rules/Z105.md + - rules/Z106.md + - rules/Z107.md + - rules/Z108.md + - rules/Z109.md + - rules/Z110.md + - rules/Z111.md + - rules/Z114.md + - rules/Z118.md + - rules/Z201.md + - rules/Z202.md + - rules/Z203.md + - rules/Z204.md + - rules/Z301.md + - rules/Z302.md + - rules/Z303.md + - rules/Z401.md + - rules/Z402.md + - rules/Z403.md + - rules/Z404.md + - rules/Z405.md + - rules/Z406.md + - rules/Z410.md + - rules/Z411.md + - rules/Z501.md + - rules/Z502.md + - rules/Z503.md + - rules/Z504.md + - rules/Z505.md + - rules/Z506.md + - rules/Z510.md + - rules/Z511.md + - rules/Z512.md + - rules/Z601.md + - rules/Z603.md - Interactive Rule Examples: - tutorials/examples/index.md - tutorials/examples/z0xx-core/z001-config-error.md @@ -114,6 +156,7 @@ nav: - Core Mechanics: explanation/core-mechanics.md - Scoring Design: explanation/scoring-design.md - Scoring System: explanation/scoring-system.md + - Baseline Tracking: explanation/baseline-tracking.md - Discovery Engine: explanation/discovery.md - Configuration Loading: explanation/configuration-loading.md - Exclusion Design: explanation/exclusion-design.md @@ -224,7 +267,7 @@ extra: # ADR-037: No hardcoded SemVer in any .html or .md source. # CI pipeline passes the current version at build time, e.g.: # uv run mkdocs build --extra zenzic_version=0.14.1 - zenzic_version: "0.26.5" # release sync + zenzic_version: "0.27.0" # release sync social: - icon: fontawesome/brands/github link: https://github.com/PythonWoods/zenzic diff --git a/pyproject.toml b/pyproject.toml index 53938237..2e4ee603 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ build-backend = "hatchling.build" [project] name = "zenzic" -version = "0.26.5" +version = "0.27.0" description = "Deterministic Document Integrity Engine and SAST for Markdown/MDX graphs." readme = "README.md" requires-python = ">=3.10" diff --git a/src/zenzic/__init__.py b/src/zenzic/__init__.py index c331b592..06abaf05 100644 --- a/src/zenzic/__init__.py +++ b/src/zenzic/__init__.py @@ -2,5 +2,5 @@ # SPDX-License-Identifier: Apache-2.0 """Zenzic — engine-agnostic static analyzer and credential scanner for Markdown documentation.""" -__version__ = "0.26.5" +__version__ = "0.27.0" __version_name__ = "Basalt" # Release codename stored separately from the package version. diff --git a/src/zenzic/cli/_check.py b/src/zenzic/cli/_check.py index 1767da6d..e16c241b 100644 --- a/src/zenzic/cli/_check.py +++ b/src/zenzic/cli/_check.py @@ -15,6 +15,7 @@ from zenzic.core.adapters import get_adapter from zenzic.core.adapters._mkdocs import check_config_assets as _mkdocs_check_assets from zenzic.core.adapters._zensical import check_config_assets as _zensical_check_assets +from zenzic.core.baseline import DEFAULT_BASELINE_FILE, BaselineManager from zenzic.core.codes import CODE_DEFINITIONS from zenzic.core.exclusion import LayeredExclusionManager from zenzic.core.reporter import Finding, ZenzicReporter @@ -62,6 +63,18 @@ ) +def _validate_only_flag(only: str | None) -> None: + if not only: + return + for code in only.split(","): + code = code.strip().upper() + if code and code not in CODE_DEFINITIONS: + _shared.console.print( + f"[bold red]Error:[/] Invalid finding code '{code}' provided to --only flag." + ) + raise typer.Exit(1) + + def _finding_severity(code: str) -> str: """Derive CLI finding severity from CodeDefinition SSoT (codes.py). @@ -134,6 +147,7 @@ def check_links( ), ) -> None: """Check for broken internal links and enforce strict warning policy when requested.""" + _validate_only_flag(only) if ci: strict = True @@ -298,6 +312,7 @@ def check_orphans( ), ) -> None: """Detect .md files not listed in the nav.""" + _validate_only_flag(only) if ci: if output_format == "text": @@ -419,6 +434,7 @@ def check_snippets( ), ) -> None: """Validate Python code blocks in documentation Markdown files.""" + _validate_only_flag(only) if ci: if output_format == "text": @@ -558,6 +574,7 @@ def check_references( 1 — Dangling References or (with --strict) warnings found. 2 — SECURITY CRITICAL: a secret was detected in a reference URL. """ + _validate_only_flag(only) if ci: strict = True @@ -730,6 +747,7 @@ def check_assets( ), ) -> None: """Detect unused images and assets in the documentation.""" + _validate_only_flag(only) if ci: if output_format == "text": @@ -853,6 +871,7 @@ def check_placeholders( ), ) -> None: """Detect pages with < 50 words or containing TODOs/stubs.""" + _validate_only_flag(only) if ci: if output_format == "text": @@ -1000,13 +1019,14 @@ def _apply_only_filter(results: _AllCheckResults, only_str: str) -> None: def _filter_flat_findings(findings: list[Finding], only_str: str | None) -> list[Finding]: - """Filter a flat list of findings keeping only the specified Z-codes.""" + """Filter a flat list of findings keeping only the specified Z-codes (except fatal config errors Z110, Z111).""" if not only_str: return findings allowed = frozenset(code.strip().upper() for code in only_str.split(",") if code.strip()) if not allowed: return findings - return [f for f in findings if f.code in allowed] + bypass_codes = {"Z110", "Z111"} + return [f for f in findings if f.code in allowed or f.code in bypass_codes] # _apply_per_file_ignores and _apply_directory_policies have moved to _governance.py. @@ -1395,6 +1415,16 @@ def check_all( "--no-header", help="Suppress the Zenzic ASCII art header.", ), + update_baseline: bool = typer.Option( + False, + "--update-baseline", + help="Generate or overwrite the baseline snapshot file (.zenzic-baseline.json).", + ), + baseline: str | None = typer.Option( + None, + "--baseline", + help="Path to a baseline snapshot file to consume (defaults to .zenzic-baseline.json if present in workspace root).", + ), ) -> None: """Run all checks: links, orphans, snippets, placeholders, assets, references. @@ -1402,6 +1432,8 @@ def check_all( directory (e.g. ``README.md``, ``content/``). Zenzic auto-selects the StandaloneAdapter when the target lives outside the configured docs directory. """ + _validate_only_flag(only) + # GAP-04: Conflict validation — --strict and --exit-zero are mutually exclusive. if strict and exit_zero: typer.echo( @@ -1523,15 +1555,58 @@ def check_all( elapsed = time.monotonic() - t0 - if output_format == "json": - with sovereign_context(force_audit=audit): - all_findings = _to_findings(results, docs_root, repo_root, config) - all_findings = _apply_per_file_ignores(all_findings, config) - all_findings = _apply_directory_policies(all_findings, config) - _append_z118_findings( - all_findings, config, repo_root, check_all=True, check_external_urls=True + with sovereign_context(force_audit=audit): + all_findings = _to_findings(results, docs_root, repo_root, config) + all_findings = _apply_per_file_ignores(all_findings, config) + all_findings = _apply_directory_policies(all_findings, config) + _append_z118_findings( + all_findings, config, repo_root, check_all=True, check_external_urls=True + ) + if only: + all_findings = _filter_flat_findings(all_findings, only) + + if _single_file is not None: + _sf_rel = str(_single_file.relative_to(repo_root)) + all_findings = [f for f in all_findings if f.rel_path == _sf_rel] + + # ── Baseline Handling ────────────────────────────────────────────────────── + baseline_file_path = Path(baseline) if baseline else (repo_root / DEFAULT_BASELINE_FILE) + + _findings_counts: dict[str, int] = {} + for _f in all_findings: + _findings_counts[_f.code] = _findings_counts.get(_f.code, 0) + 1 + _score_report = compute_score( + _findings_counts, + suppression_count=suppression_audit.total, + suppression_cap=suppression_audit.cap, + ) + + if update_baseline: + bdata = BaselineManager.create_baseline( + _score_report.score, all_findings, version_str=__version__ + ) + BaselineManager.save_baseline(bdata, baseline_file_path) + if not quiet and output_format == "text": + _shared.console.print( + f"[bold green]✓ Baseline snapshot saved to {baseline_file_path.name}[/bold green] " + f"[{ZenzicPalette.DIM}](score: {_score_report.score}/100, {len(bdata.signatures)} finding{'s' if len(bdata.signatures) != 1 else ''})[/]" ) + active_baseline = None + if baseline_file_path.is_file(): + try: + active_baseline = BaselineManager.load_baseline(baseline_file_path) + BaselineManager.apply_baseline(all_findings, active_baseline) + except Exception as exc: + if baseline is not None: + typer.echo( + f"ERROR: Failed to load baseline '{baseline_file_path}': {exc}", err=True + ) + raise typer.Exit(1) from None + + elapsed = time.monotonic() - t0 + + if output_format == "json": _shared._output_check_all_json_findings( results, all_findings, repo_root, docs_root, config, suppression_audit ) @@ -1542,18 +1617,19 @@ def check_all( breaches = sum(1 for f in all_findings if f.severity == "security_breach") if breaches: raise typer.Exit(2) - errors_count = sum(1 for f in all_findings if f.severity == "error") - if errors_count and not effective_exit_zero: - raise typer.Exit(1) + + if active_baseline is not None and not effective_exit_zero: + unbaselined = sum( + 1 for f in all_findings if not f.is_baselined and f.severity == "error" + ) + if unbaselined or _score_report.score < active_baseline.score: + raise typer.Exit(1) + elif not effective_exit_zero: + errors_count = sum(1 for f in all_findings if f.severity == "error") + if errors_count: + raise typer.Exit(1) return elif output_format == "sarif": - with sovereign_context(force_audit=audit): - all_findings = _to_findings(results, docs_root, repo_root, config) - all_findings = _apply_per_file_ignores(all_findings, config) - all_findings = _apply_directory_policies(all_findings, config) - _append_z118_findings( - all_findings, config, repo_root, check_all=True, check_external_urls=True - ) _shared._output_sarif_findings(all_findings, __version__) incidents = sum(1 for f in all_findings if f.severity == "security_incident") if incidents: @@ -1561,22 +1637,19 @@ def check_all( breaches = sum(1 for f in all_findings if f.severity == "security_breach") if breaches: raise typer.Exit(2) - errors_count = sum(1 for f in all_findings if f.severity == "error") - if errors_count and not effective_exit_zero: - raise typer.Exit(1) + + if active_baseline is not None and not effective_exit_zero: + unbaselined = sum( + 1 for f in all_findings if not f.is_baselined and f.severity == "error" + ) + if unbaselined or _score_report.score < active_baseline.score: + raise typer.Exit(1) + elif not effective_exit_zero: + errors_count = sum(1 for f in all_findings if f.severity == "error") + if errors_count: + raise typer.Exit(1) return elif output_format == "github-annotations": - with sovereign_context(force_audit=audit): - all_findings = _to_findings(results, docs_root, repo_root, config) - all_findings = _apply_per_file_ignores(all_findings, config) - all_findings = _apply_directory_policies(all_findings, config) - _append_z118_findings( - all_findings, config, repo_root, check_all=True, check_external_urls=True - ) - if _single_file is not None: - _sf_rel = str(_single_file.relative_to(repo_root)) - all_findings = [f for f in all_findings if f.rel_path == _sf_rel] - _shared._output_github_annotations(all_findings) incidents = sum(1 for f in all_findings if f.severity == "security_incident") @@ -1586,29 +1659,24 @@ def check_all( if breaches: raise typer.Exit(2) - errors_count = sum(1 for f in all_findings if f.severity == "error") - warnings_count = sum(1 for f in all_findings if f.severity == "warning") - if ( - errors_count > 0 or (effective_strict and warnings_count > 0) - ) and not effective_exit_zero: - raise typer.Exit(1) + if active_baseline is not None and not effective_exit_zero: + unbaselined = sum( + 1 + for f in all_findings + if not f.is_baselined + and (f.severity == "error" or (effective_strict and f.severity == "warning")) + ) + if unbaselined or _score_report.score < active_baseline.score: + raise typer.Exit(1) + elif not effective_exit_zero: + errors_count = sum(1 for f in all_findings if f.severity == "error") + warnings_count = sum(1 for f in all_findings if f.severity == "warning") + if errors_count > 0 or (effective_strict and warnings_count > 0): + raise typer.Exit(1) return - with sovereign_context(force_audit=audit): - all_findings = _to_findings(results, docs_root, repo_root, config) - all_findings = _apply_per_file_ignores(all_findings, config) - all_findings = _apply_directory_policies(all_findings, config) - _append_z118_findings( - all_findings, config, repo_root, check_all=True, check_external_urls=True - ) - - if _single_file is not None: - _sf_rel = str(_single_file.relative_to(repo_root)) - all_findings = [f for f in all_findings if f.rel_path == _sf_rel] - - reporter = ZenzicReporter(_shared.console, docs_root, docs_dir=str(config.docs_dir)) - if quiet: + reporter = ZenzicReporter(_shared.console, docs_root, docs_dir=str(config.docs_dir)) errors, warnings = reporter.render_quiet(all_findings) else: docs_count, assets_count = _shared._count_docs_assets( @@ -1617,8 +1685,6 @@ def check_all( if _single_file is not None: docs_count, assets_count = 1, 0 - # Z906 guardrail: if the target contains zero Markdown sources, inform - # the user with an amber warning and exit cleanly (not a system error). if docs_count == 0 and _single_file is None: _target_display = _target_hint or "./" _shared.console.print( @@ -1635,15 +1701,20 @@ def check_all( f"Credential scanner (Z201) remains active.[/]" ) - # ── DQS Score injection ──────────────────────────────────────────── - _findings_counts: dict[str, int] = {} - for _f in all_findings: - _findings_counts[_f.code] = _findings_counts.get(_f.code, 0) + 1 - _score_report = compute_score( - _findings_counts, - suppression_count=suppression_audit.total, - suppression_cap=suppression_audit.cap, - ) + if active_baseline is not None: + baselined_cnt = sum(1 for f in all_findings if f.is_baselined) + new_cnt = sum(1 for f in all_findings if not f.is_baselined) + fixed_cnt = max(0, active_baseline.findings_count - baselined_cnt) + _footer_lines.append( + f"[{ZenzicPalette.DIM}]Baseline: {active_baseline.score}/100 " + f"({baselined_cnt} baselined, {new_cnt} new)[/]" + ) + if fixed_cnt > 0: + _footer_lines.append( + f"[{ZenzicPalette.SUCCESS}]💡 {fixed_cnt} baselined issue{'s' if fixed_cnt != 1 else ''} resolved! " + f"Run 'zenzic check --update-baseline' to refresh baseline.[/]" + ) + if _score_report.security_override: _dqs_line = ( f"[bold red]DQS Final Score: 0/100[/bold red] " @@ -1669,6 +1740,7 @@ def check_all( ) _footer_lines.insert(0, _dqs_line) + reporter = ZenzicReporter(_shared.console, docs_root, docs_dir=str(config.docs_dir)) errors, warnings = reporter.render( all_findings, version=__version__, @@ -1692,8 +1764,17 @@ def check_all( if breaches: raise typer.Exit(2) - has_failures = (errors > 0) or (effective_strict and warnings > 0) - - if has_failures: - if not effective_exit_zero: + if active_baseline is not None: + unbaselined_defects = sum( + 1 + for f in all_findings + if not f.is_baselined + and (f.severity == "error" or (effective_strict and f.severity == "warning")) + ) + score_regressed = _score_report.score < active_baseline.score + if (score_regressed or unbaselined_defects > 0) and not effective_exit_zero: + raise typer.Exit(1) + else: + has_failures = (errors > 0) or (effective_strict and warnings > 0) + if has_failures and not effective_exit_zero: raise typer.Exit(1) diff --git a/src/zenzic/cli/_lab.py b/src/zenzic/cli/_lab.py index 1339f7bc..9a1c2496 100644 --- a/src/zenzic/cli/_lab.py +++ b/src/zenzic/cli/_lab.py @@ -93,10 +93,17 @@ class _Act: ), "z110": _Act( code="z110", - title="Stale Allowlist", - description="Z110 STALE_ALLOWLIST_ENTRY — stale absolute path allowlist entry; exit 0 (warning)", - example_dir="z110-stale-allowlist", - expected_pass=True, + title="Config Syntax Error", + description="Z110 CONFIG_SYNTAX_ERROR — malformed TOML syntax in .zenzic.toml; exit 1", + example_dir="z110-config-syntax-error", + expected_pass=False, + ), + "z111": _Act( + code="z111", + title="Config Schema Error", + description="Z111 CONFIG_SCHEMA_ERROR — invalid schema structure or type in .zenzic.toml; exit 1", + example_dir="z111-config-schema-error", + expected_pass=False, ), "z118": _Act( code="z118", @@ -247,6 +254,27 @@ class _Act: example_dir="z505-untagged-code-block", expected_pass=False, ), + "z510": _Act( + code="z510", + title="Heading Hierarchy Violation", + description="Z510 HEADING_HIERARCHY — heading level skipped (e.g., H3 follows H1)", + example_dir="z510-heading-hierarchy", + expected_pass=False, + ), + "z511": _Act( + code="z511", + title="Excessive Sentence Length", + description="Z511 EXCESSIVE_SENTENCE_LENGTH — sentence word count exceeds maximum limit", + example_dir="z511-excessive-sentence-length", + expected_pass=False, + ), + "z512": _Act( + code="z512", + title="Empty Section", + description="Z512 EMPTY_SECTION — heading section contains no body content before next heading or EOF", + example_dir="z512-empty-section", + expected_pass=False, + ), "z104": _Act( code="z104", title="File Not Found", @@ -334,6 +362,20 @@ class _Act: example_dir="z603-dead-suppression", expected_pass=False, ), + "z410": _Act( + code="z410", + title="Unreachable Graph Node", + description="Z410 UNREACHABLE_GRAPH_NODE — Document is isolated and unreachable from navigation entry points", + example_dir="z410-unreachable-graph-node", + expected_pass=False, + ), + "z411": _Act( + code="z411", + title="Dead End Node", + description="Z411 DEAD_END_NODE — Document has no outgoing links and forms a structural dead end", + example_dir="z411-dead-end-node", + expected_pass=False, + ), } _VALID_CODES: frozenset[str] = frozenset(_GALLERY) @@ -599,7 +641,7 @@ def _print_gallery_index() -> None: table.add_column("Title", style="bold", min_width=22) table.add_column("Description", style=ZenzicPalette.WARNING) table.add_column("Expects", justify="center", min_width=8) - for act in _GALLERY.values(): + for act in sorted(_GALLERY.values(), key=lambda a: a.code): expects = "[red]BREACH[/]" if act.expected_breach else "[yellow]FAIL[/]" table.add_row(act.code.upper(), act.title, act.description, expects) con.print(table) diff --git a/src/zenzic/cli/_standalone.py b/src/zenzic/cli/_standalone.py index 56a9d365..f14be5ee 100644 --- a/src/zenzic/cli/_standalone.py +++ b/src/zenzic/cli/_standalone.py @@ -1603,7 +1603,7 @@ def _scaffold_plugin(repo_root: Path, plugin_name: str, force: bool) -> None: description = "Custom Zenzic plugin rule package" readme = "README.md" requires-python = ">=3.11" -dependencies = ["zenzic>=0.26.5"] +dependencies = ["zenzic>=0.27.0"] [project.entry-points."zenzic.rules"] {project_slug} = "{module_name}.rules:{class_name}" diff --git a/src/zenzic/cli/templates.py b/src/zenzic/cli/templates.py index 0605e525..fb5ade22 100644 --- a/src/zenzic/cli/templates.py +++ b/src/zenzic/cli/templates.py @@ -67,6 +67,7 @@ '# placeholder_patterns = ["coming soon", "work in progress", "wip", "todo"]\n' "# placeholder_max_words = 50\n" "# snippet_min_lines = 1\n" + "# max_sentence_length = 40 # Z511 Excessive Sentence Length threshold (words)\n" "\n" "# --- EXCLUSION ZONES (Full bypass — use sparingly) ---\n" "# Paths listed here are INVISIBLE to Zenzic: no findings, no audit trail.\n" diff --git a/src/zenzic/core/adapters/_base.py b/src/zenzic/core/adapters/_base.py index f3e9ef8a..28c0a263 100644 --- a/src/zenzic/core/adapters/_base.py +++ b/src/zenzic/core/adapters/_base.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: - from zenzic.models.vsm import RouteStatus + from zenzic.models.vsm import RouteStatus, VirtualSiteMap # ── Route metadata types ───────────────────────────────────────────────────── @@ -133,3 +133,7 @@ def use_directory_urls(self) -> bool: def watched_config_files(self) -> frozenset[str]: """Return the configuration filenames that trigger a VSM rebuild in LSP mode.""" return frozenset() + + @abstractmethod + def get_entry_points(self, vsm: VirtualSiteMap) -> list[str]: + """Return canonical URLs serving as root entry points for reachability analysis.""" diff --git a/src/zenzic/core/adapters/_mkdocs.py b/src/zenzic/core/adapters/_mkdocs.py index 043afd98..6c28ef77 100644 --- a/src/zenzic/core/adapters/_mkdocs.py +++ b/src/zenzic/core/adapters/_mkdocs.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: from zenzic.core.adapters._base import RouteMetadata - from zenzic.models.vsm import RouteStatus + from zenzic.models.vsm import RouteStatus, VirtualSiteMap def _iter_plugins(doc_config: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]: @@ -849,6 +849,23 @@ def get_absolute_url_prefixes(self, repo_root: Path | None = None) -> list[str]: """MkDocs is single-instance and exports no absolute URL prefixes.""" return [] + def get_entry_points(self, vsm: VirtualSiteMap) -> list[str]: + """Return canonical URLs serving as root entry points from nav configuration.""" + nav_paths = self.get_nav_paths() + if not nav_paths: + return ["/"] if "/" in vsm else [] + + entry_points = set() + for p in nav_paths: + ep = self._map_url(Path(p)) + if ep in vsm: + entry_points.add(ep) + + if "/" in vsm: + entry_points.add("/") + + return sorted(entry_points) + @classmethod def from_repo( cls, diff --git a/src/zenzic/core/adapters/_standalone.py b/src/zenzic/core/adapters/_standalone.py index 40b3201a..4ddb3d85 100644 --- a/src/zenzic/core/adapters/_standalone.py +++ b/src/zenzic/core/adapters/_standalone.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from zenzic.core.adapters._base import RouteMetadata + from zenzic.models.vsm import VirtualSiteMap class StandaloneAdapter(BaseAdapter): @@ -136,3 +137,7 @@ def get_locale_source_roots(self, repo_root: Path) -> list[tuple[Path, str]]: # def get_absolute_url_prefixes(self, repo_root: Path | None = None) -> list[str]: # noqa: ARG002 """Standalone mode owns no absolute URL prefixes.""" return [] + + def get_entry_points(self, vsm: VirtualSiteMap) -> list[str]: + """Standalone mode has no nav tree; all routes are entry points.""" + return list(vsm.keys()) diff --git a/src/zenzic/core/adapters/_zensical.py b/src/zenzic/core/adapters/_zensical.py index 125c5bf3..2009e939 100644 --- a/src/zenzic/core/adapters/_zensical.py +++ b/src/zenzic/core/adapters/_zensical.py @@ -60,7 +60,7 @@ if TYPE_CHECKING: from zenzic.core.adapters._base import RouteMetadata - from zenzic.models.vsm import RouteStatus + from zenzic.models.vsm import RouteStatus, VirtualSiteMap # ── Config discovery & loading ──────────────────────────────────────────────── @@ -459,6 +459,22 @@ def get_absolute_url_prefixes(self, repo_root: Path | None = None) -> list[str]: """Zensical is single-instance and exports no absolute URL prefixes.""" return [] + def get_entry_points(self, vsm: VirtualSiteMap) -> list[str]: + """Return canonical URLs serving as root entry points from nav configuration.""" + if not self._has_explicit_nav: + return ["/"] if "/" in vsm else [] + + entry_points = set() + for p in self._nav_paths: + ep = self._map_url(Path(p)) + if ep in vsm: + entry_points.add(ep) + + if "/" in vsm: + entry_points.add("/") + + return sorted(entry_points) + @classmethod def from_repo( cls, diff --git a/src/zenzic/core/baseline.py b/src/zenzic/core/baseline.py new file mode 100644 index 00000000..a19d3efa --- /dev/null +++ b/src/zenzic/core/baseline.py @@ -0,0 +1,153 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +"""Baseline & Regression Tracking Engine for Zenzic. + +Enables capturing existing technical debt into a deterministic baseline file +(.zenzic-baseline.json), matching subsequent run findings against the baseline, +and flagging baselined vs new findings without line-number sensitivity. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from zenzic.core.reporter import Finding + +BASELINE_SCHEMA_VERSION = "1.0" +DEFAULT_BASELINE_FILE = ".zenzic-baseline.json" + + +def compute_finding_signature( + code: str, + rel_path: str, + match_text: str = "", + message: str = "", +) -> str: + """Compute a deterministic SHA-256 signature for a finding. + + Resilient to line-number shifts. Computed as SHA-256 over: + Normalized(RuleCode) + ":" + PosixRelativePath + ":" + ContextTarget + + Target context extraction: + 1. Uses match_text if non-empty. + 2. Otherwise extracts quoted entities from message (e.g., target URLs or file paths). + 3. Falls back to normalized message if no quoted entities are found. + + Args: + code: Rule code (e.g. "Z410", "Z101"). + rel_path: POSIX relative file path. + match_text: Explicit matched text snippet if available. + message: Diagnostic finding message. + + Returns: + 16-character hex hash string. + """ + norm_code = code.strip().upper() + norm_path = Path(rel_path).as_posix().lstrip("./") + + if match_text and match_text.strip(): + context_target = match_text.strip() + else: + quotes = re.findall(r"['\"]([^'\"]+)['\"]", message) + if quotes: + context_target = ":".join(quotes) + else: + context_target = message.strip() + + payload = f"{norm_code}:{norm_path}:{context_target}".encode() + return hashlib.sha256(payload).hexdigest()[:16] + + +@dataclass +class BaselineData: + """Container for parsed baseline data.""" + + version: str = BASELINE_SCHEMA_VERSION + created_at: str = "" + score: float = 100.0 + findings_count: int = 0 + signatures: set[str] = field(default_factory=set) + metadata: dict[str, Any] = field(default_factory=dict) + + +class BaselineManager: + """Manager for loading, creating, and matching baseline snapshots.""" + + @staticmethod + def create_baseline( + score: float, + findings: list[Finding], + version_str: str = "", + ) -> BaselineData: + """Create a BaselineData instance from current score and findings.""" + sigs: set[str] = set() + for f in findings: + sig = compute_finding_signature(f.code, f.rel_path, f.match_text, f.message) + sigs.add(sig) + + return BaselineData( + version=BASELINE_SCHEMA_VERSION, + created_at=datetime.now(timezone.utc).isoformat(), + score=round(score, 2), + findings_count=len(sigs), + signatures=sigs, + metadata={"zenzic_version": version_str}, + ) + + @staticmethod + def save_baseline(baseline: BaselineData, file_path: Path) -> None: + """Save BaselineData to a JSON file.""" + data = { + "$schema": "https://zenzic.dev/schemas/zenzic-baseline.schema.json", + "version": baseline.version, + "created_at": baseline.created_at, + "score": baseline.score, + "findings_count": len(baseline.signatures), + "signatures": sorted(baseline.signatures), + "metadata": baseline.metadata, + } + file_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + + @staticmethod + def load_baseline(file_path: Path) -> BaselineData: + """Load BaselineData from a JSON file.""" + if not file_path.is_file(): + raise FileNotFoundError(f"Baseline file '{file_path}' does not exist.") + + raw = json.loads(file_path.read_text(encoding="utf-8")) + signatures = set(raw.get("signatures", [])) + return BaselineData( + version=raw.get("version", BASELINE_SCHEMA_VERSION), + created_at=raw.get("created_at", ""), + score=float(raw.get("score", 100.0)), + findings_count=int(raw.get("findings_count", len(signatures))), + signatures=signatures, + metadata=raw.get("metadata", {}), + ) + + @staticmethod + def apply_baseline(findings: list[Finding], baseline: BaselineData) -> tuple[int, int]: + """Mark findings as is_baselined=True if their signature is in the baseline. + + Returns: + (baselined_count, new_findings_count) + """ + baselined_count = 0 + new_count = 0 + for f in findings: + sig = compute_finding_signature(f.code, f.rel_path, f.match_text, f.message) + if sig in baseline.signatures: + f.is_baselined = True + baselined_count += 1 + else: + f.is_baselined = False + new_count += 1 + return baselined_count, new_count diff --git a/src/zenzic/core/codes.py b/src/zenzic/core/codes.py index a4b37d37..b225187e 100644 --- a/src/zenzic/core/codes.py +++ b/src/zenzic/core/codes.py @@ -140,6 +140,8 @@ class ZenzicExitCode: FROZEN_CODES: frozenset[str] = frozenset( { "Z000", + "Z110", # CONFIG_SYNTAX_ERROR — malformed .zenzic.toml + "Z111", # CONFIG_SCHEMA_ERROR — invalid .zenzic.toml schema "Z201", "Z202", "Z203", @@ -153,6 +155,8 @@ class ZenzicExitCode: NON_SUPPRESSIBLE_CODES: frozenset[str] = frozenset( { + "Z110", # CONFIG_SYNTAX_ERROR — malformed .zenzic.toml; non-suppressible + "Z111", # CONFIG_SCHEMA_ERROR — invalid .zenzic.toml schema; non-suppressible "Z201", "Z202", "Z203", @@ -174,7 +178,7 @@ class ZenzicExitCode: # Aborts config loading before any analysis; not in the DQS penalty table. "Z000": CodeDefinition("error", 0.0, None), "Z001": CodeDefinition("error", 0.0, None), # CORE_CONFIG_STRUCTURE - # ── Z1xx — Link Integrity ───────────────────────────────────────────────── + # ── Z1xx — Link Integrity & Configuration Validation ────────────────────── "Z101": CodeDefinition("error", 8.0, "structural"), # LINK_BROKEN "Z102": CodeDefinition("error", 5.0, "structural"), # ANCHOR_MISSING "Z103": CodeDefinition( @@ -186,10 +190,8 @@ class ZenzicExitCode: "Z107": CodeDefinition("error", 1.0, "structural"), # CIRCULAR_ANCHOR "Z108": CodeDefinition("error", 1.0, "structural", fixable=True), # EMPTY_LINK_TEXT "Z109": CodeDefinition("error", 3.0, "structural"), # EXTERNAL_LINK_BROKEN - "Z110": CodeDefinition("warning", 1.0, "structural"), # STALE_ALLOWLIST_ENTRY - "Z111": CodeDefinition( - "error", 8.0, "structural" - ), # VIRTUAL_ROUTE_BROKEN — ADR-031 paradox resolved + "Z110": CodeDefinition("error", 0.0, None), # CONFIG_SYNTAX_ERROR — malformed TOML + "Z111": CodeDefinition("error", 0.0, None), # CONFIG_SCHEMA_ERROR — invalid schema/type "Z112": CodeDefinition( "note", 0.0, None, "inactive" ), # (reserved) — slot free, not used in v0.17.0 @@ -228,6 +230,8 @@ class ZenzicExitCode: "Z404": CodeDefinition("warning", 3.0, "brand"), # CONFIG_ASSET_MISSING "Z405": CodeDefinition("warning", 3.0, "brand"), # UNUSED_ASSET "Z406": CodeDefinition("warning", 2.0, "brand"), # NAV_CONTRACT + "Z410": CodeDefinition("warning", 5.0, "structural"), # UNREACHABLE_GRAPH_NODE + "Z411": CodeDefinition("warning", 5.0, "structural"), # DEAD_END_NODE # ── Z5xx — Content Quality ──────────────────────────────────────────────── "Z501": CodeDefinition("warning", 2.0, "content"), # PLACEHOLDER "Z502": CodeDefinition("warning", 1.0, "content"), # SHORT_CONTENT @@ -235,6 +239,9 @@ class ZenzicExitCode: "Z504": CodeDefinition("warning", 0.0, None), # QUALITY_REGRESSION — governance gate "Z505": CodeDefinition("warning", 1.0, "content", fixable=True), # UNTAGGED_CODE_BLOCK "Z506": CodeDefinition("error", 5.0, "content"), # MALFORMED_FRONTMATTER + "Z510": CodeDefinition("warning", 1.0, "content"), # HEADING_HIERARCHY + "Z511": CodeDefinition("warning", 1.0, "content"), # EXCESSIVE_SENTENCE_LENGTH + "Z512": CodeDefinition("warning", 1.0, "content"), # EMPTY_SECTION # ── Z6xx — Governance ───────────────────────────────────────────────────── "Z601": CodeDefinition("warning", 2.0, "brand"), # BRAND_OBSOLESCENCE (escalates exponentially) "Z603": CodeDefinition("warning", 1.0, "governance", fixable=True), # DEAD_SUPPRESSION @@ -258,8 +265,8 @@ class ZenzicExitCode: "Z107": "CIRCULAR_ANCHOR", "Z108": "EMPTY_LINK_TEXT", "Z109": "EXTERNAL_LINK_BROKEN", - "Z110": "STALE_ALLOWLIST_ENTRY", - "Z111": "VIRTUAL_ROUTE_BROKEN", + "Z110": "CONFIG_SYNTAX_ERROR", + "Z111": "CONFIG_SCHEMA_ERROR", "Z112": "RESERVED", "Z113": "AUTHOR_KEY_COLLISION", "Z114": "LARGE_PAGINATION_SET", @@ -283,12 +290,17 @@ class ZenzicExitCode: "Z404": "CONFIG_ASSET_MISSING", "Z405": "UNUSED_ASSET", "Z406": "NAV_CONTRACT", + "Z410": "UNREACHABLE_GRAPH_NODE", + "Z411": "DEAD_END_NODE", "Z501": "PLACEHOLDER", "Z502": "SHORT_CONTENT", "Z503": "SNIPPET_ERROR", "Z504": "QUALITY_REGRESSION", "Z505": "UNTAGGED_CODE_BLOCK", "Z506": "MALFORMED_FRONTMATTER", + "Z510": "HEADING_HIERARCHY", + "Z511": "EXCESSIVE_SENTENCE_LENGTH", + "Z512": "EMPTY_SECTION", "Z601": "BRAND_OBSOLESCENCE", "Z603": "DEAD_SUPPRESSION", "Z901": "RULE_ENGINE_ERROR", @@ -312,8 +324,8 @@ class ZenzicExitCode: "Z107": "Self-referential anchor link — slug(text) resolves to the same fragment", "Z108": "Link label is empty or contains only whitespace", "Z109": "External URL returned an HTTP error or could not be reached", - "Z110": "Stale absolute path allowlist entry declared in .zenzic.toml", - "Z111": "Link targets a virtual route (tag page, paginated index, author profile) that was never generated by any frontmatter", + "Z110": "Malformed TOML syntax in configuration file (.zenzic.toml)", + "Z111": "Invalid schema structure or type in configuration file (.zenzic.toml)", "Z112": "Reserved slot — not used in v0.17.0", "Z113": "Duplicate author key declared across two or more blog author config files", "Z114": "Blog pagination set exceeds the 200-page informational threshold", @@ -341,6 +353,8 @@ class ZenzicExitCode: "Z404": "Asset referenced in engine config not found on disk", "Z405": "Asset file not referenced by any documentation page", "Z406": "Navigation contract violation detected", + "Z410": "Document is isolated and unreachable from the navigation entry points", + "Z411": "Document has no outgoing links and forms a structural dead end", # Z5xx — Content Quality "Z501": "Page contains placeholder or stub content", "Z502": "Page word count is below the minimum threshold", @@ -348,6 +362,9 @@ class ZenzicExitCode: "Z504": "Documentation quality score regressed below the saved baseline", "Z505": "Fenced code block has no language specifier", "Z506": "Frontmatter boundary is malformed (e.g., opening delimiter is '--' instead of '---')", + "Z510": "Heading hierarchy level skipped (e.g., H3 follows H1 without an intervening H2)", + "Z511": "Sentence length exceeds the maximum readability limit", + "Z512": "Heading section contains no body content before next heading or EOF", # Z6xx — Governance "Z601": "Deprecated brand term found in documentation source", "Z603": "Inline suppression directive does not suppress any active finding. Remove the dead comment.", @@ -469,10 +486,10 @@ class CoreScanner(NamedTuple): non_suppressible=False, ), CoreScanner( - codes="Z401\u2013404", + codes="Z401\u2013404, Z410\u2013411", name="Structure Guard", capability=( - "Directory-index integrity, orphan pages, missing alt text, config asset paths" + "Directory-index integrity, orphan pages, missing alt text, config asset paths, topological orphans and dead ends" ), primary_exit=1, non_suppressible=False, diff --git a/src/zenzic/core/content.py b/src/zenzic/core/content.py new file mode 100644 index 00000000..368e0fd5 --- /dev/null +++ b/src/zenzic/core/content.py @@ -0,0 +1,262 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +"""Deterministic Semantic Linting & Readability Metrics engine for Zenzic. + +Provides mathematical content quality evaluation for Markdown/MDX graphs, +enforcing heading hierarchy (Z510), sentence length limits (Z511), and +empty section detection (Z512) with strict line-number fidelity. +""" + +from __future__ import annotations + +import re +from pathlib import Path +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from zenzic.core.rules import RuleFinding + +# ATX Heading regex matching # to ###### +_ATX_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$") +# Sentence delimiter matching ., !, or ? followed by whitespace or end of string +_SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?])\s+") + + +def check_heading_hierarchy(file_path: Path, text: str) -> list[RuleFinding]: + """Z510: Detect skipped heading levels (e.g. H3 immediately following H1).""" + from zenzic.core.rules import RuleFinding + + findings: list[RuleFinding] = [] + lines = text.splitlines() + in_code_block = False + prev_level = 0 + + for i, line in enumerate(lines, start=1): + stripped = line.strip() + if stripped.startswith("```") or stripped.startswith("~~~"): + in_code_block = not in_code_block + continue + + if in_code_block: + continue + + m = _ATX_HEADING_RE.match(stripped) + if m: + level = len(m.group(1)) + if prev_level > 0 and level > prev_level + 1: + findings.append( + RuleFinding( + rule_id="Z510", + severity="warning", + file_path=file_path, + line_no=i, + message=( + f"Heading level H{level} skips previous level H{prev_level} " + f"(expected H{prev_level + 1} or lower)." + ), + matched_line=line, + ) + ) + prev_level = level + + return findings + + +def check_sentence_lengths(file_path: Path, text: str, max_words: int = 40) -> list[RuleFinding]: + """Z511: Detect sentences exceeding max_words readability threshold.""" + from zenzic.core.rules import RuleFinding + + findings: list[RuleFinding] = [] + lines = text.splitlines() + in_code_block = False + in_frontmatter = False + + # Collect prose sentences line by line, preserving starting line number + current_sentence_parts: list[str] = [] + current_start_line = 1 + + for i, line in enumerate(lines, start=1): + stripped = line.strip() + + # Handle frontmatter + if i == 1 and stripped == "---": + in_frontmatter = True + continue + if in_frontmatter: + if stripped == "---": + in_frontmatter = False + continue + + # Handle code blocks + if stripped.startswith("```") or stripped.startswith("~~~"): + in_code_block = not in_code_block + # Flush existing sentence accumulator on code block boundary + if current_sentence_parts: + full_sent = " ".join(current_sentence_parts) + words = full_sent.split() + if len(words) > max_words: + preview = full_sent[:50] + "..." if len(full_sent) > 50 else full_sent + findings.append( + RuleFinding( + rule_id="Z511", + severity="warning", + file_path=file_path, + line_no=current_start_line, + message=f"Sentence of {len(words)} words exceeds maximum limit of {max_words} words.", + match_text=preview, + ) + ) + current_sentence_parts.clear() + continue + + if in_code_block: + continue + + # Skip headings, blockquotes, tables, HTML comments + is_bullet = bool(re.match(r"^(\*|-|\d+\.)\s+", stripped)) + if ( + not stripped + or stripped.startswith("#") + or stripped.startswith("", re.DOTALL) _POLY_MDX_COMMENT_RE: re.RegexPattern = re.compile(r"\{\/\*.*?\*\/\}", re.DOTALL) +# Math block patterns for masking (display math $$...$$ and inline math $...$) +_POLY_DISPLAY_MATH_RE: re.RegexPattern = re.compile(r"\$\$.*?\$\$", re.DOTALL) +_POLY_INLINE_MATH_RE: re.RegexPattern = re.compile(r"\$[^$\n]+\$") + # Strip whitespaces and control characters from URLs prima del check Z205. _POLY_CLEAN_URL_RE: re.RegexPattern = re.compile(r"[\s\x00-\x1F]+") @@ -296,7 +300,9 @@ def extract(self, text: str) -> list[HtmlNodeInfo]: Lista di :class:`HtmlNodeInfo`, uno per ogni tag ````/```` trovato fuori dai blocchi di codice. """ - masked = self._mask_inline_code(self._mask_fences(self._mask_comments(text))) + masked = self._mask_math( + self._mask_inline_code(self._mask_fences(self._mask_comments(text))) + ) nodes: list[HtmlNodeInfo] = [] for m in _RE_POLY_TAG.finditer(masked): tag = m.group(1).lower() @@ -313,7 +319,7 @@ def extract_ref_defs(self, text: str) -> list[ReferenceLinkNode]: Fence-skipping obbligatorio tramite _mask_fences() e _mask_comments(). First-definition-wins per la risoluzione dei duplicati. """ - masked = self._mask_fences(self._mask_comments(text)) + masked = self._mask_math(self._mask_fences(self._mask_comments(text))) nodes: list[ReferenceLinkNode] = [] seen_labels: set[str] = set() @@ -351,7 +357,7 @@ def extract_inline_links(self, text: str) -> list[ExtractedLink]: Returns: List of :class:`ExtractedLink` with node_type="inline" or "image". """ - masked = self._mask_fences(self._mask_comments(text)) + masked = self._mask_math(self._mask_fences(self._mask_comments(text))) results: list[ExtractedLink] = [] for lineno, line in enumerate(masked.splitlines(), start=1): @@ -474,6 +480,16 @@ def _mask_fences(self, text: str) -> str: result.append(" " * len(line)) return "\n".join(result) + def _mask_math(self, text: str) -> str: + """Sostituisce blocchi matematici ($$...$$ e $...$) con spazi bianchi preservando i caratteri di a capo.""" + text = _POLY_DISPLAY_MATH_RE.sub( + lambda m: "".join("\n" if c == "\n" else " " for c in m.group(0)), text + ) + text = _POLY_INLINE_MATH_RE.sub( + lambda m: "".join("\n" if c == "\n" else " " for c in m.group(0)), text + ) + return text + def _parse_node(self, tag: str, attrs_str: str, line_no: int, raw_tag: str) -> HtmlNodeInfo: """Parsing lineare della stringa ``attrs`` e classificazione governance. diff --git a/src/zenzic/models/config.py b/src/zenzic/models/config.py index 6820e86d..9263047f 100644 --- a/src/zenzic/models/config.py +++ b/src/zenzic/models/config.py @@ -344,6 +344,10 @@ class ZenzicConfig(BaseModel): default=1, description="Minimum lines for a code block to be checked (skip trivial one-liners).", ) + max_sentence_length: int = Field( + default=40, + description="Maximum words allowed in a sentence before triggering Z511 EXCESSIVE_SENTENCE_LENGTH.", + ) placeholder_max_words: int = Field( default=50, description="Pages with fewer than this many words are flagged as placeholders.", @@ -837,7 +841,9 @@ def load(cls, repo_root: Path) -> tuple[ZenzicConfig, bool]: return config, False @classmethod - def _apply_local_toml(cls, config: ZenzicConfig, repo_root: Path) -> None: + def _apply_local_toml( + cls, config: ZenzicConfig, repo_root: Path, raise_on_error: bool = False + ) -> None: """Apply machine-local overrides from ``.zenzic.local.toml``. The local file is git-ignored and machine-local. It can override a @@ -874,8 +880,10 @@ def _apply_local_toml(cls, config: ZenzicConfig, repo_root: Path) -> None: try: with local_toml.open("rb") as f: local_data = tomllib.load(f) - except tomllib.TOMLDecodeError: - return # malformed local file — silently skip to avoid hard failures + except tomllib.TOMLDecodeError as exc: + if raise_on_error: + raise exc + return # malformed local file — silently skip when not in strict validation mode # Note: Z001 is a ZenzicConfigError raised before scanning begins — # not a scanner finding code. @@ -1027,3 +1035,148 @@ def _apply_local_toml(cls, config: ZenzicConfig, repo_root: Path) -> None: # Re-compile forbidden_patterns union regex after all local merges are complete. config._recompile_forbidden_patterns() + + +def load_config_with_diagnostics( + repo_root: Path, config_file: Path | None = None +) -> tuple[ZenzicConfig | None, list[Any]]: + """Safely load ZenzicConfig, returning formal Finding objects on syntax/schema errors. + + Prevents exceptions from leaking to the CLI or LSP server (Zero-DBT). + Emits Z110 for TOMLDecodeError and Z111 for ValidationError. + Extracts line numbers from errors whenever available. + """ + from pydantic import ValidationError + + from zenzic.core.reporter import Finding + + target_file = config_file if config_file else (repo_root / ".zenzic.toml") + if not target_file.is_file() and (repo_root.parent / ".zenzic.toml").is_file(): + target_file = repo_root.parent / ".zenzic.toml" + + if not target_file.is_file(): + pyproject = repo_root / "pyproject.toml" + if not pyproject.is_file() and (repo_root.parent / "pyproject.toml").is_file(): + pyproject = repo_root.parent / "pyproject.toml" + if pyproject.is_file(): + target_file = pyproject + + if not target_file.is_file(): + try: + cfg = ZenzicConfig() + cfg.origin_file = repo_root / ".zenzic.toml" + return cfg, [] + except Exception: + pass + + content = "" + try: + content = target_file.read_text(encoding="utf-8") + except OSError: + pass + + rel_file_str = ( + str(target_file.relative_to(repo_root)) + if repo_root in target_file.parents or target_file.parent == repo_root + else target_file.name + ) + + try: + if target_file.name == "pyproject.toml": + data = tomllib.loads(content) + tool_data = data.get("tool", {}).get("zenzic", {}) + if not tool_data: + cfg = ZenzicConfig() + cfg.origin_file = target_file + return cfg, [] + ZenzicConfig._validate_no_swallowed_root_keys(tool_data) + cfg = ZenzicConfig._build_from_data(tool_data) + cfg.origin_file = target_file + ZenzicConfig._apply_local_toml(cfg, repo_root, raise_on_error=True) + return cfg, [] + else: + data = tomllib.loads(content) + ZenzicConfig._validate_no_swallowed_root_keys(data) + cfg = ZenzicConfig._build_from_data(data) + cfg.origin_file = target_file + ZenzicConfig._apply_local_toml(cfg, repo_root, raise_on_error=True) + return cfg, [] + except tomllib.TOMLDecodeError as exc: + err_file_str = rel_file_str + err_content = content + local_toml = repo_root / ".zenzic.local.toml" + if local_toml.is_file(): + try: + tomllib.loads(local_toml.read_text(encoding="utf-8")) + except tomllib.TOMLDecodeError: + err_file_str = ".zenzic.local.toml" + try: + err_content = local_toml.read_text(encoding="utf-8") + except OSError: + pass + + line_no = getattr(exc, "lineno", 1) + if line_no is None or line_no <= 0: + line_no = 1 + lines = err_content.splitlines() if err_content else [] + source_line = lines[line_no - 1] if 0 < line_no <= len(lines) else "" + finding = Finding( + rel_path=err_file_str, + line_no=line_no, + code="Z110", + severity="error", + message=f"TOML syntax error in configuration file: {exc}", + source_line=source_line, + ) + return None, [finding] + except ValidationError as exc: + findings: list[Finding] = [] + content_lines = content.splitlines() if content else [] + for err in exc.errors(): + field_name = ".".join(str(p) for p in err.get("loc", [])) + last_key = str(err["loc"][-1]) if err.get("loc") else "" + line_no = 1 + if last_key and content_lines: + for idx, line in enumerate(content_lines, start=1): + if last_key in line: + line_no = idx + break + source_line = ( + content_lines[line_no - 1] + if content_lines and 0 < line_no <= len(content_lines) + else "" + ) + msg = f"Configuration schema error: {err.get('msg', 'invalid value')} (field: '{field_name}')" + findings.append( + Finding( + rel_path=rel_file_str, + line_no=line_no, + code="Z111", + severity="error", + message=msg, + match_text=last_key, + source_line=source_line, + ) + ) + return None, findings + except Exception as exc: + from zenzic.core.exceptions import ZenzicConfigError + + err_rel_path = rel_file_str + if isinstance(exc, ZenzicConfigError) and exc.context and "file" in exc.context: + try: + f_path = Path(exc.context["file"]) + if repo_root in f_path.parents or f_path.parent == repo_root: + err_rel_path = str(f_path.relative_to(repo_root)) + else: + err_rel_path = f_path.name + except Exception: + pass + finding = Finding( + rel_path=err_rel_path, + line_no=1, + code="Z111", + severity="error", + message=f"Configuration error: {exc}", + ) + return None, [finding] diff --git a/src/zenzic/models/vsm.py b/src/zenzic/models/vsm.py index 6846bd00..f91c4279 100644 --- a/src/zenzic/models/vsm.py +++ b/src/zenzic/models/vsm.py @@ -285,11 +285,14 @@ class VirtualSiteMap(dict[str, Route]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.incoming_links: dict[str, set[Path]] = {} + self.outgoing_links: dict[str, list[str]] = {} - def remove_outgoing_links(self, path: Path) -> None: - """Discard `path` from every entry in the reverse index.""" + def remove_outgoing_links(self, path: Path, canonical_url: str = "") -> None: + """Discard `path` from every entry in the reverse index and clear its outgoing links.""" for dependent_set in self.incoming_links.values(): dependent_set.discard(path) + if canonical_url and canonical_url in self.outgoing_links: + self.outgoing_links[canonical_url] = [] def reindex_outgoing_links( self, @@ -298,9 +301,20 @@ def reindex_outgoing_links( docs_root: Path, extra_mounts: list[tuple[Path, str]], adapter: BaseAdapter, + canonical_url: str = "", ) -> None: """Rebuild the reverse-index entries emitted by `path`.""" - self.remove_outgoing_links(path) + if not canonical_url: + # Fallback O(N) lookup if not provided + try: + rel_posix = path.relative_to(docs_root).as_posix() + except ValueError: + rel_posix = path.absolute().as_posix() + canonical_url = next((url for url, r in self.items() if r.source == rel_posix), "") + + self.remove_outgoing_links(path, canonical_url=canonical_url) + + targets: set[str] = set() from zenzic.core.rules import _extract_inline_links_with_lines from zenzic.core.validator import PolyglotExtractor @@ -315,6 +329,7 @@ def _register(url: str) -> None: ) if canonical: self.incoming_links.setdefault(canonical, set()).add(path) + targets.add(canonical) for url, _lineno, _raw in _extract_inline_links_with_lines(content): _register(url) @@ -323,6 +338,9 @@ def _register(url: str) -> None: if node.href: _register(node.href) + if canonical_url: + self.outgoing_links[canonical_url] = sorted(targets) + def resolve_link_to_canonical( source_file: Path, diff --git a/static/assets/brand/png/zenzic-icon.png b/static/assets/brand/png/zenzic-icon.png new file mode 100644 index 00000000..556e5a86 Binary files /dev/null and b/static/assets/brand/png/zenzic-icon.png differ diff --git a/static/assets/brand/png/zenzic-icon.png.license b/static/assets/brand/png/zenzic-icon.png.license new file mode 100644 index 00000000..73c93a85 --- /dev/null +++ b/static/assets/brand/png/zenzic-icon.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 PythonWoods + +SPDX-License-Identifier: Apache-2.0 diff --git a/static/assets/brand/png/zenzic-wordmark-dark.png b/static/assets/brand/png/zenzic-wordmark-dark.png new file mode 100644 index 00000000..88b0abfe Binary files /dev/null and b/static/assets/brand/png/zenzic-wordmark-dark.png differ diff --git a/static/assets/brand/png/zenzic-wordmark-dark.png.license b/static/assets/brand/png/zenzic-wordmark-dark.png.license new file mode 100644 index 00000000..73c93a85 --- /dev/null +++ b/static/assets/brand/png/zenzic-wordmark-dark.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 PythonWoods + +SPDX-License-Identifier: Apache-2.0 diff --git a/static/assets/brand/png/zenzic-wordmark.png b/static/assets/brand/png/zenzic-wordmark.png index 20214486..cf609464 100644 Binary files a/static/assets/brand/png/zenzic-wordmark.png and b/static/assets/brand/png/zenzic-wordmark.png differ diff --git a/static/assets/brand/svg/zenzic-icon.svg b/static/assets/brand/svg/zenzic-icon.svg index 9dbf2031..30cbf946 100644 --- a/static/assets/brand/svg/zenzic-icon.svg +++ b/static/assets/brand/svg/zenzic-icon.svg @@ -1,21 +1,36 @@ + + + + + + + + + + + + + + + + - + - + - - - - + + + - + diff --git a/static/assets/brand/svg/zenzic-wordmark-dark.svg b/static/assets/brand/svg/zenzic-wordmark-dark.svg index 44998fba..7ef396dc 100644 --- a/static/assets/brand/svg/zenzic-wordmark-dark.svg +++ b/static/assets/brand/svg/zenzic-wordmark-dark.svg @@ -1,29 +1,59 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + - + + - + - - - - - - - - + + + + + + + + - - Zenzic + + Zenzic - - DOCUMENT INTEGRITY ENGINE + DOCUMENT INTEGRITY ENGINE diff --git a/static/assets/brand/svg/zenzic-wordmark.svg b/static/assets/brand/svg/zenzic-wordmark.svg index bd2de9f3..a7647854 100644 --- a/static/assets/brand/svg/zenzic-wordmark.svg +++ b/static/assets/brand/svg/zenzic-wordmark.svg @@ -1,29 +1,60 @@ - + + + + + + + + + + + + + + + + + + + + + + - + + - + - - + + - - - + + + - + - - Zenzic + + Zenzic - - DOCUMENT INTEGRITY ENGINE + DOCUMENT INTEGRITY ENGINE diff --git a/static/assets/social/social-card.png b/static/assets/social/social-card.png index ff86061d..579c3a55 100644 Binary files a/static/assets/social/social-card.png and b/static/assets/social/social-card.png differ diff --git a/static/assets/social/social-card.svg b/static/assets/social/social-card.svg index dfae906e..92cd5b3d 100644 --- a/static/assets/social/social-card.svg +++ b/static/assets/social/social-card.svg @@ -1,15 +1,30 @@ + + + + + + + + + + + + + + + - + - + - + @@ -18,11 +33,11 @@ - - - + + + - + @@ -30,5 +45,5 @@ Zenzic - STRICT MARKDOWN STATIC ANALYZER & CREDENTIAL SCANNER + DETERMINISTIC DOCUMENT INTEGRITY ENGINE & SAST diff --git a/tests/test_baseline.py b/tests/test_baseline.py new file mode 100644 index 00000000..2810b4b6 --- /dev/null +++ b/tests/test_baseline.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +"""Unit and integration tests for Baseline & Regression Tracking engine.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from jsonschema import validate +from typer.testing import CliRunner + +from zenzic.core.baseline import ( + DEFAULT_BASELINE_FILE, + BaselineManager, + compute_finding_signature, +) +from zenzic.core.reporter import Finding +from zenzic.main import app + + +runner = CliRunner() + + +def test_signature_computation_resilient_to_line_shifts() -> None: + """Line numbers must not affect the computed signature.""" + sig1 = compute_finding_signature("Z410", "docs/guide.md", "", "Document is isolated: '/guide/'") + sig2 = compute_finding_signature("Z410", "docs/guide.md", "", "Document is isolated: '/guide/'") + assert sig1 == sig2 + assert len(sig1) == 16 + + # Differing targets produce different signatures + sig_other = compute_finding_signature( + "Z410", "docs/guide.md", "", "Document is isolated: '/other/'" + ) + assert sig1 != sig_other + + # Context match_text precedence + sig_match1 = compute_finding_signature("Z101", "docs/a.md", "http://broken1.com", "Broken link") + sig_match2 = compute_finding_signature("Z101", "docs/a.md", "http://broken2.com", "Broken link") + assert sig_match1 != sig_match2 + + +def test_baseline_schema_validation(tmp_path: Path) -> None: + """Saved baseline JSON must strictly conform to zenzic-baseline.schema.json.""" + schema_path = Path(__file__).parent.parent / "zenzic-baseline.schema.json" + assert schema_path.is_file(), "zenzic-baseline.schema.json schema file missing." + + schema = json.loads(schema_path.read_text(encoding="utf-8")) + + findings = [ + Finding( + rel_path="docs/index.md", + line_no=15, + code="Z101", + severity="error", + message="Broken link 'missing.md'", + match_text="missing.md", + ) + ] + bdata = BaselineManager.create_baseline(90.0, findings, version_str="0.27.0") + file_path = tmp_path / DEFAULT_BASELINE_FILE + BaselineManager.save_baseline(bdata, file_path) + + raw = json.loads(file_path.read_text(encoding="utf-8")) + validate(instance=raw, schema=schema) + + +def test_baseline_manager_apply_baseline() -> None: + """apply_baseline flags matching findings as is_baselined=True.""" + f1 = Finding( + rel_path="docs/a.md", line_no=1, code="Z410", severity="warning", message="Isolated: '/a/'" + ) + f2 = Finding( + rel_path="docs/b.md", line_no=10, code="Z411", severity="warning", message="Dead end: '/b/'" + ) + + bdata = BaselineManager.create_baseline(95.0, [f1], version_str="0.27.0") + baselined_cnt, new_cnt = BaselineManager.apply_baseline([f1, f2], bdata) + + assert baselined_cnt == 1 + assert new_cnt == 1 + assert f1.is_baselined is True + assert f2.is_baselined is False + + +def test_cli_update_baseline_and_consume(tmp_path: Path) -> None: + """CLI zenzic check --update-baseline creates baseline; subsequent check exits 0.""" + docs = tmp_path / "docs" + docs.mkdir() + (docs / "index.md").write_text("# Index\n\n[Dead End](dead.md)\n") + (docs / "dead.md").write_text("# Dead End\n\nNo outgoing links.\n") + + (tmp_path / ".zenzic.toml").write_text("[site]\nengine = 'standalone'\n") + + # Step 1: Update baseline + res = runner.invoke(app, ["check", "all", str(docs), "--update-baseline"]) + assert res.exit_code in (0, 1) + baseline_file = tmp_path / DEFAULT_BASELINE_FILE + assert baseline_file.is_file() + + # Step 2: Consume baseline without new defects -> Exit 0 + res2 = runner.invoke(app, ["check", "all", str(docs), "--baseline", str(baseline_file)]) + assert res2.exit_code == 0 + assert "[BASELINED]" in res2.stdout or "Baseline:" in res2.stdout + + # Step 3: Introduce new defect -> Exit 1 + (docs / "new_orphan.md").write_text("# New Orphan\n\nNo links.\n") + res3 = runner.invoke(app, ["check", "all", str(docs), "--baseline", str(baseline_file)]) + assert res3.exit_code == 1 diff --git a/tests/test_cli.py b/tests/test_cli.py index e1d0ea5a..57560366 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2033,3 +2033,31 @@ def test_templates_root_keys_not_swallowed() -> None: assert "forbidden_patterns" in local_data, ( "'forbidden_patterns' was swallowed in LOCAL_TOML_TEMPLATE!" ) + + +def test_check_all_only_filter_excludes_z118( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Verify that zenzic check all --only z101 excludes Z118 warnings.""" + monkeypatch.chdir(tmp_path) + from typer.testing import CliRunner + + from zenzic.main import app + + docs = tmp_path / "docs" + docs.mkdir() + (docs / "index.md").write_text("# Test\n[Valid](index.md)\n", encoding="utf-8") + + toml = tmp_path / ".zenzic.toml" + toml.write_text( + '[governance]\ndirectory_policies = {"docs/unused/**" = ["Z405"]}\n', + encoding="utf-8", + ) + + runner = CliRunner() + result = runner.invoke( + app, + ["check", "all", "--only", "z101", "--no-header"], + catch_exceptions=False, + ) + assert "Z118" not in result.output diff --git a/tests/test_codes.py b/tests/test_codes.py index 12a3691a..5c1958ed 100644 --- a/tests/test_codes.py +++ b/tests/test_codes.py @@ -64,7 +64,7 @@ def test_sarif_levels_are_valid_values() -> None: # informational within the Z1xx range because it reports a threshold metric, # not a broken link. _Z1XX_NON_ERROR_EXCEPTIONS: frozenset[str] = frozenset( - {"Z106", "Z110", "Z112", "Z114", "Z118", "Z120", "Z122", "Z123"} + {"Z106", "Z112", "Z114", "Z118", "Z120", "Z122", "Z123"} ) @@ -73,16 +73,16 @@ def test_sarif_levels_are_valid_values() -> None: [c for c in CODE_NAMES if c.startswith("Z1") and c not in _Z1XX_NON_ERROR_EXCEPTIONS], ) def test_z1xx_sarif_level_is_error(code: str) -> None: - """Z1xx (Link Integrity) codes must have SARIF level 'error'.""" + """Z1xx codes must have SARIF level 'error'.""" assert CODE_SARIF_LEVELS[code] == "error", ( - f"{code} should be 'error' (Link Integrity), got '{CODE_SARIF_LEVELS[code]}'" + f"{code} should be 'error', got '{CODE_SARIF_LEVELS[code]}'" ) -def test_z110_sarif_level_is_warning() -> None: - """Z110 STALE_ALLOWLIST_ENTRY is config-hygiene — must be SARIF level 'warning'.""" - assert CODE_SARIF_LEVELS["Z110"] == "warning", ( - f"Z110 should be 'warning' (config hygiene), got '{CODE_SARIF_LEVELS['Z110']}'" +def test_z110_sarif_level_is_error() -> None: + """Z110 CONFIG_SYNTAX_ERROR is a fatal config error — must be SARIF level 'error'.""" + assert CODE_SARIF_LEVELS["Z110"] == "error", ( + f"Z110 should be 'error' (config syntax error), got '{CODE_SARIF_LEVELS['Z110']}'" ) @@ -156,11 +156,11 @@ def test_no_orphan_definitions() -> None: assert orphans == [], f"Ghost codes in CODE_DEFINITIONS (not in CODE_NAMES): {orphans}" -def test_z103_z111_z113_are_structural_with_penalty() -> None: +def test_z103_z113_are_structural_with_penalty() -> None: """ADR-031: paradox codes must have error severity, positive penalty, structural category.""" from zenzic.core.codes import CODE_DEFINITIONS - for code in ("Z103", "Z111", "Z113"): + for code in ("Z103", "Z113"): defn = CODE_DEFINITIONS[code] assert defn.severity == "error", f"{code}.severity should be 'error'" assert defn.penalty > 0.0, f"{code}.penalty should be > 0" diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py new file mode 100644 index 00000000..278c6348 --- /dev/null +++ b/tests/test_config_validation.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +"""Unit and integration tests for Configuration Validation Engine (Z110, Z111).""" + +from __future__ import annotations + +from pathlib import Path + +from zenzic.core.incremental import IncrementalAnalysisEngine +from zenzic.core.scanner import scan_docs_references +from zenzic.models.config import ZenzicConfig, load_config_with_diagnostics +from zenzic.models.vsm import VirtualBufferOverlay, build_vsm + + +def test_z110_toml_syntax_error_line_extraction(tmp_path: Path) -> None: + """Z110 extracts line numbers from TOML syntax errors in .zenzic.toml.""" + config_file = tmp_path / ".zenzic.toml" + config_file.write_text( + "[site]\nengine = 'standalone'\nplaceholder_max_words = [ unclosed_array\n", # line 3 + encoding="utf-8", + ) + + cfg, findings = load_config_with_diagnostics(tmp_path) + assert cfg is None + assert len(findings) == 1 + assert findings[0].code == "Z110" + assert findings[0].severity == "error" + assert findings[0].line_no in (3, 4) + assert "TOML syntax error" in findings[0].message + + +def test_z111_schema_error_field_and_line_extraction(tmp_path: Path) -> None: + """Z111 extracts field name and line number for Pydantic schema validation errors.""" + config_file = tmp_path / ".zenzic.toml" + config_file.write_text( + "placeholder_max_words = 'not_a_number'\n", # line 1 + encoding="utf-8", + ) + + cfg, findings = load_config_with_diagnostics(tmp_path) + assert cfg is None + assert len(findings) >= 1 + z111 = next(f for f in findings if f.code == "Z111") + assert z111.severity == "error" + assert z111.line_no == 1 + assert "placeholder_max_words" in z111.message + + +def test_scanner_halts_markdown_analysis_on_config_error(tmp_path: Path) -> None: + """Batch scanner halts Markdown scanning when Z110/Z111 exist.""" + docs = tmp_path / "docs" + docs.mkdir() + (docs / "index.md").write_text("# Title\n\n[Broken Link](nonexistent.md)\n") + + (tmp_path / ".zenzic.toml").write_text("invalid_toml = [ missing_bracket\n") + + from zenzic.core.exclusion import LayeredExclusionManager + + excl = LayeredExclusionManager(ZenzicConfig(), repo_root=tmp_path, docs_root=docs) + reports, _ = scan_docs_references(docs, excl, repo_root=tmp_path) + assert len(reports) == 1 + assert len(reports[0].findings) == 1 + assert reports[0].findings[0].code == "Z110" + + +def test_incremental_engine_emits_config_diagnostic_without_crashing(tmp_path: Path) -> None: + """LSP incremental engine converts config errors to ZenzicDiagnostic for .zenzic.toml.""" + docs = tmp_path / "docs" + docs.mkdir() + index_file = docs / "index.md" + index_file.write_text("# Index\n") + (tmp_path / ".zenzic.toml").write_text("placeholder_max_words = 'invalid'\n") + + from zenzic.core.adapters import StandaloneAdapter + + cfg = ZenzicConfig() + adapter = StandaloneAdapter() + engine = IncrementalAnalysisEngine( + config=cfg, + rule_engine=None, + adapter=adapter, + docs_root=docs, + repo_root=tmp_path, + ) + md_contents = {index_file.resolve(): "# Index\n"} + vsm = build_vsm(adapter, docs, md_contents=md_contents) + overlay = VirtualBufferOverlay(vsm) + + diags_map = engine.process_changes(vsm, overlay, changed_uris=None) + config_uri = (tmp_path / ".zenzic.toml").resolve().as_uri() + + assert config_uri in diags_map + diags = diags_map[config_uri] + assert len(diags) >= 1 + assert diags[0].code == "Z111" + + +def test_dqs_score_collapses_to_zero_on_config_error() -> None: + """Z110 and Z111 force DQS score to 0.0 with security_override = True.""" + from zenzic.core.scorer import compute_score + + report_z110 = compute_score({"Z110": 1}) + assert report_z110.score == 0 + assert report_z110.security_override is True + + report_z111 = compute_score({"Z111": 1}) + assert report_z111.score == 0 + assert report_z111.security_override is True + + +def test_local_config_validation(tmp_path: Path) -> None: + """load_config_with_diagnostics validates .zenzic.local.toml and attaches finding to it.""" + (tmp_path / ".zenzic.toml").write_text("[build_context]\nengine = 'standalone'\n") + (tmp_path / ".zenzic.local.toml").write_text("invalid_local_toml = [ unclosed_array\n") + + cfg, findings = load_config_with_diagnostics(tmp_path) + assert cfg is None + assert len(findings) == 1 + assert findings[0].code == "Z110" + assert findings[0].rel_path == ".zenzic.local.toml" diff --git a/tests/test_content_linting.py b/tests/test_content_linting.py new file mode 100644 index 00000000..5cf7c6ca --- /dev/null +++ b/tests/test_content_linting.py @@ -0,0 +1,144 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +"""Unit and integration tests for Semantic Linting & Readability Metrics (Z510, Z511, Z512).""" + +from __future__ import annotations + +import json +from io import StringIO +from pathlib import Path +from unittest.mock import patch + +from zenzic.cli._shared import _output_sarif_findings +from zenzic.core.content import ( + check_empty_sections, + check_heading_hierarchy, + check_sentence_lengths, +) +from zenzic.core.reporter import Finding + + +def test_z510_heading_hierarchy_detection(tmp_path: Path) -> None: + """Z510 flags skipped heading levels with line-number fidelity.""" + file_path = tmp_path / "doc.md" + text = ( + "# Title\n" + "\n" + "### Skipped Subheading\n" # line 3: H1 -> H3 (skips H2) + "\n" + "## Valid H2\n" # line 5 + "### Valid H3\n" # line 6 + "##### Skipped H5\n" # line 7: H3 -> H5 (skips H4) + ) + file_path.write_text(text, encoding="utf-8") + + findings = check_heading_hierarchy(file_path, text) + assert len(findings) == 2 + assert findings[0].rule_id == "Z510" + assert findings[0].line_no == 3 + assert "H3 skips previous level H1" in findings[0].message + + assert findings[1].rule_id == "Z510" + assert findings[1].line_no == 7 + assert "H5 skips previous level H3" in findings[1].message + + +def test_z511_sentence_length_and_line_fidelity(tmp_path: Path) -> None: + """Z511 flags sentences > max_words while maintaining line-number fidelity across code blocks.""" + file_path = tmp_path / "doc.md" + long_sentence = ( + "This is an exceptionally long prose sentence that continues through multiple clauses " + "and ideas without any sentence ending punctuation until it easily exceeds the maximum " + "readability limit of forty words defined in the Zenzic workspace configuration file " + "by adding extra explanatory words at the end of the sentence to guarantee it fails." + ) + text = ( + "# Title\n" + "\n" + "```python\n" + "# Code blocks should be ignored and not trigger Z511\n" + "def foo():\n" + " return 'a' * 100\n" + "```\n" + "\n" + f"{long_sentence}\n" # line 9 + ) + file_path.write_text(text, encoding="utf-8") + + findings = check_sentence_lengths(file_path, text, max_words=40) + assert len(findings) == 1 + assert findings[0].rule_id == "Z511" + assert findings[0].line_no == 9 + assert "Sentence of" in findings[0].message + + +def test_z512_empty_section_detection(tmp_path: Path) -> None: + """Z512 flags headings with zero body content before next heading or EOF.""" + file_path = tmp_path / "doc.md" + text = ( + "# Title\n" + "\n" + "Overview text for title.\n" + "\n" + "## Empty Section 1\n" # line 5: empty + "## Empty Section 2\n" # line 6: empty + "## Valid Section\n" # line 7: has body content + "\n" + "Here is body content for valid section.\n" + "\n" + "## Empty Section At EOF\n" # line 11: empty at EOF + ) + file_path.write_text(text, encoding="utf-8") + + findings = check_empty_sections(file_path, text) + assert len(findings) == 3 + assert findings[0].rule_id == "Z512" + assert findings[0].line_no == 5 + assert "Empty Section 1" in findings[0].message + + assert findings[1].rule_id == "Z512" + assert findings[1].line_no == 6 + + assert findings[2].rule_id == "Z512" + assert findings[2].line_no == 11 + + +def test_sarif_payload_contains_z510_z511_z512_rules() -> None: + """SARIF output payload automatically includes Z510, Z511, Z512 rule metadata.""" + findings = [ + Finding( + rel_path="docs/a.md", line_no=3, code="Z510", severity="warning", message="Skipped H2" + ), + Finding( + rel_path="docs/b.md", + line_no=9, + code="Z511", + severity="warning", + message="Long sentence", + ), + Finding( + rel_path="docs/c.md", + line_no=4, + code="Z512", + severity="warning", + message="Empty section", + ), + ] + + out_buffer = StringIO() + with patch("sys.stdout", out_buffer): + _output_sarif_findings(findings, "0.27.0") + + sarif_data = json.loads(out_buffer.getvalue()) + rules = sarif_data["runs"][0]["tool"]["driver"]["rules"] + rule_ids = {r["id"] for r in rules} + + assert "Z510" in rule_ids + assert "Z511" in rule_ids + assert "Z512" in rule_ids + + z510_rule = next(r for r in rules if r["id"] == "Z510") + assert ( + z510_rule["shortDescription"]["text"] + == "Heading hierarchy level skipped (e.g., H3 follows H1 without an intervening H2)" + ) diff --git a/tests/test_gallery_phase2bc.py b/tests/test_gallery_phase2bc.py index fba8a7d9..cc782be3 100644 --- a/tests/test_gallery_phase2bc.py +++ b/tests/test_gallery_phase2bc.py @@ -71,6 +71,39 @@ def test_z406_registered_in_gallery() -> None: assert "z406" in _GALLERY +def test_z410_registered_in_gallery() -> None: + assert "z410" in _GALLERY + + +def test_z411_registered_in_gallery() -> None: + assert "z411" in _GALLERY + + +def test_all_active_codes_covered_in_gallery() -> None: + from zenzic.core.codes import CODE_DEFINITIONS + + # Non-scannable internal, governance, or system escalation codes + INTERNAL_CODES = { + "z000", + "z106", + "z111", + "z112", + "z113", + "z114", + "z203", + "z504", + "z506", + "z901", + "z902", + "z906", + } + + active_codes = {code.lower() for code in CODE_DEFINITIONS if code.lower() not in INTERNAL_CODES} + gallery_codes = set(_GALLERY.keys()) + missing = active_codes - gallery_codes + assert not missing, f"Missing lab scenarios for active codes: {sorted(missing)}" + + # ── Fixture directory existence ─────────────────────────────────────────────── @@ -82,6 +115,8 @@ def test_z406_registered_in_gallery() -> None: ("z401", "z401-missing-directory-index"), ("z404", "z404-config-asset-missing"), ("z406", "z406-nav-contract"), + ("z410", "z410-unreachable-graph-node"), + ("z411", "z411-dead-end-node"), ], ) def test_fixture_directory_exists(code: str, dirname: str) -> None: diff --git a/tests/test_rules.py b/tests/test_rules.py index f756207e..67a88b9a 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -245,8 +245,9 @@ def test_scan_docs_with_custom_rules_from_config(tmp_path: Path) -> None: mgr = make_mgr(config, repo_root=tmp_path) reports, _ = scan_docs_references(docs_root, mgr, config=config) assert len(reports) == 1 - assert len(reports[0].rule_findings) == 1 - assert reports[0].rule_findings[0].rule_id == "ZZ-DRAFT" + draft_findings = [f for f in reports[0].rule_findings if f.rule_id == "ZZ-DRAFT"] + assert len(draft_findings) == 1 + assert draft_findings[0].rule_id == "ZZ-DRAFT" def test_build_rule_engine_always_built() -> None: @@ -285,8 +286,10 @@ def test_scan_docs_with_enabled_plugins_from_config(tmp_path: Path) -> None: reports, _ = scan_docs_references(docs_root, mgr, config=config) assert len(reports) == 1 - assert len(reports[0].rule_findings) == 1 - assert reports[0].rule_findings[0].rule_id == "PLUG-TODO" + plugin_findings = [f for f in reports[0].rule_findings if f.rule_id == "PLUG-TODO"] + assert len(plugin_findings) == 1 + assert plugin_findings[0].rule_id == "PLUG-TODO" + assert plugin_findings[0].message == "Plugin TODO marker found." def test_scan_docs_with_unknown_plugin_raises_contract_error(tmp_path: Path) -> None: @@ -391,12 +394,12 @@ def test_custom_rules_fire_regardless_of_engine( mgr = make_mgr(config, repo_root=repo) reports, _ = scan_docs_references(docs_root, mgr, config=config) assert len(reports) == 1, f"Expected 1 report for engine={engine!r}" - rule_findings = reports[0].rule_findings - assert len(rule_findings) == 1, ( - f"Expected 1 rule finding for engine={engine!r}, got {rule_findings}" + draft_findings = [f for f in reports[0].rule_findings if f.rule_id == "ZZ-DRAFT"] + assert len(draft_findings) == 1, ( + f"Expected 1 ZZ-DRAFT finding for engine={engine!r}, got {reports[0].rule_findings}" ) - assert rule_findings[0].rule_id == "ZZ-DRAFT" - assert rule_findings[0].is_error + assert draft_findings[0].rule_id == "ZZ-DRAFT" + assert draft_findings[0].is_error # ─── Violation dataclass ────────────────────────────────────────────────────── diff --git a/tests/test_scorer.py b/tests/test_scorer.py index 36532b8b..e5c824e0 100644 --- a/tests/test_scorer.py +++ b/tests/test_scorer.py @@ -303,11 +303,10 @@ def test_z103_structural_penalty() -> None: assert report.score == 98 # (30-2)/30*30 + 25 + 20 + 25 = 28+25+20+25=98 -def test_z111_structural_penalty_equals_z101() -> None: - """Z111 VIRTUAL_ROUTE_BROKEN: -8.0 pts, same weight as Z101 LINK_BROKEN (ADR-031).""" +def test_z111_config_schema_error_score() -> None: + """Z111 CONFIG_SCHEMA_ERROR: 0.0 DQS penalty (config abort, score 0).""" r_z111 = compute_score({"Z111": 1}) - r_z101 = compute_score({"Z101": 1}) - assert r_z111.score == r_z101.score == 92 # (30-8)/30*30 + 25 + 20 + 25 = 22+25+20+25=92 + assert r_z111.score == 0 def test_z113_structural_penalty() -> None: diff --git a/tests/test_suppressions.py b/tests/test_suppressions.py index 091860eb..8da1b1e4 100644 --- a/tests/test_suppressions.py +++ b/tests/test_suppressions.py @@ -247,3 +247,54 @@ def test_count_inline_suppressions_compatibility(self) -> None: ) tracker = SuppressionTracker(_FILE, text) assert count_inline_suppressions(text) == len(tracker.directives) == 2 + + +def test_global_usage_tracker_toml_line_resolution(tmp_path: Path) -> None: + """Verify that Z118 findings report the actual line number in .zenzic.toml.""" + from zenzic.core.suppressions import GlobalUsageTracker + from zenzic.models.config import GovernanceConfig, ZenzicConfig + + toml_path = tmp_path / ".zenzic.toml" + toml_path.write_text( + "[governance]\n" + "directory_policies = {\n" + ' "docs/assets/**" = ["Z405"],\n' + ' "docs/blog/**" = ["Z405"]\n' + "}\n", + encoding="utf-8", + ) + + config = ZenzicConfig( + governance=GovernanceConfig( + directory_policies={ + "docs/assets/**": ["Z405"], + "docs/blog/**": ["Z405"], + } + ) + ) + config.origin_file = toml_path + + tracker = GlobalUsageTracker(config) + stale = tracker.get_stale_findings(check_all=True) + + lines_by_pattern = {f.message: f.line_no for f in stale} + assert any( + "docs/assets/**" in msg and line_no == 3 for msg, line_no in lines_by_pattern.items() + ) + assert any("docs/blog/**" in msg and line_no == 4 for msg, line_no in lines_by_pattern.items()) + + +def test_global_usage_tracker_topology_policy_pair_consumption() -> None: + """Using either topological code must consume the paired directory policy family.""" + from zenzic.core.suppressions import GlobalUsageTracker + from zenzic.models.config import GovernanceConfig, ZenzicConfig + + config = ZenzicConfig( + governance=GovernanceConfig(directory_policies={"docs/historical/**": ["Z410", "Z411"]}) + ) + tracker = GlobalUsageTracker(config) + + tracker.mark_directory_policy_used("docs/historical/**", "Z411") + + assert ("docs/historical/**", "Z410") not in tracker.unused_dir_policies + assert ("docs/historical/**", "Z411") not in tracker.unused_dir_policies diff --git a/tests/test_topology.py b/tests/test_topology.py new file mode 100644 index 00000000..e9af3918 --- /dev/null +++ b/tests/test_topology.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +"""Tests for topological graph analysis.""" + +from zenzic.core.topology import detect_dead_ends, detect_orphans +from zenzic.models.vsm import Route, VirtualSiteMap + + +def test_detect_orphans(): + vsm = VirtualSiteMap() + vsm["/index.html"] = Route( + url="/index.html", source="index.md", status="REACHABLE", anchors=set() + ) + vsm["/a.html"] = Route(url="/a.html", source="a.md", status="REACHABLE", anchors=set()) + vsm["/b.html"] = Route(url="/b.html", source="b.md", status="REACHABLE", anchors=set()) + vsm["/orphan.html"] = Route( + url="/orphan.html", source="orphan.md", status="REACHABLE", anchors=set() + ) + + # Adjacency list setup + vsm.outgoing_links["/index.html"] = ["/a.html"] + vsm.outgoing_links["/a.html"] = ["/b.html"] + + entry_points = ["/index.html"] + orphans = detect_orphans(vsm, entry_points) + + assert orphans == ["/orphan.html"] + + +def test_detect_dead_ends(): + vsm = VirtualSiteMap() + vsm["/index.html"] = Route( + url="/index.html", source="index.md", status="REACHABLE", anchors=set() + ) + vsm["/a.html"] = Route(url="/a.html", source="a.md", status="REACHABLE", anchors=set()) + vsm["/dead_end.html"] = Route( + url="/dead_end.html", source="dead_end.md", status="REACHABLE", anchors=set() + ) + vsm["/asset.png"] = Route( + url="/asset.png", source="asset.png", status="REACHABLE", anchors=set() + ) + + # Adjacency list setup + vsm.outgoing_links["/index.html"] = ["/a.html"] + vsm.outgoing_links["/a.html"] = ["/dead_end.html"] + # /dead_end.html has no outgoing links + + dead_ends = detect_dead_ends(vsm) + assert dead_ends == ["/dead_end.html"] diff --git a/tests/test_validator.py b/tests/test_validator.py index ded9deff..ecaacd8f 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -1761,3 +1761,23 @@ def test_stale_allowlist_entry(tmp_path: Path) -> None: assert ( "pyproject.toml:1: Stale absolute_path_allowlist entry" in z110_errors_pyproject[0].message ) + + +def test_math_blocks_link_extraction_ignored(): + """Verify that links syntax inside display math ($$..$$) and inline math ($..$) are ignored.""" + from zenzic.core.rules import _extract_inline_links_with_lines + from zenzic.core.validator import PolyglotExtractor + + content = """# Math Test +$$\\text{Signature} = \\text{SHA256}[\\text{RuleCode} + \\text{PosixPath}](:16)$$ +Here is a normal link: [Valid Link](https://example.com/valid). +Inline math $\\text{Ref}[\\text{Code}](:32)$ should also be ignored. +""" + extractor = PolyglotExtractor() + extracted = extractor.extract_inline_links(content) + urls = [e.url for e in extracted] + assert urls == ["https://example.com/valid"] + + rule_links = _extract_inline_links_with_lines(content) + rule_urls = [u[0] for u in rule_links] + assert rule_urls == ["https://example.com/valid"] diff --git a/uv.lock b/uv.lock index 9318b9aa..f203d2b6 100644 --- a/uv.lock +++ b/uv.lock @@ -2465,7 +2465,7 @@ wheels = [ [[package]] name = "zenzic" -version = "0.26.5" +version = "0.27.0" source = { editable = "." } dependencies = [ { name = "google-re2" }, diff --git a/zenzic-baseline.schema.json b/zenzic-baseline.schema.json new file mode 100644 index 00000000..a8c74450 --- /dev/null +++ b/zenzic-baseline.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://zenzic.dev/schemas/zenzic-baseline.schema.json", + "title": "Zenzic Baseline Schema", + "description": "Schema for .zenzic-baseline.json snapshot files used for baseline and regression tracking.", + "type": "object", + "required": [ + "version", + "score", + "findings_count", + "signatures" + ], + "properties": { + "$schema": { + "type": "string", + "description": "JSON schema reference URI." + }, + "version": { + "type": "string", + "description": "Baseline format version." + }, + "created_at": { + "type": "string", + "description": "ISO 8601 timestamp of baseline creation." + }, + "score": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Document Quality Score (DQS) at baseline creation." + }, + "findings_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of findings recorded in baseline." + }, + "signatures": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Deterministic finding signatures (RuleCode + FileURI + Target/Context hash)." + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata (e.g. zenzic version, git commit)." + } + }, + "additionalProperties": false +}