#!/usr/bin/env bash # Compare current `npm run typecheck` output to the known tech-debt snapshot. # Emits ONLY new errors introduced relative to the snapshot — does not dump # the full output, so agents can read the result without burning context. # # Comparison is line/col-insensitive: each error's `(L,C):` location is masked # to `(_,_):` before sorting + comm, so a pure line shift (e.g. one added/ # removed line above the error) doesn't trigger a phantom NEW + fixed pair. # Re-run `npm run typecheck` to see real positions for any errors flagged here. # # Caveat: this checks the working tree, not the staged worktree. If you stage # a fix but leave it unstaged, or vice versa, the diff reports the working-tree # state. For a strict pre-commit gate, run from a clean stash-apply state. # # `npm run check:eslint` is now a normal green check (0 errors); no snapshot # needed there. Run it directly if you want to see warnings. # # Usage: # bash docs/known-tech-debt-lint/diff.sh set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" BASELINE_DIR="$ROOT/docs/known-tech-debt-lint" TC_BASE="$BASELINE_DIR/typecheck.snapshot.txt" # Temp files registered for cleanup on any exit path (success, error, ^C). TMP_FILES=() cleanup() { [ "${#TMP_FILES[@]}" -gt 0 ] && rm -f "${TMP_FILES[@]}"; } trap cleanup EXIT INT TERM # Tracks whether any new errors were introduced. Set to 1 by run_typecheck_diff # when delta > 0; script exits with this code at end so CI / runbooks can use # the script as a gate (e.g. `bash diff.sh && echo OK`). NEW_ERRORS=0 run_typecheck_diff() { echo "=== typecheck diff vs known-tech-debt snapshot ===" local now tc_rc now="$(mktemp)" TMP_FILES+=("$now") ( cd "$ROOT" && npm run typecheck ) >"$now" 2>&1 tc_rc=$? # Sanity-check: distinguish "tsc ran and reported errors" (rc=2, expected on # this baseline) from "tsc/npm/node failed to run at all" (rc=other, broken # toolchain). Without this guard a rc=127 "tsc: not found" or rc=1 npm error # could produce stdout with no "error TS" lines and the diff would falsely # report "no new errors" / "incidentally fixed: 33". if [ "$tc_rc" -ne 0 ] && [ "$tc_rc" -ne 2 ] && ! grep -q "error TS" "$now"; then echo " ERROR: 'npm run typecheck' did not run cleanly (exit=$tc_rc):" sed 's/^/ /' "$now" NEW_ERRORS=2 return fi # Mask `(line,col):` so a pure line shift doesn't change an error's identity. # We compare on the masked form; for NEW lines we display the masked form (the # real position is reproducible by running `npm run typecheck` directly). # `sort` (NOT `sort -u`): we want to preserve cardinality so that two identical # masked errors in the same file aren't collapsed to one — a regression that # adds a duplicate error would otherwise be hidden by the first occurrence. local mask_re='s/\([0-9]+,[0-9]+\):/(_,_):/' local now_masked base_masked now_masked="$(mktemp)" base_masked="$(mktemp)" TMP_FILES+=("$now_masked" "$base_masked") sed -E "$mask_re" "$now" | grep -E "error TS" | sort > "$now_masked" sed -E "$mask_re" "$TC_BASE" | grep -E "error TS" | sort > "$base_masked" local new new="$(comm -23 "$now_masked" "$base_masked")" if [ -z "$new" ]; then echo " no new typecheck errors" else local count count="$(printf '%s\n' "$new" | wc -l)" echo " NEW errors: $count (line/col masked — run \`npm run typecheck\` for real positions)" printf '%s\n' "$new" | sed 's/^/ /' NEW_ERRORS=1 fi local fixed fixed="$(comm -13 "$now_masked" "$base_masked")" if [ -n "$fixed" ]; then local fcount fcount="$(printf '%s\n' "$fixed" | wc -l)" echo " (incidentally fixed: $fcount)" fi } run_typecheck_diff exit "$NEW_ERRORS"