Skip to content

Commit 30d15bf

Browse files
Merge pull request #30 from PythonWoods/fix/standardize-v0.5.0a3
feat: v0.5.0a3 — Smart Init, Mutation Campaign + Tiered QA Doctrine
2 parents 9ca13ff + 980d76b commit 30d15bf

36 files changed

Lines changed: 2498 additions & 157 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ ENV/
7070
# ────────────────────────────────────────────────────────────────────────────
7171
.pytest_cache/
7272
.pytest_tmp/
73+
.hypothesis/
7374
.coverage
7475
.coverage.*
7576
htmlcov/

CHANGELOG.md

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ Versions follow [Semantic Versioning](https://semver.org/).
1313

1414
## [0.5.0a3] — 2026-04-03 — The Sentinel: Aesthetic Sprint, Parallel Anchors & Agnostic Target
1515

16-
> **Sprint 13 + 14.** Two major tracks delivered in one tag.
16+
> **Sprint 13 + 14 + 15.** Three tracks delivered in one tag.
1717
> Track A — Performance & SDK: deterministic two-phase anchor validation, `zenzic.rules` public
1818
> namespace, plugin scaffold command, Z001/Z002 split.
1919
> Track B — Aesthetic & DX: Sentinel Palette with Slate/Indigo/Rose/Amber color identity,
2020
> unified banner telemetry, agnostic target mode (`zenzic check all README.md` or
2121
> `zenzic check all content/`), `.pre-commit-hooks.yaml`, native Material header via
2222
> `MutationObserver`, and two new example projects.
23+
> Track C — The Breathing Sentinel: native `col_start`/`match_text` propagation replacing
24+
> fragile regex workarounds, surgical caret rendering, traceback gutter with 2-space
25+
> padding, vertical breathing between findings, and a dedicated success-state panel.
2326
2427
### Added
2528

@@ -74,6 +77,12 @@ Versions follow [Semantic Versioning](https://semver.org/).
7477
- `src/<module>/rules.py` module-level `BaseRule` template
7578
- minimal docs fixture and `zenzic.toml` so `zenzic check all` can run
7679

80+
- **Smart Initialization — `zenzic init --pyproject`** — when `pyproject.toml`
81+
exists, `zenzic init` interactively asks whether to embed configuration as a
82+
`[tool.zenzic]` table instead of creating a standalone `zenzic.toml`. Pass
83+
`--pyproject` to skip the prompt. `--force` overwrites an existing
84+
`[tool.zenzic]` section. Engine auto-detection works in both modes.
85+
7786
- `examples/plugin-scaffold-demo/` — living scaffold output fixture for SDK
7887
integration checks and contributor onboarding.
7988

@@ -82,6 +91,43 @@ Versions follow [Semantic Versioning](https://semver.org/).
8291

8392
### Changed
8493

94+
- **Native Data Propagation — "The Breathing Sentinel"** (`reporter.py`,
95+
`validator.py`, `scanner.py`, `rules.py`, `cli.py`) — replaced the fragile
96+
`_extract_token()` regex workaround with native `col_start`/`match_text`
97+
propagation from every checker through the full pipeline to the reporter.
98+
Every regex match site now captures `m.start()` and `m.group()` at detection
99+
time and stores them in the finding dataclass — no reverse-engineering from
100+
error messages.
101+
102+
- **`LinkInfo` NamedTuple** (`validator.py`) — new type
103+
`(url, lineno, col_start, match_text)` replacing plain `tuple[str, int]`
104+
throughout the validator pipeline. `extract_links()` and
105+
`extract_ref_links()` now return `list[LinkInfo]`.
106+
107+
- **Widened dataclasses**`LinkError`, `PlaceholderFinding`, `RuleFinding`,
108+
`Violation`, and `Finding` all gained `col_start: int = 0` and
109+
`match_text: str = ""` fields, propagated end-to-end.
110+
111+
- **Traceback Gutter** (`reporter.py`) — increased gutter padding to 2 spaces
112+
around the vertical line: `16 ❱ code` / `16 │ code`.
113+
114+
- **Vertical Breathing** (`reporter.py`) — empty `Text()` lines inserted
115+
between different findings in the same file, before/after code snippets,
116+
after file-separator rules, and before the verdict line.
117+
118+
- **Success State** (`reporter.py`) — when all checks pass, the panel renders
119+
a dedicated all-clear layout: telemetry → rule → elegant
120+
`✔ All checks passed. Your documentation is secure.` message.
121+
122+
- **Surgical Caret Rendering** (`reporter.py`) — `_render_snippet()` uses
123+
native `col_start`/`match_text` to render precise `^^^^` caret underlines;
124+
carets are suppressed when `col_start + len(match_text) > 60` (wrapping
125+
guard) or when `match_text` is empty (no position data available).
126+
127+
- **Removed `_extract_token()`** (`reporter.py`) — deleted the `_TOKEN_RE`
128+
regex pattern and `_extract_token()` function; removed `import re` from the
129+
module. The reporter no longer guesses token positions from message strings.
130+
85131
- **Sentinel Gutter Reporter** (`reporter.py`) — source-line context block uses
86132
the Sentinel Palette consistently:
87133
- `` separators and line numbers rendered in `SLATE`
@@ -159,6 +205,12 @@ Versions follow [Semantic Versioning](https://semver.org/).
159205

160206
### Fixed
161207

208+
- **mypy errors**`list[object]` annotations on `security_line` and
209+
`renderables` changed to `list[RenderableType]` (imported from
210+
`rich.console`); `config.docs_dir` (type `Path`) wrapped with `str()` when
211+
passed to `SentinelReporter(docs_dir=...)` which expects `str`.
212+
`mypy src/` — 0 errors in 30 files.
213+
162214
- **Exit-code under target mode**`has_failures` now uses filtered findings
163215
(`errors > 0`) rather than `results.failed` (which counted off-target findings
164216
such as `zenzic.toml` classified as an unused asset when `docs_dir` was patched
@@ -170,12 +222,46 @@ Versions follow [Semantic Versioning](https://semver.org/).
170222

171223
### Tests
172224

225+
- `test_validator.py` updated: added `_ul()` helper to unwrap `LinkInfo`
226+
`(url, lineno)` tuples; 13 assertion patterns updated for `LinkInfo`
227+
compatibility; 4 destructuring patterns migrated from `for u, _ in` to
228+
`for link in` with `.url` attribute access.
173229
- 4 new CLI integration tests for target mode:
174230
`test_check_all_target_not_found`, `test_check_all_target_single_file`,
175231
`test_check_all_target_file_outside_docs`, `test_check_all_target_directory`.
176232
- Test fixtures write ≥ 60-word bodies to avoid `short-content` placeholder warnings.
177-
- **587 tests pass.** `just preflight` — all gates green:
178-
ruff ✓ · mypy ✓ · pytest 81.49% coverage ✓ · REUSE ✓ · zenzic self-audit ✓ · mkdocs build --strict ✓.
233+
234+
#### Mutation Testing Campaign — "The Mutant War"
235+
236+
- **Mutation score: 86.7%** (242/279 mutants killed on `rules.py`) — up from
237+
58.1% at baseline. Target was 75%; exceeded by +11.7 pp.
238+
- **80 new mutant-killing tests** added to `test_rules.py`, organised in
239+
dedicated test classes:
240+
- `TestExtractInlineLinksWithLines` (14 tests) — edge cases for inline link
241+
extraction including empty hrefs, escaped brackets, and multi-link lines.
242+
- `TestVSMBrokenLinkRuleMutantKill` (22 tests) — `check_vsm` path/anchor
243+
resolution logic, orphan detection, severity mapping, and `continue`/`break`
244+
branch coverage.
245+
- `TestAdaptiveRuleEngineRunMutantKill` (4 tests) — `AdaptiveRuleEngine.run()`
246+
short-circuit and content propagation.
247+
- `TestAdaptiveRuleEngineRunVsmMutantKill` (6 tests) — `run_vsm()` VSM-specific
248+
finding collection and file iteration.
249+
- `TestAssertPickleableMutantKill` (2 tests) — `assert_pickleable()` deep-copy
250+
and `UNREACHABLE` assertion guard.
251+
- `TestPluginRegistryMutantKill` (27 tests) — `PluginRegistry` discovery,
252+
duplicate handling, case-sensitivity, and `validate_rule()` contract.
253+
- `TestExtractLinksDeepMutantKill` (5 tests) — fence-block skipping, reference
254+
link parsing, and empty-document edge cases.
255+
- **37 surviving mutants** classified as equivalent (no observable behaviour
256+
change) or framework limitations (unreachable defensive assertions).
257+
- **Hypothesis property-based testing** integrated with three profiles:
258+
`dev` (50 examples), `ci` (500), `purity` (1 000).
259+
- **mutmut 3.5.0** configured under `[tool.mutmut]` in `pyproject.toml`;
260+
runner: `python3 -m pytest -x`, target: `src/zenzic/core/rules.py`.
261+
- **Performance baseline** relaxed from 150 ms → 200 ms for 5 000 in-memory
262+
resolutions to accommodate CI/nox environmental variance (resolver is O(1)).
263+
- **706 tests pass.** `just preflight` — all gates green:
264+
ruff ✓ · mypy ✓ · pytest 80%+ coverage ✓ · REUSE ✓ · zenzic self-audit ✓ · mkdocs build --strict ✓.
179265

180266
## [0.5.0a2] — 2026-04-03 — The Refined Sentinel: Lean Package & Unified Workflow
181267

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ abstract: >
1515
scanner (the Shield). Built on pure functional principles in Python 3.11+, it operates
1616
source-first — no build framework required — and integrates with any Markdown-based
1717
documentation system via a plugin adapter protocol.
18-
version: 0.5.0a2
18+
version: 0.5.0a3
1919
date-released: 2026-04-03
2020
url: "https://zenzic.pythonwoods.dev/"
2121
repository-code: "https://github.com/PythonWoods/zenzic"

CONTRIBUTING.it.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@ I controlli di qualità e le attività di sviluppo sono guidati da **just** (per
2828
| Sessione | Comando Just | Comando Nox | Descrizione |
2929
|---|---|---|---|
3030
| `dev` | - | `nox -s dev` | installa i pre-commit hook + scarica icone Lucide (da eseguire una volta dopo il clone) |
31-
| `tests` | - | `nox -s tests` | pytest + branch coverage |
31+
| `tests` | `just test` | `nox -s tests` | pytest + branch coverage (profilo Hypothesis **dev**) |
32+
| `tests` (approfondito) | `just test-full` | - | pytest con profilo Hypothesis **ci** (500 esempi) |
33+
| `mutation` | - | `nox -s mutation` | mutmut su `src/zenzic/core/rules.py` |
3234
| `lint` | `just lint` | `nox -s lint` | linting ruff + self-check zenzic |
3335
| `format` | - | `nox -s format` | formattazione ruff |
3436
| `typecheck` | - | `nox -s typecheck` | mypy strict |
3537
| `reuse` | - | `nox -s reuse` | conformità alle licenze REUSE/SPDX |
3638
| `security` | - | `nox -s security` | scansione vulnerabilità CVE pip-audit |
3739
| `docs` | `just dev` | `nox -s docs` | mkdocs build --strict |
3840
| `preflight` | `just deploy` | `nox -s preflight` | tutto quanto sopra |
41+
| `clean` | `just clean` | - | Rimuove `site/`, `dist/`, `.hypothesis/`, cache |
3942
| `screenshot` | - | `nox -s screenshot` | rigenera `docs/assets/screenshot.svg` |
4043
| `bump` | - | `nox -s bump -- patch` | avanza la versione + commit + tag |
4144

CONTRIBUTING.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,15 @@ the exact same environment as CI.
3939
|:-----|:---------------|:-----------------|:------------|
4040
| Bootstrap | `just sync` || Install / update all dependency groups |
4141
| **Self-lint** | **`just check`** || **Run Zenzic on its own documentation (strict)** |
42-
| Test suite | `just test` | `nox -s tests` | pytest + branch coverage |
42+
| Test suite | `just test` | `nox -s tests` | pytest + branch coverage (Hypothesis **dev** profile) |
43+
| Test suite (thorough) | `just test-full` || pytest with Hypothesis **ci** profile (500 examples) |
44+
| Mutation testing || `nox -s mutation` | mutmut on `src/zenzic/core/rules.py` |
4345
| Full pipeline | `just preflight` | `nox -s preflight` | lint, typecheck, tests, reuse, security |
4446
| **Pre-push gate** | **`just verify`** || **preflight + production build — run before every push** |
4547
| Docs build (fast) | `just build` || mkdocs build, no strict enforcement |
4648
| Docs build (prod) | `just build-prod` | `nox -s docs` | mkdocs build --strict, mirrors CI |
4749
| Docs serve | `just serve [port]` | `nox -s docs_serve` | live-reload server (default port 8000) |
50+
| Clean | `just clean` || Remove `site/`, `dist/`, `.hypothesis/`, caches |
4851
| Pre-commit setup || `nox -s dev` | install hooks + download Lucide icons (once after clone) |
4952
| Version bump || `nox -s bump -- patch` | bump version + commit + tag |
5053
| Screenshot || `nox -s screenshot` | regenerate `docs/assets/screenshot.svg` |
@@ -215,6 +218,44 @@ just build
215218

216219
---
217220

221+
## Advanced QA: Mutants & Properties
222+
223+
Zenzic uses two advanced testing techniques to ensure the Sentinel's core is battle-hardened.
224+
225+
### Property-Based Testing (Hypothesis)
226+
227+
`tests/test_properties.py` uses [Hypothesis](https://hypothesis.readthedocs.io/) to generate
228+
thousands of random inputs and verify **invariants** that must hold for any input:
229+
230+
- `extract_links()` never crashes, always returns `LinkInfo`, line numbers stay in range.
231+
- `slug_heading()` is lowercase, idempotent, and free of leading/trailing hyphens.
232+
- `CustomRule.check()` returns valid findings with `col_start` in range.
233+
- `InMemoryPathResolver.resolve()` always returns a valid outcome type and catches path traversal.
234+
235+
Run property tests:
236+
237+
```bash
238+
uv run pytest tests/test_properties.py -x -q
239+
```
240+
241+
### Mutation Testing (mutmut)
242+
243+
[mutmut](https://mutmut.readthedocs.io/) modifies your source code (e.g. changes `>` to `>=`)
244+
and checks whether the test suite catches the mutation. A surviving mutant means a test gap.
245+
246+
Target module: `src/zenzic/core/rules.py` — the heart of the Sentinel's detection logic.
247+
248+
Run mutation testing:
249+
250+
```bash
251+
nox -s mutation
252+
```
253+
254+
**Merge requirement:** any new core rule must achieve a **mutation score > 90%**. If `mutmut`
255+
reports surviving mutants in `rules.py`, add targeted tests before merging.
256+
257+
---
258+
218259
## Release procedure (maintainers only)
219260

220261
Releases are **semi-automated**: the developer decides the bump type, one command does the rest.

README.it.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ Zenzic esegue un'**analisi statica** dei tuoi file di configurazione (`mkdocs.ym
167167

168168
Questo significa che **non è necessario installare** MkDocs, Material for MkDocs o altri plugin di build nel tuo ambiente di linting. Zenzic rimane leggero e privo di dipendenze, rendendolo ideale per pipeline CI/CD veloci e isolate.
169169

170+
### Setup progetto
171+
172+
```bash
173+
zenzic init # crea zenzic.toml con engine rilevato automaticamente
174+
zenzic init --pyproject # incorpora [tool.zenzic] in pyproject.toml
175+
```
176+
177+
Quando `pyproject.toml` esiste, `zenzic init` chiede interattivamente se incorporare
178+
la configurazione lì. Usa `--pyproject` per saltare il prompt.
179+
170180
---
171181

172182
## Utilizzo CLI

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,16 @@ This means you **do not need to install** MkDocs, Material for MkDocs, or any ot
362362
> (PDFs, ZIPs), add their glob patterns to `excluded_build_artifacts` in `zenzic.toml`
363363
> rather than pre-generating them. See the [First-Class Integrations](#first-class-integrations) section above.
364364

365+
### Project setup
366+
367+
```bash
368+
zenzic init # creates zenzic.toml with auto-detected engine
369+
zenzic init --pyproject # embeds [tool.zenzic] in pyproject.toml instead
370+
```
371+
372+
When `pyproject.toml` exists, `zenzic init` asks interactively whether to embed
373+
configuration there. Pass `--pyproject` to skip the prompt.
374+
365375
---
366376

367377
## CLI usage

RELEASE.md

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ serving as both a DX reference and a quality-gate integration test.
112112

113113
---
114114

115+
#### ⚡ Smart Initialization — `zenzic init --pyproject`
116+
117+
`zenzic init` now detects `pyproject.toml` in the project root and interactively
118+
asks whether to embed configuration as a `[tool.zenzic]` table instead of creating
119+
a standalone `zenzic.toml`.
120+
121+
```bash
122+
zenzic init # interactive: asks if pyproject.toml exists
123+
zenzic init --pyproject # skip the prompt, write directly into pyproject.toml
124+
zenzic init --force # overwrite existing config (both modes)
125+
```
126+
127+
Engine auto-detection (`mkdocs.yml``engine = "mkdocs"`, `zensical.toml`
128+
`engine = "zensical"`) works in both standalone and pyproject modes. When no
129+
engine config file is found, vanilla defaults apply.
130+
131+
---
132+
115133
#### 🛡️ Z001 / Z002 Split — Errors vs Warnings for Link Issues (closes #6)
116134

117135
`VSMBrokenLinkRule` now distinguishes:
@@ -165,8 +183,9 @@ repos:
165183
### Quality Gates
166184

167185
```text
168-
pytest 587 passed, 0 failed
169-
coverage 81.49% (gate: ≥ 80%)
186+
pytest 706 passed, 0 failed
187+
coverage 80%+ branch (gate: ≥ 80%)
188+
mutation score 86.7% (242/279 killed on rules.py — target: 75%)
170189
ruff check src/ 0 violations
171190
mypy src/ 0 errors
172191
reuse lint 262/262 files compliant
@@ -176,6 +195,31 @@ mkdocs build --strict, 0 warnings
176195

177196
---
178197

198+
### Mutation Testing Campaign — "The Mutant War"
199+
200+
v0.5.0a3 ships with a full mutation testing campaign against `src/zenzic/core/rules.py`
201+
using **mutmut 3.5.0**. The campaign raised the mutation score from 58.1% (baseline)
202+
to **86.7%** (242/279 killed) — exceeding the 75% target by +11.7 percentage points.
203+
204+
**80 new targeted tests** were added to `test_rules.py`, organised in 7 specialised
205+
test classes covering:
206+
207+
- **PluginRegistry** (27 tests) — discovery, duplicates, case-sensitivity, `validate_rule()`
208+
- **VSMBrokenLinkRule** (22 tests) — `check_vsm` path/anchor resolution, orphan detection
209+
- **Inline link extraction** (14 tests) — escaped brackets, empty hrefs, multi-link lines
210+
- **AdaptiveRuleEngine** (10 tests) — `run()` and `run_vsm()` short-circuits and propagation
211+
- **Deep link extraction** (5 tests) — fence-block skipping, reference links, empty documents
212+
- **Pickleable assertions** (2 tests) — deep-copy guard and `UNREACHABLE` sentinel
213+
214+
The 37 surviving mutants were analysed and classified as equivalent mutations
215+
(no observable behaviour change) or framework-level limitations (unreachable
216+
defensive assertions). **Practical quality saturation** has been reached.
217+
218+
Hypothesis property-based testing is integrated with three severity profiles:
219+
`dev` (50 examples), `ci` (500), `purity` (1 000).
220+
221+
---
222+
179223
## Why this release matters now
180224

181225
The documentation tooling ecosystem is fractured. MkDocs 2.0 is on the horizon, carrying breaking
@@ -217,7 +261,7 @@ installed for `zenzic check all` to pass.
217261

218262
All validation logic in Zenzic lives in pure functions: no file I/O, no network access, no global
219263
state, no terminal output. I/O happens only at the edges — CLI wrappers that read files and print
220-
findings. Pure functions are trivially testable (433 passing tests at 98.4% coverage), composable
264+
findings. Pure functions are trivially testable (706 passing tests, ≥ 80% branch-coverage gate), composable
221265
into higher-order pipelines, and deterministic across environments.
222266

223267
The score you get on a developer laptop is the score CI gets. The score CI gets is the score you
@@ -543,8 +587,9 @@ and `diff` Python APIs has been renamed to `output_format` — update any progra
543587

544588
```text
545589
zenzic check all # self-dogfood: 7/7 OK
546-
pytest # 529 passed, 0 failed
547-
coverage # ≥ 80% (hard gate)
590+
pytest # 706 passed, 0 failed
591+
coverage # ≥ 80% branch (hard gate)
592+
mutation score # 86.7% (242/279 killed on rules.py)
548593
ruff check . # 0 violations
549594
mypy src/ # 0 errors
550595
mkdocs build --strict # 0 warnings

docs/community/contribute/pull-requests.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ by installing only what each job needs. The groups are:
5757

5858
| Group | Contents | When to use |
5959
| :---- | :------- | :---------- |
60-
| `test` | `pytest`, `pytest-cov` | Running the test suite |
60+
| `test` | `pytest`, `pytest-cov`, `hypothesis`, `mutmut` | Running the test suite |
6161
| `lint` | `ruff`, `mypy`, `pre-commit`, `reuse` | Linting and type checking |
6262
| `docs` | MkDocs stack (`mkdocs-material`, etc.) | Building the documentation |
6363
| `release` | `nox`, `bump-my-version`, `pip-audit` | Releases and audits |
@@ -79,9 +79,20 @@ time:
7979
```bash
8080
zenzic check all # all seven checks
8181
zenzic check references # includes custom [[custom_rules]] evaluation
82-
pytest # full test suite
82+
pytest # full test suite (Hypothesis dev profile — 50 examples)
8383
```
8484

85+
!!! tip "Thorough property-based testing"
86+
87+
To run the test suite with the **ci** Hypothesis profile (500 examples),
88+
use `just test-full` or set the environment variable directly:
89+
90+
```bash
91+
just test-full
92+
# or
93+
HYPOTHESIS_PROFILE=ci pytest
94+
```
95+
8596
!!! note "End users vs contributors"
8697

8798
**End users** run `uvx zenzic check all` — no clone, no install, zero

docs/community/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ __Manual BibTeX:__
8383
@software{zenzic,
8484
author = {PythonWoods},
8585
title = {{Zenzic: The Agnostic Documentation Integrity Framework}},
86-
version = {0.5.0a2},
86+
version = {0.5.0a3},
8787
date = {2026-04-03},
8888
url = {https://zenzic.pythonwoods.dev/},
8989
license = {Apache-2.0},

0 commit comments

Comments
 (0)