openlatch-client 0.1.18

OpenLatch runtime enforcement node — the capture-and-enforce client for the AI Operations Platform
#!/usr/bin/env python3
"""Fail the build on POSIX-only assumptions living in cross-platform code.

The Windows cross-compile gate (`cargo xwin clippy --target
x86_64-pc-windows-msvc`) proves the tree *compiles* for Windows. It cannot
prove it *behaves*: `"/tmp/x"`, `.join("a/b")` and a bare `HOME` read all
compile perfectly for MSVC and then resolve to nothing on a real Windows box.
Those are exactly the defects that used to reach users, because the only
Windows execution in this repo happened after publishing to npm.

This scanner is the behavioural half of that gate. It reads `src/**/*.rs` and
`build.rs`, drops everything that is legitimately POSIX-only, and fails on what
is left.

Dropped, in order of precedence:

  * `#[cfg(test)]` items — fixture paths never run on a user's machine.
  * items under a POSIX-only `cfg` (`unix`, `not(windows)`, `target_os =
    "linux"` / `"macos"`, `target_family = "unix"`) — code that Windows never
    compiles, let alone runs.
  * anything carrying an explicit `portability-ok:` waiver, per line
    (trailing comment) or per file (a `//! portability-ok:` inner doc comment
    in the first 40 lines). The waiver must state a reason: it is a documented
    exception, not a mute button.

Exit codes: 0 clean, 1 violations found, 2 bad invocation.
"""

from __future__ import annotations

import re
import sys
from dataclasses import dataclass
from pathlib import Path

# ── What counts as a violation ───────────────────────────────────────

# `platform_scoped` rules describe code that is *correct* inside a platform
# `cfg` and only suspicious outside one: `%APPDATA%` read under `cfg(windows)`
# is the right Windows idiom, the same read in portable code is not. Rules
# without the flag fire everywhere — a `/tmp` literal under `cfg(windows)` is a
# bug no matter who wrote it.
@dataclass(frozen=True)
class Rule:
    name: str
    pattern: re.Pattern[str]
    hint: str
    platform_scoped: bool = False


RULES: list[Rule] = [
    Rule(
        "abs-posix-path",
        re.compile(r'"(?:/(?:tmp|etc|usr|var|opt|home|Users|Library|proc|dev)\b|~/)'),
        "absolute POSIX path literal — resolve through core::config paths or std::env::temp_dir()",
    ),
    Rule(
        "embedded-separator",
        re.compile(r'\.join\(\s*"[^"]*/[^"]*"\s*\)'),
        "path separator inside a join() segment — chain .join() per segment instead",
    ),
    Rule(
        "home-env-read",
        re.compile(r'env::var(?:_os)?\(\s*"(?:HOME|USERPROFILE|APPDATA|XDG_[A-Z_]+)"'),
        "raw home/config env read — go through the single resolver (dirs::* behind core::config)",
        platform_scoped=True,
    ),
]

# `cfg(...)` expressions that mean "Windows never sees this". No trailing `\b`
# after the closing quote — `"` to `)` is not a word boundary, and requiring one
# silently matched nothing for every `target_os = "..."` form.
POSIX_CFG = re.compile(
    r'(?:\bunix\b|target_family\s*=\s*"unix"|target_os\s*=\s*"(?:linux|macos|ios|android|freebsd)")'
)
NOT_WINDOWS_CFG = re.compile(r'not\s*\(\s*(?:windows|target_os\s*=\s*"windows")\s*\)')
WINDOWS_CFG = re.compile(r'(?:\bwindows\b|target_os\s*=\s*"windows")')

ATTR = re.compile(r"^\s*#!?\[")
CFG_ATTR = re.compile(r"^\s*#!?\[\s*cfg\s*\(")
WAIVER = re.compile(r"portability-ok:\s*\S")

# String/char literals are stripped before brace counting, so a `"{"` in a
# format string cannot desynchronise the block scanner.
LITERALS = re.compile(
    r'r#+"(?:.|\n)*?"#+'  # raw strings
    r'|"(?:\\.|[^"\\])*"'  # normal strings
    r"|'(?:\\.|[^'\\])'"  # chars
)
LINE_COMMENT = re.compile(r"//.*$")


@dataclass
class Violation:
    path: Path
    line: int
    rule: str
    text: str
    hint: str


def strip_noise(line: str) -> str:
    """Remove string literals then line comments — brace counting only."""
    return LINE_COMMENT.sub("", LITERALS.sub('""', line))


def cfg_is_posix_only(expr: str) -> bool:
    return bool(NOT_WINDOWS_CFG.search(expr) or POSIX_CFG.search(expr))


def skipped_lines(lines: list[str]) -> tuple[set[int], set[int]]:
    """Two 0-based index sets: always-skipped, and platform-cfg-skipped.

    The first covers `cfg(test)` items, POSIX-only items and multi-line raw
    strings (config templates, unit/plist/XML bodies — documentation, not code
    that resolves a path). The second adds Windows-gated items, which only
    `platform_scoped` rules ignore.
    """
    always: set[int] = set()
    platform: set[int] = set()
    for i, raw in enumerate(lines):
        if not CFG_ATTR.match(raw):
            continue
        # An attribute may wrap across lines; accumulate until parens balance.
        expr, end = raw, i
        while expr.count("(") > expr.count(")") and end + 1 < len(lines):
            end += 1
            expr += lines[end]
        is_test = re.search(r"\bcfg\s*\(\s*test\s*\)", expr) is not None
        posix_only = is_test or cfg_is_posix_only(expr)
        windows_only = not posix_only and WINDOWS_CFG.search(expr) is not None
        if not (posix_only or windows_only):
            continue
        target = always if posix_only else platform
        # Inner attribute (`#![cfg(...)]`) gates the whole remaining file.
        if raw.lstrip().startswith("#!["):
            target.update(range(i, len(lines)))
            continue
        target.update(range(i, end + 1))
        target.update(item_span(lines, end + 1))
    always |= raw_string_lines(lines)
    return always, always | platform


def raw_string_lines(lines: list[str]) -> set[int]:
    """Interior lines of multi-line raw strings (`r#"` … `"#`).

    These hold generated artifacts and the commented config template shipped by
    `openlatch init` — prose about POSIX systems, not path resolution.
    """
    inside: set[int] = set()
    open_at: int | None = None
    for i, line in enumerate(lines):
        if open_at is None:
            # Ignore raw strings that open and close on their own line.
            if re.search(r'r#+"', LITERALS.sub('""', line)):
                open_at = i
        elif re.search(r'"#+', line):
            inside.update(range(open_at, i + 1))
            open_at = None
    return inside


def item_span(lines: list[str], start: int) -> range:
    """Lines covered by the item beginning at `start` (attributes included)."""
    j = start
    while j < len(lines) and (ATTR.match(lines[j]) or lines[j].lstrip().startswith("///")):
        j += 1
    depth = 0
    opened = False
    for k in range(j, len(lines)):
        clean = strip_noise(lines[k])
        depth += clean.count("{") - clean.count("}")
        if "{" in clean:
            opened = True
        if opened and depth <= 0:
            return range(start, k + 1)
        # A braceless item (`#[cfg(unix)] use ...;`) ends at its semicolon.
        if not opened and clean.rstrip().endswith(";"):
            return range(start, k + 1)
    return range(start, len(lines))


def scan(path: Path) -> list[Violation]:
    lines = path.read_text(encoding="utf-8").splitlines()
    if any(WAIVER.search(ln) for ln in lines[:40] if ln.lstrip().startswith("//!")):
        return []
    skip_all, skip_platform = skipped_lines(lines)
    out: list[Violation] = []
    for i, raw in enumerate(lines):
        if i in skip_all:
            continue
        stripped = raw.lstrip()
        if stripped.startswith("//") or WAIVER.search(raw):
            continue
        for rule in RULES:
            if rule.platform_scoped and i in skip_platform:
                continue
            if rule.pattern.search(raw):
                out.append(Violation(path, i + 1, rule.name, raw.strip(), rule.hint))
                break
    return out


def main(argv: list[str]) -> int:
    root = Path(argv[1]) if len(argv) > 1 else Path.cwd()
    if not root.is_dir():
        print(f"error: {root} is not a directory", file=sys.stderr)
        return 2

    # `src/generated/` is typify output from schemas/ — type definitions with no
    # path resolution in them, and nothing a `portability-ok:` waiver could
    # survive, since the next codegen run would drop it. build.rs stays in
    # scope: it runs on the host, so a POSIX assumption there breaks
    # `cargo install openlatch-client` on Windows.
    targets = sorted(
        p for p in (root / "src").rglob("*.rs") if "generated" not in p.relative_to(root).parts
    )
    if (root / "build.rs").exists():
        targets.append(root / "build.rs")

    violations = [v for path in targets for v in scan(path)]
    if not violations:
        print(f"portability: clean ({len(targets)} files scanned)")
        return 0

    print("::error::POSIX-only assumptions in cross-platform code:\n")
    for v in violations:
        rel = v.path.relative_to(root)
        print(f"  {rel}:{v.line}  [{v.rule}]")
        print(f"      {v.text}")
        print(f"{v.hint}\n")
    print(f"{len(violations)} violation(s).")
    print(
        "Fix them, move the code under a POSIX-only cfg, or append "
        "`// portability-ok: <reason>` when the path really is POSIX-only."
    )
    return 1


if __name__ == "__main__":
    sys.exit(main(sys.argv))