feat: v0.31.0 Specification-Driven Development, Security Hardening & Ecosystem Governance - #233
Open
PythonWoods-Dev wants to merge 341 commits into
Open
feat: v0.31.0 Specification-Driven Development, Security Hardening & Ecosystem Governance#233PythonWoods-Dev wants to merge 341 commits into
PythonWoods-Dev wants to merge 341 commits into
Conversation
PythonWoods-Dev
force-pushed
the
feat/v0.31.0-epic3-sdd
branch
from
August 25, 2026 16:51
adbc63e to
ced8f71
Compare
…docstring zenzic inspect codes showed 0.0 instead of FATAL for Z110/Z111 (CONFIG_SYNTAX_ERROR/CONFIG_SCHEMA_ERROR): same error+0.0 shape as Z901 but genuinely fatal config-abort codes (same FROZEN_CODES class as Z000/Z001), missed by the FATAL branch's Z0/Z2 prefix check since they're numbered in the Z1xx range for historical reasons. Fixed with the same narrow, code-specific carve-out pattern as Z901's earlier fix. _check.py's DQS Final Score line showed "(Gate Failed)" after --update-baseline even when the verdict text directly below it correctly said "Analysis complete" - the same baseline-blind bug shape already fixed once in reporter.py this session, one screen away. Errors/warnings now baseline-filtered for this label; security findings unaffected. scan_docs_references()'s docstring referenced the parallel-mode threshold by a stale hardcoded number (50, the real value is 1000); switched to referencing the ADAPTIVE_PARALLEL_THRESHOLD constant by name, matching the rest of the same docstring. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
docs/reference/advanced-features.md's Hybrid Adaptive Engine table and surrounding prose stated the parallel-scan activation threshold as 50 files throughout, 20x below the real ADAPTIVE_PARALLEL_THRESHOLD value (1000). Corrected 5 genuine threshold instances; left one adjacent "50 files" mention alone since it's an unrelated illustrative example for external-URL-link deduplication. adr-021-parallel-audit.md's two "50 files" mentions were left as-is - the ADR is dated 2026-05-10, when the threshold genuinely was 50 (git history confirms it was raised to 1000 in v0.30.0, 2026-08-18, after the ADR was written) - and given a new Historical snapshot note instead, since the figure was accurate when authored. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Adds a fixed, rule-based marker/repeated-char-run lookup that flags documented-example credentials and forbidden-term matches without any probabilistic logic (Tier-0 Invariant #1). Surfaces as a [LIKELY PLACEHOLDER] CLI tag and a SARIF result property; never suppresses or downgrades the underlying non-suppressible Z201/Z204 finding. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Same root cause as the already-fixed Z202/Z203/Z108 double-emissions: a Z201/Z204 RuleFinding was injected into report.rule_findings and independently converted from report.security_findings in the same _to_findings() pass, with no skip-list entry to prevent the duplicate. The duplicate also carried the wrong severity, inflating the DQS Security Override's non-suppressible-finding count. Adds Z201/Z204 to _check.py's skip-list; a full sweep confirms no further members of this bug family remain. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…gible items Corrects docs/developers/index.md's Just command table and mutation scope against the real justfile/pyproject.toml, core-laws.md's stale Pass 1 preamble reference (also discovered the "rglob" claim itself was wrong — real mechanism is walk_files()), sovereign-verification- model.md's Exit 1->2 diagram label, and licensing.md's fabricated REUSE.toml example plus pre-commit hook numbering. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Rewrites exit_strategy.md around the real BaseAdapter(ABC) contract (ADR-078-STRICT) in place of a fictional typing.Protocol story and a fabricated get_docs_root() method; corrects explanation/index.md's 8-step teaser to the real 10-target Mirror Law protocol and brings write-a-check.md's own list into sync; rewrites mdx-asset-rationale.md around the real native-HTML/Mermaid mechanism (no .tsx/React toolchain exists in this repo); fixes core-laws.md's RouteStatus.ORPHAN_AND_ABSENT non-existent enum reference; moves technical-debt.md's Z108 entry to Closed under its real Z112 identity and repopulates Open Entries with the genuine CLI/LSP topology-model divergence gap; rewrites tailwind-mkdocs-bridge.md and its companion blog post around the real .zz-tailwind-root wrapper-scoped CSS rule; marks lpgp.md historical. Completes all findings from docs/developers/ audit Batches 0-2. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…edirects The records/index.md -> adr-vault/index.md merge (previous commit) correctly removed duplicated content but left the directory with no index page, firing Z401. Adds a genuinely minimal stub (not a re-duplication of the parent), registers it in mkdocs.yml nav to avoid trading Z401 for a Z402 orphan-page finding, and fixes 2 _redirects lines from the merge that were left redirecting the exact canonical URL that now serves real content again. Also repoints 4 individual ADR pages' broken ./index.md links to ../index.md. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
No such ADR exists anywhere in the vault or .claude/. Searched for substantial pre-existing rationale to draft a real ADR from (the ADR-089 precedent) but found none beyond the one-line comment itself -- only a related historical fact in changelogs/v0.18.x.md (a v12 predecessor used wildcard catch-alls, later replaced by this file's explicit 1:1 model). Replaced the citation with a plain-language explanation grounded in that real history, rather than inventing an ADR's worth of rationale that was never actually written down. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
test_z204_finding_not_double_emitted hardcoded the literal Unicode
cross-mark ("✘ 0 errors"), but ui.py's _detect_capabilities() (a
pre-existing, deliberate feature) disables emoji whenever the CI env
var is set -- which GitHub Actions always sets, rendering "x 0 errors"
(ASCII fallback) instead. The underlying Z201/Z204 fix logic was never
broken; only this assertion was. Reproduced the exact CI failure
locally via CI=true, then fixed by asserting against the same emoji()
helper the reporter itself uses, so the check mirrors production
behavior in any environment. Verified green under both CI=true and
CI-absent conditions, plus a full CI=true suite run matching CI's
exact invocation.
Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
The docs_dir comment claimed engine-based auto-discovery reads docusaurus.config.ts -- traced via git log -S to commit b0fca6b (v0.8.0, 2026-05-30), accurate when written but never updated after Docusaurus was removed as a supported engine in v0.14.0. Also conflated docs_dir's real behavior (fixed "docs" default, no discovery involved) with the separate discover_engine() mechanism (which never reads docusaurus.config.ts). templates.py (the zenzic init scaffold) was already correct -- confirmed no matching drift there, this was an isolated stale comment in the repo's own committed file. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Full 5-phase documentation pruning (V031_DOCUMENTATION_PRUNING_EXECUTION): - Cut 5 pages entirely (technical-debt.md, lpgp.md, tailwind-mkdocs-bridge.md, reference/brand-system.md, reference/brand-kit.md), retargeting docs/_redirects to the nearest real content successor instead of leaving dead 301s, and removing 2 now-orphaned badge SVGs and dangling internal links (community-index.md, reference/index.md). - Moved 20 ADR record pages plus release-governance-protocol.md out of the public nav (files preserved, still link-checked) via a new [governance.directory_policies] strategic exemption for Z402 (ORPHAN_PAGE) and Z103 (ORPHAN_LINK) — a 0-DQS-debt declared exemption, not a suppression of a real defect. - Corrected 2 dangerous factual claims on archived ADR pages: adr-020's stale bilingual EN/IT requirement (superseded by ADR-022) and adr-discovery's false claim that mkdocs.yml is excluded from root markers. - Trimmed one historical note (adr-agnostic-universalism.md) down to its operationally useful clause; shortened redundant mkdocs.yml nav labels site-wide with no URL/slug changes. Verified via mkdocs build --strict, just check (98/100, 0 new findings), and the full pytest suite (2100 passed) after every phase. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…attribution ADR-021 attributed Z201/Z202/Z203 SecurityFinding objects to "the credential scanner" as if it were one scanner. codes.py's CORE_SCANNERS registry has these as two distinct entries: Z201 is the Credential Scanner, Z202/Z203 are the Path Traversal Guard (fatal variant). Corrected the attribution. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
README.md and ROADMAP.md were left uncommitted from the earlier Sphinx-only roadmap realignment (V031_ROADMAP_SPHINX_ONLY_EXECUTION) — completing that commit now. Collapses the former Docusaurus/Sphinx/Hugo three-adapter plan into a single v0.32 Sphinx Adapter + Auto-Fix Audit milestone; Multi-Repo Graph promotes to v0.33, Operational Excellence renumbers to v0.34. Docusaurus and Hugo are deferred indefinitely and community-tracked via GH #50/#51 rather than core-team roadmap items. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
mkdocs build --strict was emitting an undeclared INFO-level validation.nav.omitted_files warning for every page moved out of nav by the directory_policies pruning (20 ADR records + release-governance-protocol.md). It never failed the build (INFO is below strict mode's WARNING threshold), but a prior verification's tail-truncated output made it look genuinely silent. mkdocs.yml already ships not_in_nav for exactly this declared-omission case; the archived pages were never added to it. Added them, matching the same "declare it, don't hide it" discipline already applied to their Z402/ Z103 directory_policies exemptions. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Batch A of V031_COMMIT_PRUNING_AND_BEGIN_REMEDIATION's KEEP-page remediation. implement-adapter.md's MyEngineAdapter example omitted get_entry_points(vsm), a required BaseAdapter abstractmethod — live-reproduced the exact TypeError a reader hits following the page's own testing instructions. Added a correct implementation, a new Adapter Contract Guarantees invariant, and a routing table row. write-plugin.md's check_vsm example declared 4 parameters after self; the real BaseRule.check_vsm signature and its only call site both require a 5th, context: ResolutionContext | None. Live-reproduced the real TypeError the engine raises calling the 4-param version, added the missing parameter. Both fixes verified against the real installed engine: red-state TypeError reproduced from the pre-fix code, green-state pass from the post-fix code, plus 4 new example-behavior tests for get_entry_points. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Batch B of V031_COMMIT_PRUNING_AND_BEGIN_REMEDIATION's KEEP-page remediation. The Symptom/Standard resolution text had Z105 exactly backwards: it claimed Zenzic blocks relative traversal paths and recommends absolute site-root paths. Live-verified via the real z105-absolute-path fixture that the truth is the opposite: Z105 fires on absolute paths, and the fix is a relative path, since an absolute path silently mis-resolves under a subdirectory deployment. Corrected. The suppression example's asterisk-wrapped comment syntax doesn't match the real suppression regex, but live investigation found the syntax was never the actual problem: Z105 has no inline-suppression code path wired up at all in the current engine (same gap confirmed for Z101), contradicting codes.py's own NON_INLINE_SUPPRESSIBLE_CODES registry and the published suppression-policy.md claim. That gap is logged in .claude/state/03-priority-table.md and CHANGELOG.md's Known Limitations for a real engineering decision — not fixed here. Replaced the non-functional suppression example with the real, live-verified working mechanism: absolute_path_allowlist in .zenzic.toml. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…tors Batch C of V031_COMMIT_PRUNING_AND_BEGIN_REMEDIATION's KEEP-page remediation. _execute_ast_visitors() checked and dispatched visit_link and visit_heading but never visit_code_block, despite it being a publicly documented SDK v3 hook since v0.28.0 (write-ast-rule.md, CONTRIBUTING.md, changelogs/v0.28.x.md). A custom rule overriding only visit_code_block silently received zero callbacks regardless of code-fence content. TDD: new CustomCodeBlockRule + test_sdk_v3_visit_code_block reproduces the bug (red: assert 0 == 1 against the pre-fix code). Fix adds _extract_code_blocks(), reusing core.rules._FENCE_OPEN_RE directly rather than re-declaring an equivalent fence-matching regex, and wires it into _execute_ast_visitors(). Green: 5/5 SDK v3 tests pass, full suite 2101 passed. No public signature changed. Also corrects docs/developers/reference/credential-scanner-obligations.md's Obligation 2, which described a fabricated SIGALRM-based "Regex-Canary" mechanism with zero matches in src/. The real mechanism is CustomRule's compile-time RE2 DFA rejection (ZRT-007) - live-verified, including that the doc's own "dangerous pattern" examples were also wrong (RE2 accepts and runs them in linear time; only backreferences/lookarounds are rejected). Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…g example Corrects commit 19b3074. That commit concluded Z105 (and Z101) are not inline-suppressible at all - a false conclusion caused by a testing error: every test placed the suppression comment on its own line above the finding, but SuppressionTracker.is_suppressed() matches only the exact same line as the finding (suppressions.py, d.line_no == line_no), and suppression-policy.md already documents this convention ("placed at the end of a line") - not checked before the original tests ran. Per the addendum instruction, re-verified every flagged code (Z101, Z102, Z104, Z105, Z106, Z112, Z120-Z124, Z202, Z203) individually and live with same-line placement: every code that supports inline suppression works correctly; Z112 is N/A by design (a config-level finding anchored to .zenzic.toml, not a per-line Markdown finding); Z202/Z203 are N/A by design (NON_SUPPRESSIBLE_CODES, actively enforced, required by the Exit Code Contract). Zero real gaps found. troubleshooting.md's Z105 section restored to a genuine, live-verified working inline-suppression example (correct syntax and placement) instead of the allowlist-only redirect the retracted finding produced; the allowlist stays as a documented alternative for the multi-link case. CHANGELOG.md's two affected entries corrected in place; the false Known Limitations entry removed. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Batch D of V031_COMMIT_PRUNING_AND_BEGIN_REMEDIATION's KEEP-page remediation. report-a-bug.md and request-a-change.md both described a generic prose "Title/Context/Description/Related links/.../Checklist" template sharing no field names with the real GitHub Issue Forms. Rewrote both around the real fields in bug_report.yml and feature_request.yml. report-a-docs-issue.md described a template that does not exist at all - no docs-issue template is defined in .github/ISSUE_TEMPLATE/. Corrected to say so and point to a blank issue instead. request-a-change.md's stale "static content analysis framework" product description (predates the "Deterministic Document Integrity Engine" branding) also corrected. Also fixes pull-requests.md, a 4th how-to page the batch plan never assigned to any lettered batch: `uv sync --group docs` is a real, live- reproduced failure (the MkDocs stack is a PEP 508 extra, not a dependency group). Real command is `uv sync --extra docs`, confirmed working live. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
….py path Batch E of V031_COMMIT_PRUNING_AND_BEGIN_REMEDIATION's KEEP-page remediation. adapter-api.md: removed find_placeholders/check_placeholder_content (zero matches in src/, real Z501 mechanism is PlaceholderRule in core.rules) and find_config_file (lives in the internal adapters/_mkdocs.py, not core.scanner) from the mkdocstrings member list. Corrected find_repo_root's stale root-marker prose (2 of 4 real markers named, missing search_from param). Expanded the BaseAdapter Core Methods section from 4 of 14 real abstract methods to all 18 members, cross-linked to implement-adapter.md's Adapter Contract Guarantees. cli-architecture.md: Module Map table had 12 of the real 18 src/zenzic/cli/ files - added _audit.py, _env.py, _fix.py, _lsp.py, templates.py. zenzic-style.md: corrected 6 occurrences of a nonexistent src/zenzic/ui.py module path to the real src/zenzic/core/ui.py. Authored a new S2 "Admonition Role Taxonomy" section (the checklist referenced a S2 that didn't exist), grounded in a real site-wide grep of all 8 admonition types actually in use and their observed usage patterns. Documentation only - no code behavior changed. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Batch F (final) of V031_COMMIT_PRUNING_AND_BEGIN_REMEDIATION's KEEP-page remediation. adapter-examples.md described 5 named example projects (broken-docs, i18n-standard, security_lab, standalone, plugin-scaffold-demo) - live- confirmed via ls examples/ that none exist anymore. The real fixture tree is entirely per-Z-code (examples/zNNN-slug/), already fully covered by the Z-Code Gallery's own Quick-Run Pattern and Feature-to-Example Matrix. No unique content survived the comparison, so the page was deleted rather than rewritten. mkdocs.yml nav entry removed; developers/index.md's Example Projects card retargeted to the Z-Code Gallery; docs/_redirects gained 2 new lines for the bare canonical URL and had 6 existing historical-variant lines retargeted to /tutorials/examples/ rather than left as dead 301s. Whole-file _redirects syntax audit re-run clean (0 malformed lines, 0 duplicate sources); repo-wide grep confirmed no dangling references remain. This closes all 6 batches (A-F) of the KEEP-page remediation. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…issue.yml Full bidirectional audit of .github/ISSUE_TEMPLATE/* against docs/developers/how-to/contribute/ (V031_HYGIENE_COMPLETION_AND_SIGNATURE_CHECK Phase 3) found custom_rule_proposal.yml had no documentation or links anywhere in docs/. Added contribute/propose-a-custom-rule.md describing its real 4 fields, registered in nav, linked from contribute/index.md. good_first_issue.yml was similarly unlinked - added a one-line mention rather than a full page, since the template is self-explanatory. security_vulnerability.yml was functional but never mentioned in SECURITY.md, which documented only the private reporting channels - added a note pointing to the public template for lower-severity reports. gate-bypass-postmortem.md confirmed intentionally undocumented - internal maintainer-only Break-Glass template, not a contribution path. Reverse direction (documented template describing a nonexistent form) re-confirmed clean - no new gaps beyond the 3 Batch D already fixed. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…hecks Item 1 of V031_ECOSYSTEM_ROBUSTNESS_PHASE1_CLOSE_OPEN_ITEMS. _collect_all_results() said "seven checks"; the public check_all command named 6 by name. Neither was right. Traced the real count from _AllCheckResults's 8 non-derived fields: link validation, orphan detection, snippet validation, unused-asset detection, nav-contract (Z406), directory-index (Z401), config-asset (Z404), and the combined reference/rule-engine/security pipeline (scan_docs_references, which covers Z2xx/Z3xx/Z5xx/Z6xx in one pass). The public docstring also silently omitted 3 checks (Z406/Z401/Z404) that have no standalone check <name> sub-command at all - reachable only via check all. Fixed both docstrings, a related comment, and 3 mirroring docs pages (cli.md, discovery.md, pull-requests.md). Updated the CLI help-snapshot test and its fixture to match the real corrected --help text, live- captured from the actual CLI rather than hand-written - the docstring change broke test_check_all_help_snapshot_contract on first verification, fixed before proceeding. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Item 2 of V031_ECOSYSTEM_ROBUSTNESS_PHASE1_CLOSE_OPEN_ITEMS. This CUT was identified in an earlier triage but never actually executed. Re-confirmed both original defects live before cutting: the page instructed running scripts/enforce-radical-unawareness.sh, which does not exist, and cited a zenzic-doc repo name that doesn't match the real 4-repo ecosystem. Deleted; mkdocs.yml nav entry removed; developers/index.md's intro link and card removed; release-governance-protocol.md's cross-reference removed (4 inbound references found via repo-wide grep before deleting). docs/_redirects: 6 historical-variant lines plus 2 new bare-canonical-URL lines retarget to /developers/how-to/release-governance-protocol/, the closest surviving thematically-matching content, not a generic fallback. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Item 3 of V031_ECOSYSTEM_ROBUSTNESS_PHASE1_CLOSE_OPEN_ITEMS, per the ADR-089 precedent. ADR-031 had been a phantom citation all session (5 occurrences across codes.py, scorer.py x2, the DQS mathematical model blog post, 2 example READMEs, changelogs/v0.8.md) but abundant real source material already existed to draft it for real. Documents CodeDefinition as the single source of truth for per-code severity/penalty/category, closing the Gate Paradox where 3 CI-blocking codes carried 0 DQS penalty before v0.8.0, plus the Gravity Cap (any category scoring 0.00 caps the total DQS at 70). Self-caught correction before finalizing: initially conflated the flat-cost suppression model into this same ADR, since the source blog post narrates both together - but the real code cites that model as a separate ADR-061, confirmed by grepping scorer.py/api-json.md. Narrowed ADR-031's scope to SSoT + Gravity Cap only; ADR-061 logged as a 6th phantom-citation instance, not resolved here. Registered in adr-vault/index.md and .claude/state/01-manifest.md. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Item 4 of V031_ECOSYSTEM_ROBUSTNESS_PHASE1_CLOSE_OPEN_ITEMS, implementing
V031_SKIPLIST_BUG_FAMILY_CLOSURE's Rule 21 recommendation, TDD-first.
The skip-list was a manually-maintained 18-member tuple literal - the same
fact (which codes already surface via a dedicated path) re-declared by hand
every time validator.py's link_codes or the credential scanner's dual-
construction code set changed. All four confirmed double-emission bugs this
session (Z202/Z203/Z108/Z201/Z204) shared this exact root cause.
Promoted validator.py's local link_codes to a module-level LINK_CODES
constant. Added scanner.SECURITY_FINDING_CODES = frozenset({"Z201", "Z204"})
next to the credential-mapping function it describes. _check.py's skip-list
is now (LINK_CODES - {"Z620"}) | SECURITY_FINDING_CODES, computed once at
module load - the literal tuple is gone.
New tests/test_check_skip_list_ssot_structural.py mirrors the 5 existing
Severity-SSoT structural tests: an AST scan forbidding any hardcoded
3-or-more-Z-code skip-list literal from being reintroduced, plus a
value-equality test against the live SSoT derivation.
TDD evidence: red state reproduced the real 18-Z-code hardcoded-literal AST
violation before the fix; green state after (full suite 2103 passed,
2101->2103). Live end-to-end fixture (one Z108 + one credential) confirmed
both still fire exactly once post-refactor. ruff/mypy clean.
Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Item 5 of V031_ECOSYSTEM_ROBUSTNESS_PHASE1_CLOSE_OPEN_ITEMS, implementing V031_TECHNICAL_DEBT_LEDGER_STRUCTURAL_ASSESSMENT's Rule 21 recommendation. The original target, developers/explanation/governance/technical-debt.md, was cut in this session's own documentation pruning (commit fa8204d) after the recommendation was written but before this directive picked it up - confirmed via git show fa8204d^:<path>. The recommendation's literal premise no longer applies. Implemented against a better substitute instead: docs/reference/ finding-codes.md's own H3 code headings. Its existing Mirror Law test checked Severity/Penalty/Suppressible against codes.py but never validated the heading's own name text - the same code-identity-drift shape that motivated the original recommendation. A live scan while writing the test found 4 real, previously-undetected drift instances: Z120/Z121/Z122/Z124 all had stale heading names. Fixed all 4; confirmed via docs/rules/Z120.md-Z124.md that the individual rule cards already had the correct names, so drift was isolated to this one aggregate page. TDD evidence: red state was the real live drift, not simulated - the new test failed with all 4 real mismatches before any fix. Green state after. Full suite 2104 passed. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Phase 1-2 of V031_TEST_TREE_CLEANUP_AND_SECURITY_COVERAGE_GAPS. tests/fixtures/z108_test.md and z501_test.md deleted - re-verified live (not just cited from the prior audit) that equivalent-or-more-precise coverage exists elsewhere: test_empty_inline_link_text_emits_z108 / test_empty_reference_link_text_emits_z108 (test_validator.py) and test_check_placeholder_pattern_match / test_check_placeholder_clean_page (test_dual_mode.py). Repo-wide grep confirmed zero remaining references. tests/sandboxes/hero_specimen/ and screenshot_circular/ were undiscoverable (no doc or recipe pointer) but not dead - each demonstrates a real, live-verified scenario. Added just screenshot-hero / just screenshot-circular recipes (both live-run and confirmed) and 2 rows in CONTRIBUTING.md's Running Tasks table, restoring discoverability instead of deleting working setup. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Z120-Z124 and Z205 each name one attribute in their message -- a missing alt, an unsafe target, a forbidden URL scheme -- but reported col_start 0 and marked the whole opening tag. The message described one thing and the caret underlined everything. Both the CLI's per-line caret and SARIF's region.startColumn were affected. The position is now recorded by the parser as it reads the attribute (HtmlNodeInfo.col_start, HtmlNodeInfo.attr_cols) rather than re-derived downstream by searching the line. Re-deriving is guesswork in two cases that occur in real documents: the same tag text twice on one line, and an attribute name appearing inside an earlier attribute's value. Both are covered by tests. Offsets rest on the extractor's existing length-preserving masking invariant, so a column in the masked buffer is the same column in the source. Note for RE2: Match.start() takes integer group indices only, not names. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Emits GitLab's Code Quality schema, so a .gitlab-ci.yml can declare the artifact under artifacts.reports.codequality and findings appear inline on the merge request instead of only as a downloadable file. configure-ci-cd.md documented a plain-artifact upload as a permanent workaround; it no longer needs to. The schema was read from GitLab's published documentation before implementation, per the External Platform Verification Invariant. What could not be verified is recorded rather than glossed: no GitLab instance is reachable from here, so runtime acceptance is pinned against the published contract, not observed. Severity maps explicitly onto GitLab's five-value enum. A value outside Zenzic's own set -- reachable from a plugin rule -- maps to 'minor' rather than passing through, because an illegal value makes the entire report unparseable and takes every other finding in the run with it. The fingerprint excludes the line number: GitLab tracks a violation across commits by it, so hashing the line would report everything below an inserted paragraph as newly introduced. Identical findings in one file are disambiguated by their order instead. A credential's matched text contributes to the digest but is never emitted. A suppression-cap abort emits a single blocker violation rather than nothing: an empty report is displayed by GitLab as "no code quality issues", which would show a clean merge request for a failed pipeline. check all only. The per-aspect subcommands each see one slice of the findings, and uploading one as the job's report would silently shrink the merge request's view to that slice. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…pans is_likely_placeholder is a substring test over the matched span, so its answer belongs to whoever controls the tail of that span. Five of the eight credential signatures use fixed-length quantifiers and are safe. Three do not: github-token, slack-token and gitlab-pat use open-ended quantifiers, so appending "example" or "XXXXXXXX" to a LIVE token gets the suffix swallowed into match_text and tags a real secret as a likely placeholder -- a reviewer-deception vector. Each signature now declares whether its match is length-bounded, and the classifier returns False -- meaning NOT CLASSIFIABLE, not "not a placeholder" -- for the three unbounded families. Declaring it per signature means adding a pattern forces the author to make the call rather than inherit a default that happens to be wrong. An unrecognised secret_type is treated as unbounded, so a new signature whose author forgot the flag loses a cosmetic tag instead of inheriting a hole in silence. Tightening the token alphabet was the obvious alternative and was measured, not assumed. It failed twice over: removing '.' from the github class truncated a legitimate 550-character stateless ghs_ token's match to 13 characters, and it still admitted "example" and "-example". It cost real reporting fidelity and closed nothing. User-visible: a genuinely documented ghp_/xoxb-/glpat- example no longer carries the [LIKELY PLACEHOLDER] tag. The finding, its Z201 severity and the exit code are unchanged -- no verdict path reads this flag. AWS's own published example key still flags correctly; its marker is a suffix, which is why no marker-position rule could have worked either. Z204's span is line[idx : idx + len(term)], bounded by the configured term, so forbidden terms keep classifying. The over-long match span on unbounded families is a separate defect, logged rather than bundled: fixing it needs per-vendor token-length facts that are not all published, and a too-tight bound would silently miss a real token. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
The glossary -- the canonical terminology page -- defined the term as "Document Quality Score", and scoring-system.md, its own explanation page, called it "Deterministic Quality Score". Twenty-three other occurrences across the site said "Documentation". Three spellings, one term. Aligned across the six live surfaces plus the one blog post that used the "Deterministic" variant in body text. Dated blog posts otherwise keep their original wording: a published article is a record of what was said then, not a live surface. Terminology only -- no claim about behaviour changed in this commit. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
source_paths covered only credentials.py. The stated reason for that restriction turned out to be about sandbox aborts, not cost, so scanner.py, validator.py and exclusion.py join it: 418 mutants becomes 4,798. The test-selection list was also wrong in a way that flattered the score. It named three suites; the run needs the full set that actually exercises these modules, and two suites were silently omitted -- adding them raised the score by four points with no new tests written, because the coverage was already there and the instrument was not looking at it. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
The four-module set was left wired into CI by oversight. It cost 2h50m on the 3.10 Linux job and 1h26m on 3.14 (killed, exit 143) before failing, because the 95.7% floor was measured on credentials.py alone and means nothing against four modules -- the run scored 63.4% and was guaranteed to fail from the moment source_paths changed. Windows never ran it at all (mutmut has no Windows support), so the cost was two Linux jobs, not three. source_paths returns to credentials.py: ~442 mutants, 125s measured locally, 95.9% against its own floor. The four-module list moves to mutmut_expanded_source_paths, read only by `just mutation-expanded`. That recipe swaps the list in and restores it via a trap, and now also clears mutants/ and .mutmut-cache first. Without that it inherits whatever mapping the last run left: after a `just mutation` it reports 4,380 of 4,822 mutants as "no tests" while showing a complete progress bar and exiting cleanly. Output that looks like a result and is not one. The expanded set reports rather than gates. There is no measured floor for that population yet, and inventing one is what caused this. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
The full matrix (ubuntu 3.10/3.14, windows 3.10) was restored on 2026-09-06 as a one-time pre-merge check and then left running on every PR push -- the exact cost the earlier narrowing existed to avoid, now paid on every commit. The matrix is now computed from the event: ordinary PR push -> ubuntu-latest / 3.14 only PR labelled ci:full-matrix -> full matrix (the pre-merge signal) workflow_dispatch -> full matrix (on demand) push to main -> full matrix (post-merge safety net) `labeled` is added to the pull_request trigger types so applying the label is itself a run, with no extra push needed. The mutation gate keeps its Linux-only condition and now runs once per PR push instead of twice. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…to request the full one The matrix has been conditional since 44260b5, but the only description of it was a comment inside the workflow. A mechanism a contributor has to trigger is not usable until it is documented where they will look. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
One link, no copy: CONTRIBUTING.md stays the single source. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
VS Code spells a Windows drive as file:///d%3A/..., lowercase letter and an encoded colon. On Windows, url2pathname splits on the colon before unquoting, so the encoded form hid the drive and every URI became a bogus rooted path (\d:\a\...). Everything derived from rootUri was then wrong: docs root, site map, broken-link diagnostics, rename repairs -- silently, for every Windows user, while text-keyed features such as auto-fix-on-save kept working and made the server look healthy. The core's own LSP tests build URIs with Path.as_uri(), which never produces the encoded form, so the Windows CI job was green with zero coverage of the shape a real client sends. Surfaced by the VS Code extension's first Windows run of its extension-host suite: six failures, all site-map-backed, save hook green. Only the drive colon is decoded here; the rest is left to url2pathname so ordinary escapes are not decoded twice (pinned by a test). Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…the Windows defect The previous commit fixed the drive-letter conversion in lsp/server.py, and the extension's Windows run still failed with the same six failures. The server's trace, now dumped on failure, showed why: the engine raised "relative path can't be expressed as a file URI" from process_changes -- core/incremental.py and models/vsm.py each carried their own private copy of the same conversion, with the same defect. Fixing one of three copies is what let the first fix pass its tests and fail in the field. There is now one implementation, models.vsm.uri_to_path, imported by both other sites, and a structural test asserting url2pathname is called from exactly one module under src/ so a fourth copy cannot reappear. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…t letter case workspace/willRenameFiles looked up inbound links by exact canonical URL, so a link written ./casetarget.md to a file named CaseTarget.md -- which a case-insensitive filesystem resolves -- was not repaired on rename. Found by the extension-host suite's first Windows run: the server reported the link as [Z101] under /casetarget/ while the file's route was /CaseTarget/. The handler now identifies the renamed file up to letter case whenever no other route differs from it only by case, on every platform, and declines when two such routes coexist. RenameLinkMutation gains an opt-in match_case_insensitively (default off; zenzic fix unchanged). Separately, new_abs was built with Path.resolve() on the not-yet-existing new name, which on NTFS/APFS returns the on-disk spelling of the old one, so a case-only rename compared old == new and emitted no edit. The new path is now resolved through its parent and keeps the requested name. Three tests, the third emulating the realpath collapse so it is reproducible on Linux. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…ted case in fix --rename The extension-host suite's Windows trace showed VS Code canonicalising a case-only target requested through WorkspaceEdit.renameFile onto the existing file: the participant received oldUri == newUri and nothing was renamed. The server then rewrote every inbound href to its own spelling and left the linking document dirty for nothing. An identity pair (exact comparison, so a genuine two-spelling rename still proceeds) now yields no edit, and an edit whose text equals the document is dropped. zenzic fix --rename carried the same Path(new).resolve() collapse as the LSP handler (a case-only rename compared old == new on NTFS/APFS) and the same no-op write; both corrected for parity, with the realpath collapse emulated in the test so it is reproducible on Linux. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…n the CLI The rename fixes changed user-visible behaviour that neither page described. docs/editor/vscode.md now says that a link naming the renamed file in a different letter case is repaired when no other page's URL differs only by case, that Zenzic declines rather than guesses when two such pages exist, and that on a case-insensitive filesystem a rename changing only case may never reach the extension -- observed on Windows, where the server received the same URI as both names and nothing was renamed -- with the no-op answered by no edit. docs/reference/cli.md states that fix --rename matches exactly, so a case-mismatched href is not repaired there even where the filesystem resolves it, and why the editor can do what this command does not. Verified by execution rather than asserted: the CLI repaired ./CaseTarget.md and left ./casetarget.md untouched in a throwaway repository. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…he editor already did On a case-insensitive filesystem [target](./casetarget.md) resolves to CaseTarget.md, and the Language Server's auto-repair-on-rename rewrote such a link while this command compared resolved paths character for character and left it behind -- the same rename produced different results depending on which surface performed it. _fix_rename now accepts identity up to letter case whenever no OTHER discovered page folds equal to OLD, and declines when one does. The predicate deliberately differs from the Language Server's exactly-one form: willRenameFiles fires before the rename, so there the file still exists, whereas OLD need not exist here -- git mv first, then zenzic fix --rename, is the documented workflow, and requiring a surviving match would have declined every such rename. Four cases are pinned by test, including that one. The comparison is str.casefold() over the pages the command already enumerates, never os.path.normcase and never a filesystem probe: normcase is the identity on POSIX and lowercases on NTFS, which would make output depend on the host and put platform-specific behaviour in the Core (ADR-075). It adds no I/O -- the list is already in memory -- and on a 5,000-file corpus a controlled A/B on the same rename is indistinguishable from noise. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Two expansions of DQS were in use. "Documentation Quality Score" is now the form on every live surface, including zenzic-baseline.schema.json, which is served at zenzic.dev/schemas/ and whose score field used the short form. Published, dated blog posts keep their original wording -- a published article is a record of what was said then, not a live surface (the policy 90e7f1e set). The two posts corrected here are draft: true and 404 on the live site, so that exemption does not apply to them. Fixing them at source rather than in the derived syndication copies was necessary: derive_syndication.py regenerates those from the canonical posts, and a direct edit to a copy is undone by the next run -- confirmed by running it. SECURITY.md was unregistered in .bumpversion.toml here and in zenzic-vscode while zenzic-action had it, so at a tag both would have gone on claiming 0.30.x was current and the new release unsupported, on the page a user reads when deciding whether to upgrade after a vulnerability (ADR-092). The table writes 0.30.x rather than the full version, so the search uses the major/minor components; {current_version} matches nothing there. Verified by dry-run: the rewrite is found at a real line and produces 0.31.x. Also adds the CHANGELOG entry for the Z301 exit-code change, which needs user action: a dangling reference used to hard-fail a plain check and now exits 0 unless --strict, so a pipeline that relied on it stops failing silently. Signed-off-by: PythonWoods <dev@pythonwoods.dev>
Thirty-nine posts ended however their author happened to stop: on prose, on an upgrade command, on a See Also list. Exactly one carried the trademark notice. The syndicated copies on Hashnode were worse -- eight posts, no two footers alike, two with none at all -- and none of it was caught by anything: it was found by reading the published posts one at a time. Each post now ends with a Resources list and the trademark disclaimer. Source Code, Documentation and License are always present; the VS Code Extension, GitHub Action and Finding Codes links appear only where the article's subject calls for them, which is what the existing footers already did before they diverged. The GitHub URL uses the PythonWoods/* form because it 301-redirects and so survives an eventual org-name reversion -- verified per URL, not assumed: that form 404s for zenzic-mcp and for the bare profile URL, both of which must be written PythonWoods-Dev. Neither appears in a footer. The bullet marker follows each post rather than the standard: markdownlint infers list style per file, so two posts written with asterisks take asterisks. The rendered output is identical. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
iter_markdown_sources and six sibling sites compared path.suffix verbatim against DOC_SUFFIXES, while the adapters, the language server and parts of the scanner compared path.suffix.lower(). A file named NOTES.MD was therefore analysed by the editor and invisible to zenzic check all. Because scanner.py enumerates through that same discovery, a file it skipped was a file the credential scanner never read: docs/notes.MD holding an AWS key produced no Z201 and exit 1, where a byte-identical copy named control.md exits 2. The non-suppressible security tier did not run, and the run reported success. A link to such a file was additionally reported Z101 'not in the Virtual Site Map' -- a broken-link error against a link that resolves. Fixed at all eight comparison sites, not only the reproduced one. Guarded by a structural test over src/ rather than a parity fixture: the CLI and the LSP share iter_markdown_sources, so a defect inside it moves both sides together and a parity comparison still reports agreement -- verified by reverting the fix and watching the new fixture pass. Files now analysed that were not before: any .MD/.MDX case variant under docs_dir. A repository containing them may see new findings and a new exit code on the first run after upgrading. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…in the release group The suffix-case fix changed which files get analysed, and two pages stated the domain without addressing case. docs/editor/vscode.md and docs/explanation/architecture.md now say the comparison ignores letter case, matching the CHANGELOG entry. Also updates httpx2 2.10.0 -> 2.12.0 in the lock. pip-audit began reporting CVE-2026-84379/84380/84382 against it; it is a transitive dependency of bump-my-version in the release dependency group, appears nowhere in the published wheel's Requires-Dist, and therefore reaches no user of the package -- but it failed the gate, so it is fixed rather than carried. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
_local-checks handled one asymmetry -- the opt-in key set while .justfile.local is missing -- and treated its mirror image as a fresh contributor clone: no key, no private recipes, print a benign note, exit 0. On a maintainer machine that skipped the opt-in step, that is a `just verify` reporting success while running no private gate at all, which is the failure mode the note was meant to describe and instead concealed. The signal that distinguishes the two is already on disk. A contributor fork has neither .claude/ nor .human/; a maintainer clone has at least one. When a governance tree is present and the clone is not opted in, verify now blocks and names the fix. Verified on four cases: the defect condition exits 1; a genuine contributor fork exits 0 and is not blocked; a correctly configured maintainer clone exits 0 and runs the gates; the pre-existing key-without-recipes branch still exits 1. Signed-off-by: PythonWoods <dev@pythonwoods.dev>
Two in test_fix.py assert that a case-only rename declines when a twin page exists. That needs two directory entries differing only by letter case, which NTFS and APFS cannot hold -- the second write overwrites the first. The guard was there but ran after both writes, and .exists() returns True for both spellings on a case-insensitive filesystem because both resolve to the one surviving file, so it never fired. Moved into the fixture, probing the lowercase name while only the uppercase file exists, which is the observation that actually distinguishes the platforms. test_lsp.py already skipped for exactly this reason and recorded why. The pattern was known, applied in one file and missed in its sibling -- that inconsistency is how the next one slips through too. The third compared a path as a literal forward-slash string in a test written to verify Windows behaviour: as_posix() rather than str(), so the comparison fails on what is asserted rather than on the separator. Swept both patterns across the suite: no other test constructs a case twin, and the two other stringified relative paths are already normalised or feed a resolver that normalises by contract. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
…ystem The remaining Windows failure is a different condition from the one fixed in 696466f, and skipping it needs its own reason rather than the same one. Here the old page is absent and only the lowercase twin exists. On NTFS that is a contradiction, not a hard setup: CaseTarget.md was never created yet resolves to the surviving casetarget.md, because the two spellings name one directory entry. Renaming the absent page is therefore renaming the twin, and Path.resolve() returns the on-disk spelling, so old and the other page become the same path -- there is no second page left to decline against. The guard probes that directly: a file that was never written reporting exists(). A reader can reproduce it in one line, which is what distinguishes this from asserting the platform is different. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Measured, not assumed: the gate took 240s of a 372s job on 3.10 and 206s of 294s on 3.14 -- about two thirds of elapsed time on both. It shares no input or output with the steps around it, so running it concurrently takes its duration off the critical path entirely. This is a split, not a removal. The job has no continue-on-error, so a failure fails the workflow exactly as it did as a step. A mutation score that only warned would be worse than none: a check counted as protection while blocking nothing. Keeping it blocking is the whole reason for splitting the step rather than dropping it from push. One Linux slot rather than two: mutmut has no Windows support (boxed/mutmut#397) and the 95.7% floor is measured on a single interpreter, so running it on both Python versions measured the same thing twice. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
Both pages said the extension and Language Server target .md and .mdx. That is true of the engine, and now verified by execution rather than by reading a file type list: .mdx, .MDX and .Mdx all reach the security tier and exit 2 on a credential, and the server publishes identical diagnostics at identical caret positions for .md and .mdx buffers driven over stdio. Neither page said that VS Code has no built-in mdx language, so on a stock install a .mdx file opens as Plain Text and nothing activates at all. The symptom is silence, not an error. Both pages now say so. discovery.md additionally records how an .mdx file is really parsed — as Markdown with raw HTML. Three consequences follow, each confirmed in isolation: <a> and <img> participate in every link, asset and forbidden-scheme check in any letter case; other JSX components such as <Link to="..."> are invisible to the link graph, so a broken target there is unreported; and a Markdown link written inside an MDX comment or a JSX string attribute is still reported, though neither renders as a link. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
The editor page was corrected earlier today to state a precondition that has since been removed rather than merely documented: the extension now contributes the mdx language itself, so a .mdx file resolves and starts Zenzic with nothing else installed, and a workspace merely containing .mdx files activates it before anything is opened. The page now says that, records that the gap existed before v0.31.0, and notes that a dedicated MDX extension is still worth installing for syntax highlighting and does not conflict — the extension contributes the language identifier but no grammar. Signed-off-by: PythonWoods <gianluca.catalano@gmail.com>
The console was built with color_system=None. In Rich that parameter's default is the string "auto"; passing None explicitly does not mean "detect it", it means this console has no colour system -- so colour was disabled unconditionally, for every user, unless --force-color was passed. Terminal detection was never the problem. The console correctly reported is_terminal=True and no_color=False; the explicit None overrode it. Isolated under one pty with TERM=xterm-256color: Console(color_system=None) reports None and emits nothing, Console() reports 256 and emits an escape sequence. No existing test could see this. They all run under pytest with captured output, where is_terminal is False and the output is monochrome either way. The new test runs snippets under a real pty and carries a positive control -- plain Rich must colour there, or its assertions prove nothing -- plus a NO_COLOR regression guard. --force-color keeps its purpose: forcing truecolor depth for a destination that is not a TTY, such as a CI log. It is no longer the only way to see colour at a prompt. Signed-off-by: PythonWoods <dev@pythonwoods.dev>
A Markdown link written inside an HTML comment, an MDX comment, or a JSX string attribute was extracted and reported as a broken Z101. None of them renders as a link. The HTML-comment case reaches plain .md, so every commented-out link in any document was a false positive -- the defect was framed as MDX-only and is not. PolyglotExtractor already masks comments correctly and returns only the genuine link. _extract_inline_links_with_lines, which feeds the Z101 rule, masked math, fences and inline code while its docstring claimed comments too. The fix routes that path through the existing mask instead of adding a second implementation of what counts as content, so the two cannot drift apart again. JSX attribute values get their own mask beside the others, length-preserving like them: caret columns and Z108 offsets are computed against masked text and must not move. Its first pattern used a lookbehind and RE2 rejected it, which is the invariant working -- rewritten to capture the leading whitespace. Every case is asserted in both directions. A mask that swallowed the genuine link in the same file would silence the false positive too, and a one-directional test would pass on it. Links in JSX expression attributes are not covered and behave as before: a missed construct leaves a false positive, over-masking hides a real link. Signed-off-by: PythonWoods <dev@pythonwoods.dev>
The colour tests need a real pty, and they get one with `script`, a util-linux tool. Windows has no drop-in equivalent that hands a child process a pty from pytest, so all four failed the full matrix with WinError 2. Skipping is the honest outcome rather than a weaker assertion: a test that ran on Windows without a real terminal would observe is_terminal=False and pass whether the colour system was configured correctly or not, which is precisely the blindness this file exists to remove. Signed-off-by: PythonWoods <dev@pythonwoods.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR has grown substantially past its original Epic 3 scope; this description reflects
its real, current diff (286 commits, ~650 files touched).
Specification-Driven Development (Epic 3)
Z521(required table columns),Z522(table cell enum whitelisting),
Z523(heading order),Z412(cross-namespace graphtraceability) — with matching Policy-as-Code config surfaces (
required_table_columns,table_cell_enums,required_heading_order,traceability_targets).zenzic score --trend(append-only.zenzic-history.jsonl),zenzic doctor+zenzic adr newon a new[doctor]config surface,zenzic fix --rename OLD NEW, and afixablefield surfaced injson/sarifoutput andinspect codes.Dual-Track Distribution & ADR-089
global-ephemeral) and extended ADR-089 immutable SHA pinning as an ecosystem-wide mandate
across
zenzic,zenzic-action, andzenzic-vscode.Security hardening program
code_severity(),_ALWAYS_EVALUATED_CODES), closing hardcoded-severity bugs across_check.py,rules.py,incremental.py,scanner.py,governance.py,suppressions.py,content.py— nowguarded by a single comprehensive AST structural test superseding nine narrower ones.
--onlycould silenceZ2xx findings entirely;
excluded_dirs/excluded_file_patternssilencedZ202/Z203/Z205(only the credential half of the tier was covered); a leading
../on an absolute hrefdefeated traversal detection entirely; a relative
../hop to a real repo file named likea system root misfired non-suppressible
Z203instead ofZ202;excluded_external_urlsprefix matching was vulnerable to host spoofing (CWE-20).
zenzic auditnow routes through the same exit-code authority ascheck/guard, so acredential breach can no longer report exit 1 there while
check allcorrectly exits 2._validate_plugin_codewas checking attributesBaseRuleneverdefines and was a complete no-op — now validates the real
rule_id, with core-distributedentry points correctly exempted from the third-party namespace check.
LSP hardening & capabilities
textDocument/willSaveWaitUntil), auto-repairof inbound links on rename (
workspace/willRenameFiles), and hover-based suppressionexplanations.
alone with no location check; a redundant ghost-diagnostic-clearing pass that could erase a
live security finding; a
didChangefull-sync message missing"text"silently blankingthe buffer; a crashed code-action mutation producing no signal.
Documentation
reference/,how-to/,explanation/,developers/) plus the landing page and the Z-Code Gallery — hundreds offactual corrections (stale exit-code claims, fabricated examples, wrong Zensical/MkDocs
capability claims, dead code references) verified against live execution, not restated
from memory.
draft: true); two launch articles(SDD, deterministic tooling / pre-commit distribution) drafted and kept current, also
still
draft: truepending a tagged release.CI / governance tooling
just verifyand made hard-blocking (fail_under=80)..claude/governance-manifest tooling; a git-hook-installationgate (Rule 31) so an uninstalled pre-commit hook can't silently pass a whole session.
Test plan
pytest tests/— full suite passingruff check/ruff format --check/mypy— cleanzenzic check all --strict --no-header— DQS 98/100, gate passedzenzic lab all— all gallery scenarios meet expectationsmkdocs build --strict— clean buildjust verify— clean (pre-commit, pip-audit, coverage-enforced pytest, structuralaudit, docs build)