#!/usr/bin/env bash
# check-runtime-growth.sh — GROWTH guard for lang/runtime/el_runtime.c.
#
# Sibling to scripts/check-single-runtime.sh. That one guards against the file
# being COPIED (a lagging fork shipped to prod and dropped learned hebb edges).
# Nothing guarded against it GROWING — so it grew from 10,607 to 20,527 lines in
# 3.5 months, while under an explicit commit-message promise that it was a
# temporary shim about to be deleted.
#
# This enforces the RATCHET in lang/runtime/BUDGET: the numbers may only go down.
#
# It also checks two invariants that keep the multi-file runtime honest:
#   * every .c in lang/runtime/ is either in SOURCES or explicitly optional
#   * lang/install.sh's hardcoded download list matches SOURCES
#
# Exits non-zero on any violation. Run from anywhere; resolves the repo root.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

RUNTIME_DIR="lang/runtime"
TARGET="$RUNTIME_DIR/el_runtime.c"
BUDGET_FILE="$RUNTIME_DIR/BUDGET"
SOURCES_FILE="$RUNTIME_DIR/SOURCES"

FAIL=0

for f in "$TARGET" "$BUDGET_FILE" "$SOURCES_FILE"; do
  if [ ! -f "$f" ]; then
    echo "FATAL: required file missing: $f" >&2
    exit 1
  fi
done

budget() {
  local key="$1"
  sed -e 's/#.*//' "$BUDGET_FILE" | awk -v k="$key" '$1==k {print $2; found=1} END{if(!found) exit 1}'
}

MAX_LINES="$(budget max_lines)"       || { echo "FATAL: no 'max_lines' in $BUDGET_FILE" >&2; exit 1; }
MAX_ENGRAM="$(budget max_engram_fns)" || { echo "FATAL: no 'max_engram_fns' in $BUDGET_FILE" >&2; exit 1; }

# ---------------------------------------------------------------------------
# The message every failure prints. The guard that existed before this one told
# you what was wrong but not where the code should go — so it was easy to
# "fix" by arguing with the guard. This one names the destination.
# ---------------------------------------------------------------------------
where_it_goes() {
  cat >&2 <<'MSG'

  WHERE THE CODE ACTUALLY GOES
  ----------------------------
  Placement is a LINK-TIME concern. The compiler cannot tell which .c a symbol
  came from: `builtin_arity` in el-compiler/src/codegen.el maps NAME -> ARITY
  INT only, the El name is emitted as the exact C symbol, and `ld` resolves it.
  The SHIPPED compiler already links from ten translation units — check it:

      nm lang/dist/platform/elc | grep -E 'T _(engram_think|vindex_insert)'

  So a builtin defined in a sibling .c is EXACTLY as linkable as one defined in
  el_runtime.c. Pick the file that owns the concern:

      engram store ops ......... lang/runtime/engram_store.c
      ANN / vector index ....... lang/runtime/engram_vindex.c
      geometry, priming ........ lang/runtime/engram_geometry.c
      reasoning operators ...... lang/runtime/engram_reason.c
      grounding, consistency ... lang/runtime/engram_verify.c
      think, stance ............ lang/runtime/engram_cognition.c

  No existing file owns it?  Create one, add ONE line to lang/runtime/SOURCES,
  and every build path picks it up. Every runtime file EXCEPT el_runtime.c is
  deliberately uncapped.

  Belongs to a downstream program, not the runtime?  Declare
  `c_source "path/to/file.c"` in that program's manifest.el — elb already links
  it (parse_manifest_c_sources, lang/elb.el:82).

  See lang/AGENTS.md "Where a new C builtin goes".
MSG
}

# --- 1. Line-count ratchet ---------------------------------------------------
LINES="$(wc -l < "$TARGET" | tr -d ' ')"
if [ "$LINES" -gt "$MAX_LINES" ]; then
  echo "FAIL: $TARGET grew past its budget." >&2
  echo "      now:    $LINES lines" >&2
  echo "      budget: $MAX_LINES lines (lang/runtime/BUDGET: max_lines)" >&2
  echo "      over by: $((LINES - MAX_LINES))" >&2
  echo "" >&2
  echo "This file is a 2026-05-03 build shim that was scheduled for deletion and" >&2
  echo "never retired. It does not get to grow. Do NOT raise the budget." >&2
  where_it_goes
  FAIL=1
fi

# --- 2. Engram-concern ratchet ----------------------------------------------
# ~47.5% of el_runtime.c is engram code, and engram already owns six sibling
# files. This count is the Stage 3 scoreboard: it may only go down.
ENGRAM_FNS="$(grep -cE '^(static +)?[A-Za-z_][A-Za-z0-9_ *]*\b(engram|eg|cog)_[a-z0-9_]+\(' "$TARGET" || true)"
if [ "$ENGRAM_FNS" -gt "$MAX_ENGRAM" ]; then
  echo "FAIL: new engram/eg_/cog_ function(s) added to $TARGET." >&2
  echo "      now:    $ENGRAM_FNS definitions" >&2
  echo "      budget: $MAX_ENGRAM (lang/runtime/BUDGET: max_engram_fns)" >&2
  echo "" >&2
  echo "Engram code belongs in the six engram_*.c files that already exist." >&2
  where_it_goes
  FAIL=1
fi

# --- 3. Ratchet-down nudge (advisory, never fails) ---------------------------
if [ "$LINES" -lt "$MAX_LINES" ]; then
  echo "NOTE: $TARGET is $((MAX_LINES - LINES)) lines under budget — lower" >&2
  echo "      'max_lines' to $LINES in $BUDGET_FILE in this same commit, so the" >&2
  echo "      ground you gained cannot be quietly given back." >&2
fi
if [ "$ENGRAM_FNS" -lt "$MAX_ENGRAM" ]; then
  echo "NOTE: $((MAX_ENGRAM - ENGRAM_FNS)) engram fn(s) moved out — lower" >&2
  echo "      'max_engram_fns' to $ENGRAM_FNS in $BUDGET_FILE in this same commit." >&2
fi

# --- 4. Every runtime .c is accounted for ------------------------------------
# A new .c that is in neither SOURCES nor the optional list will not be
# compiled by any build path — it would be silently dead. Catch that here.
OPTIONAL_RE='^(el_android|el_gtk4|el_lvgl|el_sdl2|el_win32|el_runtime_win32|eg_cosine_batch_strategy_ggml|vindex_bench)\.c$'
mapfile -t IN_SOURCES < <(scripts/el-runtime-sources.sh)
for path in "$RUNTIME_DIR"/*.c; do
  base="$(basename "$path")"
  if printf '%s\n' "${IN_SOURCES[@]}" | grep -qxF "$base"; then continue; fi
  if [[ "$base" =~ $OPTIONAL_RE ]]; then continue; fi
  echo "FAIL: $path is in neither lang/runtime/SOURCES nor the platform-optional" >&2
  echo "      list in this guard. It will not be compiled by any build path." >&2
  echo "      Add it to SOURCES (one line), or add it to OPTIONAL_RE here if it" >&2
  echo "      is a platform/strategy variant that is linked in deliberately." >&2
  FAIL=1
done

# --- 5. install.sh must not drift from SOURCES -------------------------------
# install.sh runs on machines with no repo checkout, so it cannot call
# el-runtime-sources.sh and has to hardcode the list. That copy is exactly the
# kind of duplicate that silently drifted before — so it is checked, not trusted.
INSTALL_SH="lang/install.sh"
if [ -f "$INSTALL_SH" ]; then
  EXPECTED="$(scripts/el-runtime-sources.sh | sort)"
  ACTUAL="$(sed -n '/^RUNTIME_SOURCES=(/,/^)/p' "$INSTALL_SH" \
            | grep -oE '[a-z_0-9]+\.c' | sort)"
  if [ "$EXPECTED" != "$ACTUAL" ]; then
    echo "FAIL: $INSTALL_SH RUNTIME_SOURCES has drifted from $SOURCES_FILE." >&2
    echo "      Only in SOURCES:    $(comm -23 <(echo "$EXPECTED") <(echo "$ACTUAL") | tr '\n' ' ')" >&2
    echo "      Only in install.sh: $(comm -13 <(echo "$EXPECTED") <(echo "$ACTUAL") | tr '\n' ' ')" >&2
    echo "      An SDK that ships the wrong set produces a lib/ that cannot link." >&2
    FAIL=1
  fi
fi

if [ "$FAIL" -ne 0 ]; then
  exit 1
fi

echo "OK: el_runtime.c within budget ($LINES/$MAX_LINES lines, $ENGRAM_FNS/$MAX_ENGRAM engram fns);"
echo "    runtime sources accounted for; install.sh in step with SOURCES."
