diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb539e..4eac2f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- +## [0.2.10] — 2026-08-05 + +### Changed +- **Pins now point at the commits their version labels claim.** The + `action-mirror` and `provenance-mirror` pins were both commented `# v0.2.0` + while sitting **two commits before** that tag. The two missing commits were + the org migration (`bhyi4/` → `mirror-stack/`) and the SPEC v1.1 reference + update, so an install got pre-migration docs under a v0.2.0 label — a claim + the installer had no way to see was false. +- `measure-mirror` pin bumped **v0.28.1 → v0.29.0**: adds ㉘ + `subspace_claim_check` (a declaration auditor, holdout PASS 22/22 after two + sealed kills), `Finding.data`, and a fix for a latent `KeyError` in the + finding formatters on `INFO`/`N/A` levels. + +### Added +- `tests/test_pins_are_releases.py` — each pin's SHA is checked against the + GitHub tag its comment names, plus offline checks that every pin is a full + 40-hex lowercase SHA and carries a version label. Skips when the API is + unreachable rather than failing offline. Reverse-verified: restoring the old + `action-mirror` pin makes it fail. + +### Note +This server exposes a curated 19 tools, not all 28 measure-mirror probes — +`mm_verify` is the umbrella for the rest. ㉘ is reachable through it; no new +tool was added, by design. + +--- + ## [0.2.9] — 2026-07-21 ### Changed diff --git a/mirror_stack_mcp/__init__.py b/mirror_stack_mcp/__init__.py index 8c661ad..1845120 100644 --- a/mirror_stack_mcp/__init__.py +++ b/mirror_stack_mcp/__init__.py @@ -1,2 +1,2 @@ """🪞🔎🪪 Mirror Stack unified MCP server.""" -__version__ = "0.2.9" +__version__ = "0.2.10" diff --git a/pyproject.toml b/pyproject.toml index 6e8fe70..b09f354 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,21 +4,28 @@ build-backend = "setuptools.build_meta" [project] name = "mirror-stack-mcp" -version = "0.2.9" +version = "0.2.10" description = "Unified MCP server for the Mirror Stack — claims, actions, provenance + verify-all in one server" readme = "README.md" requires-python = ">=3.10" license = { text = "Apache-2.0" } authors = [{ name = "Mother Seara", email = "ruinbl82@gmail.com" }] dependencies = [ - "mcp>=1.0", - # Pinned to exact commits for reproducible installs. An unpinned git dep silently - # tracks each mirror's default branch, so an install made before a mirror's release - # freezes on whatever was HEAD then and never refreshes — that is how a local install - # ran stale measure-mirror 0.14.3 long after 0.15.1 landed. Bump these on purpose. - "measure-mirror @ git+https://github.com/mirror-stack/measure-mirror@abe0c19d3894c1278a9bf5281a45946ef0e1eea4", # v0.28.1 — power_check honors alpha/target_power (was hardcoded 1.96/0.842: text-vs-number lie); v0.28.0 SECURITY verify_chain prev_seal (SPEC §4 rule 3); v0.27.0 full 64-hex seals; v0.26 ㉗ prereg_lint - "action-mirror @ git+https://github.com/mirror-stack/action-mirror@fa1fc4998936e6bf179b5c14825fb09e78a36087", # v0.2.0 — full 64-hex SHA-256 seals (security; legacy 16-hex verify via prefix match); v0.1.0 ledgers conform to MIRROR-SPEC v1.0 - "provenance-mirror @ git+https://github.com/mirror-stack/provenance-mirror@321f84abc4ffd91da43a5ebc866b5ce02cf3c276", # v0.2.0 — full 64-hex SHA-256 seals (security; legacy 16-hex verify via prefix match); v0.1.0 ledgers conform to MIRROR-SPEC v1.0 + "mcp>=1.0,<2", # upper bound: mcp 2.x removed mcp.server.fastmcp (same fix as yeoul) + # Pinned to exact RELEASE commits for reproducible installs. An unpinned git + # dep silently tracks each mirror's default branch, so an install made before + # a mirror's release freezes on whatever was HEAD then and never refreshes — + # that is how a local install ran stale measure-mirror 0.14.3 long after + # 0.15.1 landed. Bump these on purpose. + # + # Each SHA below is the commit its version TAG points at, verified against + # the GitHub tag API by tests/test_pins_are_releases.py. The previous + # action-mirror and provenance-mirror pins were labelled "v0.2.0" while + # sitting TWO commits before that tag, so an install got the pre-org-migration + # docs (stale `bhyi4/` self-references, SPEC v1.0) under a v0.2.0 label. + "measure-mirror @ git+https://github.com/mirror-stack/measure-mirror@b3b6dec8893c8e645ad0c9d50d08fc3c9417eee1", # v0.29.0 — ㉘ subspace_claim_check (declaration auditor, holdout PASS 22/22); Finding.data; INFO/N-A formatter crash fixed; CHANGELOG + catalog-count sync guards + "action-mirror @ git+https://github.com/mirror-stack/action-mirror@15b0c5b775e916a22d837ddc0714e85aef7ec74e", # v0.2.0 — full 64-hex SHA-256 seals (security; legacy 16-hex verify via prefix match); org migration + SPEC v1.1 references + "provenance-mirror @ git+https://github.com/mirror-stack/provenance-mirror@2d1d52d280352567410d140270f7714571bf784f", # v0.2.0 — full 64-hex SHA-256 seals (security; legacy 16-hex verify via prefix match); org migration + SPEC v1.1 references ] [project.optional-dependencies] diff --git a/tests/test_pins_are_releases.py b/tests/test_pins_are_releases.py new file mode 100644 index 0000000..b8d0214 --- /dev/null +++ b/tests/test_pins_are_releases.py @@ -0,0 +1,61 @@ +"""Every git pin must point at the commit its version label claims. + +Found by a family-wide consistency audit: the `action-mirror` and +`provenance-mirror` pins were both commented `# v0.2.0` while sitting TWO +commits *before* the v0.2.0 tag. The two missing commits were the org migration +(`bhyi4/` → `mirror-stack/`) and the SPEC v1.1 reference update — so anyone +installing this package got pre-migration docs under a v0.2.0 label. + +A pin whose comment names a release it does not point to is a lie the installer +cannot see, which is exactly the defect class this stack exists to catch. +""" +import json +import re +import urllib.error +import urllib.request +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +PIN_RE = re.compile( + r'"(?P[a-z-]+) @ git\+https://github\.com/(?P[\w-]+)/' + r'(?P[\w-]+)@(?P[0-9a-f]{40})",\s*#\s*(?Pv?[\d.]+)') + + +def _pins(): + text = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + pins = [m.groupdict() for m in PIN_RE.finditer(text)] + assert pins, "no git pins parsed from pyproject.toml — did the format change?" + return pins + + +def test_every_pin_declares_a_version(): + for pin in _pins(): + assert pin["ver"], f"{pin['pkg']} pin has no version comment" + + +def test_pin_shas_are_full_length_and_lowercase(): + """A short SHA is ambiguous and an uppercase one breaks string comparison.""" + text = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + for sha in re.findall(r'git\+https://github\.com/[\w-]+/[\w-]+@([0-9a-fA-F]+)', text): + assert len(sha) == 40, f"pin {sha} is not a full 40-hex commit SHA" + assert sha == sha.lower(), f"pin {sha} is not lowercase" + + +@pytest.mark.parametrize("pin", _pins(), ids=lambda p: p["pkg"]) +def test_pin_matches_its_tagged_release(pin): + url = f"https://api.github.com/repos/{pin['owner']}/{pin['repo']}/tags" + try: + with urllib.request.urlopen(url, timeout=15) as r: + tags = json.load(r) + except (urllib.error.URLError, TimeoutError) as e: # offline / rate limited + pytest.skip(f"GitHub tags unreachable for {pin['repo']}: {e}") + want = pin["ver"] if pin["ver"].startswith("v") else "v" + pin["ver"] + match = [t for t in tags if t["name"] == want] + assert match, ( + f"{pin['pkg']} pin claims {want} but that tag does not exist " + f"(tags: {[t['name'] for t in tags][:5]})") + assert match[0]["commit"]["sha"] == pin["sha"], ( + f"{pin['pkg']} pin is commented {want} but points at {pin['sha'][:8]}, " + f"while {want} is {match[0]['commit']['sha'][:8]}")