import io

tp = r"C:\Users\decid\Documents\projects\spt-claude-code\traceable-reqs.toml"
t = io.open(tp, encoding="utf-8").read()


def sub(old, new):
    global t
    assert t.count(old) == 1, t.count(old)
    t = t.replace(old, new)


# 1. The accepted risk in REQ-COMMUNE-DROP-DETECT was not a risk. It was already the case.
sub(
    "ACCEPTED RISK, stated so it can be falsified rather than discovered: this design holds only while core's ingest is SLOWER than a hook cycle, which is what guarantees the drop is still on disk when the next hook looks; sub-second ingest would let a Bash-written drop vanish unseen. That is deliberately NOT a hazard requirement - it is a property of core's timing, not of our code, so it cannot be unit-tested and lives as a call-site comment naming the funnel/service path as its fallback.",
    "THE ACCEPTED RISK WAS ALREADY REALITY, and this is the correction (2026-09-06, v0.38.0, claude-spt-bs#26). The clause below was written as a remote hypothetical: 'holds only while core's ingest is SLOWER than a hook cycle; sub-second ingest would let a Bash-written drop vanish unseen'. MEASURED three times on the field node immediately after v0.37.0 shipped: ingest completes in 2.8s / 3.8s / 5.8s, and a hook cycle is routinely LONGER than that because the gap between hooks includes the agent's own thinking time. Two real field drops armed nothing and were never even EXAMINED. The >15s figure the design rested on was an estimate this requirement named as load-bearing and never measured - naming an assumption is not testing it, and the cheapest measurement in the project decided its outcome. The file legs are RETAINED and remain correct for what they cover (a drop still on disk, i.e. a commune written mid-work with another tool call following within a few seconds); REQ-COMMUNE-FRAME-BACKSTOP covers the rest, and the two are complementary by construction rather than redundant.",
)

# 2. the new requirement + its sibling entry
sub(
    """[[requirements]]
id = "REQ-HAZARD-COMMUNE-DROP-REARM\"""",
    '''[[requirements]]
id = "REQ-COMMUNE-FRAME-BACKSTOP"
title = "Once spt-core has INGESTED a commune, its `COMMUNE` io frame is the only surviving evidence of it - the drop file is deleted by the same act - so `Stop` polls `api io-events <id> --session-id <sid> --json` and arms the wake/clear boundary from a frame whose payload carries the wake marker. WHY, and it is the correction to REQ-COMMUNE-DROP-DETECT's central assumption: ingest is MEASURED at 2.8-5.8s on the field node, not the >15s that design assumed, and a hook cycle is routinely longer than that because the gap between hooks includes the agent's own thinking time. A commune written as a turn's FINAL act is therefore typically gone before that turn's Stop runs, and no file-stat leg can see it - which is precisely the handoff shape the whole feature exists for. THE TWO LEGS ARE COMPLEMENTARY, NOT REDUNDANT: the disk leg wins while the drop is still on disk (before ingest, no poll needed), the frame leg wins once it is not (after ingest), and between them the window has no gap. Ordered disk-then-frame at Stop so only an already-consumed drop costs the extra call, and both ahead of the quiet-window check so whichever arms, the same Stop holds the window and records stop_seen. AUTHENTICATION IS NOT THE OBSTACLE IT LOOKED LIKE: io-events takes --session-id as both cursor key and proof, and every hook carries a sid; it is a process WITHOUT one (a between-turns ResidentService) that cannot read the funnel. NEVER CLEAR ON A GUESS: a frame marked `truncated` is REFUSED BY NAME rather than armed or silently skipped - the payload caps at the 16KB class and the marker conventionally sits at the END of a commune, exactly what a cap cuts, so arming would reset a session that never asked for it; 28 COMMUNE frames were observed carrying no `digest_seq`, so there is no adapter-side follow, and that gap is filed upstream. THE SEEDING POLL IS BLIND BY DESIGN (a new session's first io-events poll returns nothing and seeds its cursor silently), so the seed is spent at SessionStart where nothing-returned is the expected answer - if the first Stop were the seeding poll, a commune ingested in the session's FIRST turn would be seeded past and lost, silently and only on the first turn."
required_stages = ["impl", "unit"]  # ACTIVATED 2026-09-06 (v0.38.0, claude-spt-bs#26). impl: hook.rs `arm_from_commune_frame` called from handle_stop after the disk leg, plus the SessionStart cursor seed. unit: the_frame_leg_arms_from_a_commune_core_already_ingested, the_frame_poll_authenticates_with_the_sessions_own_sid, a_plain_commune_frame_and_a_peer_message_never_arm (the kind filter - a peer MESSAGE quoting the marker is text, never a trigger), a_truncated_commune_frame_refuses_loudly_instead_of_guessing, the_frame_leg_stands_down_when_an_arm_is_already_pending, a_refused_frame_poll_is_loud, session_start_seeds_the_io_cursor_so_the_first_stop_is_not_the_blind_poll. Mutation-verified six ways including dropping the kind filter and swallowing the refusal. int DEFERRED: the live E2E is a real Bash-written across-commune, written as a turn's final act, firing a real boundary on this node - which is exactly the test v0.37.0 failed.

[[requirements]]
id = "REQ-HAZARD-COMMUNE-DROP-REARM"''',
)

# 3. the rearm hazard now guards two sources, not one
sub(
    "Both halves are load-bearing and both get a test - an arms-exactly-once test and a stat-not-read test asserting the cost model, since a detector that is correct but re-reads is a regression nobody would notice.",
    "Both halves are load-bearing and both get a test - an arms-exactly-once test and a stat-not-read test asserting the cost model, since a detector that is correct but re-reads is a regression nobody would notice. EXTENDED 2026-09-06 (v0.38.0) TO A SECOND SOURCE: with REQ-COMMUNE-FRAME-BACKSTOP the same drop can be seen TWICE - once on disk before ingest, once as a COMMUNE frame after - so the frame leg stands down whenever a clearing latch is already armed. One drop, one boundary, whichever leg saw it first.",
)
io.open(tp, "w", encoding="utf-8", newline="\n").write(t)
print("ok")
