#!/bin/sh
# ci/publish/mirror-public.sh — per-release SOURCE-ONLY mirror snapshot to the public repo
# (ADR-0008). The public repo (git remote "mirror", default SaberMage/claude-spt) receives ONE
# squashed snapshot commit of the release tree minus the enumerated exclusions, plus the vX.Y.Z
# tag at that snapshot — pushed with PLAIN git only (no gh, no GitHub API). The public tree is
# deliberately NOT buildable or installable, and the public repo gets NO GitHub releases.
#
# Modes:
#   sh ci/publish/mirror-public.sh <committish|tree>     # PLAN (default): build + guard + list, no network
#   sh ci/publish/mirror-public.sh --apply vX.Y.Z        # fetch mirror main, commit, push snapshot + tag
#
# The exclusion list below is THE authoritative enumeration (ADR-0008): every file that names the
# private home rides it. The leak guard hard-fails the mirror on any case-insensitive content OR
# filename match for the BigscreenVR org, the private repo name, or the -bs shorthand — a new leak
# vector is added HERE, consciously, never silently scrubbed.
# [impl->REQ-DIST-SOURCE-MIRROR]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
cd "$ROOT" || exit 1

MIRROR_REMOTE="${SPTC_MIRROR_REMOTE:-mirror}"

# Enumerated exclusions (paths relative to repo root; dirs exclude recursively).
EXCLUDES="
adapter/claude-spt.toml
ci/publish
dist
docs/RELEASE-RUNBOOK.md
docs/adr/0008-development-relocates-to-a-private-home.md
docs/plans/MIGRATION-RELEASE-PLAN.md
TAG-VERDICT-PLAN.md
BAG-MILESTONE-PLAN.md
PORTER-TRUSTCARRY-PLAN.md
SPILL-DETECT-PLAN.md
ALT-ACCOUNT-ROOTS-PLAN.md
SEAL-CARRY-PLAN.md
SKILL-TOOL-INJECTION-PLAN.md
ATTR-PASSTHROUGH-PLAN.md
README.md
AGENTS.md
traceable-reqs.toml
docs/TRACEABILITY.md
tests/mirror-public.sh
tests/manifest-shortcut.sh
adapter/strings/skills/setup.md
plugin/sptc/skills/setup/SKILL.md
"
# tests/mirror-public.sh + tests/manifest-shortcut.sh: both pin the private-home literal by
# design (leak-probe fixtures / the U3 coordinate guard) and both test plumbing the mirror
# already excludes (ci/publish, the adapter manifest) — meaningless in the public tree.
# setup.md + setup SKILL.md (v0.23.0): the setup flow's install command necessarily names the
# private home — shipped product surface, not scrubbable; the public tree is not installable
# anyway (manifest + release plumbing excluded).
# ALT-ACCOUNT-ROOTS-PLAN.md: same class — a JIT working plan citing the private board item.
# SPILL-DETECT-PLAN.md: same class again — a JIT working plan that cites the private board
# item and the core-side sibling tracker by coordinate. Its durable half lives in
# traceable-reqs.toml (REQ-SPILL-PARTIAL-INLINE / REQ-SPILL-REMAINDER-SKIP-DETECT), which the
# mirror already excludes; the plan itself is parked working state, not public product.
# PORTER-TRUSTCARRY-PLAN.md: same class as the two root plans above — a JIT working plan that
# cites the private release tracker's milestone/member coordinates. The DURABLE half of that work
# lives in docs/KNOWN-HAZARDS.md, which stays public and is written WITHOUT those coordinates.
# ATTR-PASSTHROUGH-PLAN.md, SEAL-CARRY-PLAN.md, SKILL-TOOL-INJECTION-PLAN.md: the same class,
# and added TOGETHER on purpose. ATTR-PASSTHROUGH-PLAN.md was the one actually tripping the guard
# (it cites the private board item by coordinate) and it had been red since the commit that minted
# it â a root JIT plan was landed without the EXCLUDES line every other root plan has. The other
# two pass the guard TODAY only because neither happens to name the private home yet, which is luck
# rather than a property: the guard pins org/repo literals, so "guard passed" has never meant
# "belongs in public". Every root *-PLAN.md is parked internal working state whose durable half
# lives in traceable-reqs.toml and docs/KNOWN-HAZARDS.md, so the class is excluded as a class
# instead of one file at a time after each new leak.

# Leak-guard patterns (case-insensitive, content + filenames).
GUARD_PATTERNS="bigscreenvr claude-spt-bs spt-bs"

APPLY=0
if [ "${1:-}" = "--apply" ]; then APPLY=1; shift; fi
REF="${1:-}"
[ -n "$REF" ] || { echo "usage: mirror-public.sh [--apply] <committish|vX.Y.Z>" >&2; exit 2; }

if [ "$APPLY" -eq 1 ]; then
  case "$REF" in
    v[0-9]*.[0-9]*.[0-9]*) ;;
    *) echo "FAIL: --apply requires a release tag (vX.Y.Z), got: $REF" >&2; exit 2 ;;
  esac
  git rev-parse -q --verify "refs/tags/$REF" >/dev/null || {
    echo "FAIL: tag $REF does not exist locally" >&2; exit 1; }
fi

# ── Build the snapshot tree in a throwaway index (worktree untouched). ──────────────────────────
TMPIDX=$(mktemp "${TMPDIR:-/tmp}/sptc-mirror-idx.XXXXXX") || exit 1
trap 'rm -f "$TMPIDX"' EXIT INT TERM
export GIT_INDEX_FILE="$TMPIDX"

git read-tree "$REF^{tree}" || { echo "FAIL: cannot read tree of $REF" >&2; exit 1; }
for p in $EXCLUDES; do
  git ls-files -z -- "$p" | git update-index -z --force-remove --stdin || exit 1
done
TREE=$(git write-tree) || exit 1
unset GIT_INDEX_FILE

# ── Leak guard: content + filenames, case-insensitive, against the STAGED tree. ─────────────────
leak=0
for pat in $GUARD_PATTERNS; do
  hits=$(git grep -i -l -e "$pat" "$TREE" 2>/dev/null | sed "s/^$TREE://")
  if [ -n "$hits" ]; then
    printf 'LEAK (content, pattern "%s"):\n%s\n' "$pat" "$hits" >&2; leak=1
  fi
  fhits=$(git ls-tree -r --name-only "$TREE" | grep -i -e "$pat" || true)
  if [ -n "$fhits" ]; then
    printf 'LEAK (filename, pattern "%s"):\n%s\n' "$pat" "$fhits" >&2; leak=1
  fi
done
[ "$leak" -eq 0 ] || { echo "FAIL: leak guard — add the file to EXCLUDES (consciously) or remove the reference" >&2; exit 1; }

# ── Assert the exclusions actually left the tree (belt + suspenders). ────────────────────────────
for p in $EXCLUDES; do
  if git ls-tree -r --name-only "$TREE" | grep -q "^$p\(/\|$\)"; then
    echo "FAIL: excluded path still present in snapshot tree: $p" >&2; exit 1
  fi
done

echo "snapshot tree: $TREE (source ref: $REF)"
echo "files: $(git ls-tree -r --name-only "$TREE" | wc -l | tr -d ' ')"

if [ "$APPLY" -eq 0 ]; then
  echo "--- snapshot listing (top 2 levels) ---"
  git ls-tree --name-only "$TREE"
  echo "DRY-RUN ok — re-run with --apply vX.Y.Z to push the snapshot + tag to remote '$MIRROR_REMOTE'."
  exit 0
fi

# ── Apply: squash snapshot on the mirror's main + tag, pushed by sha (no local refs created). ───
git fetch "$MIRROR_REMOTE" main || { echo "FAIL: cannot fetch $MIRROR_REMOTE main" >&2; exit 1; }
PARENT=$(git rev-parse FETCH_HEAD) || exit 1
if git rev-parse -q --verify "$PARENT^{tree}" >/dev/null && [ "$(git rev-parse "$PARENT^{tree}")" = "$TREE" ]; then
  echo "mirror already at this snapshot tree; nothing to push (tag push still attempted)."
  COMMIT=$PARENT
else
  COMMIT=$(git commit-tree "$TREE" -p "$PARENT" -m "release $REF — source snapshot

Squashed source-only snapshot of the $REF release tree. Release plumbing and
internal working files are excluded by the mirror contract; this tree is not
buildable or installable, and this repository carries no release assets.") || exit 1
fi
git push "$MIRROR_REMOTE" "$COMMIT:refs/heads/main" "$COMMIT:refs/tags/$REF" || {
  echo "FAIL: push to $MIRROR_REMOTE rejected" >&2; exit 1; }
echo "mirrored: $REF -> $MIRROR_REMOTE (commit $COMMIT)"
