Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

[tool.bumpversion]
current_version = "0.26.3"
current_version = "0.26.4"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)((?P<pre_l>a|b|rc)(?P<pre_n>\\d+))?"
serialize = [
"{major}.{minor}.{patch}{pre_l}{pre_n}",
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/security_vulnerability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ body:
attributes:
label: Zenzic version
description: Output of `zenzic --version`
placeholder: "0.26.3"
placeholder: "0.26.4"
validations:
required: true

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#
# repos:
# - repo: https://github.com/PythonWoods/zenzic
# rev: v0.26.3
# rev: v0.26.4
# hooks:
# - id: zenzic-verify # quality gate — corrisponde a `just verify` lato zenzic
# - id: zenzic-guard # fast staged-file credential scan
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ Versions follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [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
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -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.3
date-released: 2026-07-28
version: 0.26.4
date-released: 2026-07-29
url: "https://zenzic.dev"
repository-code: "https://github.com/PythonWoods/zenzic"
repository-artifact: "https://pypi.org/project/zenzic/"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Zenzic Core is headless and emits standardized **SARIF** JSON, ensuring seamless
"tool": {
"driver": {
"name": "zenzic",
"version": "0.26.3",
"version": "0.26.4",
"rules": [
{
"id": "Z101",
Expand Down Expand Up @@ -215,7 +215,7 @@ uv tool upgrade zenzic
To run a specific version ephemerally without altering your global environment:

```bash
uvx zenzic@0.26.3 check all
uvx zenzic@0.26.4 check all
```

---
Expand Down
10 changes: 5 additions & 5 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

| Field | Value |
| :------- | :--------- |
| Version | v0.26.3 |
| Version | v0.26.4 |
| Codename | Magnetite |
| Date | 2026-07-28 |
| Date | 2026-07-29 |
| Status | Stable |

## Release Checklist
Expand All @@ -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.3`)
- [ ] `pyproject.toml` version matches the tag (`0.26.4`)
- [ ] `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).
Expand Down Expand Up @@ -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.3" v0.26.3
git tag -s -m "Release v0.26.4" v0.26.4
git push origin main --tags

```

- [ ] Create GitHub Release from the tag, using the `## [0.26.3]` CHANGELOG section as the release body.
- [ ] Create GitHub Release from the tag, using the `## [0.26.4]` CHANGELOG section as the release body.

## Changelog Reference

Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,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.3" # release sync
zenzic_version: "0.26.4" # release sync
social:
- icon: fontawesome/brands/github
link: https://github.com/PythonWoods/zenzic
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ build-backend = "hatchling.build"

[project]
name = "zenzic"
version = "0.26.3"
version = "0.26.4"
description = "Deterministic Document Integrity Engine and SAST for Markdown/MDX graphs."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/zenzic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
# SPDX-License-Identifier: Apache-2.0
"""Zenzic — engine-agnostic static analyzer and credential scanner for Markdown documentation."""

__version__ = "0.26.3"
__version__ = "0.26.4"
__version_name__ = "Basalt" # Release codename stored separately from the package version.
2 changes: 1 addition & 1 deletion src/zenzic/cli/_standalone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.3"]
dependencies = ["zenzic>=0.26.4"]

[project.entry-points."zenzic.rules"]
{project_slug} = "{module_name}.rules:{class_name}"
Expand Down
37 changes: 33 additions & 4 deletions src/zenzic/lsp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ def __init__(self, stdin: BinaryIO | None = None, stdout: BinaryIO | None = None
# Phase 5: Decoupled Incremental Engine (ADR-075)
self.engine: IncrementalAnalysisEngine | None = None

# Diagnostics tracking to prevent ghost diagnostics on file deletion
self.file_diagnostics: set[str] = set()

def send_message(self, message: dict[str, Any]) -> None:
"""Encode and send a JSON-RPC message to stdout."""
body = json.dumps(message, separators=(",", ":")).encode("utf-8")
Expand Down Expand Up @@ -389,6 +392,7 @@ def _handle_file_changes(self, changes: list[dict[str, Any]]) -> None:
"params": {"uri": uri, "diagnostics": []},
}
)
self.file_diagnostics.discard(uri)
continue # Deleted files must NOT be re-added to dirty_documents

self.dirty_documents[uri] = 0.0
Expand Down Expand Up @@ -541,8 +545,9 @@ def _sync_workspace_and_publish(self, incremental_uris: set[str] | None = None)
assert isinstance(self.vsm, VirtualSiteMap)

# Instantiate engine if needed (ADR-075: transport-agnostic analysis)
assert self.rule_engine is not None # Ensure engine exists
is_full_rebuild = self.engine is None
if self.engine is None:
assert self.rule_engine is not None
self.engine = IncrementalAnalysisEngine(
config=self.config,
rule_engine=self.rule_engine,
Expand All @@ -558,6 +563,7 @@ def _sync_workspace_and_publish(self, incremental_uris: set[str] | None = None)
# Serialize at transport boundary and publish via JSON-RPC
# to_lsp_dict() is the ONLY serialization site in the codebase
for uri, typed_diags in results.items():
self.file_diagnostics.add(uri)
self.send_message(
{
"jsonrpc": "2.0",
Expand All @@ -569,6 +575,26 @@ def _sync_workspace_and_publish(self, incremental_uris: set[str] | None = None)
}
)

# State Hygiene (LSP-FIX-017): Clear ghost diagnostics for files that no longer exist.
# If a file was deleted or its route removed, process_changes won't return it in results,
# so we must actively detect missing URIs and broadcast an empty diagnostics array.
# PERF: Only run this on full topology rebuilds to avoid O(N) resolve() calls during incremental typing.
if is_full_rebuild and self.engine is not None:
dead_uris = []
for uri in list(self.file_diagnostics):
path = uri_to_path(uri).resolve()
if path not in self.engine.md_contents_cache:
self.send_message(
{
"jsonrpc": "2.0",
"method": "textDocument/publishDiagnostics",
"params": {"uri": uri, "diagnostics": []},
}
)
dead_uris.append(uri)
for dead_uri in dead_uris:
self.file_diagnostics.remove(dead_uri)

# DQS emission intentionally removed (LSP-FIX-014).
# The LSP operates in incremental mode and only sees topological findings
# (Z1xx/Z4xx). Content findings (Z5xx) on closed files are never analysed,
Expand Down Expand Up @@ -745,6 +771,9 @@ def _handle_code_action(self, params: dict[str, Any], msg_id: int | str | None)

if diag_code and diag_code not in NON_SUPPRESSIBLE_CODES:
insert_line = max(0, diag.get("range", {}).get("start", {}).get("line", 0))
# Use a large character index to append to the end of the line
insert_char = 9999

suppress_action = {
"title": f"Suppress {diag_code} for this line",
"kind": "quickfix",
Expand All @@ -754,10 +783,10 @@ def _handle_code_action(self, params: dict[str, Any], msg_id: int | str | None)
uri: [
{
"range": {
"start": {"line": insert_line, "character": 0},
"end": {"line": insert_line, "character": 0},
"start": {"line": insert_line, "character": insert_char},
"end": {"line": insert_line, "character": insert_char},
},
"newText": f"<!-- zenzic:ignore:{diag_code} -->\n",
"newText": f" <!-- zenzic:ignore:{diag_code} -->",
}
]
}
Expand Down
6 changes: 3 additions & 3 deletions tests/test_lsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1551,10 +1551,10 @@ def test_lsp_code_action_suppression(tmp_path) -> None:
assert actions[0]["title"] == "Suppress Z101 for this line"
assert actions[0]["kind"] == "quickfix"
edit = actions[0]["edit"]["changes"][doc_uri][0]
assert edit["newText"] == "<!-- zenzic:ignore:Z101 -->\n"
assert edit["newText"] == " <!-- zenzic:ignore:Z101 -->"
assert edit["range"] == {
"start": {"line": 1, "character": 0},
"end": {"line": 1, "character": 0},
"start": {"line": 1, "character": 9999},
"end": {"line": 1, "character": 9999},
}

# 2. Test Z201 (Non-suppressible security gate)
Expand Down
3 changes: 0 additions & 3 deletions tests/test_lsp_incremental.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,3 @@ def test_no_incoming_links_in_language_server(tmp_path: Path) -> None:
assert not hasattr(server, "incoming_links"), (
"LanguageServer must not manage graph topology (ADR-075 Radical Unawareness)"
)
assert not hasattr(server, "file_diagnostics"), (
"LanguageServer must not cache diagnostic payloads in parallel store (Mirror Law)"
)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading