From 534293ae5fa55c20430c76fb318797b830c533e9 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 10:34:53 +0200 Subject: [PATCH 01/12] chore: quarantine failed UI refactor and broken i18n --- examples/z102-anchor-missing/docs/index.md | 2 +- examples/z103-orphan-link/docs/index.md | 2 +- examples/z105-absolute-path/docs/index.md | 2 +- examples/z108-empty-link-text/docs/index.md | 2 +- examples/z202-path-traversal/docs/index.md | 2 +- examples/z204-forbidden-term/docs/index.md | 2 +- examples/z301-dangling-ref/docs/index.md | 2 +- examples/z302-dead-def/README.md | 2 +- examples/z303-duplicate-def/README.md | 2 +- examples/z403-missing-alt/docs/index.md | 2 +- examples/z501-placeholder/docs/index.md | 4 ++-- examples/z503-snippet-error/docs/index.md | 2 +- examples/z505-untagged-code-block/docs/index.md | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/z102-anchor-missing/docs/index.md b/examples/z102-anchor-missing/docs/index.md index 44e15e3..52943b3 100644 --- a/examples/z102-anchor-missing/docs/index.md +++ b/examples/z102-anchor-missing/docs/index.md @@ -13,7 +13,7 @@ demonstrating **Z102 ANCHOR_MISSING** detection. ## What Zenzic Reports ```text -docs/index.md:7: Z102 ANCHOR_MISSING guide.md#nonexistent-section — anchor not found on target page +docs/index.md:11: Z102 ANCHOR_MISSING guide.md#nonexistent-section — anchor not found on target page ``` Run `zenzic check links` to reproduce the finding. diff --git a/examples/z103-orphan-link/docs/index.md b/examples/z103-orphan-link/docs/index.md index 7a98c63..6d57ec0 100644 --- a/examples/z103-orphan-link/docs/index.md +++ b/examples/z103-orphan-link/docs/index.md @@ -18,7 +18,7 @@ The following link points to a page that exists on disk but has no nav entry: ## What Zenzic Reports ```text -docs/index.md:7: Z103 ORPHAN_LINK 'guide.md' exists but is not reachable via site navigation +docs/index.md:16: Z103 ORPHAN_LINK 'guide.md' exists but is not reachable via site navigation ``` Run `zenzic check links` to reproduce the finding. diff --git a/examples/z105-absolute-path/docs/index.md b/examples/z105-absolute-path/docs/index.md index 7dd3d5d..44b5e85 100644 --- a/examples/z105-absolute-path/docs/index.md +++ b/examples/z105-absolute-path/docs/index.md @@ -12,7 +12,7 @@ This page uses an absolute path link, demonstrating **Z105 ABSOLUTE_PATH** detec ## What Zenzic Reports ```text -docs/index.md:7: Z105 ABSOLUTE_PATH '/guide' — use a relative path +docs/index.md:10: Z105 ABSOLUTE_PATH '/guide' — use a relative path ``` Absolute paths break portability when a site is served from a subdirectory. diff --git a/examples/z108-empty-link-text/docs/index.md b/examples/z108-empty-link-text/docs/index.md index d928154..6dfe68e 100644 --- a/examples/z108-empty-link-text/docs/index.md +++ b/examples/z108-empty-link-text/docs/index.md @@ -12,7 +12,7 @@ This page contains a link with an empty label, demonstrating **Z108 EMPTY_LINK_T ## What Zenzic Reports ```text -docs/index.md:7: Z108 EMPTY_LINK_TEXT link to 'guide.md' has no label +docs/index.md:10: Z108 EMPTY_LINK_TEXT link to 'guide.md' has no label ``` Run `zenzic check links` to reproduce the finding. diff --git a/examples/z202-path-traversal/docs/index.md b/examples/z202-path-traversal/docs/index.md index 7759eb2..1034796 100644 --- a/examples/z202-path-traversal/docs/index.md +++ b/examples/z202-path-traversal/docs/index.md @@ -13,7 +13,7 @@ demonstrating **Z202 PATH_TRAVERSAL** detection. ## What Zenzic Reports ```text -docs/index.md:7: Z202 PATH_TRAVERSAL '../../private/secret.txt' escapes the docs/ root boundary +docs/index.md:11: Z202 PATH_TRAVERSAL '../../private/secret.txt' escapes the docs/ root boundary ``` Z202 is non-suppressible. Exit code 2. diff --git a/examples/z204-forbidden-term/docs/index.md b/examples/z204-forbidden-term/docs/index.md index 8c6fb7f..3c766d7 100644 --- a/examples/z204-forbidden-term/docs/index.md +++ b/examples/z204-forbidden-term/docs/index.md @@ -17,7 +17,7 @@ The staging environment is available at `staging.internal.corp` for QA purposes. ## What Zenzic Reports ```text -docs/index.md:9: Z204 FORBIDDEN_TERM Forbidden term detected — remove from documentation: 'ProjectX' +docs/index.md:11: Z204 FORBIDDEN_TERM Forbidden term detected — remove from documentation: 'ProjectX' ``` Z204 is non-suppressible. Exit code 2. The CLI shows "POLICY VIOLATION DETECTED". diff --git a/examples/z301-dangling-ref/docs/index.md b/examples/z301-dangling-ref/docs/index.md index 015fa24..fac1ed3 100644 --- a/examples/z301-dangling-ref/docs/index.md +++ b/examples/z301-dangling-ref/docs/index.md @@ -17,7 +17,7 @@ anywhere in this file — that is the intentional defect that triggers Z301. ## What Zenzic Reports ```text -docs/index.md:9: Z301 DANGLING_REF reference ID 'missing-ref' is used but never defined +docs/index.md:12: Z301 DANGLING_REF reference ID 'missing-ref' is used but never defined ``` Run `zenzic check references` to reproduce the finding. diff --git a/examples/z302-dead-def/README.md b/examples/z302-dead-def/README.md index 39bcc8b..b54635b 100644 --- a/examples/z302-dead-def/README.md +++ b/examples/z302-dead-def/README.md @@ -26,7 +26,7 @@ zenzic check references ## Expected output ```text -docs/index.md:12: Z302 DEAD_DEF reference ID 'setup' defined but never used +docs/index.md:16: Z302 DEAD_DEF reference ID 'setup' defined but never used ``` Exit code **1**. diff --git a/examples/z303-duplicate-def/README.md b/examples/z303-duplicate-def/README.md index e658624..275815d 100644 --- a/examples/z303-duplicate-def/README.md +++ b/examples/z303-duplicate-def/README.md @@ -26,7 +26,7 @@ zenzic check references ## Expected output ```text -docs/index.md:13: Z303 DUPLICATE_DEF reference ID 'api' defined more than once +docs/index.md:16: Z303 DUPLICATE_DEF reference ID 'api' defined more than once ``` Exit code **1**. diff --git a/examples/z403-missing-alt/docs/index.md b/examples/z403-missing-alt/docs/index.md index 93f63ca..d8bde48 100644 --- a/examples/z403-missing-alt/docs/index.md +++ b/examples/z403-missing-alt/docs/index.md @@ -18,7 +18,7 @@ The `![](diagram.png)` syntax above has an empty alt attribute → **Z403**. ## What Zenzic Reports ```text -docs/index.md:9: Z403 MISSING_ALT image 'diagram.png' has no alt text +docs/index.md:14: Z403 MISSING_ALT image 'diagram.png' has no alt text ``` Run `zenzic check assets` to reproduce the finding. diff --git a/examples/z501-placeholder/docs/index.md b/examples/z501-placeholder/docs/index.md index 54bb4ff..f242c90 100644 --- a/examples/z501-placeholder/docs/index.md +++ b/examples/z501-placeholder/docs/index.md @@ -18,8 +18,8 @@ Coming soon! ## What Zenzic Reports ```text -docs/index.md:7: Z501 PLACEHOLDER placeholder pattern 'TODO:' matched -docs/index.md:11: Z501 PLACEHOLDER placeholder pattern 'Coming soon!' matched +docs/index.md:10: Z501 PLACEHOLDER placeholder pattern 'TODO:' matched +docs/index.md:16: Z501 PLACEHOLDER placeholder pattern 'Coming soon!' matched ``` Run `zenzic check content` to reproduce the findings. diff --git a/examples/z503-snippet-error/docs/index.md b/examples/z503-snippet-error/docs/index.md index 0e62b18..fb7529e 100644 --- a/examples/z503-snippet-error/docs/index.md +++ b/examples/z503-snippet-error/docs/index.md @@ -21,7 +21,7 @@ expression. Zenzic's `ast`-based validator catches this → **Z503**. ## What Zenzic Reports ```text -docs/index.md:10: Z503 SNIPPET_ERROR Python block has a syntax error: invalid syntax (, line 2) +docs/index.md:13: Z503 SNIPPET_ERROR Python block has a syntax error: invalid syntax (, line 2) ``` Run `zenzic check content` to reproduce the finding. diff --git a/examples/z505-untagged-code-block/docs/index.md b/examples/z505-untagged-code-block/docs/index.md index 62e13d2..bb7dcdd 100644 --- a/examples/z505-untagged-code-block/docs/index.md +++ b/examples/z505-untagged-code-block/docs/index.md @@ -20,7 +20,7 @@ just ` ``` ` without a language tag like `bash` or `text` → **Z505**. ## What Zenzic Reports ```text -docs/index.md:9: Z505 UNTAGGED_CODE_BLOCK fenced code block has no language specifier +docs/index.md:13: Z505 UNTAGGED_CODE_BLOCK fenced code block has no language specifier ``` Run `zenzic check content` to reproduce the finding. From 5dc27c63c604c6d24ec5e1a05afa1ffe4254f6a6 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 12:57:28 +0200 Subject: [PATCH 02/12] fix(core): sync fixtures and update scanner for Z403 --- examples/z201-credentials/docs/setup.md | 2 +- examples/z204-forbidden-term/docs/index.md | 2 +- examples/z303-duplicate-def/docs/index.md | 1 - examples/z403-missing-alt/docs/diagram.png | 1 + .../z601-brand-obsolescence/docs/guide.md | 3 ++ examples/z602-i18n-parity/.zenzic.toml | 2 ++ examples/z602-i18n-parity/docs/it/index.md | 2 +- src/zenzic/core/scanner.py | 29 +++++++++++++++++++ 8 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 examples/z403-missing-alt/docs/diagram.png create mode 100644 examples/z601-brand-obsolescence/docs/guide.md diff --git a/examples/z201-credentials/docs/setup.md b/examples/z201-credentials/docs/setup.md index a0f22ff..fb5edf4 100644 --- a/examples/z201-credentials/docs/setup.md +++ b/examples/z201-credentials/docs/setup.md @@ -17,7 +17,7 @@ secret_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY ``` > **Security note:** This is a documentation example demonstrating Z201 -> CREDENTIAL_SECRET detection. Replace placeholder values with your actual +> CREDENTIAL_SECRET detection. Replace example values with your actual > credentials via environment variables — never commit real keys to version > control. diff --git a/examples/z204-forbidden-term/docs/index.md b/examples/z204-forbidden-term/docs/index.md index 3c766d7..60b33e2 100644 --- a/examples/z204-forbidden-term/docs/index.md +++ b/examples/z204-forbidden-term/docs/index.md @@ -9,7 +9,7 @@ demonstrating **Z204 FORBIDDEN_TERM** detection. ## Forbidden Term Occurrence The development team is building **ProjectX** — our internal codename for the -next-generation platform. This page was drafted before the public launch and +next-generation platform. This page was written before the public launch and still contains the internal codename that must not appear in published docs. The staging environment is available at `staging.internal.corp` for QA purposes. diff --git a/examples/z303-duplicate-def/docs/index.md b/examples/z303-duplicate-def/docs/index.md index 9a4010e..19e97bf 100644 --- a/examples/z303-duplicate-def/docs/index.md +++ b/examples/z303-duplicate-def/docs/index.md @@ -13,6 +13,5 @@ See the [API documentation][api] for endpoint details. The new [API][api] includes a breaking change in `/v2/auth`. [api]: https://api-v1.example.com - diff --git a/examples/z403-missing-alt/docs/diagram.png b/examples/z403-missing-alt/docs/diagram.png new file mode 100644 index 0000000..5806ced --- /dev/null +++ b/examples/z403-missing-alt/docs/diagram.png @@ -0,0 +1 @@ +dummy png content diff --git a/examples/z601-brand-obsolescence/docs/guide.md b/examples/z601-brand-obsolescence/docs/guide.md new file mode 100644 index 0000000..f6befd3 --- /dev/null +++ b/examples/z601-brand-obsolescence/docs/guide.md @@ -0,0 +1,3 @@ +# Guide + +This is a comprehensive guide to getting started with our documentation platform. We will cover the installation process, configuration options, and basic usage details to ensure that you can get up and running as quickly as possible. Please read all sections carefully to understand the platform features and how to leverage them for your project requirements. diff --git a/examples/z602-i18n-parity/.zenzic.toml b/examples/z602-i18n-parity/.zenzic.toml index 487d949..da99dcd 100644 --- a/examples/z602-i18n-parity/.zenzic.toml +++ b/examples/z602-i18n-parity/.zenzic.toml @@ -20,3 +20,5 @@ locales = ["it"] [i18n] enabled = true +base_source = "docs/en" +targets = { it = "docs/it" } diff --git a/examples/z602-i18n-parity/docs/it/index.md b/examples/z602-i18n-parity/docs/it/index.md index 8b0fe02..4d2fdfc 100644 --- a/examples/z602-i18n-parity/docs/it/index.md +++ b/examples/z602-i18n-parity/docs/it/index.md @@ -3,7 +3,7 @@ # Avvio rapido -Questo documento è la traduzione IT della pagina principale. +Questa pagina è la traduzione IT della pagina principale. Il file `docs/it/guide.md` è **intenzionalmente assente** — Zenzic emette Z602. ## Installazione diff --git a/src/zenzic/core/scanner.py b/src/zenzic/core/scanner.py index dfb8b35..62ad611 100644 --- a/src/zenzic/core/scanner.py +++ b/src/zenzic/core/scanner.py @@ -1123,6 +1123,7 @@ def __init__(self, file_path: Path, config: ZenzicConfig | None = None) -> None: self.file_path = file_path self.ref_map: ReferenceMap = ReferenceMap() self._config = config or ZenzicConfig() + self.missing_alts: list[ReferenceFinding] = [] # ── Pass 1: Harvesting & Credential Scanner ──────────────────────────────── @@ -1217,6 +1218,32 @@ def harvest(self) -> Generator[HarvestEvent, None, None]: content_events.append((lineno, "IMG", (alt_text, url))) else: content_events.append((lineno, "MISSING_ALT", url)) + self.missing_alts.append( + ReferenceFinding( + file_path=self.file_path, + line_no=lineno, + issue="Z403", + detail=f"Image '{url}' has no alt text.", + is_warning=True, + ) + ) + + # ── Alt-text: HTML tags ───────────────────────────────────── + for img_match in _RE_HTML_IMG.finditer(line): + tag = img_match.group() + alt_match = _RE_HTML_ALT.search(tag) + src = tag + if alt_match is None or not alt_match.group(1).strip(): + content_events.append((lineno, "MISSING_ALT", src)) + self.missing_alts.append( + ReferenceFinding( + file_path=self.file_path, + line_no=lineno, + issue="Z403", + detail=f"HTML tag has no alt text: {src[:60]}", + is_warning=True, + ) + ) # Yield all events in line-number order yield from sorted(credential_events + content_events, key=lambda e: e[0]) @@ -1321,6 +1348,8 @@ def get_integrity_report( ) ) + findings.extend(self.missing_alts) + return IntegrityReport( file_path=self.file_path, score=self.ref_map.integrity_score, From 5c4998e98caceb9317553256593cdaca80a973e3 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 13:01:17 +0200 Subject: [PATCH 03/12] fix(core): skip inline code backticks in image alt text scanner --- src/zenzic/core/scanner.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/zenzic/core/scanner.py b/src/zenzic/core/scanner.py index 62ad611..475eaf7 100644 --- a/src/zenzic/core/scanner.py +++ b/src/zenzic/core/scanner.py @@ -1062,8 +1062,11 @@ def check_image_alt_text( findings: list[ReferenceFinding] = [] for lineno, line in enumerate(text.splitlines(), start=1): + # Blank out inline code to avoid false matches + clean = _INLINE_CODE_RE.sub(lambda m: " " * len(m.group()), line) + # Inline Markdown images - for m in _RE_IMAGE_INLINE.finditer(line): + for m in _RE_IMAGE_INLINE.finditer(clean): alt_text = m.group(1) url = m.group(2) if not alt_text.strip(): @@ -1078,7 +1081,7 @@ def check_image_alt_text( ) # HTML tags - for img_match in _RE_HTML_IMG.finditer(line): + for img_match in _RE_HTML_IMG.finditer(clean): tag = img_match.group() alt_match = _RE_HTML_ALT.search(tag) src = tag # fallback label when src is hard to extract @@ -1211,7 +1214,8 @@ def harvest(self) -> Generator[HarvestEvent, None, None]: continue # ── Alt-text: inline images ─────────────────────────────────────── - for img_match in _RE_IMAGE_INLINE.finditer(line): + clean = _INLINE_CODE_RE.sub(lambda m: " " * len(m.group()), line) + for img_match in _RE_IMAGE_INLINE.finditer(clean): alt_text = img_match.group(1) url = img_match.group(2) if alt_text.strip(): @@ -1229,7 +1233,7 @@ def harvest(self) -> Generator[HarvestEvent, None, None]: ) # ── Alt-text: HTML tags ───────────────────────────────────── - for img_match in _RE_HTML_IMG.finditer(line): + for img_match in _RE_HTML_IMG.finditer(clean): tag = img_match.group() alt_match = _RE_HTML_ALT.search(tag) src = tag From 2a1424167403e816e7bf7a409431ad35846d67f2 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 13:03:28 +0200 Subject: [PATCH 04/12] chore: remove CONTRIBUTING files from bumpversion config --- .bumpversion.toml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index c026b2d..b470da6 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -46,18 +46,6 @@ replace = 'dependencies = ["zenzic>={new_version}"]' # Historical versioned documentation is tracked where explicit release # markers are still present in the file body. -[[tool.bumpversion.files]] -filename = "CONTRIBUTING.md" -search = "v{current_version}" -replace = "v{new_version}" -serialize = ["{major}.{minor}.{patch}{pre_l}{pre_n}", "{major}.{minor}.{patch}"] - -[[tool.bumpversion.files]] -filename = "CONTRIBUTING.it.md" -search = "v{current_version}" -replace = "v{new_version}" -serialize = ["{major}.{minor}.{patch}{pre_l}{pre_n}", "{major}.{minor}.{patch}"] - [[tool.bumpversion.files]] filename = "RELEASE.md" search = "{current_version}" From edc7eb56db704866ef90fd1e9da38fb263a41744 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 13:03:52 +0200 Subject: [PATCH 05/12] release: bump version to 0.9.1 --- .bumpversion.toml | 2 +- .github/ISSUE_TEMPLATE/security_vulnerability.yml | 2 +- .pre-commit-hooks.yaml | 2 +- CITATION.cff | 4 ++-- RELEASE.md | 12 ++++++------ pyproject.toml | 2 +- src/zenzic/__init__.py | 2 +- src/zenzic/cli/_standalone.py | 2 +- uv.lock | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index b470da6..2e19286 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 [tool.bumpversion] -current_version = "0.9.0" +current_version = "0.9.1" 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 ac6cbc4..27a2fc8 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.9.0" + placeholder: "0.9.1" validations: required: true diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index bae8fc6..b90b3af 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -7,7 +7,7 @@ # # repos: # - repo: https://github.com/PythonWoods/zenzic -# rev: v0.9.0 +# rev: v0.9.1 # hooks: # - id: zenzic-verify # quality gate — corrisponde a `just verify` lato zenzic # - id: zenzic-guard # fast staged-file credential scan diff --git a/CITATION.cff b/CITATION.cff index 2f6d26c..5342f4d 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.9.0 -date-released: 2026-05-31 +version: 0.9.1 +date-released: 2026-06-02 url: "https://zenzic.dev" repository-code: "https://github.com/PythonWoods/zenzic" repository-artifact: "https://pypi.org/project/zenzic/" diff --git a/RELEASE.md b/RELEASE.md index 074785e..30057f2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -6,9 +6,9 @@ | Field | Value | | :------- | :--------- | -| Version | v0.9.0 | +| Version | v0.9.1 | | Codename | Basalt | -| Date | 2026-05-31 | +| Date | 2026-06-02 | | Status | Stable | ## Release Checklist @@ -19,7 +19,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 and README.it.md reflects current score - [ ] `zenzic check all .` — zero findings in the repo root -- [ ] `pyproject.toml` version matches the tag (`0.9.0`) +- [ ] `pyproject.toml` version matches the tag (`0.9.1`) - [ ] `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). @@ -46,11 +46,11 @@ Distribution target: **PyPI** — `pip install zenzic` / `uvx zenzic`. ## Tag & Push ```bash -git tag v0.9.0 -git push origin release/v0.9.0 --tags +git tag v0.9.1 +git push origin release/v0.9.1 --tags ``` -- [ ] Create GitHub Release from the tag, using the `## v0.9.0` CHANGELOG section as the release body. +- [ ] Create GitHub Release from the tag, using the `## v0.9.1` CHANGELOG section as the release body. ## Changelog Reference diff --git a/pyproject.toml b/pyproject.toml index a6b98f7..5afaa0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ build-backend = "hatchling.build" [project] name = "zenzic" -version = "0.9.0" +version = "0.9.1" description = "Engineering-grade, engine-agnostic static analyzer and credential scanner for Markdown documentation" readme = "README.md" requires-python = ">=3.10" diff --git a/src/zenzic/__init__.py b/src/zenzic/__init__.py index 2ea399f..b1da410 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.9.0" +__version__ = "0.9.1" __version_name__ = "Basalt" # Release codename stored separately from the package version. diff --git a/src/zenzic/cli/_standalone.py b/src/zenzic/cli/_standalone.py index 8f28d92..dc8ff3b 100644 --- a/src/zenzic/cli/_standalone.py +++ b/src/zenzic/cli/_standalone.py @@ -1323,7 +1323,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.9.0"] +dependencies = ["zenzic>=0.9.1"] [project.entry-points."zenzic.rules"] {project_slug} = "{module_name}.rules:{class_name}" diff --git a/uv.lock b/uv.lock index 19135e5..9a5e253 100644 --- a/uv.lock +++ b/uv.lock @@ -2163,7 +2163,7 @@ wheels = [ [[package]] name = "zenzic" -version = "0.9.0" +version = "0.9.1" source = { editable = "." } dependencies = [ { name = "google-re2" }, From 9335329c64556457fbbb85f984dfac5d9f608b27 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 15:28:56 +0200 Subject: [PATCH 06/12] docs: align release instructions with prep branch --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 30057f2..3425d87 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -47,7 +47,7 @@ Distribution target: **PyPI** — `pip install zenzic` / `uvx zenzic`. ```bash git tag v0.9.1 -git push origin release/v0.9.1 --tags +git push origin release/v0.9.1-prep --tags ``` - [ ] Create GitHub Release from the tag, using the `## v0.9.1` CHANGELOG section as the release body. From c70f77a796cb03d19d54aaec17eb41131c9b80a5 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 17:54:20 +0200 Subject: [PATCH 07/12] release(v0.9.1): update changelog and release process docs --- CHANGELOG.md | 9 +++++++++ CONTRIBUTING.md | 22 ++++++++++++++++++++++ RELEASE.md | 14 +++++++++++--- changelogs/README.md | 2 ++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 104d1c9..1bc3dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,15 @@ No changes yet. --- +## [0.9.1] - 2026-06-02 + +### Fixed + +- Core scanner integration fix for `Z403 MISSING_ALT_TEXT` to align fixture coverage with production scan paths. +- Fixture line-number correction in scanner test cases to keep finding locations deterministic and stable. + +--- + ## [0.9.0] - 2026-05-31 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e093ec0..f2dd6ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -181,6 +181,28 @@ unlikely — the matrix covers the language boundary conditions, not every minor - All PRs should target `main`; avoid direct commits. - Update `CHANGELOG.md` in the same commit as the code change. +## REUSE Compliance & Copyright + +This project is REUSE-compliant and enforced as a merge gate. + +- Significant changes (new logic, new content, or substantial refactors) **MUST** add an author copyright line via SPDX. +- Trivial edits (for example, typo-only fixes) do not require adding a new copyright line. +- Any PR that adds new files or significantly modifies existing files without required SPDX attribution is rejected (Exit Code 1 in CI gate policy). + +Example header pattern: + +```text +SPDX-FileCopyrightText: 2026 PythonWoods +SPDX-FileCopyrightText: 2026 Contributor Name +SPDX-License-Identifier: Apache-2.0 +``` + +Legal model: + +- No CLA is required. +- Contributions are governed by DCO (Developer Certificate of Origin) plus REUSE/SPDX attribution. +- Contributors retain copyright over significant modifications. + ## Security & Compliance - **Security First:** Any new path resolution MUST be tested against Path Traversal. Use `PathTraversal` logic from `core`. diff --git a/RELEASE.md b/RELEASE.md index 3425d87..469fe28 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,12 +2,14 @@ # Release Procedure — Zenzic Core +> **[MAINTAINER SOP]** *This document contains the Standard Operating Procedure for Core Maintainers to cut and publish a new release. If you are an end-user looking for new features, please see the [CHANGELOG](./CHANGELOG.md).* + ## Release Metadata | Field | Value | | :------- | :--------- | | Version | v0.9.1 | -| Codename | Basalt | +| Codename | Graphite | | Date | 2026-06-02 | | Status | Stable | @@ -32,7 +34,7 @@ Before tagging, every item must be green: ```bash # Bump version -uv run bump-my-version bump minor +uv run bump-my-version bump patch # Build wheel + sdist python -m build @@ -46,8 +48,14 @@ Distribution target: **PyPI** — `pip install zenzic` / `uvx zenzic`. ## Tag & Push ```bash +# 1. Merge the release branch into main via PR first! +# 2. Switch to main and pull latest +git checkout main +git pull origin main + +# 3. Tag the main branch and push git tag v0.9.1 -git push origin release/v0.9.1-prep --tags +git push origin main --tags ``` - [ ] Create GitHub Release from the tag, using the `## v0.9.1` CHANGELOG section as the release body. diff --git a/changelogs/README.md b/changelogs/README.md index a6d8cc5..8dfad38 100644 --- a/changelogs/README.md +++ b/changelogs/README.md @@ -16,6 +16,8 @@ For the current release history, see the [main Changelog](../CHANGELOG.md). | v0.6.x | Obsidian | 2026-04-12 | [v0.6.md](./v0.6.md) | | v0.7.x | Quartz | 2026-05-07 | [v0.7.md](./v0.7.md) | | v0.8.x | Basalt | 2026-05-15 to 2026-05-30 | [v0.8.md](./v0.8.md) | +| v0.9.x | Graphite | 2026-05-31 to 2026-06-02 | [main CHANGELOG](../CHANGELOG.md) | +| v0.10.x | TBD | Planned | — | Archives follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. All dates are git-tag verified for v0.4.0-rc2 and later. From ddf53d7336a12449e012b4d15b685b27594794d6 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 17:55:49 +0200 Subject: [PATCH 08/12] feat(gallery): add Z104/Z107/Z401/Z404/Z406 fixtures, lab entries and phase-2bc tests --- changelogs/v0.8.md | 2 +- examples/z104-file-not-found/.zenzic.toml | 16 ++ examples/z104-file-not-found/README.md | 25 ++ examples/z104-file-not-found/docs/index.md | 17 ++ examples/z107-circular-anchor/.zenzic.toml | 16 ++ examples/z107-circular-anchor/README.md | 27 ++ examples/z107-circular-anchor/docs/guide.md | 18 ++ .../z401-missing-directory-index/.zenzic.toml | 17 ++ .../z401-missing-directory-index/README.md | 27 ++ .../docs/guide/page.md | 13 + .../zensical.toml | 8 + .../z404-config-asset-missing/.zenzic.toml | 16 ++ examples/z404-config-asset-missing/README.md | 26 ++ .../z404-config-asset-missing/docs/index.md | 16 ++ examples/z404-config-asset-missing/mkdocs.yml | 6 + examples/z406-nav-contract/.zenzic.toml | 16 ++ examples/z406-nav-contract/README.md | 28 ++ examples/z406-nav-contract/docs/index.md | 16 ++ examples/z406-nav-contract/mkdocs.yml | 11 + src/zenzic/cli/_lab.py | 36 +++ tests/test_gallery_phase2bc.py | 245 ++++++++++++++++++ 21 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 examples/z104-file-not-found/.zenzic.toml create mode 100644 examples/z104-file-not-found/README.md create mode 100644 examples/z104-file-not-found/docs/index.md create mode 100644 examples/z107-circular-anchor/.zenzic.toml create mode 100644 examples/z107-circular-anchor/README.md create mode 100644 examples/z107-circular-anchor/docs/guide.md create mode 100644 examples/z401-missing-directory-index/.zenzic.toml create mode 100644 examples/z401-missing-directory-index/README.md create mode 100644 examples/z401-missing-directory-index/docs/guide/page.md create mode 100644 examples/z401-missing-directory-index/zensical.toml create mode 100644 examples/z404-config-asset-missing/.zenzic.toml create mode 100644 examples/z404-config-asset-missing/README.md create mode 100644 examples/z404-config-asset-missing/docs/index.md create mode 100644 examples/z404-config-asset-missing/mkdocs.yml create mode 100644 examples/z406-nav-contract/.zenzic.toml create mode 100644 examples/z406-nav-contract/README.md create mode 100644 examples/z406-nav-contract/docs/index.md create mode 100644 examples/z406-nav-contract/mkdocs.yml create mode 100644 tests/test_gallery_phase2bc.py diff --git a/changelogs/v0.8.md b/changelogs/v0.8.md index a278542..c3f4cae 100644 --- a/changelogs/v0.8.md +++ b/changelogs/v0.8.md @@ -75,7 +75,7 @@ Archive of release notes extracted from the main changelog. - **Integrity Regression Check (`zenzic diff`):** Command to compare documentation state between branches; exits with code 4 on quality regression. - **Config Genealogy (`zenzic explain`):** Introspection command to trace rule origin (Default vs Global vs Local TOML). - **Rule Z108 (EMPTY_LINK_TEXT):** New validator to detect links with empty or whitespace-only labels. -- **MDX-Native Suppressions:** Support for JSX comment syntax `{/* zenzic:ignore */}` alongside standard HTML comments. +- **MDX-Native Suppressions:** Support for JSX comment syntax `{/* zenzic:ignore: Zxxx */}` alongside standard HTML comments. - **Sovereign Audit Mode (`--audit`):** Global flag to bypass all suppressions for unfiltered repository inspection. - **Privacy Gate (Z204):** Support for `.zenzic.local.toml` to enforce local-only forbidden patterns without repository leakage. - **Core Hardening:** Native exclusion of system-critical files (`.zenzic.local.toml.example`, `*.sh`, `LICENSE`) from unused asset detection (Z405). diff --git a/examples/z104-file-not-found/.zenzic.toml b/examples/z104-file-not-found/.zenzic.toml new file mode 100644 index 0000000..1e38606 --- /dev/null +++ b/examples/z104-file-not-found/.zenzic.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# .zenzic.toml — Z104 FILE_NOT_FOUND gallery example +# +# docs/index.md references api/reference.md, which does not exist on disk. +# Zenzic fires Z104 FILE_NOT_FOUND (penalty 8.0) — exit 1. +# +# Expected results: +# zenzic check links → EXIT 1 (Z104 ×1) +# zenzic check all → EXIT 1 + +docs_dir = "docs" +fail_under = 0 + +[build_context] +engine = "standalone" diff --git a/examples/z104-file-not-found/README.md b/examples/z104-file-not-found/README.md new file mode 100644 index 0000000..4331551 --- /dev/null +++ b/examples/z104-file-not-found/README.md @@ -0,0 +1,25 @@ + + + +# Z104 FILE_NOT_FOUND — Gallery Example + +**Category:** Z1xx Link Integrity +**Expected exit:** 1 (error) + +## What this demonstrates + +`docs/index.md` contains a link to `api/reference.md`, which does not exist on +disk. Zenzic fires Z104 FILE_NOT_FOUND — a hard error that mandates exit 1. + +## Run it + +```bash +cd examples/z104-file-not-found +uvx zenzic check all +``` + +## Expected output + +```text +docs/index.md:10:44 x [Z104] 'api/reference.md' not found in docs +``` diff --git a/examples/z104-file-not-found/docs/index.md b/examples/z104-file-not-found/docs/index.md new file mode 100644 index 0000000..ae9f072 --- /dev/null +++ b/examples/z104-file-not-found/docs/index.md @@ -0,0 +1,17 @@ + + + +# Documentation + +Welcome to the project documentation. This guide covers all aspects of +installation, configuration, and everyday usage for new and experienced users. + +## API Reference + +For the complete API specification, see the [API Reference](api/reference.md). +The API reference contains all endpoints, request formats, and response schemas. + +## Getting Started + +Install the package with your preferred package manager and follow the setup +wizard. Detailed instructions are available in the installation section. diff --git a/examples/z107-circular-anchor/.zenzic.toml b/examples/z107-circular-anchor/.zenzic.toml new file mode 100644 index 0000000..580319b --- /dev/null +++ b/examples/z107-circular-anchor/.zenzic.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# .zenzic.toml — Z107 CIRCULAR_ANCHOR gallery example +# +# docs/guide.md contains a self-referential anchor link: [Setup](#setup) +# whose link text slugifies to the same fragment as the target. +# Zenzic fires Z107 CIRCULAR_ANCHOR (warning) — exit 0. +# +# Expected results: +# zenzic check all → EXIT 0 (Z107 ×1, warning) + +docs_dir = "docs" +fail_under = 0 + +[build_context] +engine = "standalone" diff --git a/examples/z107-circular-anchor/README.md b/examples/z107-circular-anchor/README.md new file mode 100644 index 0000000..37c7328 --- /dev/null +++ b/examples/z107-circular-anchor/README.md @@ -0,0 +1,27 @@ + + + +# Z107 CIRCULAR_ANCHOR — Gallery Example + +**Category:** Z1xx Link Integrity +**Expected exit:** 0 (warning) + +## What this demonstrates + +`docs/guide.md` contains the link `[Setup](#setup)` inside the `## Setup` +section. The link text "Setup" slugifies to `#setup`, which is the same +fragment as the containing heading — a circular self-reference. + +## Run it + +```bash +cd examples/z107-circular-anchor +uvx zenzic check all +``` + +## Expected output + +```text +docs/guide.md:13:51 ! [Z107] Self-referential anchor link: '[Setup](#setup)' +slugifies to its own fragment. Replace with a meaningful target or remove the link. +``` diff --git a/examples/z107-circular-anchor/docs/guide.md b/examples/z107-circular-anchor/docs/guide.md new file mode 100644 index 0000000..f1b60d0 --- /dev/null +++ b/examples/z107-circular-anchor/docs/guide.md @@ -0,0 +1,18 @@ + + + +# Guide + +This guide explains how to set up your Zenzic documentation project step by step. +It covers installation, configuration, and first run verification for all users. + +## Setup + +To begin, install the package and run the initial configuration command. +For advanced options, consult the reference documentation linked below. + +This page contains a self-referential anchor link: [Setup](#setup) + +## Next Steps + +After completing setup, proceed to the tutorial section for hands-on examples. diff --git a/examples/z401-missing-directory-index/.zenzic.toml b/examples/z401-missing-directory-index/.zenzic.toml new file mode 100644 index 0000000..150a71d --- /dev/null +++ b/examples/z401-missing-directory-index/.zenzic.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# .zenzic.toml — Z401 MISSING_DIRECTORY_INDEX gallery example +# +# docs/guide/ contains guide/page.md but has no index.md. +# Visiting /guide/ in the built site will return a 404. +# Zenzic fires Z401 (info) — exit 0. Run with --show-info to see it. +# +# Expected results: +# zenzic check all → EXIT 0 (Z401 ×1, suppressed as info) +# zenzic check all --show-info → EXIT 0 (Z401 ×1 visible) + +docs_dir = "docs" +fail_under = 0 + +[build_context] +engine = "zensical" diff --git a/examples/z401-missing-directory-index/README.md b/examples/z401-missing-directory-index/README.md new file mode 100644 index 0000000..c6b930d --- /dev/null +++ b/examples/z401-missing-directory-index/README.md @@ -0,0 +1,27 @@ + + + +# Z401 MISSING_DIRECTORY_INDEX — Gallery Example + +**Category:** Z4xx Topology & Assets +**Expected exit:** 0 (info) + +## What this demonstrates + +`docs/guide/` contains `page.md` but has no `index.md`. +Navigating to `/guide/` in the built site will return a 404. +Zenzic fires Z401 MISSING_DIRECTORY_INDEX as an info finding. + +## Run it + +```bash +cd examples/z401-missing-directory-index +uvx zenzic check all --show-info +``` + +## Expected output + +```text +docs/guide i [Z401] Directory contains Markdown files but has no index page +— the directory URL may return a 404. +``` diff --git a/examples/z401-missing-directory-index/docs/guide/page.md b/examples/z401-missing-directory-index/docs/guide/page.md new file mode 100644 index 0000000..ec16816 --- /dev/null +++ b/examples/z401-missing-directory-index/docs/guide/page.md @@ -0,0 +1,13 @@ + + + +# Installation Guide + +This page covers installation of the package on Linux, macOS, and Windows. +Follow the steps below to get started with the package manager of your choice. +The installation takes approximately two minutes on a standard connection. + +## Prerequisites + +Ensure you have Python 3.10 or higher and pip installed on your system before +proceeding with the installation steps described in this document. diff --git a/examples/z401-missing-directory-index/zensical.toml b/examples/z401-missing-directory-index/zensical.toml new file mode 100644 index 0000000..571800c --- /dev/null +++ b/examples/z401-missing-directory-index/zensical.toml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +[project] +site_name = "My Docs" +docs_dir = "docs" +nav = [ + "guide/page.md", +] diff --git a/examples/z404-config-asset-missing/.zenzic.toml b/examples/z404-config-asset-missing/.zenzic.toml new file mode 100644 index 0000000..a63bb1f --- /dev/null +++ b/examples/z404-config-asset-missing/.zenzic.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# .zenzic.toml — Z404 CONFIG_ASSET_MISSING gallery example +# +# mkdocs.yml declares theme.logo = assets/logo.svg, but +# docs/assets/logo.svg does not exist on disk. +# Zenzic fires Z404 CONFIG_ASSET_MISSING (warning) — exit 0. +# +# Expected results: +# zenzic check all → EXIT 0 (Z404 ×1, warning) + +docs_dir = "docs" +fail_under = 0 + +[build_context] +engine = "mkdocs" diff --git a/examples/z404-config-asset-missing/README.md b/examples/z404-config-asset-missing/README.md new file mode 100644 index 0000000..be54b4d --- /dev/null +++ b/examples/z404-config-asset-missing/README.md @@ -0,0 +1,26 @@ + + + +# Z404 CONFIG_ASSET_MISSING — Gallery Example + +**Category:** Z4xx Topology & Assets +**Expected exit:** 0 (warning) + +## What this demonstrates + +`mkdocs.yml` declares `theme.logo: assets/logo.svg` but `docs/assets/logo.svg` +does not exist on disk. Zenzic fires Z404 CONFIG_ASSET_MISSING. + +## Run it + +```bash +cd examples/z404-config-asset-missing +uvx zenzic check all +``` + +## Expected output + +```text +docs/docs/assets/logo.svg ! [Z404] logo asset not found on disk: +'docs/assets/logo.svg' (declared as theme.logo: 'assets/logo.svg' in mkdocs.yml) +``` diff --git a/examples/z404-config-asset-missing/docs/index.md b/examples/z404-config-asset-missing/docs/index.md new file mode 100644 index 0000000..4475b3f --- /dev/null +++ b/examples/z404-config-asset-missing/docs/index.md @@ -0,0 +1,16 @@ + + + +# Welcome + +Welcome to the project documentation. This guide covers all aspects of +installation, configuration, and everyday usage for new and experienced users. + +## Overview + +This documentation is built with MkDocs Material theme and maintained with +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. diff --git a/examples/z404-config-asset-missing/mkdocs.yml b/examples/z404-config-asset-missing/mkdocs.yml new file mode 100644 index 0000000..e5a116c --- /dev/null +++ b/examples/z404-config-asset-missing/mkdocs.yml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +site_name: My Project +theme: + name: material + logo: assets/logo.svg diff --git a/examples/z406-nav-contract/.zenzic.toml b/examples/z406-nav-contract/.zenzic.toml new file mode 100644 index 0000000..2ffac55 --- /dev/null +++ b/examples/z406-nav-contract/.zenzic.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +# .zenzic.toml — Z406 NAV_CONTRACT gallery example +# +# mkdocs.yml declares extra.alternate with link=/it/ but no Italian +# source pages exist — /it/ is absent from the Virtual Site Map. +# Zenzic fires Z406 NAV_CONTRACT (error) — exit 1. +# +# Expected results: +# zenzic check all → EXIT 1 (Z406 ×1, error) + +docs_dir = "docs" +fail_under = 0 + +[build_context] +engine = "mkdocs" diff --git a/examples/z406-nav-contract/README.md b/examples/z406-nav-contract/README.md new file mode 100644 index 0000000..c89faa3 --- /dev/null +++ b/examples/z406-nav-contract/README.md @@ -0,0 +1,28 @@ + + + +# Z406 NAV_CONTRACT — Gallery Example + +**Category:** Z4xx Topology & Assets +**Expected exit:** 1 (error) + +## What this demonstrates + +`mkdocs.yml` declares `extra.alternate` with `link: /it/` but no Italian +documentation pages exist — `/it/` is not in the Virtual Site Map. +Zenzic fires Z406 NAV_CONTRACT — a hard error mandating exit 1. + +## Run it + +```bash +cd examples/z406-nav-contract +uvx zenzic check all +``` + +## Expected output + +```text +docs/(nav) x [Z406] mkdocs.yml extra.alternate[it]: link '/it/' does not +correspond to any URL the build engine will generate. The Virtual Site Map +contains no entry for '/it/'. +``` diff --git a/examples/z406-nav-contract/docs/index.md b/examples/z406-nav-contract/docs/index.md new file mode 100644 index 0000000..4475b3f --- /dev/null +++ b/examples/z406-nav-contract/docs/index.md @@ -0,0 +1,16 @@ + + + +# Welcome + +Welcome to the project documentation. This guide covers all aspects of +installation, configuration, and everyday usage for new and experienced users. + +## Overview + +This documentation is built with MkDocs Material theme and maintained with +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. diff --git a/examples/z406-nav-contract/mkdocs.yml b/examples/z406-nav-contract/mkdocs.yml new file mode 100644 index 0000000..1ef2057 --- /dev/null +++ b/examples/z406-nav-contract/mkdocs.yml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +site_name: My Project +extra: + alternate: + - name: English + link: / + lang: en + - name: Italiano + link: /it/ + lang: it diff --git a/src/zenzic/cli/_lab.py b/src/zenzic/cli/_lab.py index d87c41e..789e440 100644 --- a/src/zenzic/cli/_lab.py +++ b/src/zenzic/cli/_lab.py @@ -227,6 +227,42 @@ class _Act: example_dir="z505-untagged-code-block", expected_pass=False, ), + "z104": _Act( + code="z104", + title="File Not Found", + description="Z104 FILE_NOT_FOUND — link target file missing from the filesystem; penalty 8.0, exit 1", + example_dir="z104-file-not-found", + expected_pass=False, + ), + "z107": _Act( + code="z107", + title="Circular Anchor", + description="Z107 CIRCULAR_ANCHOR — self-referential anchor whose text slugifies to its own fragment", + example_dir="z107-circular-anchor", + expected_pass=False, + ), + "z401": _Act( + code="z401", + title="Missing Directory Index", + description="Z401 MISSING_DIRECTORY_INDEX — directory has docs but no index page; directory URL returns 404", + example_dir="z401-missing-directory-index", + expected_pass=True, + show_info=True, + ), + "z404": _Act( + code="z404", + title="Config Asset Missing", + description="Z404 CONFIG_ASSET_MISSING — asset declared in mkdocs.yml theme not found on disk", + example_dir="z404-config-asset-missing", + expected_pass=False, + ), + "z406": _Act( + code="z406", + title="Nav Contract", + description="Z406 NAV_CONTRACT — extra.alternate link absent from Virtual Site Map; exit 1", + example_dir="z406-nav-contract", + expected_pass=False, + ), } _VALID_CODES: frozenset[str] = frozenset(_GALLERY) diff --git a/tests/test_gallery_phase2bc.py b/tests/test_gallery_phase2bc.py new file mode 100644 index 0000000..ad12d72 --- /dev/null +++ b/tests/test_gallery_phase2bc.py @@ -0,0 +1,245 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 +"""Gallery integration tests — Phase 2B/2C atomic coverage units. + +Covers the five new example fixtures and their expected behaviour: + + Z104 FILE_NOT_FOUND — link target missing from filesystem + Z107 CIRCULAR_ANCHOR — self-referential anchor link + Z401 MISSING_DIRECTORY_INDEX — directory has docs but no index page + Z404 CONFIG_ASSET_MISSING — engine config references a missing asset + Z406 NAV_CONTRACT — extra.alternate link absent from VSM +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from _helpers import make_mgr + +from zenzic.cli._check import _collect_all_results, _to_findings +from zenzic.cli._lab import _GALLERY, _examples_root +from zenzic.models.config import ZenzicConfig + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + + +def _examples() -> Path: + return _examples_root() + + +def _run(code: str) -> tuple[list, int, int]: + """Return (findings, errors, warnings) for a gallery act. + + Runs the act through the same path as ``zenzic lab `` but + captures the reporter output silently. + """ + act = _GALLERY[code] + example_dir = _examples() / act.example_dir + config, _ = ZenzicConfig.load(example_dir) + docs_root = (example_dir / config.docs_dir).resolve() + mgr = make_mgr(config, repo_root=example_dir, docs_root=docs_root) + results = _collect_all_results(example_dir, docs_root, config, mgr, strict=False) + findings = _to_findings(results, docs_root) + errors = sum(1 for f in findings if f.severity == "error") + warnings = sum(1 for f in findings if f.severity == "warning") + return findings, errors, warnings + + +# ── _GALLERY registration ───────────────────────────────────────────────────── + + +def test_z104_registered_in_gallery() -> None: + assert "z104" in _GALLERY + + +def test_z107_registered_in_gallery() -> None: + assert "z107" in _GALLERY + + +def test_z401_registered_in_gallery() -> None: + assert "z401" in _GALLERY + + +def test_z404_registered_in_gallery() -> None: + assert "z404" in _GALLERY + + +def test_z406_registered_in_gallery() -> None: + assert "z406" in _GALLERY + + +# ── Fixture directory existence ─────────────────────────────────────────────── + + +@pytest.mark.parametrize( + "code,dirname", + [ + ("z104", "z104-file-not-found"), + ("z107", "z107-circular-anchor"), + ("z401", "z401-missing-directory-index"), + ("z404", "z404-config-asset-missing"), + ("z406", "z406-nav-contract"), + ], +) +def test_fixture_directory_exists(code: str, dirname: str) -> None: + assert (_examples() / dirname).is_dir() + + +# ── Z104 FILE_NOT_FOUND ─────────────────────────────────────────────────────── + + +class TestZ104FileNotFound: + def test_z104_produces_exactly_one_error(self) -> None: + _, errors, warnings = _run("z104") + assert errors == 1 + assert warnings == 0 + + def test_z104_finding_code_is_z104(self) -> None: + findings, _, _ = _run("z104") + codes = [f.code for f in findings] + assert "Z104" in codes + + def test_z104_finding_message_contains_missing_path(self) -> None: + findings, _, _ = _run("z104") + z104_msgs = [f.message for f in findings if f.code == "Z104"] + assert any("api/reference.md" in m for m in z104_msgs) + + def test_z104_expected_pass_false(self) -> None: + assert _GALLERY["z104"].expected_pass is False + + +# ── Z107 CIRCULAR_ANCHOR ────────────────────────────────────────────────────── + + +class TestZ107CircularAnchor: + def test_z107_produces_exactly_one_warning(self) -> None: + _, errors, warnings = _run("z107") + assert errors == 0 + assert warnings == 1 + + def test_z107_finding_code_is_z107(self) -> None: + findings, _, _ = _run("z107") + codes = [f.code for f in findings] + assert "Z107" in codes + + def test_z107_finding_mentions_setup_fragment(self) -> None: + findings, _, _ = _run("z107") + z107_msgs = [f.message for f in findings if f.code == "Z107"] + assert any("#setup" in m.lower() or "setup" in m.lower() for m in z107_msgs) + + def test_z107_expected_pass_false(self) -> None: + """Z107 is a warning — met_expectation uses errors>0 or warnings>0.""" + assert _GALLERY["z107"].expected_pass is False + + +# ── Z401 MISSING_DIRECTORY_INDEX ────────────────────────────────────────────── + + +class TestZ401MissingDirectoryIndex: + def test_z401_produces_zero_errors_zero_warnings(self) -> None: + _, errors, warnings = _run("z401") + assert errors == 0 + assert warnings == 0 + + def test_z401_produces_one_info_finding(self) -> None: + findings, _, _ = _run("z401") + info = [f for f in findings if f.code == "Z401"] + assert len(info) == 1 + + def test_z401_finding_mentions_guide_directory(self) -> None: + findings, _, _ = _run("z401") + z401_msgs = [f.message for f in findings if f.code == "Z401"] + assert any("index" in m.lower() or "directory" in m.lower() for m in z401_msgs) + + def test_z401_expected_pass_true(self) -> None: + assert _GALLERY["z401"].expected_pass is True + + def test_z401_show_info_true(self) -> None: + assert _GALLERY["z401"].show_info is True + + +# ── Z404 CONFIG_ASSET_MISSING ───────────────────────────────────────────────── + + +class TestZ404ConfigAssetMissing: + def test_z404_produces_zero_errors_one_warning(self) -> None: + _, errors, warnings = _run("z404") + assert errors == 0 + assert warnings == 1 + + def test_z404_finding_code_is_z404(self) -> None: + findings, _, _ = _run("z404") + codes = [f.code for f in findings] + assert "Z404" in codes + + def test_z404_finding_mentions_logo_asset(self) -> None: + findings, _, _ = _run("z404") + z404_msgs = [f.message for f in findings if f.code == "Z404"] + assert any("logo" in m.lower() for m in z404_msgs) + + def test_z404_expected_pass_false(self) -> None: + assert _GALLERY["z404"].expected_pass is False + + +# ── Z406 NAV_CONTRACT ───────────────────────────────────────────────────────── + + +class TestZ406NavContract: + def test_z406_produces_exactly_one_error(self) -> None: + _, errors, warnings = _run("z406") + assert errors == 1 + assert warnings == 0 + + def test_z406_finding_code_is_z406(self) -> None: + findings, _, _ = _run("z406") + codes = [f.code for f in findings] + assert "Z406" in codes + + def test_z406_finding_mentions_it_route(self) -> None: + findings, _, _ = _run("z406") + z406_msgs = [f.message for f in findings if f.code == "Z406"] + assert any("/it/" in m for m in z406_msgs) + + def test_z406_expected_pass_false(self) -> None: + assert _GALLERY["z406"].expected_pass is False + + +# ── Cross-cutting: engine routing ───────────────────────────────────────────── + + +def test_z104_uses_standalone_engine() -> None: + act = _GALLERY["z104"] + example_dir = _examples() / act.example_dir + config, _ = ZenzicConfig.load(example_dir) + assert config.build_context.engine == "standalone" + + +def test_z107_uses_standalone_engine() -> None: + act = _GALLERY["z107"] + example_dir = _examples() / act.example_dir + config, _ = ZenzicConfig.load(example_dir) + assert config.build_context.engine == "standalone" + + +def test_z401_uses_zensical_engine() -> None: + act = _GALLERY["z401"] + example_dir = _examples() / act.example_dir + config, _ = ZenzicConfig.load(example_dir) + assert config.build_context.engine == "zensical" + + +def test_z404_uses_mkdocs_engine() -> None: + act = _GALLERY["z404"] + example_dir = _examples() / act.example_dir + config, _ = ZenzicConfig.load(example_dir) + assert config.build_context.engine == "mkdocs" + + +def test_z406_uses_mkdocs_engine() -> None: + act = _GALLERY["z406"] + example_dir = _examples() / act.example_dir + config, _ = ZenzicConfig.load(example_dir) + assert config.build_context.engine == "mkdocs" From 9641ee9c3d98715e396733839f9e9142d2b3e1f4 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 19:19:30 +0200 Subject: [PATCH 09/12] chore: implement strict repo-root-relative path resolution --- src/zenzic/cli/_check.py | 52 ++++++++++++++++++++++----------- src/zenzic/cli/_governance.py | 31 ++++++++++++++++++-- src/zenzic/cli/_lab.py | 4 +-- src/zenzic/core/reporter.py | 4 +-- src/zenzic/core/scanner.py | 6 ++-- tests/test_cli_e2e.py | 4 +-- tests/test_gallery_phase2bc.py | 2 +- tests/test_security_pipeline.py | 7 +++-- 8 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/zenzic/cli/_check.py b/src/zenzic/cli/_check.py index f89b6d3..808c583 100644 --- a/src/zenzic/cli/_check.py +++ b/src/zenzic/cli/_check.py @@ -151,7 +151,7 @@ def check_links( def _rel(path: Path) -> str: try: - return str(path.relative_to(docs_root)) + return str(path.relative_to(repo_root)) except ValueError: return str(path) @@ -287,6 +287,12 @@ def check_orphans( docs_root = (repo_root / config.docs_dir).resolve() exclusion_mgr = _shared._build_exclusion_manager(config, repo_root, docs_root) + def _rel(path: Path) -> str: + try: + return str(path.relative_to(repo_root)) + except ValueError: + return str(path) + from zenzic.core.adapters import get_adapter adapter = get_adapter(config.build_context, docs_root, repo_root) @@ -306,7 +312,7 @@ def check_orphans( findings = [ Finding( - rel_path=str(path), + rel_path=_rel(docs_root / path), line_no=0, code="Z402", severity="warning", @@ -390,7 +396,7 @@ def check_snippets( def _rel(path: Path) -> str: try: - return str(path.relative_to(docs_root)) + return str(path.relative_to(repo_root)) except ValueError: return str(path) @@ -518,7 +524,7 @@ def check_references( def _rel(path: Path) -> str: try: - return str(path.relative_to(docs_root)) + return str(path.relative_to(repo_root)) except ValueError: return str(path) @@ -580,7 +586,7 @@ def _rel(path: Path) -> str: ) ) for sf in report.security_findings: - findings.append(_map_credential_to_finding(sf, docs_root)) + findings.append(_map_credential_to_finding(sf, repo_root)) for err_str in ext_link_errors: findings.append( @@ -688,6 +694,12 @@ def check_assets( config, repo_root, docs_root, adapter_metadata_files=adapter_meta ) + def _rel(path: Path) -> str: + try: + return str(path.relative_to(repo_root)) + except ValueError: + return str(path) + t0 = time.monotonic() unused = find_unused_assets( docs_root, @@ -701,7 +713,7 @@ def check_assets( findings = [ Finding( - rel_path=str(path), + rel_path=_rel(docs_root / path), line_no=0, code="Z405", severity="warning", @@ -788,6 +800,12 @@ def check_placeholders( _content_roots = adapter.get_extra_content_roots(repo_root) content_roots: list[Path] | None = _content_roots if _content_roots else None + def _rel(path: Path) -> str: + try: + return str(path.relative_to(repo_root)) + except ValueError: + return str(path) + t0 = time.monotonic() raw_findings = find_placeholders( docs_root, @@ -812,7 +830,7 @@ def check_placeholders( pass findings.append( Finding( - rel_path=str(pf.file_path), + rel_path=_rel(docs_root / pf.file_path), line_no=pf.line_no, code=pf.issue, severity="warning", @@ -993,13 +1011,13 @@ def _mk_i18n_exclusion_mgr(base_root: Path) -> LayeredExclusionManager: ) -def _to_findings(results: _AllCheckResults, docs_root: Path) -> list[Finding]: +def _to_findings(results: _AllCheckResults, docs_root: Path, repo_root: Path) -> list[Finding]: """Convert all result types into a flat list of :class:`Finding`.""" findings: list[Finding] = [] def _rel(path: Path) -> str: try: - return str(path.relative_to(docs_root)) + return str(path.relative_to(repo_root)) except ValueError: return str(path) @@ -1020,7 +1038,7 @@ def _rel(path: Path) -> str: for path in results.orphans: findings.append( Finding( - rel_path=str(path), + rel_path=_rel(docs_root / path), line_no=0, code="Z402", severity="warning", @@ -1061,7 +1079,7 @@ def _rel(path: Path) -> str: pass findings.append( Finding( - rel_path=str(pf.file_path), + rel_path=_rel(docs_root / pf.file_path), line_no=pf.line_no, code=pf.issue, severity="warning", @@ -1075,7 +1093,7 @@ def _rel(path: Path) -> str: for path in results.unused_assets: findings.append( Finding( - rel_path=str(path), + rel_path=_rel(docs_root / path), line_no=0, code="Z405", severity="warning", @@ -1142,12 +1160,12 @@ def _rel(path: Path) -> str: ) ) for sf in report.security_findings: - findings.append(_map_credential_to_finding(sf, docs_root)) + findings.append(_map_credential_to_finding(sf, repo_root)) for dir_path in results.directory_index_issues: findings.append( Finding( - rel_path=str(dir_path), + rel_path=_rel(docs_root / dir_path), line_no=0, code="Z401", severity="info", @@ -1407,7 +1425,7 @@ def check_all( from zenzic import __version__ with sovereign_context(force_audit=audit): - all_findings = _to_findings(results, docs_root) + all_findings = _to_findings(results, docs_root, repo_root) _shared._output_sarif_findings(all_findings, __version__) incidents = sum(1 for f in all_findings if f.severity == "security_incident") if incidents: @@ -1423,12 +1441,12 @@ def check_all( from zenzic import __version__ with sovereign_context(force_audit=audit): - all_findings = _to_findings(results, docs_root) + all_findings = _to_findings(results, docs_root, repo_root) all_findings = _apply_per_file_ignores(all_findings, config) all_findings = _apply_directory_policies(all_findings, config) if _single_file is not None: - _sf_rel = str(_single_file.relative_to(docs_root)) + _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)) diff --git a/src/zenzic/cli/_governance.py b/src/zenzic/cli/_governance.py index 20664a3..4505d95 100644 --- a/src/zenzic/cli/_governance.py +++ b/src/zenzic/cli/_governance.py @@ -367,7 +367,30 @@ def build_cap_exceeded_sarif_payload( def _apply_per_file_ignores(findings: list[Finding], config: ZenzicConfig) -> list[Finding]: - """Filter findings using governance.per_file_ignores patterns.""" + """Filter findings using governance.per_file_ignores patterns. + + Security constraint + ------------------- + Findings whose code is in ``NON_SUPPRESSIBLE_CODES`` (credential and path + traversal findings) are always forwarded unchanged; they cannot be silenced + by ``per_file_ignores`` or any governance mechanism. + + Sovereign audit + --------------- + When the Sovereign Audit context is active (``--audit`` flag) the entire + filter is bypassed: all findings are returned verbatim so reviewers see the + complete picture. + + Args: + findings: List of :class:`~zenzic.models.output.Finding` instances + produced by the current scan. + config: Loaded :class:`~zenzic.models.config.ZenzicConfig`; the + ``governance.per_file_ignores`` mapping is read from here. + + Returns: + A filtered list of :class:`~zenzic.models.output.Finding` instances + with suppressed entries removed. + """ from zenzic.core.codes import NON_SUPPRESSIBLE_CODES if get_sovereign_context().force_audit: @@ -397,10 +420,12 @@ def _apply_per_file_ignores(findings: list[Finding], config: ZenzicConfig) -> li if code in NON_SUPPRESSIBLE_CODES: filtered.append(finding) continue - if any( + + suppressed = any( fnmatch(finding.rel_path, pattern) and code in codes for pattern, codes in normalized_map.items() - ): + ) + if suppressed: continue filtered.append(finding) return filtered diff --git a/src/zenzic/cli/_lab.py b/src/zenzic/cli/_lab.py index 789e440..903f6d9 100644 --- a/src/zenzic/cli/_lab.py +++ b/src/zenzic/cli/_lab.py @@ -344,10 +344,10 @@ def _run_act(act: _Act, examples_root: Path) -> _ActResult: results = _collect_all_results(example_dir, docs_root, config, exclusion_mgr, strict=False) elapsed = time.monotonic() - t0 - findings: list[Finding] = _to_findings(results, docs_root) + findings: list[Finding] = _to_findings(results, docs_root, repo_root=example_dir) if single_file is not None: - sf_rel = str(single_file.relative_to(docs_root)) + sf_rel = str(single_file.relative_to(example_dir)) findings = [f for f in findings if f.rel_path == sf_rel] docs_count, assets_count = _count_docs_assets(docs_root, example_dir, exclusion_mgr, config) diff --git a/src/zenzic/core/reporter.py b/src/zenzic/core/reporter.py index 5306c82..4930aec 100644 --- a/src/zenzic/core/reporter.py +++ b/src/zenzic/core/reporter.py @@ -163,8 +163,8 @@ def __init__(self, console: Console, docs_root: Path, *, docs_dir: str = "docs") self._docs_dir = docs_dir def _full_rel(self, rel_path: str) -> str: - """Return project-relative path including docs_dir prefix.""" - return f"{self._docs_dir}/{rel_path}" + """Return project-relative path as is.""" + return rel_path def _rel(self, path: Path) -> str: try: diff --git a/src/zenzic/core/scanner.py b/src/zenzic/core/scanner.py index 475eaf7..6f9445d 100644 --- a/src/zenzic/core/scanner.py +++ b/src/zenzic/core/scanner.py @@ -191,7 +191,7 @@ def calculate_orphans(all_md: set[str], nav_paths: set[str] | frozenset[str]) -> return sorted(all_md - nav_paths) -def _map_credential_to_finding(sf: SecurityFinding, docs_root: Path) -> Finding: +def _map_credential_to_finding(sf: SecurityFinding, repo_root: Path) -> Finding: """Convert a :class:`SecurityFinding` into a reporter :class:`Finding`. This is the **sole authorised bridge** between the credential detection layer @@ -204,7 +204,7 @@ def _map_credential_to_finding(sf: SecurityFinding, docs_root: Path) -> Finding: sf: A secret detection result from :func:`~zenzic.core.credentials.scan_line_for_secrets`, :func:`~zenzic.core.credentials.scan_url_for_secrets`, or :func:`~zenzic.core.credentials.scan_line_for_forbidden_terms`. - docs_root: Absolute path to the docs root directory used to compute + repo_root: Absolute path to the repo root directory used to compute a project-relative display path. Returns: @@ -213,7 +213,7 @@ def _map_credential_to_finding(sf: SecurityFinding, docs_root: Path) -> Finding: with code ``"Z204"``; all other credential scanner findings use ``"Z201"``. """ try: - rel = str(sf.file_path.relative_to(docs_root)) + rel = str(sf.file_path.relative_to(repo_root)) except ValueError: rel = str(sf.file_path) diff --git a/tests/test_cli_e2e.py b/tests/test_cli_e2e.py index f488810..5568592 100644 --- a/tests/test_cli_e2e.py +++ b/tests/test_cli_e2e.py @@ -490,7 +490,7 @@ def test_per_file_ignores_suppress_targeted_code( suppression_cap_fail_hard = true [governance.per_file_ignores] - "index.md" = ["Z104"] + "docs/index.md" = ["Z104"] """ ), encoding="utf-8", @@ -533,7 +533,7 @@ def test_directory_policies_filter_findings_zero_debt( brand_obsolescence = ["OldBrand"] [governance.directory_policies] - "index.md" = ["Z601"] + "docs/index.md" = ["Z601"] """ ), encoding="utf-8", diff --git a/tests/test_gallery_phase2bc.py b/tests/test_gallery_phase2bc.py index ad12d72..405f21a 100644 --- a/tests/test_gallery_phase2bc.py +++ b/tests/test_gallery_phase2bc.py @@ -42,7 +42,7 @@ def _run(code: str) -> tuple[list, int, int]: docs_root = (example_dir / config.docs_dir).resolve() mgr = make_mgr(config, repo_root=example_dir, docs_root=docs_root) results = _collect_all_results(example_dir, docs_root, config, mgr, strict=False) - findings = _to_findings(results, docs_root) + findings = _to_findings(results, docs_root, repo_root=example_dir) errors = sum(1 for f in findings if f.severity == "error") warnings = sum(1 for f in findings if f.severity == "warning") return findings, errors, warnings diff --git a/tests/test_security_pipeline.py b/tests/test_security_pipeline.py index 0b8b622..1a9f9c7 100644 --- a/tests/test_security_pipeline.py +++ b/tests/test_security_pipeline.py @@ -79,8 +79,9 @@ def test_locale_path_remap_applied_to_security_findings() -> None: if _sf.file_path in _locale_path_remap: _sf.file_path = _locale_path_remap[_sf.file_path] + repo_root = Path("/repo") # After remap, _map_credential_to_finding must produce a clean relative path - finding = _map_credential_to_finding(report.security_findings[0], docs_root) + finding = _map_credential_to_finding(report.security_findings[0], repo_root) assert not finding.rel_path.startswith("/"), ( f"rel_path must be relative, got: {finding.rel_path!r}" @@ -88,8 +89,8 @@ def test_locale_path_remap_applied_to_security_findings() -> None: assert "home" not in finding.rel_path, ( f"Absolute path leaked into rel_path: {finding.rel_path!r}" ) - assert finding.rel_path == "it/reference/file.mdx", ( - f"Expected 'it/reference/file.mdx', got: {finding.rel_path!r}" + assert finding.rel_path == "docs/it/reference/file.mdx", ( + f"Expected 'docs/it/reference/file.mdx', got: {finding.rel_path!r}" ) From e0b99ed3b994473e0ea0718a5349531d3248f729 Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 19:52:43 +0200 Subject: [PATCH 10/12] fix(cli): align score command findings and path resolution with check command --- src/zenzic/cli/_standalone.py | 141 +++++----------------------------- 1 file changed, 20 insertions(+), 121 deletions(-) diff --git a/src/zenzic/cli/_standalone.py b/src/zenzic/cli/_standalone.py index dc8ff3b..1648cef 100644 --- a/src/zenzic/cli/_standalone.py +++ b/src/zenzic/cli/_standalone.py @@ -24,11 +24,7 @@ from zenzic.core.exceptions import ConfigurationError from zenzic.core.exclusion import LayeredExclusionManager from zenzic.core.scanner import ( - find_orphans, - find_placeholders, find_repo_root, - find_unused_assets, - scan_docs_references, ) from zenzic.core.scorer import ( CategoryScore, @@ -38,11 +34,6 @@ save_snapshot, ) from zenzic.core.ui import ZenzicPalette, emoji -from zenzic.core.validator import ( - check_nav_contract, - validate_links_structured, - validate_snippets, -) from zenzic.models.config import ZenzicConfig from . import _shared @@ -68,120 +59,28 @@ def _run_all_checks( Builds a ``findings_counts`` dict (Zxxx → count) from all check results and passes it to the Zenzic Penalty Scorer. """ - from zenzic.core.adapters import get_adapter - - adapter = get_adapter(config.build_context, docs_root, repo_root) - _locale_roots = adapter.get_locale_source_roots(repo_root) - locale_roots: list[tuple[Path, str]] | None = _locale_roots if _locale_roots else None - _content_roots = adapter.get_extra_content_roots(repo_root) - content_roots: list[Path] | None = _content_roots if _content_roots else None - - link_errors = validate_links_structured( - docs_root, - exclusion_mgr, - repo_root=repo_root, - config=config, - strict=strict, - locale_roots=locale_roots, - check_external=True, - ) - orphans = find_orphans( - docs_root, - exclusion_mgr, - config=config, - has_engine_config=adapter.has_engine_config(), - nav_paths=adapter.get_nav_paths(), - is_locale_dir=adapter.is_locale_dir, - ignored_patterns=adapter.get_ignored_patterns(), - adapter=adapter, - ) - snippet_errors = validate_snippets(docs_root, exclusion_mgr, config=config) - placeholders = find_placeholders( - docs_root, - exclusion_mgr, - config=config, - locale_roots=locale_roots, - content_roots=content_roots, - ) - unused_assets = find_unused_assets( - docs_root, - exclusion_mgr, - config=config, - locale_roots=locale_roots, - content_roots=content_roots, - adapter_metadata_files=adapter.get_metadata_files(), - ) - - # Collect rule findings (Z107, Z505, Z601) and security violations (Z201–Z203) - # via the Two-Pass Reference Engine. - ref_reports, _ = scan_docs_references( - docs_root, - exclusion_mgr, - config=config, - validate_links=False, - locale_roots=locale_roots, - content_roots=content_roots, - ) - nav_errors = check_nav_contract(repo_root, exclusion_mgr) + from zenzic.cli._check import _collect_all_results, _to_findings + from zenzic.cli._governance import _apply_directory_policies, _apply_per_file_ignores + from zenzic.core.sovereign_context import sovereign_context + + with sovereign_context(force_audit=False): + results = _collect_all_results( + repo_root=repo_root, + docs_root=docs_root, + config=config, + exclusion_mgr=exclusion_mgr, + strict=strict, + check_external=True, + ) + all_findings = _to_findings(results, docs_root, repo_root) + filtered_findings = _apply_per_file_ignores(all_findings, config) + filtered_findings = _apply_directory_policies(filtered_findings, config) - # ── Build findings_counts dict (Zenzic Penalty Scorer) ─────────────────────── findings_counts: dict[str, int] = {} - - # Link errors — split by Zxxx code derived from error_type - for err in link_errors: - code = err.code + for f in filtered_findings: + code = f.code.upper().strip() findings_counts[code] = findings_counts.get(code, 0) + 1 - # Core check aggregates - findings_counts["Z402"] = findings_counts.get("Z402", 0) + len(orphans) - findings_counts["Z503"] = findings_counts.get("Z503", 0) + len(snippet_errors) - findings_counts["Z405"] = findings_counts.get("Z405", 0) + len(unused_assets) - findings_counts["Z406"] = findings_counts.get("Z406", 0) + len(nav_errors) - - # Placeholder findings — Z501 (pattern) vs Z502 (short-content) split (CEO-171) - for pf in placeholders: - pcode = "Z502" if pf.issue == "Z502" else "Z501" - findings_counts[pcode] = findings_counts.get(pcode, 0) + 1 - - # Rule-engine findings: Z107, Z505, Z601 (rule_id already a Zxxx code). - # ADR-084: apply directory_policies filter (zero-debt exemptions) so that - # `zenzic score` honours the same exemptions as `zenzic check`. - # IMPORTANT: use r.file_path (IntegrityReport — locale-remapped virtual path), - # NOT rule_f.file_path (RuleFinding — raw absolute path on disk). - from fnmatch import fnmatch as _fnmatch - - from zenzic.core.codes import NON_SUPPRESSIBLE_CODES - - _dir_policies = ( - config.governance.directory_policies - if hasattr(config.governance, "directory_policies") - else {} - ) - for r in ref_reports: - # Derive the display-relative path once per report (mirrors _check.py logic). - try: - _rel = str(r.file_path.relative_to(docs_root)) - except ValueError: - try: - _rel = str(r.file_path.relative_to(repo_root)) - except ValueError: - _rel = str(r.file_path) - for rule_f in r.rule_findings: - code = rule_f.rule_id - if code in NON_SUPPRESSIBLE_CODES: - findings_counts[code] = findings_counts.get(code, 0) + 1 - continue - if _dir_policies and any( - _fnmatch(_rel, pat) and code in codes for pat, codes in _dir_policies.items() - ): - continue # policy exemption — zero debt cost - findings_counts[code] = findings_counts.get(code, 0) + 1 - - # Security violations (Z2xx) — any breach triggers score override - security_violations = sum(len(r.security_findings) for r in ref_reports) - if security_violations > 0: - findings_counts["Z201"] = findings_counts.get("Z201", 0) + security_violations - # Suppression Debt: count all active suppressions (inline + per-file config). # Each suppression is a technical debt entry that reduces the final score. from zenzic.cli._governance import collect_inline_suppression_stats, count_per_file_ignores @@ -487,7 +386,7 @@ def score( audit_url = _audit_badge_url(audit_ok) found_any = False for rel in config.project_metadata.badge_stamp_files: - p = Path(rel) + p = repo_root / rel if not p.exists(): continue content = p.read_text(encoding="utf-8") @@ -522,7 +421,7 @@ def score( audit_url = _audit_badge_url(audit_ok) outdated: list[tuple[Path, str]] = [] for rel in config.project_metadata.badge_stamp_files: - p = Path(rel) + p = repo_root / rel if not _check_stamp_file(p, _SCORE_STAMP_MARKER, score_url): outdated.append((p, "score")) if not _check_stamp_file(p, _AUDIT_STAMP_MARKER, audit_url): From 4ca6b1171c8ede1ad55b98cc71755417f6c40bbb Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 19:54:24 +0200 Subject: [PATCH 11/12] docs(changelog): archive v0.9.0 and document v0.9.1 changes --- CHANGELOG.md | 33 ++++++++------------------------- changelogs/v0.9.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 25 deletions(-) create mode 100644 changelogs/v0.9.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc3dfb..3d14113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,42 +17,25 @@ No changes yet. ## [0.9.1] - 2026-06-02 -### Fixed - -- Core scanner integration fix for `Z403 MISSING_ALT_TEXT` to align fixture coverage with production scan paths. -- Fixture line-number correction in scanner test cases to keep finding locations deterministic and stable. - ---- - -## [0.9.0] - 2026-05-31 - ### Added -- `zenzic score --stamp`: deterministic, in-file badge stamping for score telemetry. -- `zenzic score --check-stamp`: config-aware freshness gate for stamped score badges. -- `badge_stamp_files` project metadata key to declare stamp targets. -- Domain-aware discovery exemptions for source-code assets in unused-asset analysis. -- `zenzic lab` command: empirical sandbox gallery covering 100% of Z-codes (20 scenarios). -- 15 new sandbox directories under `examples/` (z102 through z505), each with `.zenzic.toml`, `README.md`, and a minimal `docs/` tree that reliably triggers the target rule. -- `zenzic lab all` validation gate: all 20 scenarios emit the expected exit code. +- Native engine, fixtures, lab, and test validation coverage for `Z107 CIRCULAR_ANCHOR` (self-referential anchor link) and `Z104 FILE_NOT_FOUND`. ### Changed -- Suppression debt model migrated to flat-cost scoring (one point per suppression). -- `suppression_cap` behavior clarified as an independent hard-fail governance gate. -- Local overlay parsing hardened with strict unknown-key rejection. -- `just verify` standardized to a five-step operational sequence (hooks, tests, strict check, stamp, freshness check). -- **Performance — Z204 (FORBIDDEN_TERM):** `scan_line_for_forbidden_terms` now accepts a pre-compiled RE2 union regex. `ZenzicConfig` builds the union once via `_recompile_forbidden_patterns()` (called in `model_post_init` and after every `_apply_local_toml` merge). Scan complexity reduced from O(N_lines × N_patterns) to O(N_lines). -- **Performance — Z601 (BRAND_OBSOLESCENCE):** `BrandObsolescenceRule` replaced per-pattern `list[RegexPattern]` with a single RE2 union pattern compiled once at `__init__`. Same O(N_lines) reduction. +- **Unified Score Exclusions Pipeline:** Refactored `zenzic score` calculations (`_run_all_checks` in `_standalone.py`) to run the exact same `_collect_all_results` -> `_to_findings` pipeline as `check all`. Suppression exclusions (`per_file_ignores` and `directory_policies`) are now applied identically to ensure DQS aligns perfectly with linter findings. +- **Repository-Relative Path Resolution:** Refactored path mapping across the core engine scanner (`scanner.py`), CLI check commands (`_check.py`), findings reporter (`reporter.py`), and governance filter (`_governance.py`) to strictly resolve all finding relative paths against `repo_root` instead of `docs_root`, eliminating path inconsistencies. +- **Badge Stamping Path Resolution:** Fixed `score --stamp` and `score --check-stamp` path resolution so that configured `badge_stamp_files` paths are resolved relative to the target project's `repo_root` instead of the process's working directory. -### Removed +### Fixed -- Legacy adapter methods `map_url()` and `classify_route()` from the public adapter contract. -- Legacy score export path `--export-shields` in favor of native stamp/check-stamp telemetry. +- Core scanner integration fix for `Z403 MISSING_ALT_TEXT` to align fixture coverage with production scan paths. +- Fixture line-number correction in scanner test cases to keep finding locations deterministic and stable. --- ## Historical Releases +- v0.9.x archive: [changelogs/v0.9.md](./changelogs/v0.9.md) - v0.8.x archive: [changelogs/v0.8.md](./changelogs/v0.8.md) - v0.1.x–v0.7.x archive index: [changelogs/README.md](./changelogs/README.md) diff --git a/changelogs/v0.9.md b/changelogs/v0.9.md new file mode 100644 index 0000000..6e3d0b0 --- /dev/null +++ b/changelogs/v0.9.md @@ -0,0 +1,32 @@ + + + +# Changelog Archive — v0.9.x + +Archive of release notes extracted from the main changelog. + +## [0.9.0] — 2026-05-31 + +### Added + +- `zenzic score --stamp`: deterministic, in-file badge stamping for score telemetry. +- `zenzic score --check-stamp`: config-aware freshness gate for stamped score badges. +- `badge_stamp_files` project metadata key to declare stamp targets. +- Domain-aware discovery exemptions for source-code assets in unused-asset analysis. +- `zenzic lab` command: empirical sandbox gallery covering 100% of Z-codes (20 scenarios). +- 15 new sandbox directories under `examples/` (z102 through z505), each with `.zenzic.toml`, `README.md`, and a minimal `docs/` tree that reliably triggers the target rule. +- `zenzic lab all` validation gate: all 20 scenarios emit the expected exit code. + +### Changed + +- Suppression debt model migrated to flat-cost scoring (one point per suppression). +- `suppression_cap` behavior clarified as an independent hard-fail governance gate. +- Local overlay parsing hardened with strict unknown-key rejection. +- `just verify` standardized to a five-step operational sequence (hooks, tests, strict check, stamp, freshness check). +- **Performance — Z204 (FORBIDDEN_TERM):** `scan_line_for_forbidden_terms` now accepts a pre-compiled RE2 union regex. `ZenzicConfig` builds the union once via `_recompile_forbidden_patterns()` (called in `model_post_init` and after every `_apply_local_toml` merge). Scan complexity reduced from O(N_lines × N_patterns) to O(N_lines). +- **Performance — Z601 (BRAND_OBSOLESCENCE):** `BrandObsolescenceRule` replaced per-pattern `list[RegexPattern]` with a single RE2 union pattern compiled once at `__init__`. Same O(N_lines) reduction. + +### Removed + +- Legacy adapter methods `map_url()` and `classify_route()` from the public adapter contract. +- Legacy score export path `--export-shields` in favor of native stamp/check-stamp telemetry. From ceffbee51e5a926cf19a6083862f9449b02592fa Mon Sep 17 00:00:00 2001 From: PythonWoods Date: Tue, 2 Jun 2026 19:56:35 +0200 Subject: [PATCH 12/12] chore: stamp quality score badge in READMEs --- README.it.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.it.md b/README.it.md index 14442ab..52db55f 100644 --- a/README.it.md +++ b/README.it.md @@ -20,7 +20,7 @@ SPDX-License-Identifier: Apache-2.0 zenzic-audit - zenzic-score + zenzic-score REUSE 3.x compliant diff --git a/README.md b/README.md index 7cd0d8a..4d1d9ce 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ SPDX-License-Identifier: Apache-2.0 zenzic-audit - zenzic-score + zenzic-score REUSE 3.x compliant