From d155f952f5f08700eb081d82658640540438c910 Mon Sep 17 00:00:00 2001 From: Ricardo Accioly Date: Tue, 22 Sep 2026 18:13:47 -0400 Subject: [PATCH] release: v0.19.0 Minor, not patch: `websec init --claimspec` is new public CLI surface, and the README's release section states that in the 0.x series a feature addition increments the minor version. Contents, all already on main since v0.18.0: - feat(claimspec): the `ignore` kind writer (#157) - fix(detectors): the substance of #142 and #143, with controls (#155) - fix(detectors): #128 and #143 with the controls they were missing (#154) - the calibration table's prose contradicted its own counts - docs: technical brief + GitHub Pages (#153, #156), spec 002 public evidence and three README corrections (#158) The migration note calls out the one thing a CI author should expect: FEWER findings, because four false-positive sources are fixed. Each ships with the paired true-positive control, so it is a precision gain rather than a coverage loss. tests/test_explained_brief.py: the brief's release-count assertion counted every changelog heading and compared it to a literal 28 -- the running total. The brief is a snapshot of STATED_VERSION and says so on its own last page, so that pin failed on every release while proving nothing extra: a brief cannot overstate a release that shipped after it was written. It now counts releases at-or-before STATED_VERSION. The equality and the prose check both remain, so a brief that overstates what existed at its own version still fails. The original loose regex is kept as the counting basis -- a three-component regex drops the grouped `## [0.2.x]` heading and yields 27. Logged as bug-339. Deliberately NOT done here: the brief still reads v0.18.0. Bumping its masthead means re-stating the release count, moving the cadence figure's last point and regenerating the PDF -- a design pass on a published artifact, not release mechanics. Verified: 1579 tests OK (skipped=2); docguard guard exit unchanged from baseline; docguard specs --check clean. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 18 +++++++++++++++++- pyproject.toml | 2 +- tests/test_explained_brief.py | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d57331..d6c0cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). -## [Unreleased] +## [0.19.0] — 2026-09-22 + +Migration: **none required.** The one new surface is an opt-in flag, and nothing existing changes +shape. The change a CI author should expect is **fewer findings**: four false-positive sources are +fixed here, so a `--fail-on` gate or a baseline diff that previously tripped on a +`timing-unsafe-compare` against a length, a `weak-password-hash` over an identifier, a +browser-extension handler whose sender check sat behind an alias, or a PII projection that actually +removed the field will now come back clean. That is a precision gain, not a coverage loss — each fix +ships with the paired true-positive control proving the real case still fires. + +**Precision work, with the controls attached.** The theme of this release is that a detector fix is +only accepted alongside a test proving it did not go quiet. Every false-positive fix below names the +near misses that must still report, and one piece of #143 is deliberately left out because it never +fired end-to-end and carried no control. The claimspec `ignore` writer applies the same standard to +an export format: entries whose reason cannot be established honestly are omitted **and counted**, +so a partial policy can never read as a whole one. + ### Added — the claimspec `ignore` writer diff --git a/pyproject.toml b/pyproject.toml index ff9ec8f..6be2300 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "websec-validator" -version = "0.18.0" +version = "0.19.0" description = "Defensive, local-first security recon that briefs your AI coding agent on your own codebase — read-only by default (code in, artifacts out): facts + tailored probe scripts, no LLM, no server, no running app." readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_explained_brief.py b/tests/test_explained_brief.py index 6689b1f..763ab30 100644 --- a/tests/test_explained_brief.py +++ b/tests/test_explained_brief.py @@ -89,7 +89,15 @@ def test_stated_version_shipped_and_is_current_in_masthead(self): changelog = (REPO / "CHANGELOG.md").read_text(encoding="utf-8") self.assertIn(f"## [{STATED_VERSION}]", changelog) self.assertTrue(_has(f"Technical Brief · v{STATED_VERSION}")) - releases = len(re.findall(r"^## \[\d+\.\d+", changelog, flags=re.M)) + # The brief is a SNAPSHOT of STATED_VERSION and says so ("asserted ... at the version in + # the masthead"), so it must count the releases that existed at that version -- not the + # running total. Binding it to the total made every future release fail this assertion + # while proving nothing extra: a brief cannot overstate a release that shipped after it. + # Same counting basis as before (a grouped heading like "[0.2.x]" counts once), + # newest first, so slicing from STATED_VERSION keeps only what had shipped by then. + shipped = re.findall(r"^## \[(\d+\.\d+[^\]]*)\]", changelog, flags=re.M) + self.assertIn(STATED_VERSION, shipped) + releases = len(shipped[shipped.index(STATED_VERSION):]) self.assertTrue(_has("twenty-eight releases"), "release count wording drifted") self.assertEqual(releases, 28) breaking = changelog.count("### Changed — BREAKING")