#!/bin/sh
# Integration proof: `spt adapter translate-proof` spawns + feeds the claude-spt
# [message-idle-translation-binary] (`claude-spt translate` subcommand) EXACTLY as the daemon does at idle
# delivery (init line then the --event envelope), reads back the emitted keystroke-command stream,
# and gates it. This is the EMIT half of REQ-DIST-IDLE-TRANSLATE — the cross-process author-time proof
# (ADR-0022, the EMIT-half mirror of digest-proof). It does NOT exercise the daemon's atomic PTY APPLY
# or controller buffering — that half stays the real-claude bringup int (deferred). [int->REQ-DIST-IDLE-TRANSLATE]
#
# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV binary straight
# from its build dir against the bare-file manifest: --dir resolves the binary (before PATH) exactly
# as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior form
# staged a disposable install dir and `adapter add`/`adapter remove claude-spt`, which soft-removed
# the REAL registered claude-spt on cleanup. Read-only now, so NO SPTC_ACCEPTANCE gate. Needs
# spt >= 0.13.2 (the --dir/--manifest options) + a built binary. Idempotent.
# Run: sh ci/idle-translate/translate-proof-int.sh   (exit 0 = pass).
#
# GREEN against the {commit}-terminated binary: emits the 6-command choreography ctrl+s · 50ms ·
# {text:"<envelope>"} · 70ms · {key:enter} · {commit:true}, exit 0 / TRANSLATE_PROOF_OK / "commit: yes".
# The submit is a discrete {key:enter} AFTER the text (a trailing \r byte does NOT submit a CC message,
# corrected 2026-06-23). The no-{commit} form FAULTs (the F-016 defect the proof's no-commit gate catches).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
. "$ROOT/ci/lib/spt-probe.sh"
MANIFEST="$ROOT/adapter/claude-spt.toml"
RELDIR="$ROOT/tools/claude-spt/target/release"   # consolidated binary; manifest command = {adapter_dir}/claude-spt translate (D3)
EVENT='<EVENT type="msg" from="ci">translate-proof int probe</EVENT>'

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse, but through
# the positive-control'd probe: a bare `--help | grep -q -- '--dir'` cannot tell "the verb exists and
# predates --dir" from "the verb REFUSED", and skipping on the second is how a suite reports "too old"
# about a version it never measured. [impl->REQ-HAZARD-VERSION-GATE-FAIL-CLOSED]
sptc_flag_supported '--dir' spt adapter translate-proof
case $? in
  0) : ;;
  1) echo "SKIP: spt 'adapter translate-proof' has no --dir/--manifest (needs v0.13.2). Binary itself: cargo tests green."
     exit 0 ;;
  *) echo "FAIL: 'spt adapter translate-proof --help' did not answer, so the --dir capability is UNKNOWN." >&2
     echo "      Refusing to skip: that would report a version verdict this probe never measured." >&2
     echo "      If the verb was renamed or retired, port this int — see docs/plans/CORE-054-MIGRATION-PLAN.md." >&2
     exit 1 ;;
esac

BIN="$RELDIR/claude-spt"
[ -x "$BIN" ] || BIN="$RELDIR/claude-spt.exe"
[ -x "$BIN" ] || { echo "SKIP: binary not built (run sh ci/digest/build.sh)"; exit 0; }

# Proof the dev binary in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
# pins the bare-file gh_release manifest — no extracted install, no registry touch.
out=$(spt adapter translate-proof claude-spt --event "$EVENT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
rc=0
case "$out" in
  *TRANSLATE_PROOF_OK*) echo "ok  translate-proof: TRANSLATE_PROOF_OK" ;;
  *) echo "FAIL: translate-proof did not pass:"; printf '%s\n' "$out"; exit 1 ;;
esac
# The {commit} terminator is the F-016 fix — assert the proof saw it (no-commit would FAULT live).
case "$out" in
  *"commit: yes"*) echo "ok  emitted the mandatory {commit} terminator (commit: yes)" ;;
  *) echo "FAIL: proof reports no commit terminator (the F-016 fault condition):"; printf '%s\n' "$out"; rc=1 ;;
esac
# The submit is a discrete enter keypress (NOT a trailing \r in the text — a \r byte does not submit CC).
case "$out" in
  *enter*) echo "ok  emitted the discrete {key:enter} submit" ;;
  *) echo "FAIL: no {key:enter} submit in the command stream:"; printf '%s\n' "$out"; rc=1 ;;
esac
# ...and the text command must NOT carry a trailing \r anymore.
case "$out" in
  *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  text command carries no trailing \\r" ;;
esac
# The DEFAULT path for a well-formed peer message is now STUB+PARK (ADR-0007 Slice C): the body is
# parked and only `<msg from="…"/>` is typed. Assert that, because until 2026-08-04 this int asserted
# multi-line framing against THIS event and had been red-but-unrun ever since the stub migration —
# run-gates builds the idle-translate crate but never runs this int, so nobody saw it.
# [int->REQ-STUB-MSG-DELIVERY]
case "$out" in
  *'<msg from=\"ci\"/>'*) echo "ok  a well-formed peer msg delivers the STUB (body parked, not typed)" ;;
  *) echo "FAIL: expected the msg stub <msg from=\"ci\"/> on the default path:"; printf '%s\n' "$out"; rc=1 ;;
esac

# The envelope is FRAMED across multiple lines for visual distinction: a raw \n after the opening
# tag and before the closing </EVENT>. CC soft-newlines a bare \n (empirically gated 2026-06-24), so
# this renders as one user turn spanning lines, not an early submit. The emitted {text} carries the
# two deliberate framing newlines (JSON-escaped as \n in the command stream).
#
# DRIVEN THROUGH A FULL-DELIVERED ENVELOPE, and that is the whole point of the second run. Framing is
# a property of full delivery, which stub+park replaced as the DEFAULT but did not retire: an
# ineligible event (from-less here, also EVENT-PART / empty-or-whitespace body / truncated) and the
# sender-side full-inject override both still deliver "byte-identical to the pre-stub world —
# multi-line framed envelope, full choreography" (REQ-STUB-FULL-INJECT-OVERRIDE). Pointing this
# assertion at a stub-eligible event tested the wrong path and read as a broken contract when the
# contract was fine.
EVENT_FULL='<EVENT type="msg">translate-proof int probe</EVENT>'   # no `from` ⇒ stub-ineligible ⇒ full
outfull=$(spt adapter translate-proof claude-spt --event "$EVENT_FULL" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
# [int->REQ-DIST-IDLE-MULTILINE]
case "$outfull" in
  *'>\n'*'\n</EVENT>'*) echo "ok  full-delivered envelope framed across lines (\\n after opening tag and before </EVENT>)" ;;
  *) echo "FAIL: full-delivered text is not framed across lines (multi-line envelope missing):"; printf '%s\n' "$outfull"; rc=1 ;;
esac
# ...and it must NOT have parked/stubbed — otherwise the assertion above proves nothing about framing.
case "$outfull" in
  *'<msg from='*) echo "FAIL: the from-less envelope was stubbed; it is supposed to deliver FULL:"; printf '%s\n' "$outfull"; rc=1 ;;
  *) echo "ok  the ineligible envelope delivered FULL (never stubbed)" ;;
esac

# ── CHECKPOINT branch — ARM emit (the SPLIT clear/wake fix) ──────────────────────────────────────
# An envelope carrying `json="{"wake_arm":"v1",…}"` ARMS the wake in the translation binary's memory
# and emits CLEAR-ONLY — the wake is WITHHELD until a later `{"wake_fire":"v1"}` signal (self-sent
# by the SessionStart hook AFTER /clear completes). translate-proof feeds a single event to a FRESH
# process, so it proves the ARM emit (clear-only, no wake, no long settle); the stateful arm→fire→wake
# ordering is proven by the crate unit tests (arm_then_fire_orders_clear_before_wake) + the live E2E.
# The same block is the int evidence for the checkpoint-commune feature (envelope → detect → arm/clear)
# AND the race-fix split.
CKPT='<EVENT type="msg" from="self" json="{&quot;wake_arm&quot;:&quot;v1&quot;,&quot;directive&quot;:&quot;Resume now&quot;}">checkpoint requested</EVENT>'
ck=$(spt adapter translate-proof claude-spt --event "$CKPT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
# [int->REQ-DIST-CHECKPOINT-COMMUNE] [int->REQ-HAZARD-CHECKPOINT-CLEAR-RACE]
case "$ck" in
  *'text  "/clear"'*) echo "ok  checkpoint envelope ARMS + emits /clear" ;;
  *) echo "FAIL: checkpoint envelope did not emit /clear:"; printf '%s\n' "$ck"; rc=1 ;;
esac
# The wake is WITHHELD in the ARM emit (the split ordering fix — no wake text, no 500ms straddle).
case "$ck" in
  *'text  "Resume now"'*) echo "FAIL: ARM emit leaked the wake (must be withheld until fire):"; printf '%s\n' "$ck"; rc=1 ;;
  *) echo "ok  ARM emit withholds the wake (no race — fires only after /clear)" ;;
esac
case "$ck" in
  *'delay 500ms'*) echo "FAIL: ARM emit still carries the old 500ms straddle:"; printf '%s\n' "$ck"; rc=1 ;;
  *) echo "ok  ARM emit has no post-/clear straddle delay (retired)" ;;
esac
case "$ck" in
  *'commit: yes'*) echo "ok  ARM emit terminates with the mandatory {commit}" ;;
  *) echo "FAIL: ARM emit missing the {commit} terminator:"; printf '%s\n' "$ck"; rc=1 ;;
esac
# Default-wake checkpoint (no `directive` field) still ARMS + emits clear-only.
CKPT_DEF='<EVENT type="msg" from="self" json="{&quot;wake_arm&quot;:&quot;v1&quot;}">checkpoint requested</EVENT>'
ckd=$(spt adapter translate-proof claude-spt --event "$CKPT_DEF" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$ckd" in
  *'text  "/clear"'*) echo "ok  default-wake checkpoint ARMS + emits /clear" ;;
  *) echo "FAIL: default-wake checkpoint did not emit /clear:"; printf '%s\n' "$ckd"; rc=1 ;;
esac
# A normal (non-checkpoint) message must NOT fire the macro.
case "$out" in
  *'/clear'*) echo "FAIL: a normal message emitted /clear (checkpoint false-positive):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  normal delivery never fires the /clear macro" ;;
esac
# The RETIRED legacy shapes (`{"checkpoint":"v1",…}` / `{"checkpoint_fire":"v1"}`, accepted through
# v0.38.6 as the staged rename's receive-both window) are ordinary messages now: no /clear, no wake.
LEGACY='<EVENT type="msg" from="self" json="{&quot;checkpoint&quot;:&quot;v1&quot;,&quot;wake&quot;:&quot;Resume now&quot;}">legacy shape</EVENT>'
lg=$(spt adapter translate-proof claude-spt --event "$LEGACY" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
# [int->REQ-WAKE-LEGACY-RETIRED]
case "$lg" in
  *'/clear'*|*'text  "Resume now"'*) echo "FAIL: the retired legacy ARM shape still arms/clears:"; printf '%s
' "$lg"; rc=1 ;;
  *) echo "ok  retired legacy ARM shape is an ordinary message (no /clear, no wake)" ;;
esac
LEGACY_FIRE='<EVENT type="msg" from="self" json="{&quot;checkpoint_fire&quot;:&quot;v1&quot;}">legacy fire</EVENT>'
lgf=$(spt adapter translate-proof claude-spt --event "$LEGACY_FIRE" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$lgf" in
  *'<wake/>'*) echo "FAIL: the retired legacy FIRE shape still fires the wake stub:"; printf '%s
' "$lgf"; rc=1 ;;
  *) echo "ok  retired legacy FIRE shape fires nothing" ;;
esac

# ── BOUNDARY RENAME — the /clear-boundary name STASH (v0.15.1 fold) ──────────────────────────────
# An envelope carrying `json="{"rename":"v1","name":…}"` (self-sent by the SessionStart hook on a
# `clear` boundary, BEFORE the checkpoint_fire) STASHES the name and answers a bare {commit} — the
# rename keystrokes ride the fire's ONE combined sequence (two back-to-back inject sequences raced
# at the post-clear boundary and the wake submitted INSIDE the /rename argument — flynn 2026-07-06).
# translate-proof feeds a single event to a FRESH process, so it proves the STASH emit (bare commit,
# zero keystrokes); the stateful rename→fire combined emit is proven by the crate unit
# checkpoint_boundary_is_one_combined_sequence_rename_then_wake + the live e2e.
RN='<EVENT type="msg" from="self" json="{&quot;rename&quot;:&quot;v1&quot;,&quot;name&quot;:&quot;ci @ NODE (proj/)&quot;}">boundary rename</EVENT>'
rn=$(spt adapter translate-proof claude-spt --event "$RN" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
# [int->REQ-DIST-BOUNDARY-RENAME]
case "$rn" in
  *'/rename'*) echo "FAIL: rename envelope emitted keystrokes (must STASH-only — the fold):"; printf '%s\n' "$rn"; rc=1 ;;
  *) echo "ok  rename envelope emits NO keystrokes (stash-only, rides the fire's combined sequence)" ;;
esac
case "$rn" in
  *'commit: yes'*) echo "ok  rename stash answers the mandatory bare {commit}" ;;
  *) echo "FAIL: rename stash missing the {commit} terminator:"; printf '%s\n' "$rn"; rc=1 ;;
esac
# A normal message must NOT trigger the rename branch (the marker is json-attr-only, unforgeable
# from a body).
case "$out" in
  *'/rename'*) echo "FAIL: a normal message emitted /rename (rename false-positive):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  normal delivery never fires /rename" ;;
esac

# ── SPAWN RENAME — the startup/resume name+colour, emitted AT ONCE (v0.41.1, Request #40) ─────────
# The same directive flagged `"emit":"now"`: a spawn has no fire coming (only `clear` fires), so the
# binary emits the rename+colour sequence immediately instead of stashing it. This is the case a
# fresh process CAN prove end to end: the keystrokes must be in this one answer.
SR='<EVENT type="msg" from="self" json="{&quot;rename&quot;:&quot;v1&quot;,&quot;name&quot;:&quot;ci @ NODE (proj/)&quot;,&quot;color&quot;:&quot;orange&quot;,&quot;emit&quot;:&quot;now&quot;}">spawn rename</EVENT>'
sr=$(spt adapter translate-proof claude-spt --event "$SR" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
# [int->REQ-SESSION-COLOR]
case "$sr" in
  *'/rename ci @ NODE (proj/)'*) echo "ok  spawn rename emits /rename at once (no fire needed)" ;;
  *) echo "FAIL: spawn rename (emit:now) did not type /rename:"; printf '%s\n' "$sr"; rc=1 ;;
esac
case "$sr" in
  *'/color orange'*) echo "ok  spawn rename carries the colour half" ;;
  *) echo "FAIL: spawn rename (emit:now) did not type /color:"; printf '%s\n' "$sr"; rc=1 ;;
esac

[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }
