From d474b5cdc628fed3a05c3b29c5fe994cbefbbf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Hern=C3=A1ndez?= Date: Wed, 9 Sep 2026 01:04:09 -0400 Subject: [PATCH] The plugin directory has been told we are on 0.4.0 since August .claude-plugin/plugin.json said 0.4.0 while NuGet, the MCP registry, the GitHub releases and the desktop all shipped 0.7.1. Three releases behind, and nothing failed, because the release checklist never listed the file. It is the version a stranger sees first: a plugin directory reads it, and the plugin route is the one channel in this project's whole funnel with any measured human traffic. Every other channel we invested in is a machine- readable index, and the traffic says so -- 450 unique cloners against 77 unique viewers in fourteen days, a ratio unchanged since July. Found while checking whether we needed to build a plugin at all. We did not; it already existed. The check that stopped me building it twice is the one that surfaced this. So the version now has a guard rather than a checklist. It is typed in seven places across five files, and ReleaseVersionTests holds all of them to what SignsOfAI.Core ships, failing by file name. server.json gets its own test because it carries the version twice and the registry rejects them out of step -- after NuGet has already published, which is the worst moment to find out. Verified by mutation: with 0.4.0 restored it fails naming the file and both versions. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015PEbbiYSNPw7jE3LrPNhyF --- .claude-plugin/plugin.json | 2 +- .../ReleaseVersionTests.cs | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests/SignsOfAI.Core.Tests/ReleaseVersionTests.cs diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a052e94..72320f4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "signs-of-ai", "description": "Measure a text for the signs of AI writing and read back the evidence — offline, EN/ES, with the tool's own false-positive rate attached.", - "version": "0.4.0", + "version": "0.7.1", "author": { "name": "Pedro Hernández — PeopleWorks", "url": "https://github.com/peopleworks" diff --git a/tests/SignsOfAI.Core.Tests/ReleaseVersionTests.cs b/tests/SignsOfAI.Core.Tests/ReleaseVersionTests.cs new file mode 100644 index 0000000..e132f3c --- /dev/null +++ b/tests/SignsOfAI.Core.Tests/ReleaseVersionTests.cs @@ -0,0 +1,75 @@ +using System.Text.RegularExpressions; +using Xunit; + +namespace SignsOfAI.Core.Tests; + +/// +/// The version is typed into seven places across five files, and a release bumps them by hand. +/// .claude-plugin/plugin.json was three releases behind — it said 0.4.0 while every channel +/// shipped 0.7.1 — and nothing failed, because the release checklist never listed it. +/// +/// That file is what a plugin directory reads, which makes it the version a stranger sees first. +/// So this holds every one of them to the engine's own, and fails by file name when a release +/// forgets one. The eighth time something this project says about itself went stale. +/// +public class ReleaseVersionTests +{ + private static readonly string Root = FindRoot(); + + /// The authority: what SignsOfAI.Core ships as. + private static readonly string Engine = Read( + Path.Combine("src", "SignsOfAI.Core", "SignsOfAI.Core.csproj"), + @"(?[^<]+)"); + + private static string FindRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, "src"))) + dir = dir.Parent; + + Assert.NotNull(dir); + return dir!.FullName; + } + + private static string Read(string relativePath, string pattern) + { + var text = File.ReadAllText(Path.Combine(Root, relativePath)); + var match = Regex.Match(text, pattern); + Assert.True(match.Success, $"{relativePath} no longer carries a version matching {pattern}."); + return match.Groups["v"].Value.Trim(); + } + + public static TheoryData Declarations() => new() + { + // Published to NuGet. + { Path.Combine("src", "SignsOfAI.Cli", "SignsOfAI.Cli.csproj"), @"(?[^<]+)" }, + { Path.Combine("src", "SignsOfAI.Mcp", "SignsOfAI.Mcp.csproj"), @"(?[^<]+)" }, + // What the download page offers, which the release workflow also enforces. + { Path.Combine("src", "SignsOfAI.UI", "Services", "DesktopRelease.cs"), + @"public const string Version = ""(?[^""]+)"";" }, + // What a plugin directory shows a stranger. This is the one that drifted. + { Path.Combine(".claude-plugin", "plugin.json"), @"""version"":\s*""(?[^""]+)""" }, + }; + + [Theory] + [MemberData(nameof(Declarations))] + public void Every_shipped_manifest_declares_the_engine_version(string relativePath, string pattern) + { + Assert.True(Engine == Read(relativePath, pattern), + $"{relativePath} says {Read(relativePath, pattern)} and the engine ships {Engine}. " + + "A release bumps all of them or none of them."); + } + + [Fact] + public void The_mcp_registry_manifest_declares_it_in_both_places() + { + // server.json carries the version twice, and publishing with them out of step is rejected by + // the registry after NuGet has already gone out — the worst moment to find out. + var path = Path.Combine("src", "SignsOfAI.Mcp", ".mcp", "server.json"); + var found = Regex.Matches(File.ReadAllText(Path.Combine(Root, path)), + @"""version"":\s*""(?[^""]+)""").Select(m => m.Groups["v"].Value).ToList(); + + Assert.Equal(2, found.Count); + Assert.All(found, v => Assert.Equal(Engine, v)); + } +}