← back to Tk 11331 Exec

scripts/d2_daily_sweep.sh

167 lines

#!/usr/bin/env bash
# TK-11331 Decision 2 — hands-off DAILY SWEEP wrapper.
# One batch per invocation (launchd fires this once/day). Idempotent, fail-loud,
# self-stopping at 3600, self-disabling launchd job when done.
#
# Per run:
#   1. read persisted offset (init 431)
#   2. node d2_metafields.mjs --apply --offset=$OFFSET --limit=1000   (PG-mirror-first, then Shopify)
#   3. node verify_d2.mjs $OFFSET 1000   (Admin API = authoritative, NOT CDN)
#        -> ABORT + write FAILED marker + DO NOT advance offset if any NON-compliant
#   4. append the batch to ~/.claude/yolo-queue/executed-reversible/ledger.jsonl (via log-exec.mjs)
#   5. advance the offset state
#   6. when offset >= 3600: write DONE marker + self-disable (launchctl bootout own job) + ledger completion
#
# Kill switch (manual): launchctl bootout gui/$(id -u)/com.steve.tk11331-d2-sweep
# Clear a FAILED halt (manual, after investigating): rm data/d2-sweep-FAILED
set -euo pipefail

REPO="/Users/macstudio3/Projects/tk-11331-exec"
DATA="$REPO/data"
LEDGER_LOGGER="$HOME/.claude/yolo-queue/executed-reversible/log-exec.mjs"
JOB_LABEL="com.steve.tk11331-d2-sweep"
LIMIT=1000
AGENT="vp-dw-commerce"
TICKET="TK-11331"

STATE="$DATA/d2-sweep-state.json"
FAILED="$DATA/d2-sweep-FAILED"
DONE="$DATA/d2-sweep-DONE"
RUNLOG="$DATA/d2-sweep-runs.log"
LOCK="$DATA/.d2-sweep.lock"

cd "$REPO"
mkdir -p "$DATA"

ts(){ date '+%Y-%m-%dT%H:%M:%S%z'; }
say(){ echo "[$(ts)] $*" | tee -a "$RUNLOG"; }

# ---- single-flight lock (mkdir is atomic) ----
if ! mkdir "$LOCK" 2>/dev/null; then
  say "LOCK held ($LOCK) — a run is already in progress. Exiting."
  exit 0
fi
trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT

# ---- fail-loud halt: refuse to run while a prior FAILED marker stands ----
if [[ -f "$FAILED" ]]; then
  say "HALTED — prior FAILED marker present: $FAILED"
  say "         $(cat "$FAILED" 2>/dev/null | tr '\n' ' ')"
  say "         Investigate, then 'rm $FAILED' to resume. NOT advancing."
  exit 1
fi

# ---- already complete? ----
if [[ -f "$DONE" ]]; then
  say "DONE marker present — nothing to do. (Job should already be booted-out.)"
  exit 0
fi

# ---- total rows in the D2 set (dynamic; expected 3600) ----
TOTAL="$(node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync("data/d2_set.json","utf8")).length))')"
if ! [[ "$TOTAL" =~ ^[0-9]+$ ]]; then
  say "ERROR — could not read TOTAL from data/d2_set.json"; exit 1
fi

# ---- read / init offset ----
if [[ -f "$STATE" ]]; then
  OFFSET="$(node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).offset))' "$STATE")"
else
  OFFSET=431   # 431 already done+verified on Day-1 (indices 0..430)
  say "No state file — initializing offset=$OFFSET"
fi
if ! [[ "$OFFSET" =~ ^[0-9]+$ ]]; then
  say "ERROR — bad offset in state: '$OFFSET'"; exit 1
fi

# ---- stop condition: everything swept ----
if (( OFFSET >= TOTAL )); then
  say "COMPLETE — offset=$OFFSET >= total=$TOTAL. Writing DONE marker + self-disabling."
  printf 'D2 sweep complete: offset=%s reached total=%s at %s\n' "$OFFSET" "$TOTAL" "$(ts)" > "$DONE"
  node "$LEDGER_LOGGER" --agent "$AGENT" --ticket "$TICKET" \
    --action "D2 daily-sweep COMPLETE — all $TOTAL non-compliant products swept (min->5 + unit_of_measure 'Sold Per Yard'). Job self-disabling." \
    --blast "$TOTAL" \
    --undo "cd $REPO && node d2_rollback.mjs --apply" \
    --verify "cd $REPO && node verify_d2.mjs 0 $TOTAL  # expect NON-compliant=0" \
    2>&1 | tee -a "$RUNLOG" || say "WARN — completion ledger append failed (non-fatal)"
  launchctl bootout "gui/$(id -u)/$JOB_LABEL" 2>&1 | tee -a "$RUNLOG" || \
    say "NOTE — bootout returned non-zero (job may already be unloaded)."
  say "Self-disabled. Steve may now 'rm ~/Library/LaunchAgents/$JOB_LABEL.plist' to fully remove."
  exit 0
fi

# ---- how many rows THIS batch actually touches (last day is a partial) ----
REMAIN=$(( TOTAL - OFFSET ))
ROWS=$(( REMAIN < LIMIT ? REMAIN : LIMIT ))
say "START batch offset=$OFFSET limit=$LIMIT (rows=$ROWS, total=$TOTAL) — throttle <=1000/day"

# ---- 1) APPLY (PG mirror first, then Shopify — inside d2_metafields.mjs) ----
if ! node d2_metafields.mjs --apply --offset="$OFFSET" --limit="$LIMIT" 2>&1 | tee -a "$RUNLOG"; then
  printf 'APPLY FAILED at offset=%s (rows=%s) at %s — d2_metafields.mjs exited non-zero\n' \
    "$OFFSET" "$ROWS" "$(ts)" > "$FAILED"
  say "APPLY FAILED — wrote $FAILED, NOT advancing offset."
  exit 1
fi

# ---- 2) VERIFY via Admin API (authoritative). Fail loud on any non-compliant. ----
VERIFY_OUT="$(node verify_d2.mjs "$OFFSET" "$LIMIT" 2>&1)"
echo "$VERIFY_OUT" | tee -a "$RUNLOG"
NONCOMPLIANT="$(printf '%s' "$VERIFY_OUT" | grep -oE 'NON-compliant=[0-9]+' | grep -oE '[0-9]+' | tail -1)"
CHECKED="$(printf '%s' "$VERIFY_OUT" | grep -oE 'checked=[0-9]+' | grep -oE '[0-9]+' | tail -1)"

if [[ -z "$NONCOMPLIANT" || -z "$CHECKED" ]]; then
  printf 'VERIFY UNREADABLE at offset=%s (rows=%s) at %s — no NON-compliant/checked line from verify_d2.mjs. Output: %s\n' \
    "$OFFSET" "$ROWS" "$(ts)" "$(printf '%s' "$VERIFY_OUT" | tr '\n' ' ')" > "$FAILED"
  say "VERIFY UNREADABLE — wrote $FAILED, NOT advancing offset."
  exit 1
fi
if (( CHECKED == 0 )); then
  printf 'VERIFY checked=0 at offset=%s at %s — nothing was verified (suspicious).\n' \
    "$OFFSET" "$(ts)" > "$FAILED"
  say "VERIFY checked=0 — wrote $FAILED, NOT advancing offset."
  exit 1
fi
if (( NONCOMPLIANT != 0 )); then
  printf 'VERIFY FAILED at offset=%s (rows=%s) at %s — NON-compliant=%s (expected 0).\n' \
    "$OFFSET" "$ROWS" "$(ts)" "$NONCOMPLIANT" > "$FAILED"
  say "VERIFY FAILED — NON-compliant=$NONCOMPLIANT. Wrote $FAILED, NOT advancing offset. HALTED."
  exit 1
fi
say "VERIFY OK — checked=$CHECKED NON-compliant=0"

# ---- 3) LEDGER this batch (reversible tier) ----
NEXT=$(( OFFSET + LIMIT ))
node "$LEDGER_LOGGER" --agent "$AGENT" --ticket "$TICKET" \
  --action "D2 daily-batch: global.v_prods_quantity_order_min->5 + global.unit_of_measure->'Sold Per Yard' on $ROWS products (d2_set offset=$OFFSET..$(( OFFSET+ROWS-1 )); PG mirror first, then Shopify). Admin-API verified NON-compliant=0." \
  --blast "$ROWS" \
  --undo "cd $REPO && node d2_rollback.mjs --apply   # reverts full data/d2-restore-map.jsonl" \
  --verify "cd $REPO && node verify_d2.mjs $OFFSET $LIMIT  # expect NON-compliant=0" \
  2>&1 | tee -a "$RUNLOG" || say "WARN — ledger append failed (non-fatal); batch already applied+verified."

# ---- 4) ADVANCE offset state ----
node -e '
  const fs=require("fs");
  const [state,next,rows,total,ts]=[process.argv[1],+process.argv[2],+process.argv[3],+process.argv[4],process.argv[5]];
  fs.writeFileSync(state, JSON.stringify({offset:next,last_batch:{applied_rows:rows,advanced_at:ts},total},null,2)+"\n");
' "$STATE" "$NEXT" "$ROWS" "$TOTAL" "$(ts)"
say "ADVANCED offset $OFFSET -> $NEXT (applied $ROWS rows)."

# ---- 5) if that was the last batch, finish now (DONE + self-disable) ----
if (( NEXT >= TOTAL )); then
  say "That was the final batch — finalizing."
  printf 'D2 sweep complete: last batch offset=%s advanced to %s (total=%s) at %s\n' \
    "$OFFSET" "$NEXT" "$TOTAL" "$(ts)" > "$DONE"
  node "$LEDGER_LOGGER" --agent "$AGENT" --ticket "$TICKET" \
    --action "D2 daily-sweep COMPLETE — final batch landed; all $TOTAL non-compliant products swept. Job self-disabling." \
    --blast "$ROWS" \
    --undo "cd $REPO && node d2_rollback.mjs --apply" \
    --verify "cd $REPO && node verify_d2.mjs 0 $TOTAL  # expect NON-compliant=0" \
    2>&1 | tee -a "$RUNLOG" || say "WARN — completion ledger append failed (non-fatal)"
  launchctl bootout "gui/$(id -u)/$JOB_LABEL" 2>&1 | tee -a "$RUNLOG" || \
    say "NOTE — bootout returned non-zero (job may already be unloaded)."
  say "Self-disabled. Steve may 'rm ~/Library/LaunchAgents/$JOB_LABEL.plist' to fully remove."
fi

say "END batch — success."
exit 0