Skip to content

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

Description

@LCbarsenault

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:

  1. 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'.
  2. ~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/purlinscripts/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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions