Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .agents/checks/review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Review Checks

Checks for AI review agents. The authoritative rules live in [SAFETY.md](../../SAFETY.md),
[AGENTS.md](../../AGENTS.md), and [docs/tcb-model.md](../../docs/tcb-model.md) — this file is
the reviewer's distillation.

- Look up every touched `pkg/` package in the SAFETY.md partition table first — the review bar
differs between the safety-critical core and the periphery. Flag core changes with 🌶️ and
state the blast radius (data corruption, lost writes, wrong-table swap, stranded slot).
- Core packages: every loop, queue, retry, and wait must be bounded. An unbounded anything in
a core package is a review-blocking defect.
- Dangerous APIs accept proof types (`statement.Classified`, `PreflightedTable`,
`VerifiedShadow`, `CleanWatermark`, `TableLock`) with package-private constructors — never a
raw string or bool that a caller could fabricate. Core code re-verifies its own
preconditions; it never trusts that the planner or CLI checked.
- Invariant enforcement points carry a `// INV: <id>` comment matching
[docs/invariants.md](../../docs/invariants.md); violations use `ErrInvariantViolation`
naming the ID and abort fail-closed — never a warning, never retried.
- New dependencies inside a core package require a recorded decision (core dependency list:
`pgx/v5`, `pglogrepl`, stdlib). `github.com/block/spirit` must never be imported as a
module — ideas are ported with citations, not code.
- Connections go through `pkg/dbconn` (bounded `lock_timeout` / `statement_timeout`) — flag
raw `pgx` pools in production code.
- SQL parsing goes through `pg_query_go`; flag `strings.Split(";")` or any hand-parsing. A
parse failure is an error surfaced to the caller.
- Errors: wrapped with context and identifiers; no log-and-continue, no silent branch cases,
no discarded `Close()` errors, no `nolint`, no `--no-verify`. No panics in library code —
invariant violations return `ErrInvariantViolation` fail-closed. Postgres errors are matched
by SQLSTATE (`errors.As` → `*pgconn.PgError`, `.Code`), never by message text.
- Goroutines: every goroutine has an owner, a bounded lifetime, and a stop path — flag
fire-and-forget `go func()`. Core logic takes time from an injected clock — flag inline
`time.Now()`/`time.Sleep` in core packages.
- Comments describe *what* and *why*, never history — flag bug/PR references,
"previously X" notes, and counts or thresholds that will go stale. Log messages state what
*will* happen, not what *might*. No internal company details (cluster names, hostnames,
org names) in code, comments, commits, or PRs.
- Tests: real PostgreSQL for core logic (no mocked-DB tests), testify, `t.Context()`
(cleanups use `context.WithoutCancel(t.Context())`), named polling deadlines — flag bare
`time.Sleep` readiness waits and any timeout increase that masks a flake instead of fixing
the root cause.
- CI coverage: behavior that varies by PostgreSQL major must be exercised across the
supported matrix (14–18), not just the default version.
1 change: 1 addition & 0 deletions .cursorrules
48 changes: 48 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/sh
set -e

# Pre-commit: fast local feedback on staged Go files. Docs-only commits
# (no staged .go files) skip the Go checks entirely so Markdown edits stay
# instant. CI remains the authoritative gate.
#
# Enable with: make setup (sets core.hooksPath .githooks)

# Ensure we're at the worktree root (not the git common dir). Unset GIT_DIR
# first — git sets it during hooks and it makes subprocess git commands
# resolve the wrong repo root.
unset GIT_DIR
cd "$(git rev-parse --show-toplevel)"

staged_go=$(git diff --cached --name-only --diff-filter=d -- '*.go')
if [ -z "$staged_go" ]; then
echo "pre-commit: no staged Go files; skipping Go checks."
exit 0
fi

# Ensure GOROOT matches the Go version in go.mod. External tools (hermit,
# asdf) may set GOROOT to a different version, causing "compile: version
# does not match go tool version" errors in golangci-lint and go build.
GO_VERSION=$(sed -n 's/^go //p' go.mod | head -1)
if [ -n "$GO_VERSION" ]; then
for candidate in \
"/opt/homebrew/opt/go/libexec" \
"/usr/local/go" \
"$HOME/sdk/go${GO_VERSION}" \
"$HOME/Library/Caches/hermit/pkg/go-${GO_VERSION}"; do
if [ -x "$candidate/bin/go" ]; then
CANDIDATE_VERSION=$("$candidate/bin/go" version 2>/dev/null | grep -o "go${GO_VERSION}" || true)
if [ -n "$CANDIDATE_VERSION" ]; then
export GOROOT="$candidate"
export PATH="$GOROOT/bin:$PATH"
break
fi
fi
done
fi
export PATH="$PATH:$HOME/go/bin:/usr/local/go/bin:/opt/homebrew/bin"

# Everything staged must still build.
go build ./...

# gofmt + golangci-lint on staged files: auto-fix, re-stage, verify.
scripts/lint-fix.sh
115 changes: 115 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/bin/sh
set -e

# Pre-push: run unit tests (go test -race, integration skipped) for the
# packages touched by the commits being pushed. Docs-only pushes skip
# entirely. CI remains the authoritative gate and runs the full suite
# including the PostgreSQL matrix — this hook does not replace it.
#
# Reads the standard pre-push stdin records:
# <local ref> <local sha> <remote ref> <remote sha>

unset GIT_DIR
cd "$(git rev-parse --show-toplevel)"

remote_name="$1"

# Ensure GOROOT matches the Go version in go.mod (see pre-commit).
GO_VERSION=$(sed -n 's/^go //p' go.mod | head -1)
if [ -n "$GO_VERSION" ]; then
for candidate in \
"/opt/homebrew/opt/go/libexec" \
"/usr/local/go" \
"$HOME/sdk/go${GO_VERSION}" \
"$HOME/Library/Caches/hermit/pkg/go-${GO_VERSION}"; do
if [ -x "$candidate/bin/go" ]; then
CANDIDATE_VERSION=$("$candidate/bin/go" version 2>/dev/null | grep -o "go${GO_VERSION}" || true)
if [ -n "$CANDIDATE_VERSION" ]; then
export GOROOT="$candidate"
export PATH="$GOROOT/bin:$PATH"
break
fi
fi
done
fi
export PATH="$PATH:$HOME/go/bin:/usr/local/go/bin:/opt/homebrew/bin"

# Resolve a base ref for new-branch pushes (no remote sha yet): prefer the
# push remote's default branch, then origin/main, then main.
base_ref=""
for cand in "refs/remotes/$remote_name/HEAD" "refs/remotes/$remote_name/main" "refs/remotes/origin/main" "main"; do
if git rev-parse --verify --quiet "$cand" >/dev/null 2>&1; then
base_ref="$cand"
break
fi
done

# is_zero_sha returns success when the sha is all zeros (branch create/delete
# sentinel), independent of SHA-1 vs SHA-256 length.
is_zero_sha() {
case "$1" in
*[!0]*) return 1 ;;
*) return 0 ;;
esac
}

changed_go_files=""
while read -r local_ref local_sha remote_ref remote_sha; do
# Skip branch deletions — nothing to test.
if is_zero_sha "$local_sha"; then
continue
fi

if is_zero_sha "$remote_sha"; then
# New branch on the remote: test what this branch introduces relative
# to the base. Fall back to the single tip commit if no base exists.
if [ -n "$base_ref" ]; then
range_base=$(git merge-base "$base_ref" "$local_sha" 2>/dev/null || true)
else
range_base=""
fi
if [ -z "$range_base" ]; then
range_base="${local_sha}~1"
fi
else
range_base="$remote_sha"
fi

files=$(git diff --name-only --diff-filter=d "$range_base" "$local_sha" -- '*.go' 2>/dev/null || true)
if [ -n "$files" ]; then
if [ -z "$changed_go_files" ]; then
changed_go_files="$files"
else
changed_go_files="$changed_go_files
$files"
fi
fi
done

changed_go_files=$(printf '%s\n' "$changed_go_files" | sed '/^$/d' | sort -u)
if [ -z "$changed_go_files" ]; then
echo "pre-push: no Go changes in pushed commits; skipping unit tests."
exit 0
fi

# Map changed files to package directories that still exist and contain Go
# source for the default build.
pkgs=""
for dir in $(printf '%s\n' "$changed_go_files" | xargs -n1 dirname | sort -u); do
testable_pkg=$(go list -e -f '{{if or (or .GoFiles .TestGoFiles) .XTestGoFiles}}{{.ImportPath}}{{end}}' "./$dir" 2>/dev/null || true)
if [ -d "$dir" ] && [ -n "$testable_pkg" ]; then
pkgs="$pkgs ./$dir"
fi
done

pkgs=$(printf '%s' "$pkgs" | sed 's/^ *//')
if [ -z "$pkgs" ]; then
echo "pre-push: changed Go files map to no testable packages; skipping unit tests."
exit 0
fi

echo "pre-push: running unit tests (race, integration skipped) for changed packages:"
for p in $pkgs; do echo " $p"; done

# shellcheck disable=SC2086
SKIP_INTEGRATION=1 go test -race -count=1 $pkgs
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
80 changes: 77 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,70 @@ on:
branches: [main]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
# Detect whether the change touches anything other than *.md / docs/**.
# PRs that touch only docs skip the heavy jobs below; pushes to main
# always run. Fails open: if detection fails, treat as a code change.
changes:
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
code: ${{ steps.out.outputs.code }}
steps:
- name: Detect non-docs changes
if: github.event_name == 'pull_request'
id: filter
continue-on-error: true
uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3
with:
filters: |
code:
- '**'
- '!**/*.md'
- '!docs/**'
- name: Resolve code-change flag
id: out
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
FILTER_OUTCOME: ${{ steps.filter.outcome }}
FILTER_CODE: ${{ steps.filter.outputs.code }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" != "pull_request" ]; then
code=true
elif [ "$FILTER_OUTCOME" != "success" ]; then
echo "::warning::paths-filter failed; running CI instead of skipping"
code=true
else
code="$FILTER_CODE"
fi
echo "code=$code" >> "$GITHUB_OUTPUT"
echo "event=$EVENT_NAME code=$code"

lint:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@v6
# Pin the same golangci-lint major used locally (v2 config format);
# the action's default binary lags and cannot load a v2 config.
- uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.12.2

build:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -25,9 +78,13 @@ jobs:
- run: make build

# The integration suite runs against every Aurora-supported PostgreSQL
# major (see the version-support research doc): the version floor is a
# promise CI enforces, not documentation.
# major (see docs/postgresql-version-support.md): the version floor is a
# promise CI enforces, not documentation. These are vanilla PostgreSQL
# images — real Aurora engine-version validation is a separate gate that
# cannot run in public CI.
test:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -41,3 +98,20 @@ jobs:
with:
go-version-file: go.mod
- run: make test

# Single required status for branch protection. Succeeds when nothing
# failed — including docs-only PRs where the heavy jobs were skipped.
ci-ok:
if: always()
needs: [changes, lint, build, test]
runs-on: ubuntu-latest
steps:
- name: Check job results
env:
RESULTS: ${{ toJSON(needs) }}
run: |
echo "$RESULTS"
if echo "$RESULTS" | grep -Eq '"result": *"(failure|cancelled)"'; then
echo "a required job failed or was cancelled"
exit 1
fi
48 changes: 45 additions & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@ version: "2"

linters:
enable:
- bodyclose
- misspell
- nolintlint
- errcheck # unchecked errors
- govet # suspicious constructs
- staticcheck # comprehensive static analysis
- ineffassign # useless assignments
- unused # unused code
- gocritic # opinionated but catches real bugs
- errorlint # proper error wrapping/comparison
- modernize # suggest modern Go idioms (wg.Go, etc.)
- noctx # requests without context
- bodyclose # unclosed HTTP response bodies
- usetesting # enforce t.Context() over context.Background() in tests
- unparam # unused/constant function parameters and results
- misspell # commonly misspelled words
- nolintlint # ill-formed or unexplained nolint directives
- revive # doc comments on exported symbols (see settings)
- gochecknoinits # no init() functions
- gochecknoglobals # no package-level mutable state (error sentinels exempt)
- containedctx # no context.Context stored in struct fields
settings:
usetesting:
context-background: true
context-todo: true
revive:
# Only the doc-comment rules; everything else revive offers is either
# covered by other linters or a judgment call that lives in AGENTS.md.
rules:
- name: exported
- name: package-comments
exclusions:
rules:
# Test helpers intentionally keep uniform signatures and fixed arguments
# for readability, so unused/constant params there are not worth churn.
- path: _test\.go
linters:
- unparam
# Test files and test-only support code may use fixtures and shared
# state; the no-globals bar applies to production code.
- path: (_test\.go|^internal/testutil/)
linters:
- gochecknoglobals

formatters:
enable:
- gofmt
- goimports
1 change: 1 addition & 0 deletions .goosehints
Loading
Loading