Skip to content

feat: v0.31.0 Specification-Driven Development, Security Hardening & Ecosystem Governance - #233

Open
PythonWoods-Dev wants to merge 341 commits into
mainfrom
feat/v0.31.0-epic3-sdd
Open

feat: v0.31.0 Specification-Driven Development, Security Hardening & Ecosystem Governance#233
PythonWoods-Dev wants to merge 341 commits into
mainfrom
feat/v0.31.0-epic3-sdd

Conversation

@PythonWoods-Dev

@PythonWoods-Dev PythonWoods-Dev commented Aug 23, 2026

Copy link
Copy Markdown
Owner

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)

  • Native GFM Table AST parsing plus four new rules: Z521 (required table columns), Z522
    (table cell enum whitelisting), Z523 (heading order), Z412 (cross-namespace graph
    traceability) — 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 new on a new [doctor] config surface, zenzic fix --rename OLD NEW, and a
    fixable field surfaced in json/sarif output and inspect codes.

Dual-Track Distribution & ADR-089

  • Formalized the 3-track distribution hierarchy (pre-commit / project dependency /
    global-ephemeral) and extended ADR-089 immutable SHA pinning as an ecosystem-wide mandate
    across zenzic, zenzic-action, and zenzic-vscode.

Security hardening program

  • Unified Z2xx severity/exit-code derivation behind one authority (code_severity(),
    _ALWAYS_EVALUATED_CODES), closing hardcoded-severity bugs across _check.py, rules.py,
    incremental.py, scanner.py, governance.py, suppressions.py, content.py — now
    guarded by a single comprehensive AST structural test superseding nine narrower ones.
  • Closed five real bypasses of the non-suppressible security tier: --only could silence
    Z2xx findings entirely; excluded_dirs/excluded_file_patterns silenced Z202/Z203/Z205
    (only the credential half of the tier was covered); a leading ../ on an absolute href
    defeated traversal detection entirely; a relative ../ hop to a real repo file named like
    a system root misfired non-suppressible Z203 instead of Z202; excluded_external_urls
    prefix matching was vulnerable to host spoofing (CWE-20).
  • zenzic audit now routes through the same exit-code authority as check/guard, so a
    credential breach can no longer report exit 1 there while check all correctly exits 2.
  • Plugin rule loading: _validate_plugin_code was checking attributes BaseRule never
    defines and was a complete no-op — now validates the real rule_id, with core-distributed
    entry points correctly exempted from the third-party namespace check.

LSP hardening & capabilities

  • New opt-in capabilities: auto-fix on save (textDocument/willSaveWaitUntil), auto-repair
    of inbound links on rename (workspace/willRenameFiles), and hover-based suppression
    explanations.
  • Fixed four server-side robustness gaps: a config-file-change shortcut matching by filename
    alone with no location check; a redundant ghost-diagnostic-clearing pass that could erase a
    live security finding; a didChange full-sync message missing "text" silently blanking
    the buffer; a crashed code-action mutation producing no signal.
  • One shared security-scan implementation now serves both the CLI and LSP paths.

Documentation

  • Full-corpus remediation across all four Diátaxis quadrants (reference/, how-to/,
    explanation/, developers/) plus the landing page and the Z-Code Gallery — hundreds of
    factual corrections (stale exit-code claims, fabricated examples, wrong Zensical/MkDocs
    capability claims, dead code references) verified against live execution, not restated
    from memory.
  • Seven Foundations-series blog articles drafted (still draft: true); two launch articles
    (SDD, deterministic tooling / pre-commit distribution) drafted and kept current, also
    still draft: true pending a tagged release.

CI / governance tooling

  • Coverage measurement wired into just verify and made hard-blocking (fail_under=80).
  • SHA-pinned CI actions; .claude/ governance-manifest tooling; a git-hook-installation
    gate (Rule 31) so an uninstalled pre-commit hook can't silently pass a whole session.

Test plan

  • pytest tests/ — full suite passing
  • ruff check / ruff format --check / mypy — clean
  • zenzic check all --strict --no-header — DQS 98/100, gate passed
  • zenzic lab all — all gallery scenarios meet expectations
  • mkdocs build --strict — clean build
  • just verify — clean (pre-commit, pip-audit, coverage-enforced pytest, structural
    audit, docs build)

@PythonWoods-Dev PythonWoods-Dev changed the title v0.31.0: Specification-Driven Development, Dual-Track Distribution & Z205 Contract Fix feat: v0.31.0 Specification-Driven Development, Dual-Track Distribution & Z205 Contract Fix Aug 23, 2026
…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>
PythonWoods-Dev and others added 21 commits September 7, 2026 00:16
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>
@PythonWoods-Dev PythonWoods-Dev added the ci:full-matrix Run the full CI matrix (ubuntu 3.10/3.14, windows 3.10) on this PR -- apply before merging label Sep 9, 2026
PythonWoods-Dev and others added 8 commits September 9, 2026 18:23
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full-matrix Run the full CI matrix (ubuntu 3.10/3.14, windows 3.10) on this PR -- apply before merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants