Skip to content

fix: cross-platform audit Pass-1 (Windows) + C# support — v0.9.5 (#3)#4

Merged
rlabarca merged 5 commits into
mainfrom
fix/windows-static-checks-3
Jun 29, 2026
Merged

fix: cross-platform audit Pass-1 (Windows) + C# support — v0.9.5 (#3)#4
rlabarca merged 5 commits into
mainfrom
fix/windows-static-checks-3

Conversation

@rlabarca

Copy link
Copy Markdown
Owner

Fixes #3purlin:audit was completely broken on Windows.

What was broken (issue #3)

scripts/audit/static_checks.py failed on Windows for three independent reasons, each fixed here:

  1. import fcntl (POSIX-only) crashed the module on load → every subcommand died with ModuleNotFoundError.
  2. ~16 open() calls omitted encoding=UnicodeDecodeError reading the tool's own UTF-8 files under cp1252.
  3. --load-criteria printed / glyphs to an ASCII stdout → UnicodeEncodeError (surfaced during this work).

Plus: Pass 1 had no C#/.NET support — xUnit/NUnit/MSTest tests produced zero proofs.

What changed

  • fcntl imported under try/except ImportError with a _HAS_FCNTL flag; _lock_exclusive/_unlock use fcntl.flock (POSIX) or msvcrt.locking (Windows). Cache serialization (RULE-25) preserved (static_checks RULE-29).
  • encoding='utf-8' on every text-mode open() + stdout/stderr reconfigured to UTF-8 at startup (static_checks RULE-30).
  • New check_csharp analyzer: parses [Trait("PurlinProof", …)] markers, C#-aware brace/string scanner, assert-true / no-assertion detection across xUnit/NUnit/MSTest/FluentAssertions; .cs routed via new analyze_test_file() dispatch (static_checks RULE-31).
  • skills/audit/SKILL.md documents a python3 → python → py -3 interpreter fallback (skill_audit RULE-15).

Verification

  • static_checks VERIFIED 29/29, skill_audit 15/15.
  • New PROOF-44..52 + skill_audit PROOF-15. Independent audit: 7/7 behavioral proofs STRONG (100% integrity), 0 WEAK/HOLLOW.
  • Windows is verified by simulation (injected fake msvcrt) + a faithful cp1252 repro (--load-criteria under PYTHONUTF8=0/LC_ALL=C). Not yet a Windows CI runner — see release notes caveat.

Tagged v0.9.5.

🤖 Generated with Claude Code

rlabarca and others added 5 commits June 24, 2026 15:55
Reword RULE-25 to drop the fcntl pin (POSIX flock / Windows msvcrt).
Add RULE-29 (portable import + lock), RULE-30 (utf-8 file I/O), RULE-31
(C# .cs Pass-1 analyzer) with proofs PROOF-44..52. Add skill_audit
RULE-15 + PROOF-15 (python3->python->py -3 interpreter probe). Note C#
deterministic Pass-1 coverage in supported_frameworks.md. Addresses #3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make scripts/audit/static_checks.py run on Windows, where purlin:audit was
completely broken:

- RULE-29: import fcntl under try/except ImportError with a _HAS_FCNTL flag;
  add _lock_exclusive/_unlock that use fcntl.flock on POSIX and msvcrt.locking
  on Windows (retry loop matches flock's blocking semantics). write_audit_cache
  locks through the helpers.
- RULE-30: add encoding='utf-8' to every text-mode open(), and reconfigure
  stdout/stderr to UTF-8 at startup so --load-criteria no longer dies with
  UnicodeDecodeError (reading audit_criteria.md) or UnicodeEncodeError
  (printing its ✓/⚠ glyphs) under a cp1252/ASCII locale.
- RULE-31: add check_csharp — parses [Trait("PurlinProof", ...)] markers, finds
  each [Fact]/[Theory] body via a C#-aware brace scanner, and runs assert-true /
  no-assertion detection (xUnit/NUnit/MSTest Assert.*, FluentAssertions .Should()).
  Extract analyze_test_file() dispatch seam; route .cs to it.
- skill_audit RULE-15: SKILL.md documents a python3 -> python -> py -3 fallback.

Tests: PROOF-44/45 (AST guards), 46-49 (C#), 50 (Windows-sim msvcrt lock,
@integration), 51 (POSIX subprocess pipeline, @e2e), 52 (cp1252 locale repro
via --load-criteria, @e2e); skill_audit PROOF-15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Building the fix surfaced a third Windows failure mode: --load-criteria
prints criteria glyphs (✓/⚠) to an ASCII stdout and dies with
UnicodeEncodeError even after the read-side encoding fix. RULE-30 now covers
all text I/O — open() encoding AND stdout/stderr reconfigure — and PROOF-52
reproduces it via --load-criteria under PYTHONUTF8=0/LC_ALL=C.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump VERSION and templates/config.json to 0.9.5. Fixes #3: purlin:audit
now runs on Windows (fcntl optional + msvcrt lock fallback, UTF-8 text I/O,
UTF-8 stdout) and gains deterministic C#/.NET Pass-1 coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@LCbarsenault

Copy link
Copy Markdown

Tested on Windows — fix works ✅ (one false-positive to note)

Tested this branch on Windows 11 / CPython 3.12 against a real .NET 7 (Blazor Server + xUnit) project. Installed this branch's scripts/audit/static_checks.py over the released 0.9.4 copy, removed all local workarounds (no PYTHONPATH, no fcntl shim, no env vars), cleared __pycache__, and ran each command that used to crash:

Operation (previously crashed on Windows) Result
--read-cache (Unix-only import fcntl) ✅ works
--write-cache (fcntl.flock) ✅ works — the fcntlmsvcrt locking fallback holds
--load-criteria (cp1252 decode of the criteria md, then stdout encode of the ) Criteria-Version: 16 + full text, exit 0
--check-proof-file ✅ clean
Pass-1 on a C# xUnit file (was a silent no-op pre-fix) ✅ now parses [Trait("PurlinProof", "feature:PROOF-N:RULE-N:tier")] and runs the structural checks

All three Windows encoding/fcntl facets from #3 are resolved, and the C# Pass-1 support is a great addition — deterministic structural checks now actually run against .cs test bodies. Regression-swept it across ~75 real unit/integration/e2e tests: no crashes, and well-formed tests correctly report pass.


⚠️ One false positive: C# no_assertions doesn't recognize Playwright's fluent assertions

A C# test that asserts only via Playwright's Assertions.Expect(...).ToBeVisibleAsync() is flagged HOLLOW:

PROOF-7 (RULE-7) Nav_ShowsUser_AndSignOut
  status: "fail", check: "no_assertions"
  reason: "test method has no Assert./.Should()/.Verify() call"

…but the test does assert:

await Assertions.Expect(page.Locator("text=" + user)).ToBeVisibleAsync();
await Assertions.Expect(page.Locator("a[href='logout']")).ToBeVisibleAsync();

The no_assertions heuristic looks for Assert. / .Should() / .Verify(), which misses Playwright's assertion API. Tests that also use a plain Assert. pass fine; the ones asserting solely through Expect(...) get false-flagged — common in browser e2e suites.

Suggested: add Playwright's forms to the recognized-assertion set — Assertions.Expect(, .Expect(, and the ToBe*Async() / ToHave*Async() families (FluentAssertions .Should() is already covered).

Net: the Windows fix is good to merge from my testing; the Playwright-assertion gap is a minor follow-up. Thanks for the quick turnaround!

@LCbarsenault

Copy link
Copy Markdown

Note (separate from this PR's static_checks.py fix): C# proof JSONs have empty test_file, which blocks the new C# Pass-1 from being driven by the audit skill

While running the audit end-to-end on Windows with this branch, the deterministic engine itself worked great — but the audit skill couldn't auto-locate the C# test code, because every proof entry the xUnit logger emitted has an empty test_file:

{ "feature": "auth_credentials", "id": "PROOF-1", "rule": "RULE-1",
  "test_file": "",
  "test_name": "UtilityAssistant.Tests.AuthLogicTests.Evaluate_NullRow_IsInvalidCredentials",
  "status": "pass", "tier": "unit" }

test_name (fully-qualified, includes the class) is populated, but test_file is blank. The logger does MakeRelative(_root, tc.CodeFilePath ?? ""), and under dotnet test here TestCase.CodeFilePath comes back null (source info isn't surfaced to the logger), so it falls back to "".

Why it matters for "C# support": the audit skill (Step 2 Pass-1 and Pass-2) reads each proof's test_file to find the code to scan / read. With it empty, auto-discovery fails — I had to supply the file paths manually to exercise the new C# Pass-1. So the cross-platform/C# Pass-1 code is correct and works when given a path (confirmed across 82 proofs, 0 crashes), but it isn't reachable through the skill on a C# project until test_file is populated.

Options (any one closes it):

  • Populate CodeFilePath in the logger path — may require the test project to emit source info (full PDBs / SourceInformationProvider), so possibly a documented requirement rather than a code fix.
  • Or have the audit skill fall back to resolving the file from the fully-qualified test_name (namespace/class → search **/*.cs) when test_file is empty.

Not a blocker for merging the static_checks.py fix — flagging it because it's the practical gap between "C# Pass-1 works" and "C# Pass-1 is usable via purlin:audit." Happy to open a separate issue if you'd prefer.

rlabarca added a commit that referenced this pull request Jun 25, 2026
…udit reachability)

PR #4 follow-up: under `dotnet test`, TestCase.CodeFilePath is often null, so the
xUnit logger emits proof entries with an empty test_file — which blocked
purlin:audit from locating the .cs code for Pass 1 (scan) and Pass 2 (read).

static_checks.py gains resolve_test_file_from_name() + a `--resolve-source
<test_name>` CLI: it derives the declaring type from the fully-qualified
test_name (segment before the final .method) and searches the project's source
files (skipping bin/obj/node_modules/.git) for its class/struct/record/interface
declaration, returning the repo-relative POSIX path of the best match (RULE-32,
PROOF-56). skills/audit/SKILL.md uses this as a fallback when test_file is empty,
and documents CollectSourceInformation + full PDBs as the native way to populate
it (skill_audit RULE-16, PROOF-16). docs/testing-workflow-guide.md notes both.

Also normalizes the pytest proof plugin's test_file to forward slashes
(item.fspath.relto yields OS-native separators on Windows) — a no-op on POSIX —
and normalizes the committed windows-tier proof file accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rlabarca

Copy link
Copy Markdown
Owner Author

Thanks for the thorough Windows pass and the two follow-up notes — both are addressed in a stacked follow-up PR: #5.

Playwright C# assertions (your false-positive note): check_csharp's no_assertions guard now recognizes Playwright fluent assertions — Expect(...) / Assertions.Expect(...) chained to a To*Async() matcher (ToBeVisibleAsync, ToHaveTextAsync, ToContainTextAsync, …). So Nav_ShowsUser_AndSignOut and friends pass; a bare Expect(x) with no matcher chain is still flagged (static_checks RULE-31, PROOF-55). To keep the broader acceptance from waving through weak tests, Pass-2 audit criteria gained a "presence/visibility-only assertion" WEAK check (Criteria-Version 17).

Empty test_file blocking C# auto-discovery (your second note): went with the skill-side resolver. static_checks.py now has --resolve-source <test_name>, which derives the declaring type from the fully-qualified test_name and locates the .cs file (skipping bin/obj); purlin:audit falls back to it whenever test_file is empty, so C# Pass-1/Pass-2 is reachable without a populated path (static_checks RULE-32 / PROOF-56, skill_audit RULE-16 / PROOF-16). The native route — RunConfiguration.CollectSourceInformation=true + full PDBs — is now documented as the way to populate it directly. No separate issue needed; closed by #5.

Windows is now real CI, not simulation: RULE-29/30 are verified on a windows-latest Action via a new @windows proof tier (PROOF-53/54), in addition to the host sims.

All 40 features VERIFIED at 100% integrity on the follow-up branch; the Windows Action is green.

@rlabarca

Copy link
Copy Markdown
Owner Author

Verified on real Windows by the reporter and green on the windows-latest Action. Merging into main for the v0.9.5 release. The follow-up (#5 — Playwright C# assertions, real Windows CI proofs, and the C# test_file resolver) merges right after this.

@rlabarca
rlabarca merged commit 48de011 into main Jun 29, 2026
@rlabarca
rlabarca deleted the fix/windows-static-checks-3 branch June 29, 2026 20:10
@rlabarca
rlabarca restored the fix/windows-static-checks-3 branch June 29, 2026 20:12
rlabarca added a commit that referenced this pull request Jun 29, 2026
… CI proofs

Pass-1 C# no_assertions now recognizes Playwright fluent assertions —
Expect(...)/Assertions.Expect(...) chained to a To*Async() matcher — so tests
that assert solely through Playwright are no longer false-flagged HOLLOW
(PR #4 review). A bare Expect(x) with no matcher chain is still flagged
(RULE-31, PROOF-55).

To keep the broader structural acceptance from masking weak tests, Pass-2 audit
criteria gain a "presence/visibility-only assertion" WEAK check
(audit_criteria Criteria-Version 16 -> 17).

Windows RULE-29/30 are now verified for real on a windows-latest GitHub Actions
runner (.github/workflows/windows-proofs.yml) via the new @windows proof tier —
PROOF-53 (native msvcrt lock path) and PROOF-54 (--load-criteria under the
native console codec) — committed back by CI, in addition to the host
simulations (PROOF-50/52). The schema_proof_format anchor (RULE-4) and
spec_format.md (Format-Version 8) admit the platform-gated windows tier; the
feature-scoped overwrite is per (feature, tier), so host runs that skip these
tests never clobber the CI-emitted proof file.

Also bumps .claude-plugin/plugin.json to 0.9.5 to match VERSION (the manifest
was not bumped during the v0.9.5 release; purlin_version caught the drift).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rlabarca added a commit that referenced this pull request Jun 29, 2026
…udit reachability)

PR #4 follow-up: under `dotnet test`, TestCase.CodeFilePath is often null, so the
xUnit logger emits proof entries with an empty test_file — which blocked
purlin:audit from locating the .cs code for Pass 1 (scan) and Pass 2 (read).

static_checks.py gains resolve_test_file_from_name() + a `--resolve-source
<test_name>` CLI: it derives the declaring type from the fully-qualified
test_name (segment before the final .method) and searches the project's source
files (skipping bin/obj/node_modules/.git) for its class/struct/record/interface
declaration, returning the repo-relative POSIX path of the best match (RULE-32,
PROOF-56). skills/audit/SKILL.md uses this as a fallback when test_file is empty,
and documents CollectSourceInformation + full PDBs as the native way to populate
it (skill_audit RULE-16, PROOF-16). docs/testing-workflow-guide.md notes both.

Also normalizes the pytest proof plugin's test_file to forward slashes
(item.fspath.relto yields OS-native separators on Windows) — a no-op on POSIX —
and normalizes the committed windows-tier proof file accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rlabarca added a commit that referenced this pull request Jun 29, 2026
… C# test_file resolver (#5)

Follow-up to #4. Recognizes Playwright fluent assertions in C# Pass-1 (RULE-31/PROOF-55) with a Pass-2 visibility-only WEAK criterion (audit_criteria v17); verifies Windows RULE-29/30 for real on a windows-latest Action via a new @windows proof tier (PROOF-53/54); resolves empty C# test_file from the fully-qualified test_name so purlin:audit can reach C# Pass-1/Pass-2 (RULE-32/PROOF-56, skill_audit RULE-16). Also bumps the plugin manifest to 0.9.5 to match VERSION.
@rlabarca
rlabarca deleted the fix/windows-static-checks-3 branch June 29, 2026 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

static_checks.py not cross-platform: fcntl import + cp1252 open() crash purlin:audit on Windows

2 participants