diff --git a/.gitignore b/.gitignore index 7917355..75f4e44 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,12 @@ yarn-error.log* .pnpm-store/ .tmp/ +# Local tool state: CodeGraph index, IDE, agent tooling. They are not gate +# inputs, and their churn would otherwise invalidate the pre-commit stamp. +.codegraph/ +.idea/ +.zcode/ + # Environment variables .env .env.local diff --git a/.husky/pre-commit b/.husky/pre-commit index 98475b5..6c60fd7 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -pnpm test +pnpm run check diff --git a/AGENTS.md b/AGENTS.md index f8ab1b9..d4b043e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,10 +42,8 @@ Complete ALL of these before ANY commit: - Review the full diff independently: every change necessary, correct, and not over-engineered. - Clean up generated temporary files and temporary or unhelpful comments. - Load the [code-review](https://github.com/mattpocock/skills/tree/main/skills/engineering/code-review) skill if available, run it to review all the changes, and independently verify or follow each of its suggestions when its multi-axis analysis done. -- Run the following commands concurrently for maximum efficiency - - First, run `pnpm run lint` to fix all lint errors. - - Second, run `pnpm run typecheck` to fix all type errors. - - Third, run `pnpm run coverage` to run and ensure 100% per-file coverage. Please capture the ALL content of the full output without truncation or tailing. +- Run `pnpm run check`: it runs `pnpm run lint`, `pnpm run typecheck` and `pnpm run coverage` concurrently, prints each tool's COMPLETE output, and fails when any of them does (the same logs stay in `.tmp/check-logs/`). + - Fix every lint and type error, and ensure 100% per-file coverage. Please capture the ALL content of the full output without truncation or tailing. - Example for per-file 100% coverage output: ``` % Coverage report from v8 @@ -54,6 +52,7 @@ Complete ALL of these before ANY commit: -------------------------|---------|----------|---------|---------|------------------- -------------------------|---------|----------|---------|---------|------------------- ``` + - The `pre-commit` hook runs this same gate, and a green run stamps the worktree into `.tmp/check.stamp`, so a commit of unchanged content skips it; `pnpm run check --full` forces a full re-run. ## Layout & responsive diff --git a/package.json b/package.json index f855151..cd2990c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "build": "tsdown", "lint": "oxlint", "lint:fix": "oxlint --fix", + "check": "bash scripts/check.sh", "watch": "bash scripts/watch.sh", "web": "bash scripts/web.sh", "register": "bash scripts/register.sh", diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..841114a --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# The pre-commit gate: lint, typecheck and the full coverage suite, run +# concurrently, each tool's complete output kept in .tmp/check-logs. +# +# A green run fingerprints the worktree into .tmp/check.stamp, so the husky +# hook skips the full suite for content that just passed. Any tracked or +# untracked edit invalidates the stamp, and so does a stamp older than an hour: +# gitignored inputs (lib/, node_modules, staged dsh baselines) can drift without +# changing the fingerprint. `--full` always re-runs. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +STAMP=".tmp/check.stamp" +LOGS=".tmp/check-logs" +INDEX=".tmp/check-index" +STAMP_TTL_MIN=60 + +# The fingerprint is the worktree's tree object, built through a THROWAWAY +# index: staged and unstaged edits plus untracked files all count, while +# gitignored outputs (.tmp, coverage, lib, node_modules, IDE/tool state) do not. +fingerprint() { + local tree + mkdir -p .tmp + rm -f "$INDEX" + if ! GIT_INDEX_FILE="$INDEX" git add -A -- . >/dev/null; then + rm -f "$INDEX" + return 1 + fi + tree="$(GIT_INDEX_FILE="$INDEX" git write-tree)" || tree="" + rm -f "$INDEX" + [[ -n "$tree" ]] || return 1 + printf '%s\n' "$tree" +} + +FULL=0 +[[ "${1:-}" == "--full" ]] && FULL=1 + +# Command substitutions do not inherit errexit on bash 3.2 (macOS), so both +# fingerprint calls check their status explicitly. +BEFORE="$(fingerprint)" || { echo "==> check: cannot fingerprint the worktree" >&2; exit 1; } +if (( FULL == 0 )) && [[ -f "$STAMP" ]] && [[ "$(cat "$STAMP")" == "$BEFORE" ]] \ + && [[ -n "$(find "$STAMP" -mmin "-$STAMP_TTL_MIN" 2>/dev/null)" ]]; then + echo "==> check: worktree unchanged since the last green gate — skipping. Run 'pnpm run check --full' to force a full run." + exit 0 +fi + +rm -rf "$LOGS" +mkdir -p "$LOGS" + +echo "==> check: lint, typecheck and coverage in parallel" +pnpm run lint >"$LOGS/lint.log" 2>&1 & LINT=$! +pnpm run typecheck >"$LOGS/typecheck.log" 2>&1 & TYPECHECK=$! +pnpm run coverage >"$LOGS/coverage.log" 2>&1 & COVERAGE=$! + +FAILED="" +wait "$LINT" || FAILED="$FAILED lint" +wait "$TYPECHECK" || FAILED="$FAILED typecheck" +wait "$COVERAGE" || FAILED="$FAILED coverage" + +for name in lint typecheck coverage; do + echo + echo "===== $name =====" + cat "$LOGS/$name.log" +done + +echo +if [[ -n "$FAILED" ]]; then + echo "==> check FAILED:$FAILED" >&2 + exit 1 +fi + +# A gate run over a moving worktree proves nothing, so an edit during the run +# must not be stamped as verified. +AFTER="$(fingerprint)" || { echo "==> check: cannot fingerprint the worktree" >&2; exit 1; } +if [[ "$BEFORE" != "$AFTER" ]]; then + echo "==> check: the worktree changed while the gate ran — re-run before committing." >&2 + exit 1 +fi + +printf '%s\n' "$BEFORE" > "$STAMP" +echo "==> check passed: lint, typecheck and 100% per-file coverage (lint/typecheck/coverage output above; logs in $LOGS)"