← 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