Bug: purlin:audit is completely broken on Windows (scripts/audit/static_checks.py)
Summary
On Windows, the entire audit pipeline fails. There are two independent, blocking,
platform-portability bugs in scripts/audit/static_checks.py, both hit before any
real audit work happens:
import fcntl (Unix-only) at module top level crashes the script on every
invocation — --read-cache, --load-criteria, --check-proof-file, Pass 1, and
cache writes all die with ModuleNotFoundError: No module named 'fcntl'.
- ~16
open() calls omit encoding=, so on Windows they default to the locale
codec (cp1252) and raise UnicodeDecodeError when reading the tool's own UTF-8
files (e.g. references/audit_criteria.md).
Fixing only #1 is not enough — the next call (--load-criteria) then dies on #2.
A third, non-crashing gap is noted at the end (Pass 1 does not recognize C#/xUnit proof
markers).
Environment
- OS: Windows 11
- Python: 3.12.10 (CPython, MSC v.1943 64-bit) — the
python3 the skill invokes
- Purlin plugin: installed
0.9.4 (~/.claude/plugins/cache/purlin/purlin/0.9.4/);
source repo at rlabarca/purlin — scripts/audit/static_checks.py is byte-identical
between the two, so the fix applies to both.
Bug 1 — import fcntl makes the script unimportable on Windows
Reproduction
$ python3 scripts/audit/static_checks.py --read-cache
Traceback (most recent call last):
File ".../scripts/audit/static_checks.py", line 16, in <module>
import fcntl
ModuleNotFoundError: No module named 'fcntl'
Any subcommand fails identically, because the import is at module load time.
Cause
fcntl is imported unconditionally (line 16) but used in only one place — advisory
locking around the audit-cache write:
# line 16
import fcntl
# lines 791-843 (write_audit_cache)
with open(lock_path, 'w') as lock_file:
fcntl.flock(lock_file, fcntl.LOCK_EX) # line 792
try:
...
tmp_path = cache_path + '.tmp'
with open(tmp_path, 'w') as f:
json.dump(pruned, f, indent=2)
os.replace(tmp_path, cache_path) # already atomic
finally:
fcntl.flock(lock_file, fcntl.LOCK_UN) # line 843
Proposed fix
Guard the import and the two flock calls. The cache write already uses
tmp + os.replace (atomic), and the audit CLI is single-process, so skipping the
advisory lock on platforms without fcntl is safe.
# replace line 16
try:
import fcntl
except ImportError: # fcntl is Unix-only; Windows has no advisory flock
fcntl = None
# lines 791-843
with open(lock_path, 'w') as lock_file:
if fcntl is not None:
fcntl.flock(lock_file, fcntl.LOCK_EX)
try:
...
finally:
if fcntl is not None:
fcntl.flock(lock_file, fcntl.LOCK_UN)
Optional hardening (only if true cross-process locking on Windows is desired):
use msvcrt.locking() behind a small with _cache_lock(lock_path): context manager
that picks fcntl.flock on POSIX and msvcrt.locking on Windows. The simple guard
above is sufficient for the documented single-user workflow.
Bug 2 — open() calls without encoding= crash on Windows (cp1252)
Reproduction (after Bug 1 is guarded)
$ python3 scripts/audit/static_checks.py --load-criteria --project-root <proj>
Traceback (most recent call last):
...
File ".../scripts/audit/static_checks.py", line 875, in load_criteria
criteria = f.read()
File ".../encodings/cp1252.py", line 23, in decode
return codecs.charmap_decode(input, self.errors, decoding_table)[0]
UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 1146:
character maps to <undefined>
The file being read is the tool's own references/audit_criteria.md, which contains a
non-ASCII byte (e.g. a typographic dash). On Windows, open() defaults to cp1252, not
UTF-8, so the read fails. The same failure occurs for any spec/proof/config file that
contains a non-ASCII byte.
Cause
Every text open() in the file omits encoding=, inheriting the platform default
(locale.getpreferredencoding() = cp1252 on Windows). Affected lines:
- Reads: 258, 302, 557, 628, 648, 698, 761, 874, 880, 887, 896, 1028
- Text writes: 839, 909, 932
(Line 791 opens the lock file 'w' but writes no content — encoding is irrelevant there.)
Proposed fix
Add encoding='utf-8' to every text open(). Examples:
# reads
with open(filepath, encoding='utf-8') as f: # 258, 302, 557
with open(spec_path, encoding='utf-8') as f: # 628, 648
with open(proof_json_path, encoding='utf-8') as f: # 698
with open(cache_path, encoding='utf-8') as f: # 761
with open(builtin_path, encoding='utf-8') as f: # 874
with open(cached_path, encoding='utf-8') as f: # 880
with open(config_path, encoding='utf-8') as f: # 887
with open(extra_path, encoding='utf-8') as f: # 896
with open(live_keys_file, encoding='utf-8') as f: # 1028
# text writes
with open(tmp_path, 'w', encoding='utf-8') as f: # 839, 909, 932
This makes file I/O deterministic across platforms and matches the UTF-8 content the
tool itself produces. (Equivalent alternative: open with encoding='utf-8' rather than
relying on PYTHONUTF8/PYTHONIOENCODING, which users should not have to set.)
Secondary observation (non-blocking) — Pass 1 has no C#/.NET support
After both crashes are fixed, Pass 1 still provides no structural-defect coverage for
.NET projects. Running:
$ python3 scripts/audit/static_checks.py tests/.../MoveRegistrationLogicTests.cs <feature> --spec-path specs/<feature>.md
{ "proofs": [] }
returns zero proofs because the static checker recognizes Python/JS proof markers but
not C#/xUnit [Trait("PurlinProof", "<feature>:PROOF-N:RULE-N:unit")] attributes (as
emitted by the Purlin xUnit test logger). The proof-file structural checks
(--check-proof-file) and the (LLM) Pass 2 still work, but deterministic Pass 1
silently no-ops on C# tests. Suggest either documenting this limitation or adding a C#
attribute parser so Pass 1 (assert-true / no-assertion / logic-mirroring detection)
covers xUnit/NUnit/MSTest test bodies.
Suggested skill-level change (skills/audit/SKILL.md)
The skill hardcodes python3 .../static_checks.py. Two small robustness improvements:
- Interpreter fallback: on Windows,
python3 is not always on PATH (the launcher
is often py -3 or python). Recommend the skill probe for an interpreter
(python3 -> python -> py -3) or document the requirement.
- No code change needed for the crashes — once
static_checks.py is made
cross-platform (Bugs 1 & 2), the skill works unchanged. The fix belongs in the
script, not the skill instructions.
Severity / impact
- Blocking on Windows.
purlin:audit cannot run at all — no cache read, no criteria
load, no proof-file checks, no Pass 1, no cache writes.
- Cross-platform correctness. The encoding bug can also surface on non-Windows
systems whose locale is not UTF-8.
- Low-risk fix. Both fixes are localized, behavior-preserving on POSIX (UTF-8 is
already the POSIX default; fcntl is still used there), and add no dependencies.
Bug:
purlin:auditis completely broken on Windows (scripts/audit/static_checks.py)Summary
On Windows, the entire audit pipeline fails. There are two independent, blocking,
platform-portability bugs in
scripts/audit/static_checks.py, both hit before anyreal audit work happens:
import fcntl(Unix-only) at module top level crashes the script on everyinvocation —
--read-cache,--load-criteria,--check-proof-file, Pass 1, andcache writes all die with
ModuleNotFoundError: No module named 'fcntl'.open()calls omitencoding=, so on Windows they default to the localecodec (cp1252) and raise
UnicodeDecodeErrorwhen reading the tool's own UTF-8files (e.g.
references/audit_criteria.md).Fixing only #1 is not enough — the next call (
--load-criteria) then dies on #2.A third, non-crashing gap is noted at the end (Pass 1 does not recognize C#/xUnit proof
markers).
Environment
python3the skill invokes0.9.4(~/.claude/plugins/cache/purlin/purlin/0.9.4/);source repo at
rlabarca/purlin—scripts/audit/static_checks.pyis byte-identicalbetween the two, so the fix applies to both.
Bug 1 —
import fcntlmakes the script unimportable on WindowsReproduction
Any subcommand fails identically, because the import is at module load time.
Cause
fcntlis imported unconditionally (line 16) but used in only one place — advisorylocking around the audit-cache write:
Proposed fix
Guard the import and the two
flockcalls. The cache write already usestmp + os.replace(atomic), and the audit CLI is single-process, so skipping theadvisory lock on platforms without
fcntlis safe.Optional hardening (only if true cross-process locking on Windows is desired):
use
msvcrt.locking()behind a smallwith _cache_lock(lock_path):context managerthat picks
fcntl.flockon POSIX andmsvcrt.lockingon Windows. The simple guardabove is sufficient for the documented single-user workflow.
Bug 2 —
open()calls withoutencoding=crash on Windows (cp1252)Reproduction (after Bug 1 is guarded)
The file being read is the tool's own
references/audit_criteria.md, which contains anon-ASCII byte (e.g. a typographic dash). On Windows,
open()defaults to cp1252, notUTF-8, so the read fails. The same failure occurs for any spec/proof/config file that
contains a non-ASCII byte.
Cause
Every text
open()in the file omitsencoding=, inheriting the platform default(
locale.getpreferredencoding()= cp1252 on Windows). Affected lines:(Line 791 opens the lock file
'w'but writes no content — encoding is irrelevant there.)Proposed fix
Add
encoding='utf-8'to every textopen(). Examples:This makes file I/O deterministic across platforms and matches the UTF-8 content the
tool itself produces. (Equivalent alternative: open with
encoding='utf-8'rather thanrelying on
PYTHONUTF8/PYTHONIOENCODING, which users should not have to set.)Secondary observation (non-blocking) — Pass 1 has no C#/.NET support
After both crashes are fixed, Pass 1 still provides no structural-defect coverage for
.NET projects. Running:
returns zero proofs because the static checker recognizes Python/JS proof markers but
not C#/xUnit
[Trait("PurlinProof", "<feature>:PROOF-N:RULE-N:unit")]attributes (asemitted by the Purlin xUnit test logger). The proof-file structural checks
(
--check-proof-file) and the (LLM) Pass 2 still work, but deterministic Pass 1silently no-ops on C# tests. Suggest either documenting this limitation or adding a C#
attribute parser so Pass 1 (assert-true / no-assertion / logic-mirroring detection)
covers xUnit/NUnit/MSTest test bodies.
Suggested skill-level change (
skills/audit/SKILL.md)The skill hardcodes
python3 .../static_checks.py. Two small robustness improvements:python3is not always on PATH (the launcheris often
py -3orpython). Recommend the skill probe for an interpreter(
python3->python->py -3) or document the requirement.static_checks.pyis madecross-platform (Bugs 1 & 2), the skill works unchanged. The fix belongs in the
script, not the skill instructions.
Severity / impact
purlin:auditcannot run at all — no cache read, no criteriaload, no proof-file checks, no Pass 1, no cache writes.
systems whose locale is not UTF-8.
already the POSIX default;
fcntlis still used there), and add no dependencies.