Split monolithic tai.py into tai_pkg package, keep tai-orig.py as original

tai.py (8643 lines) is now a thin facade over tai_pkg/ (33 feature modules, one class or feature per file). tai-orig.py preserves the pre-split single-file version byte for byte (sha256 verified against snapshot); import tai and ./tai.py behavior unchanged.

Equivalence proof, all run on this tree immediately pre-commit: differential battery tai-orig.py vs facade 3718 checks 0 failures (surface, pure fuzz, tool dispatch, store/audit, release ops, fake-HOME install roundtrip, CLI runs); test_tai.py 261 OK; test_seal.py 16 OK; --version/--help byte-identical; dir(tai) zero removals.

Fix included: opencode wire-protocol schema echo leaked into replies; opencode_parse now scans with JSONDecoder.raw_decode capped at 200 scans (13 OpencodeParseTests). Test count 261 = 237 HEAD + 8 pre-existing uncommitted ToolArgumentsTests (now committed with this change, disclosed) + 13 OpencodeParseTests + 3 EmbeddedStringsTests. Patch-target-only test migration (24 hunks), zero assertion changes. tai_pkg clean-regenerated after mid-verification tamper was found and reverted.
This commit is contained in:
2026-10-10 19:25:02 +02:00
parent da51e44359
commit 8df5cf7a45
39 changed files with 19175 additions and 8582 deletions
+130
View File
@@ -0,0 +1,130 @@
"""Shell side-effect snapshots and output spill helpers."""
import glob
import os
import re
import shlex
from tai_pkg.consts import Consts
from tai_pkg.text import Text
__all__ = ["ShellSnap"]
class ShellSnap:
"""ShellSnap."""
@staticmethod
def shell_target_paths(command, workdir):
spaced = re.sub(r"([;&|()<>])", r" \1 ", command)
try:
tokens = shlex.split(spaced, posix=True)
except ValueError:
return []
found = []
segment = []
def flush():
found.extend(ShellSnap.segment_targets(segment))
for token in tokens:
if token in Consts.SHELL_SEGMENT:
flush()
segment = []
else:
segment.append(token)
flush()
resolved = []
for item in found:
if len(resolved) >= Consts.SHELL_SNAPSHOT_MAX_FILES:
break
base = item if os.path.isabs(item) else os.path.join(workdir, item)
if any(mark in item for mark in ("*", "?", "[")):
try:
hits = sorted(glob.glob(base, recursive=True))
except (OSError, ValueError):
continue
else:
hits = [base]
for hit in hits:
if len(resolved) >= Consts.SHELL_SNAPSHOT_MAX_FILES:
break
if os.path.isfile(hit):
resolved.append(os.path.abspath(hit))
elif os.path.isdir(hit):
for root, _dirs, files in os.walk(hit):
for entry in sorted(files):
if len(resolved) >= Consts.SHELL_SNAPSHOT_MAX_FILES:
break
full = os.path.join(root, entry)
if os.path.isfile(full):
resolved.append(os.path.abspath(full))
if len(resolved) >= Consts.SHELL_SNAPSHOT_MAX_FILES:
break
seen = []
for item in resolved:
if item not in seen:
seen.append(item)
return seen
@staticmethod
def segment_targets(tokens):
targets = []
pos = 0
while pos < len(tokens):
token = tokens[pos]
if "=" in token and not token.startswith((">", "<", "-", "/")):
pos += 1
elif token in Consts.SHELL_WRAPPERS:
pos += 1
elif token == "timeout" and pos + 1 < len(tokens) and re.fullmatch(r"[0-9.]+[smhd]?", tokens[pos + 1] or ""):
pos += 2
else:
break
rest = tokens[pos:]
for index, token in enumerate(rest):
if token in (">", ">>") and index + 1 < len(rest):
cand = rest[index + 1]
if cand and not cand.startswith("&") and not cand.startswith("/dev/"):
targets.append(cand)
if not rest:
return targets
name = os.path.basename(rest[0])
operands = [item for item in rest[1:] if not item.startswith("-") or item in ("-", "--")]
operands = [item for item in operands if item not in (">", ">>", "<", "--")]
if name == "rm":
targets.extend(operands)
elif name in ("mv", "cp") and operands:
targets.append(operands[-1])
elif name == "tee":
targets.extend(operands)
elif name == "truncate":
for item in operands:
if re.fullmatch(r"[0-9]+[KMGTPE]?[iB]?", item or ""):
continue
targets.append(item)
elif name == "dd":
for item in operands:
if item.startswith("of="):
targets.append(item[3:])
elif name == "shred":
targets.extend(operands)
return [item for item in targets if item and item != "-"]
@staticmethod
def spill_output(store, kind, title, text, tags=(), profile=None):
profile = profile if profile is not None else store.profile
record_id = store.add_record(kind, title, text, tags, profile)
store.add_chunks("record", record_id, Text.chunk_lines(text), profile)
return record_id
@staticmethod
def spilled_view(text, record_id, size, head=1500, tail=500):
return text[:head] + "\n...[%d chars spilled to %s, record_read pages the rest]...\n" % (size, record_id) + text[-tail:]
@staticmethod
def spill_or_truncate(store, kind, title, text, tags=(), profile=None):
if store is None or len(text) <= Consts.SPILL_LIMIT:
return Text.truncate(text)
record_id = ShellSnap.spill_output(store, kind, title, text, tags, profile)
return ShellSnap.spilled_view(text, record_id, len(text))