"""Delete tag_scan's peer-dispatch half. Keep the commune shortcut and the transcript primitives."""
import io, re

p = r'C:\Users\decid\Documents\projects\spt-claude-code\tools\claude-spt\src\tag_scan.rs'
s = io.open(p, encoding='utf-8').read()
NL = chr(10)

lines = s.split(NL)


def find_item(name, kind='fn'):
    """Return (start,end) line indices of an item, including its leading /// doc block."""
    pat = re.compile(r'^(pub )?' + kind + r' ' + re.escape(name) + r'\b')
    for i, l in enumerate(lines):
        if pat.match(l):
            start = i
            # walk back over doc comments / attributes
            while start > 0 and (lines[start - 1].startswith('///')
                                 or lines[start - 1].startswith('#[')
                                 or lines[start - 1].startswith('//')):
                start -= 1
            # walk forward to the closing brace at column 0
            j = i
            while j < len(lines) and lines[j] != '}':
                j += 1
            return start, j
    raise AssertionError('not found: ' + kind + ' ' + name)


# order matters: collect ranges, then delete from the bottom up
targets = [
    ('code_ranges', 'fn'),
    ('in_code', 'fn'),
    ('parse_tag_sections', 'fn'),
    ('push_section', 'fn'),
    ('TagSection', 'struct'),
]
ranges = sorted((find_item(n, k) for n, k in targets), reverse=True)
for a, b in ranges:
    del lines[a:b + 1]

s = NL.join(lines)

# Dispatch loses its `sends` field; plan_dispatch loses the tag arm.
old_dispatch = '''/// The dispatch plan from a batch of new assistant-output texts: peer sends + commune bodies.
#[derive(Debug, PartialEq, Eq)]
pub struct Dispatch {
    pub sends: Vec<TagSection>,
    pub communes: Vec<String>,
}'''
new_dispatch = '''/// The dispatch plan from a batch of new assistant-output texts.
///
/// PEER SENDS ARE NO LONGER OURS. This struct carried a `sends: Vec<TagSection>` until the release
/// that declared `[io] compliance = true`: spt-core now parses `@<...@>` out of the ingest payloads
/// this adapter reports, and per-target outcomes reach the author through the now-signal's
/// DISPATCH_RESULTS category. The published rule is that the declaration and the deletion ride the
/// same change, so that no version exists in which both parsers see the same text and send it twice
/// — which is why this shrank to one field rather than keeping a disabled arm around.
/// [impl->REQ-IO-COMPLIANCE-DECLARED]
#[derive(Debug, PartialEq, Eq)]
pub struct Dispatch {
    pub communes: Vec<String>,
}'''
assert s.count(old_dispatch) == 1
s = s.replace(old_dispatch, new_dispatch)

old_plan = '''pub fn plan_dispatch(texts: &[String]) -> Dispatch {
    let mut sends = Vec::new();
    let mut communes = Vec::new();
    for t in texts {
        if let Some(body) = commune_body(t) {
            communes.push(body); // collision rule: a commune is NOT tag-scanned
        } else {
            sends.extend(parse_tag_sections(t));
        }
    }
    Dispatch { sends, communes }
}'''
new_plan = '''pub fn plan_dispatch(texts: &[String]) -> Dispatch {
    let mut communes = Vec::new();
    for t in texts {
        if let Some(body) = commune_body(t) {
            communes.push(body);
        }
    }
    Dispatch { communes }
}'''
assert s.count(old_plan) == 1
s = s.replace(old_plan, new_plan)

# plan_dispatch's doc comment describes an arm that no longer exists.
old_doc = '''/// Plan the dispatch from the new assistant-output texts (already sliced to what is new since the byte
/// cursor). Per text: a commune marker routes to `communes` and suppresses tag scanning (collision
/// rule / meta-recursion guard); otherwise the text is scanned for `@<…@>` peer-message sections.
/// [impl->REQ-TAG-PEER-MESSAGING] [impl->REQ-COMMUNE-OUTPUT-SHORTCUT]'''
new_doc = '''/// Plan the commune writes from the new assistant-output texts (already sliced to what is new since
/// the byte cursor). A text opening with the commune marker becomes a commune body; everything else
/// is ordinary output, which the hook layer reports to spt-core as its io payload and which core —
/// not this module — parses for shortform.
/// [impl->REQ-COMMUNE-OUTPUT-SHORTCUT] [impl->REQ-IO-COMPLIANCE-DECLARED]'''
assert s.count(old_doc) == 1
s = s.replace(old_doc, new_doc)

# The module header advertises a grammar this module no longer owns.
old_head_start = s.index('//! `tag_scan` —')
old_head_end = s.index('use serde_json::Value;')
new_head = '''//! `tag_scan` — the transcript-reading half of the hook's turn scan: the commune-output shortcut
//! plus the two primitives the io payload legs are built on. No I/O: the hook layer owns the
//! transcript tail, the commune-file write, and the cursor read/advance.
//!
//! **The peer-message grammar used to live here and is gone.** `@<targets body @>` is parsed by
//! spt-core now, out of the ingest payloads this adapter reports (`state busy --payload-stdin`,
//! `state idle --payload-stdin`, and the mid-turn `--mid` spans), enabled by `[io] compliance =
//! true` in our manifest. The published ordering rule is that the declaration and the deletion ship
//! in ONE change, so that no version exists in which core and an adapter both parse the same text
//! and send it twice — this module holding a copy of the grammar, even an unused one, is exactly
//! what that rule forbids. Per-target outcomes now reach the author through the now-signal's
//! DISPATCH_RESULTS category, which core documents as the only channel by design.
//! [impl->REQ-IO-COMPLIANCE-DECLARED]
//!
//! What remains is ours alone:
//!
//!   - **commune** `>>commune<<` at output start — the rest of that output is the commune body,
//!     written to `.claude/<id>-commune.md`. spt-core knows nothing of this marker; it carries the
//!     file's bytes through verbatim and acts on none of them.
//!   - `assistant_texts_from_jsonl` / `complete_line_prefix` — the transcript primitives the io
//!     payload legs read, and the byte cursor's guarantee that a complete line is consumed exactly
//!     once across the PreToolUse and Stop hooks (REQ-HAZARD-IO-SPAN-OVERLAP).

'''
s = s[:old_head_start] + new_head + s[old_head_end:]

# constants for the retired grammar
for dead in ['const OPEN: &str = "@<";' + NL, 'const CLOSE: &str = "@>";' + NL]:
    assert s.count(dead) == 1, dead
    s = s.replace(dead, '')

io.open(p, 'w', encoding='utf-8', newline=NL).write(s)
print('tag_scan stripped; remaining parse_tag_sections refs (tests still to go):', s.count('parse_tag_sections'))
