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
withis aLockRecord - G3 — Release truncates (inode-stable), never unlinks.
- G4 — Both sync + async over one
_LockCore;blocking+timeout_scover - 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_windowsand callis_windows() - OS2 — patchable without faking
os.name(C-003). - OS3 — Windows-only C-module import guards (
import msvcrt/import fcntlat
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 …outsidekernel/locks.py(src/ only). - Call-ban:
msvcrt.locking(...),fcntl.flock/lockf(...),FileLock(...)outsidekernel/locks.py. - Floor: assert
kernel/locks.pycontainsmsvcrt.lockingANDfcntl.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; excludessrc/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.