#!/usr/bin/env bash # CI guard: fail if any SELECT projection on `pages` that feeds rowToPage() # drops `source_id`. After v0.32.8, Page.source_id is required at the type # level; a projection that omits the column makes rowToPage return a Page # with source_id=undefined, which TypeScript's `: string` then lies about. # # This complements the type-system guard. The grep finds the specific 4-tuple # shape (id, slug, type, title) without source_id — the exact pre-v0.32.8 # pattern that codex's plan review flagged. # # Usage: scripts/check-source-id-projection.sh # Exit: 0 when no matches, 1 when matches found. set -euo pipefail ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" cd "$ROOT" # Allowlist: SELECT shapes that legitimately don't need source_id (single-col # `SELECT slug FROM pages` for getAllSlugs / resolveSlugs, SELECT id for # subqueries, COUNT, etc.) These don't feed rowToPage. # # The shape that DOES feed rowToPage starts `SELECT id, ... slug, ... type, ... title` # (in some order). The pattern below matches "id" + "slug" + "type" + "title" # in a SELECT projection — that's the rowToPage feeder signature. FOUND_BAD=0 # Use multiline-aware grep so the SELECT can span lines. pcre2grep would be # cleaner but isn't universally available; do a simple two-pass instead: # 1. Pull each SELECT-from-pages block. # 2. For each, check if it has the rowToPage signature WITHOUT source_id. check_file() { local file="$1" # Extract every SELECT...FROM pages block (across lines, up to 12 lines) # then test each. awk ' /SELECT/ { buf = $0 lines = 1 while (lines < 12 && (!match(buf, /FROM[[:space:]]+pages\b/))) { if ((getline next_line) <= 0) break buf = buf " " next_line lines++ } if (match(buf, /FROM[[:space:]]+pages\b/)) { # Has id, slug, type, title (rowToPage feeder) but NO source_id? if (match(buf, /\bid\b/) && match(buf, /\bslug\b/) && match(buf, /\btype\b/) && match(buf, /\btitle\b/) && !match(buf, /\bsource_id\b/)) { print FILENAME ": SELECT projection missing source_id:" print " " buf exit 1 } } } ' "$file" || return 1 return 0 } EXIT=0 for f in src/core/postgres-engine.ts src/core/pglite-engine.ts; do if ! check_file "$f"; then EXIT=1 fi done # Also check RETURNING clauses (putPage uses INSERT ... RETURNING). # Same shape: returns a row that feeds rowToPage. for f in src/core/postgres-engine.ts src/core/pglite-engine.ts; do awk ' /RETURNING/ { buf = $0 lines = 1 while (lines < 6 && !match(buf, /\`/)) { if ((getline next_line) <= 0) break buf = buf " " next_line lines++ } if (match(buf, /\bid\b/) && match(buf, /\bslug\b/) && match(buf, /\btype\b/) && match(buf, /\btitle\b/) && !match(buf, /\bsource_id\b/)) { print FILENAME ": RETURNING projection missing source_id:" print " " buf exit 1 } } ' "$f" || EXIT=1 done if [ "$EXIT" = 1 ]; then echo echo "ERROR: SELECT/RETURNING projection on \`pages\` is missing source_id." echo " After v0.32.8, Page.source_id is required at the type level." echo " Add \`source_id\` to the projection or rowToPage will lie." echo " See ~/.claude/plans/gleaming-soaring-mccarthy.md F2 finding." exit 1 fi echo "OK: all rowToPage feeder projections include source_id"