Releases: rlabarca/purlin
Release list
v0.9.5 — Cross-platform audit Pass-1 (Windows) & C# support
Fixed
purlin:auditnow runs on Windows. The audit toolchain shells out toscripts/audit/static_checks.py, which was unusable on Windows for three independent reasons, each fixed here (issue #3):import fcntlcrashed the module on load.fcntlis POSIX-only, so every subcommand (--read-cache,--load-criteria,--check-proof-file, Pass 1,--write-cache,--prune-cache) died withModuleNotFoundErrorbefore doing any work.fcntlis now imported undertry/except ImportErrorbehind a_HAS_FCNTLflag, and the audit-cache lock goes through_lock_exclusive/_unlockhelpers that usefcntl.flockon POSIX andmsvcrt.lockingon Windows — preserving the exclusive read→merge→write serialization that keeps concurrent subagent writers from clobbering each other (static_checksRULE-25/RULE-29).- Text I/O assumed UTF-8. ~16
open()calls omittedencoding=, so on a Windows cp1252/ASCII locale they raisedUnicodeDecodeErrorreading the tool's own UTF-8 files (e.g.references/audit_criteria.md). Every text-modeopen()now specifiesencoding='utf-8'(static_checksRULE-30). --load-criteriaprinted non-ASCII to a non-UTF-8 console. Even after the read fix, the criteria text (which contains✓/⚠glyphs) raisedUnicodeEncodeErroron an ASCII stdout.static_checks.pynow reconfigures stdout/stderr to UTF-8 at startup (static_checksRULE-30).
Added
- Deterministic Pass-1 coverage for C#/.NET tests. Pass 1 previously recognized only pytest/Jest/Shell markers, so xUnit/NUnit/MSTest tests carrying
[Trait("PurlinProof", "feature:PROOF-N:RULE-N:tier")]produced zero proofs and the structural checker silently no-op'd on.csfiles. The newcheck_csharpanalyzer parses those trait markers, locates each[Fact]/[Theory]method body via a C#-aware brace/string scanner, and runs assert-true / no-assertion detection — recognizing xUnitAssert.*, NUnitAssert.That, MSTestAssert.*, FluentAssertions.Should(), and Playwright fluent assertions (Expect(...)/Assertions.Expect(...)chained to aTo*Async()matcher) as assertions (static_checksRULE-31). A bareExpect(x)with no matcher chain is still flaggedno_assertions. The dispatch was extracted intoanalyze_test_file()and now routes.cs.references/supported_frameworks.mddocuments the new coverage. To keep this broader structural acceptance from masking weak tests, the Pass-2 audit criteria gained a "presence/visibility-only assertion" WEAK check (references/audit_criteria.md→ Criteria-Version 17). - Portable interpreter guidance in
purlin:audit.skills/audit/SKILL.mddocuments apython3 → python → py -3fallback for invokingstatic_checks.py, since stock Windows does not putpython3on PATH (skill_auditRULE-15). purlin:auditreaches C# tests even whentest_fileis empty. Underdotnet test,TestCase.CodeFilePathis often null (no surfaced source info), so the xUnit logger emits proof entries with a blanktest_file— which previously blocked the audit skill from locating the.cscode to scan (Pass 1) or read (Pass 2).static_checks.pynow exposes--resolve-source <test_name>, which derives the declaring type from the fully-qualifiedtest_nameand locates its.csfile (skippingbin/obj), andskills/audit/SKILL.mduses it as a fallback (static_checksRULE-32,skill_auditRULE-16). Populatingtest_filenatively still requiresRunConfiguration.CollectSourceInformation=truewith full PDBs — now documented.
Testing
- New proofs:
static_checksPROOF-44/45 (AST structural guards — no unconditionalfcntlimport;encoding='utf-8'on every text-modeopen()), PROOF-46–49 (C# assert-true / no-assertion / multi-framework assertion recognition /.csdispatch), PROOF-50 (Windows lock path driven through an injected fakemsvcrt,@integration), PROOF-51 (full CLI pipeline via real subprocess,@e2e), PROOF-52 (the cp1252 failure reproduced by running--load-criteriaunderPYTHONUTF8=0/LC_ALL=C,@e2e);skill_auditPROOF-15. PROOF-55 covers the Playwright assertion recognition (and the bare-Expectnegative). Windows behavior is now verified for real on awindows-latestGitHub Actions runner —.github/workflows/windows-proofs.ymlruns PROOF-53 (nativemsvcrtlock path,_HAS_FCNTLgenuinely False) and PROOF-54 (--load-criteriaunder the native console codec) in the dedicated@windowsproof tier and commits the results back — in addition to the host simulation (fakemsvcrt) and ASCII-locale subprocess repro. Independent audit of the new proofs: STRONG at 100% integrity, 0 WEAK/HOLLOW.
v0.9.4 — Plugin-bundled MCP server & e2e proof quality
Fixed
-
MCP server no longer version-pinned in consumer projects.
purlin:initwrote a project-level.mcp.jsonentry with an absolute path into the versioned plugin cache (~/.claude/plugins/cache/purlin/purlin/<version>/scripts/mcp/purlin_server.py). Project-scope.mcp.jsontakes precedence over plugin-provided servers and old cache directories stick around, so every plugin update silently stranded the project on the previous release's server — fresh data, stale code, no error (this is exactly how a 0.9.3 project kept rendering dashboards with 0.9.1 logic). The server is now bundled with the plugin itself:.claude-plugin/plugin.jsondeclares it undermcpServerswith${CLAUDE_PLUGIN_ROOT}, which Claude Code resolves to the installed plugin path on every launch, so the server always tracks the installed version (skill_initRULE-38/39). Init no longer writes apurlinentry into the project's.mcp.json; it removes the legacy entry while preserving any other MCP servers in the file (skill_initRULE-40).Upgrading an existing project: after updating the plugin, run
purlin:init --mcponce (new flag — runs only the migration step) to remove the stale entry, then/reload-plugins(or restart the session). -
sync_statusflags the legacy entry automatically. When the project's.mcp.jsondefines apurlinserver pinned to a plugin-cache path, every status report opens with an advisory naming the pinned path and a→ Run: purlin:init --mcpdirective — so pre-0.9.4 projects surface the migration on their first status call instead of relying on release notes (sync_statusRULE-38). Dev checkouts (purlin entries pointing outside the plugin cache) are exempt.
Added
- E2E proofs must be observable flows. Reverse-engineering UI-heavy codebases with
purlin:spec-from-codeproduced implementation-coupled proofs tagged@e2e("Assertconfig.tsAES-decrypts...", "AssertloginRedirectuses scope X") — satisfiable without launching the app, steering builders into unit-style tests that audit WEAK/HOLLOW. The quality guide now has a canonical "E2E proof descriptions (observable flows)" section (references/spec_quality_guide.md):@e2eproofs read as arrange → act → observe through the real running app, never name source files or internal functions, observe data contracts at the boundary they cross (outbound request, rendered output, storage state after a real flow), and stay tool-agnostic — executable by Playwright, Cypress, an MCP-driven browser, or screenshot + vision (purlin_referencesRULE-13). - E2E Proof Tier Integrity audit criteria.
references/audit_criteria.md(Criteria-Version 16) adds two Pass 2 WEAK criteria at HIGH priority applying to ALL@e2eproofs, not just design anchors: tier mismatch (test tagged@e2enever drives a browser/renderer/full stack — or the converse, a render/flow test with no tier tag) and source-constant assertion (asserting a config constant where the rule describes runtime behavior) (purlin_referencesRULE-14). The version bump does not invalidate audit caches — the cache key excludes the criteria version. - E2E runner reality check in
purlin:spec-from-code. Phase 1 records ane2e_capableflag; when a category's generated proofs include@e2eand no e2e runner is detected, the skill warns in the category review block and the Phase 4 summary instead of silently emitting unrunnable proofs (skill_spec_from_codeRULE-32).references/supported_frameworks.mdgains an "End-to-end (browser) proofs" section documenting interim proof emission through the existing Vitest/Jest markers or shellpurlin_proofwrappers (purlin_referencesRULE-15).
Changed
purlin:spec-from-codeenforces proof quality at generation time. The step 7 tier review now runs an inverse check — every@e2edescription must match its tag or be rewritten/retagged (skill_spec_from_codeRULE-30) — and step 11 validation rejects proof descriptions naming source files or internal symbols (skill_spec_from_codeRULE-31).purlin:specapplies the same observable-flow check in Validate-Before-Commit (skill_specRULE-8).
Testing
- New proofs:
skill_spec_from_codePROOF-42..45,skill_specPROOF-9,purlin_referencesPROOF-13/14/15 (grep guards over the skill and reference text). Independent audit of the new proofs: 0 WEAK/HOLLOW (all structural documentation guards, excluded from integrity scoring); 3 advisory regex-precision findings applied before commit. Full suite 372 passed, 40/40 features VERIFIED. skill_initPROOF-40/41 now parse.claude-plugin/plugin.jsondirectly (assertingmcpServers.purlinusespython3with${CLAUDE_PLUGIN_ROOT}args) and PROOF-42 verifies the legacy.mcp.jsonmigration instructions, replacing the old greps for project-level.mcp.jsoncreation.
v0.9.3 — Dashboard visibility before tests exist
Quality-of-life release fixing the "empty dashboard" experience after purlin:spec-from-code: a freshly specced project now shows its full rule set and coverage plan in purlin-report.html before a single test has run.
Added
- Planned proofs in the dashboard. The report data (
.purlin/report-data.js) now includes thePROOF-Nentries declared in each spec's## Proofsection that have no executed result yet — status"planned", tier parsed from the proof's@tag(default unit), empty test location (report_dataRULE-8). The dashboard renders them greyed with a "not run" tag in the Proof column instead of a bare dash (purlin_reportRULE-33), so the intended coverage plan is visible immediately after spec generation. Planned proofs are display-only: proved/total counts, vhash, rule status, and feature status are computed from executed proofs exactly as before (report_dataRULE-22). Proof results are still written only when tests run (purlin:unit-test/purlin:build/purlin:verify).
Changed
- Category sections are expanded by default. Previously every category started collapsed, so a project with many small categories looked like it had no specs at all. Categories now render expanded; collapsing one is remembered per browser via localStorage (
purlin_reportRULE-20, persistence unchanged per RULE-21). purlin:spec-from-codeno longer creates single-spec folders. A new Phase 2 taxonomy step merges single-feature categories into the closest related category, or — when nothing fits — places the spec directly atspecs/<name>.mdwith no folder (skill_spec_from_codeRULE-29). Uncategorized specs display under "other" in the dashboard.- Docs and skill wording aligned with actual behavior. PASSING criteria say "all rules" (matching
_determine_status); verify receipt format documented asfeatures=N/T; installation/testing/lifecycle guides corrected (committed digest, real GitHub Actions example, shell plugin path); spec-from-code guide documents the post-generation build lifecycle; lifecycle PM diagram regenerated.
Testing
- New proofs: planned-proof emission and dedup against executed results including required/global anchor rules (
report_dataPROOF-22), coverage isolation (PROOF-23), greyed "not run" rendering with no audit tag (purlin_reportPROOF-33, Playwright), and the single-spec folder ban (skill_spec_from_codePROOF-41). Dashboard PROOF-19/20/21 updated for the expanded-by-default behavior. Independent audit of the changed proofs: 5 STRONG, 0 WEAK/HOLLOW.
v0.9.2 — .NET test support (xUnit)
Incremental release adding .NET to the supported test ecosystems, backed by a refactor of the proof-plugin specs.
Added
-
xUnit/.NET proof plugin.
scripts/proof/xunit_purlin.csis a customdotnet testlogger (ITestLoggerWithParameters, FriendlyNamepurlin) that collects proof traits in-process — no.trxpost-parsing — and emits feature-scoped proof JSON per the shared proof-plugin contract. Mark tests with a trait:[Fact] [Trait("PurlinProof", "feature_name:PROOF-1:RULE-1:unit")] public void ValidLogin() { ... }
Because the marker is a test trait rather than a parsed title string, NUnit
[Category]/[Property]and MSTest[TestProperty]surface the same way — the logger covers C#, F#, and VB.NET test projects. Run withdotnet test --logger purlin -- RunConfiguration.CollectSourceInformation=true. Setup is manual for now (the .NET test platform only discovers loggers from assemblies named*TestLogger.dll) — seereferences/formats/proofs_format.mdfor wiring steps. Proven by an integration suite that drives a realdotnet test --logger purlinrun; independent audit: 6/6 STRONG. -
purlin:initpresents every shipped framework. The framework selection list now covers all shipped plugins — pytest, Jest, Vitest, C, PHP, SQL, Shell, and xUnit (plus "other") — instead of only the auto-detected subset (skill_initRULE-48). Frameworks that need manual wiring (xUnit) print their setup steps after the plugin file is copied.
Changed
- Proof-plugin specs split per framework. The monolithic
proof_pluginsspec is gone, replaced by one spec per plugin (proof_plugins_pytest,_jest,_vitest,_shell,_c,_php,_sql,_xunit) plus aproof_commonanchor that holds the shared contract: feature-scoped overwrite, tier file naming, marker→JSON field mapping. Proof markers were re-homed to their per-plugin specs. references/supported_frameworks.mdis now Format-Version 5: adds an Additional Plugins (manual setup) section for shipped-but-not-auto-scaffolded plugins (currently xUnit).
Testing
- New integration proofs drive the compiled xUnit logger through a real
dotnet testrun (dev/test_multilang_proof_plugins.py::TestXUnitProofPlugin). - Strengthened existing proofs: C plugin proofs regenerated from a real
gccrun, Vitest RULE-1 marker parse now asserted inline, pytest "call phase only" boundary proved, and three audit-pipeline proofs recorded from a full-suite run.
v0.9.1 — Vitest reporter & JS/TS audit fixes
Bug-fix release addressing two reported issues in JavaScript/TypeScript proof handling.
Fixed
- Vitest reporter silently emitted zero proofs on Vitest 2.x+ (#1).
scripts/proof/vitest_purlin.tscollected proofs inonTaskUpdate, whose pack shape changed in Vitest 2 ([id, result, meta], whereresultlacksname/file), so the marker regex never matched and tests passed with no.proofs-*.jsonwritten. The reporter now collects inonFinished(files)via a recursive file→suite→test walk — the hook whose shape is stable across Vitest 2.x–4.x — mappingresult.stateto pass/fail, skipping unrun tasks, and resolvingtest_filefrom the file task'sfilepath. - All Vitest projects now scaffold the native
vitest_purlin.ts. Vitest never calls Jest'sonTestResult/onRunCompletehooks, so the previous "Vitest →jest_purlin.js" mapping was also silently broken. Vitest loads.tsreporters natively via Vite, so one reporter covers both JS and TS projects. check_jsaudit regex misfired on common Vitest patterns (#2).scripts/audit/static_checks.py::check_js()used a lazy(.*?)\}\s*\)body matcher that truncated at the first inner}(options objects, destructured params, type assertions) and a[^"']*title class that dropped titles containing apostrophes. Replaced with a stdlib brace-balancing tokenizer that tracks string/template/regex literals and comments.- Claude plugin manifest version lagged the release.
.claude-plugin/plugin.json— the version consumers install against via the plugin marketplace — was left at0.9.0. It now reads0.9.1, and a new guard (purlin_versionRULE-5 / PROOF-5) asserts the manifest version stays in lockstep with theVERSIONfile, joining the existingtemplates/config.jsoncheck so this can't silently recur.
Testing
- PROOF-29 was strengthened to actually drive the compiled reporter via a synthetic Vitest 2.x+ task tree (the old proof only ran
tsc+nodeon hand-built JSON and never invoked the reporter). New regression proofs cover the exact issue #2 repro. - Docs aligned:
references/supported_frameworks.md(Format-Version 3),references/formats/proofs_format.md, anddocs/testing-workflow-guide.mdnow describe the native Vitest reporter and tested version range.