diff --git a/.bumpversion.toml b/.bumpversion.toml index 5e984c4..b47639a 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 [tool.bumpversion] -current_version = "0.10.1" +current_version = "0.10.2" 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 7a80f8e..f3b9a15 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.10.1" + placeholder: "0.10.2" validations: required: true diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 7c19428..33ae468 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -7,7 +7,7 @@ # # repos: # - repo: https://github.com/PythonWoods/zenzic -# rev: v0.10.1 +# rev: v0.10.2 # hooks: # - id: zenzic-verify # quality gate — corrisponde a `just verify` lato zenzic # - id: zenzic-guard # fast staged-file credential scan diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c3f375..d43d0bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,15 @@ No changes yet. --- +## [0.10.2] - 2026-06-07 + +### Fixed + +- **Core Engine (AST Parser):** Fixed a blindspot in the AST parser where image nodes (`![alt][id]`) were not being harvested into the `used_ids` set, causing false-positive Z302 (Orphan Definition) warnings. +- **Core Engine (Path Resolver):** The local path resolver now strips URL fragments (`#...`) and query strings (`?...`) before interrogating the filesystem. This prevents false-positive Z101/Z104 errors when using GFM suffixes on local file links (e.g., `../assets/img.png#gh-light-mode-only`). + +--- + ## [0.10.1] - 2026-06-07 ### Changed diff --git a/CITATION.cff b/CITATION.cff index 6fe43ec..ed56b3d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -15,7 +15,7 @@ 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.10.1 +version: 0.10.2 date-released: 2026-06-07 url: "https://zenzic.dev" repository-code: "https://github.com/PythonWoods/zenzic" diff --git a/RELEASE.md b/RELEASE.md index 50b2766..f6f1eec 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,7 +8,7 @@ | Field | Value | | :------- | :--------- | -| Version | v0.10.1 | +| Version | v0.10.2 | | Codename | Magnetite | | Date | 2026-06-07 | | Status | Stable | @@ -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 and README.it.md reflects current score - [ ] `zenzic check all .` — zero findings in the repo root -- [ ] `pyproject.toml` version matches the tag (`0.10.1`) +- [ ] `pyproject.toml` version matches the tag (`0.10.2`) - [ ] `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). @@ -54,11 +54,11 @@ git checkout main git pull origin main # 3. Tag the main branch and push -git tag v0.10.1 +git tag v0.10.2 git push origin main --tags ``` -- [ ] Create GitHub Release from the tag, using the `## v0.10.1` CHANGELOG section as the release body. +- [ ] Create GitHub Release from the tag, using the `## v0.10.2` CHANGELOG section as the release body. ## Changelog Reference diff --git a/pyproject.toml b/pyproject.toml index 5be6378..a3fed40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ build-backend = "hatchling.build" [project] name = "zenzic" -version = "0.10.1" +version = "0.10.2" 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 ba68322..a604986 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.10.1" +__version__ = "0.10.2" __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 91176bd..d4494ca 100644 --- a/src/zenzic/cli/_standalone.py +++ b/src/zenzic/cli/_standalone.py @@ -1270,7 +1270,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.10.1"] +dependencies = ["zenzic>=0.10.2"] [project.entry-points."zenzic.rules"] {project_slug} = "{module_name}.rules:{class_name}" diff --git a/src/zenzic/cli/_target_resolver.py b/src/zenzic/cli/_target_resolver.py index ea9b286..53837cc 100644 --- a/src/zenzic/cli/_target_resolver.py +++ b/src/zenzic/cli/_target_resolver.py @@ -26,6 +26,7 @@ def _resolve_target(repo_root: Path, config: ZenzicConfig, raw: str) -> Path: *repo_root/docs_dir*. Files must have the ``.md`` extension. Exits with code 1 if nothing is found or the extension is wrong. """ + raw = raw.split("#")[0].split("?")[0] p = Path(raw) candidates: list[Path] = ( [p] if p.is_absolute() else [repo_root / p, repo_root / config.docs_dir / p] diff --git a/src/zenzic/core/scanner.py b/src/zenzic/core/scanner.py index 3b24f5b..27e382c 100644 --- a/src/zenzic/core/scanner.py +++ b/src/zenzic/core/scanner.py @@ -1271,8 +1271,6 @@ def cross_check(self) -> list[ReferenceFinding]: clean = _INLINE_CODE_RE.sub(lambda m: " " * len(m.group()), line) for m in _RE_REF_LINK.finditer(clean): - if m.start() > 0 and clean[m.start() - 1] == "!": - continue text = m.group(2) ref_id = m.group(3) if m.group(3) else text # collapsed ref url = self.ref_map.resolve(ref_id) @@ -1292,10 +1290,12 @@ def cross_check(self) -> list[ReferenceFinding]: # Shortcut reference links: [text] (CommonMark §4.7) for m in _RE_REF_SHORTCUT.finditer(clean): - if m.start() > 0 and clean[m.start() - 1] in "!]": + if m.start() > 0 and clean[m.start() - 1] == "]": continue tail = clean[m.end() : m.end() + 1] - if tail in "[:(": + if tail in "[(": + continue + if tail == ":" and clean[: m.start()].strip() == "": continue ref_id = m.group(1) self.ref_map.resolve(ref_id) # mark as used if defined diff --git a/tests/test_references.py b/tests/test_references.py index 3a2fc51..acc444d 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -485,6 +485,15 @@ def test_collapsed_reference_link(self, tmp_path: Path) -> None: errors = [f for f in findings if not f.is_warning] assert errors == [] + def test_image_reference_link_resolved(self, tmp_path: Path) -> None: + """![alt][ref] is an image reference — should resolve to [ref]: url.""" + content = "[imgref]: https://example.com/img.png\n\nSee ![alt][imgref].\n" + scanner = self._make_scanner(tmp_path, content) + findings = scanner.cross_check() + errors = [f for f in findings if not f.is_warning] + assert errors == [] + assert "imgref" in scanner.ref_map.used_ids + def test_multiple_dangling_refs(self, tmp_path: Path) -> None: content = ( "[valid]: https://example.com\n\nSee [A][ghost1] and [B][ghost2] and [C][valid].\n" diff --git a/tests/test_target_resolver.py b/tests/test_target_resolver.py new file mode 100644 index 0000000..4fefa40 --- /dev/null +++ b/tests/test_target_resolver.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2026 PythonWoods +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path + +from zenzic.cli._target_resolver import _resolve_target +from zenzic.models.config import ZenzicConfig + + +def test_resolve_target_strips_fragments_and_queries(tmp_path: Path) -> None: + """_resolve_target must strip #fragments and ?queries before Path.exists() checks.""" + repo_root = tmp_path / "repo" + docs_dir = repo_root / "docs" + docs_dir.mkdir(parents=True) + + # Create a dummy md file + target_file = docs_dir / "page.md" + target_file.touch() + + config = ZenzicConfig(docs_dir=Path("docs")) + + # Test with fragment + raw_fragment = "docs/page.md#gh-light-mode-only" + resolved_fragment = _resolve_target(repo_root, config, raw_fragment) + assert resolved_fragment == target_file.resolve() + + # Test with query string + raw_query = "docs/page.md?version=1.0" + resolved_query = _resolve_target(repo_root, config, raw_query) + assert resolved_query == target_file.resolve() + + # Test with both + raw_both = "docs/page.md?version=1.0#gh-light-mode-only" + resolved_both = _resolve_target(repo_root, config, raw_both) + assert resolved_both == target_file.resolve() diff --git a/uv.lock b/uv.lock index 280585c..9d6b243 100644 --- a/uv.lock +++ b/uv.lock @@ -2163,7 +2163,7 @@ wheels = [ [[package]] name = "zenzic" -version = "0.10.1" +version = "0.10.2" source = { editable = "." } dependencies = [ { name = "google-re2" },