Contracts

lock-primitive.md

Contract — Canonical lock primitive (kernel/locks.py)

Seeded from MachineFileLock (specify_cli/core/file_lock.py, already sidecar-safe + pure stdlib). This is a move + sync-surface extension.

Public surface (illustrative — /tasks + implementer finalize signatures)

# async facade (preserves current MachineFileLock behaviour)
class MachineFileLock:
    def __init__(self, lock_path: Path, *, blocking: bool = False,
                 timeout_s: float | None = None) -> None: ...
    async def __aenter__(self) -> LockRecord: ...      # yields HOLDER metadata, never a payload handle
    async def __aexit__(self, *exc) -> None: ...        # truncate, not unlink

# sync facade (new — for bootstrap, status/locking, verdict queue, checkout, tracker, windows_migrate, pre_review_gate)
class SyncMachineFileLock:                              # or a `machine_file_lock(...)` contextmanager
    def __init__(self, lock_path: Path, *, blocking: bool = False,
                 timeout_s: float | None = None, reentrant: bool = False) -> None: ...
    def __enter__(self) -> LockRecord: ...
    def __exit__(self, *exc) -> None: ...

def read_lock_record(lock_path: Path) -> LockRecord | None: ...   # reads the SIDECAR record, never a payload
def force_release(lock_path: Path) -> None: ...                   # stale-holder recovery; truncate

Guarantees (map to FR-006/007, NFR-005, C-004)

a path that exists only to be locked; this exact path is opened, OS-locked, and truncated directly — nothing is derived from it, and it must never be a payload path (passing a payload path would truncate and overwrite that payload).

(holder metadata). There is no API to read the protected resource through the held lock. → #4703 impossible by construction.

non-blocking-retry and blocking-with-timeout.

status/locking.py.

verdict_commit_queue.py's double survives migration.

(enforced by the FR-010 gate).

  • G1 — Caller supplies a dedicated lock-only path: caller passes lock_path,
  • G2 — No payload handle yielded: the value bound by with is a LockRecord
  • G3 — Release truncates (inode-stable), never unlinks.
  • G4 — Both sync + async over one _LockCore; blocking+timeout_s cover
  • G5 — Re-entrancy available (reentrant=True / thread-local) for
  • G6 — Test-double seam: a supported injection point so
  • G7 — Sole raw-primitive holder: the only module allowed raw msvcrt/fcntl

Non-goals

  • Not a distributed lock. Not a datastore lock. Loopback/local filesystem only.

Migration acceptance (per site)

Each migrated site: (a) constructs the canonical primitive, (b) no raw msvcrt/fcntl/filelock remains, (c) its allowlist entry in the FR-010 gate is removed, (d) the gate is green at that commit, (e) the site's own behaviour test (re-entrancy / timeout / cross-process, as applicable) passes under the SC-004 parity harness.

safe-delete-and-os-seam.md

Contract — Safe-delete util + OS-detection seam + gates

A. Safe-delete util (specify_cli/core/safe_delete.py)

def force_writable(path: Path) -> None:
    """Restore the owner write bit before removing a managed asset.
    NO-FOLLOW: chmod(stat.S_IMODE(path.lstat().st_mode) | stat.S_IWRITE)."""

def safe_unlink(path: Path) -> None: ...   # force_writable on retry, then unlink
def safe_rmdir(path: Path) -> None: ...     # force_writable on retry, then rmdir
def safe_rmtree(path: Path) -> None: ...    # rmtree with an onerror force-writable shim

Guarantees

outside the managed tree (FR-002, SC-006).

route through it, their private copies deleted (FR-003). The frozen migration m_3_2_0rc45… copy is untouched (C-002).

  • SD1 — no-follow (lstat) + masked (S_IMODE): never touches a symlink target
  • SD2 — Windows read-only refusal handled (clear bit → remove) (FR-001).
  • SD3 — one implementation; installer.py / agent_skills.py / asset_preparation.py

Acceptance test (SC-006, the negative proof): create a managed symlink whose target lies OUTSIDE the managed tree; delete via safe_unlink; assert the delete succeeds and the target's mode + existence are unchanged.

B. OS-detection seam (kernel/paths.py)

def is_windows() -> bool:
    """Canonical Windows check. Reach it via the module attribute so tests can
    monkeypatch `kernel.paths.is_windows` WITHOUT faking os.name."""
    return os.name == "nt"

Guarantees

(patchable), never an inline literal (FR-005).

module scope) may stay raw (documented allowlist in the FR-012 gate).

  • OS1 — consumers do from kernel.paths import is_windows and call is_windows()
  • OS2 — patchable without faking os.name (C-003).
  • OS3 — Windows-only C-module import guards (import msvcrt/import fcntl at

C. Gates (tests/architectural/) — DIRECTIVE_043

Both gates scan src/ ONLY (post-tasks squad MF-1) — a deliberate divergence from the clock template's SCAN_ROOTS=(src,tests,scripts); tests//scripts/ legitimately use raw primitives + platform branches (the parity harness must). Exemption files are per-WP (_exemptions/lock-ban-wp0N.txt) so parallel migration WPs shrink independently (S-2). There is no safe-delete gate — FR-003 is count-only (MF-4).

Lock gate (test_lock_primitive_ban.py) — FR-010, NFR-003

  • Import-ban: import msvcrt|fcntl|filelock, from filelock import … outside kernel/locks.py (src/ only).
  • Call-ban: msvcrt.locking(...), fcntl.flock/lockf(...), FileLock(...) outside kernel/locks.py.
  • Floor: assert kernel/locks.py contains msvcrt.locking AND fcntl.flock (door can't be deleted to pass).
  • Self-mutation: synthetic violation string must be flagged.
  • Allowlist: shrink-only per-WP files, seeded fail-closed with every current src/ site; excludes src/specify_cli/upgrade/migrations/.
  • Predicate honours the holder (C-004): permits kernel/locks.py's own raw calls + sidecar-record read.

OS-detection gate (test_os_detection_ban.py) — FR-012

platform.system() == "Windows", sys.platform.startswith("win") — outside kernel/paths.py (src/ only).

(if <win>: import msvcrt/fcntl). Routable branches (token_manager:109, pre_review_gate:260, abstract.py:44, compat/*) are ROUTED, not allowlisted.

  • Ban all four idioms (S-1) — os.name == "nt", sys.platform == "win32",
  • Floor + self-mutation as above.
  • Sanctioned-raw allowlist: the module-scope Windows-only C-module import guards ONLY

Terminal state

tracked follow-up sub-issue that exists (A-01/SC-005).

  • Lock allowlist: empty (full scope) OR exactly the deferred filelock sites with a
  • OS allowlist: import-guards only.