#!/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
# NO PATHNAME EXPANSION. $EXCLUDES is expanded unquoted below and carries git pathspec
# patterns (`:(glob)*-PLAN.md`). Without -f the SHELL resolves them against the working
# tree and git is handed whatever happens to exist on disk instead of a pathspec over the
# COMMITTED tree - a guard reading the tree it exists not to trust, and passing.
set -f
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
:(glob)*-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).
# `:(glob)*-PLAN.md` - the root JIT-plan CLASS, excluded as a predicate rather than as a list.
# Every root *-PLAN.md is parked internal working state whose durable half lives in
# traceable-reqs.toml and docs/KNOWN-HAZARDS.md; a plan cites private board coordinates freely,
# so the class belongs out of the public tree by construction, not by anyone remembering.
#
# It became a predicate on 2026-08-28, having spent three commits as a lie: 3201839's comment
# already CLAIMED the class was "excluded as a class instead of one file at a time", while the
# code under it stayed eight enumerated literals. That is the shape that cost us once already -
# ATTR-PASSTHROUGH-PLAN.md was minted without its EXCLUDES line and the guard was red from
# d2355ed onward, undetected, because a rule keyed on the instances that exist today stays green
# until the case nobody raised arrives. The next plan file was always going to be that case.
#
# Two measured reasons this is spelled exactly the way it is, both of which fail SILENTLY:
#   `:(glob)` stops `*` at the `/`. Git pathspec globs match at any depth by default, so a bare
#   `*-PLAN.md` selects 42 files where the root class is 8 - it would have quietly unpublished
#   every docs/plans/*-PLAN.md, which IS public product.
#   `set -f` (below) stops the SHELL from expanding the pattern first. `for p in $EXCLUDES` is an
#   unquoted expansion, so without it the glob resolves against the WORKING TREE and git never
#   sees a pathspec - the guard would then be reading the very tree it exists not to trust.
# Both mistakes produce a guard that PASSES. Neither produces an error.

# 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; }
# Resolve every pattern to the CONCRETE paths it selects in the committed tree, ONCE, before
# anything is removed. Both the removal and the belt+suspenders assertion below run off this
# list. The assertion used to re-walk $EXCLUDES and interpolate each entry into a grep REGEX,
# which silently cannot work for a pathspec: `^:(glob)*-PLAN.md\(/\|$\)` matches no tree path,
# so the check would have passed by never testing anything - vacuously, for the one entry that
# most needed testing. A pattern that selects nothing is also reported here rather than ignored:
# a stale exclusion is a rule everyone believes is protecting them - but it is a NOTICE, not a
# failure: some entries here are DEFENSIVE (`dist` is build output that is never committed, and
# excluding it is what keeps it that way if it ever is). Measured immediately: hard-failing on
# an empty selection took `dist` down on the first run.
EXCL_PATHS=""
for p in $EXCLUDES; do
  m=$(git ls-files -- "$p") || exit 1
  if [ -z "$m" ]; then
    echo "notice: exclusion pattern selects nothing in $REF (defensive, or stale): $p" >&2
    continue
  fi
  EXCL_PATHS="$EXCL_PATHS$m
"
done
printf '%s' "$EXCL_PATHS" | git update-index --force-remove --stdin || exit 1
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). ────────────────────────────
SNAP=$(git ls-tree -r --name-only "$TREE") || exit 1
for f in $EXCL_PATHS; do
  if printf '%s
' "$SNAP" | grep -qxF "$f"; then
    echo "FAIL: excluded path still present in snapshot tree: $f" >&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)"
