#!/usr/bin/env python3
"""mutate-w3.py apply|revert <arm> — WEBSERVE W3 gate mutation arms (doyle, 2026-09-08).

Each arm names ONE file, ONE exact old line, ONE new line. apply REFUSES unless the old line
matches exactly once (MATCH_COUNT==1) and prints the landed line ("MUTATION LANDED"); revert is
`git checkout -- <file>` and asserts 0 dirty lines. Old/new are Python string literals here,
never heredoc-passed, so no backslash level is lost (memory: a double backslash collapses inside
a quoted heredoc). Run from the gate worktree root.

Arms (pre-registered in GATE-W3-272-PLAN.md §3; the PREDICTED reds are the driver's to check):
  M1  docs_dir containment at LOAD accepts `..` -> predicted red: docs_dir_parent_traversal_is_refused_at_register
      (hertz, spt-runtime) + a_docs_dir_that_leaves_the_adapter_directory_is_refused_by_name (todlando);
      intact: docs_dir_absolute_path_is_refused_at_register, missing_docs_dir_registers_adapter, the resolve-time cell.
  M2  changelog_page stops stripping -> predicted red: traceability_comments_are_stripped_and_changelog_is_scanned
      + `xtask check` (regenerated page drifts AND the internal-codes scan fires); intact: non_comment_lines_preserve_bytes_and_order.
  M3  lanhost set gate skips the exe-sha compare -> predicted red: the_set_gate_refuses_by_name_and_serves_nothing
      at its sha-mismatch arm (2) + the lanhost lib unit asserting REFUSED_SHA_MISMATCH; intact: the happy-path e2e cell.
"""
import subprocess, sys

ARMS = {
    "M1": {
        "file": "crates/spt-store/src/serving.rs",
        "old": "            Component::Normal(_) | Component::CurDir => {}",
        "new": "            Component::Normal(_) | Component::CurDir | Component::ParentDir => {} // MUTATION M1: `..` accepted at load",
    },
    "M2": {
        "file": "crates/xtask/src/main.rs",
        "old": "    let stripped = strip_internal_codes(&changelog);",
        "new": "    let stripped = changelog.clone(); // MUTATION M2: the stripper is a no-op",
    },
    "M3": {
        "file": "crates/spt-daemon/src/lanhost.rs",
        "old": "    if !host.artifact_sha256.eq_ignore_ascii_case(exe_sha256) {",
        "new": "    if false && !host.artifact_sha256.eq_ignore_ascii_case(exe_sha256) { // MUTATION M3: sha compare skipped",
    },
}


def main():
    if len(sys.argv) != 3 or sys.argv[1] not in ("apply", "revert", "count") or sys.argv[2] not in ARMS:
        print("usage: mutate-w3.py apply|revert|count M1|M2|M3", file=sys.stderr)
        sys.exit(2)
    verb, arm = sys.argv[1], sys.argv[2]
    spec = ARMS[arm]
    path = spec["file"]
    if verb == "count":
        src = open(path, encoding="utf-8", newline="").read()
        print(src.count(spec["old"]))
        return
    if verb == "apply":
        src = open(path, encoding="utf-8", newline="").read()
        n = src.count(spec["old"])
        print(f"MATCH_COUNT={n}")
        if n != 1:
            print(f"REFUSE: arm {arm} needs exactly one site in {path}, found {n}", file=sys.stderr)
            sys.exit(7)
        out = src.replace(spec["old"], spec["new"], 1)
        open(path, "w", encoding="utf-8", newline="").write(out)
        for i, line in enumerate(open(path, encoding="utf-8", newline="").read().split("\n"), 1):
            if spec["new"] in line:
                print(f"MUTATED LINE {path}:{i}: {line.strip()}")
                print(f"MUTATION LANDED arm={arm}")
                return
        print("REFUSE: mutation did not land", file=sys.stderr)
        sys.exit(6)
    else:
        subprocess.run(["git", "checkout", "--", path], check=True)
        dirty = subprocess.run(["git", "diff", "--", path], capture_output=True, text=True).stdout
        lines = len(dirty.splitlines())
        print(f"REVERTED arm={arm} file={path} DIRTY_LINES={lines}")
        if lines:
            sys.exit(5)


if __name__ == "__main__":
    main()
