Warden plan

26 - CR-25: ALCE citation-fidelity floor (current pipeline)

← eval suite index


tier: needs-model requires: [needs-postgres]


26 - CR-25: ALCE citation-fidelity floor (current pipeline)

Acceptance sentence. ALCE citation recall and precision of the deployed pipeline's wiki bodies against their cited signals are measured, recorded, and persisted as the pre-registered floor the artifact engine's renders (CR-24) must not come in below.

Criterion implemented (verbatim from the register)

Register: ~/.studio/master.withrobin.ai/project/rfc.one/criteria-register.md (status there: PENDING OWNER ACCEPTANCE; accepted by the 2026-08-31 proxy screen).

### CR-25 — ALCE citation-fidelity floor (current pipeline) - Statement: ALCE citation recall and precision of current wiki bodies against their cited signals, pre-registered as the citation-fidelity floor the engine's renders must not come in below. - Metric: ALCE citation recall and citation precision, NLI-computed with the 20% human check. - Dataset/inputs: Wiki bodies and their cited signals as stored in the deployed system (wikis.content; SIGNAL_CITED_BY_WIKI edges plus per-section citation declarations — verified in code at the 2026-08-31 proxy screen); an NLI model. - Threshold: None on the measurement itself — it establishes the floor; the engine's renders (CR-24) must clear it, and the render-time ALCE gate (§7 stage iv) uses it as the pre-registered floor. - Drop condition: n/a (floor measurement). - Source: Companion §11.3 (runnable-today floor form), §11.7. - Tier: runnable — the deployed system stores wiki bodies and cited signals; no new architecture. Pre-registered to run first (§11.7).

What it proves

The floor is real and recorded: wiki bodies with per-section citation_declarations exist in the corpus, and an NLI model judges each declared section against its cited signals. This is a measurement, not a bar — the plan gates only on the measurement completing over the register's dataset: every declaration in the corpus, every citation. A capped run (the smoke-budget defaults below) records the same observations and judgments but emits the verdict as an advisory skip and persists no floor — the register's dataset is "wiki bodies and their cited signals as stored in the deployed system", and a deterministic subsample of it is not that dataset. Only a full-coverage run lands the numbers in warden state (cr25_alce_recall_floor, cr25_alce_precision_floor) where the future CR-24 plan reads its non-inferiority floor.

Statement unit and judgment rules (pinned so the 20% human check audits the same thing the model judged):

Prerequisites

None of these exist on a fresh box — all are operator setup, and run.sh refuses to run any plan at all (exit 3) until the first is done:

Step 1: corpus probe + live NLI model probe

set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
# shellcheck disable=SC1091
source "${WARDEN_ENV_FILE:?WARDEN_ENV_FILE not set — run this plan via .warden/run.sh}"

NLI_MODEL="${WARDEN_NLI_MODEL:-anthropic/claude-haiku-4.5}"
CR25_DATA=0
CR25_LIVE=0

# Empty output means psql itself failed (command missing, greenlight-pg down,
# or schema absent) — an infra outage, not a corpus with 0 declarations. The
# headline verdict assertion is emitted in every branch so a pending floor is
# always visible in the run summary.
DECL_COUNT=$(psql "$DATABASE_URL" -tAc "
  select count(*)
  from wikis w, jsonb_array_elements(w.citation_declarations) d
  where w.deleted_at is null and jsonb_array_length(d->'signalIds') > 0;") || DECL_COUNT=
if [ -z "${DECL_COUNT:-}" ]; then
  warden_skip "CR-25 — corpus carries wiki bodies with citation declarations" "robin_ci unreachable (psql failed — greenlight-pg down, psql not on PATH, or schema absent); corpus state unknown, see raw log"
  warden_skip "CR-25 — ALCE citation-fidelity floor measured and recorded" "pending: robin_ci unreachable — floor not measurable this run"
elif [ "$DECL_COUNT" -gt 0 ]; then
  warden_pass "CR-25 — corpus carries wiki bodies with citation declarations" "$DECL_COUNT declarations with >=1 cited signalId"
  CR25_DATA=1
else
  warden_skip "CR-25 — corpus carries wiki bodies with citation declarations" "0 declarations in robin_ci — restore the corpus in the same invocation: bash .warden/run.sh --destructive 16-corpus-evals 26-cr-25 (a default all-plans run resets robin_ci between 16 and this plan)"
  warden_skip "CR-25 — ALCE citation-fidelity floor measured and recorded" "pending: corpus not restored in this invocation — floor not measurable this run"
fi

if [ "$CR25_DATA" = "1" ]; then
  PROBE_OUT=$(mktemp)
  PROBE_CODE=$(curl -sS --max-time 60 -o "$PROBE_OUT" -w '%{http_code}' \
    https://openrouter.ai/api/v1/chat/completions \
    -H "Authorization: Bearer ${OPENROUTER_API_KEY:?}" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg m "$NLI_MODEL" '{model:$m, temperature:0, max_tokens:2, messages:[{role:"user", content:"Reply with the single word OK."}]}')" ) || PROBE_CODE=000
  if [ "$PROBE_CODE" = "200" ] && jq -e '.choices[0].message.content' "$PROBE_OUT" >/dev/null 2>&1; then
    CR25_LIVE=1
  else
    warden_skip "CR-25 — ALCE citation-fidelity floor measured and recorded" "pending: no live OpenRouter key / NLI model on this box (probe HTTP $PROBE_CODE, model $NLI_MODEL)"
  fi
  rm -f "$PROBE_OUT"
fi

Step 2: NLI judgments → recall/precision floor → observations + state

set -uo pipefail
source "$WARDEN_LIB/assert.sh"
cd "${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"

if [ "${CR25_DATA:-0}" = "1" ] && [ "${CR25_LIVE:-0}" = "1" ]; then
  MAXD="${WARDEN_CR25_MAX_DECLS:-12}"
  JUDGMENTS="$WARDEN_DIR/runs/cr25-alce-judgments-$WARDEN_RUN_ID.jsonl"

  SAMPLE=$(psql "$DATABASE_URL" -tAc "
    select coalesce(jsonb_agg(r), '[]'::jsonb) from (
      select w.lookup_key as wiki, w.content as content,
             d->>'sectionAnchor' as anchor,
             jsonb_array_length(d->'signalIds') as ncited,
             (select coalesce(jsonb_agg(s.content order by s.lookup_key), '[]'::jsonb)
                from jsonb_array_elements_text(d->'signalIds') as cited(sig)
                join signals s on s.lookup_key = cited.sig
               where s.deleted_at is null) as sources
      from wikis w, jsonb_array_elements(w.citation_declarations) d
      where w.deleted_at is null and jsonb_array_length(d->'signalIds') > 0
      order by w.lookup_key, d->>'sectionAnchor'
      limit $MAXD
    ) r;")
  [ -n "${SAMPLE:-}" ] || SAMPLE='[]'

  # One statement = the section under the anchored heading, up to the next
  # heading of any depth. Same slug rule as 16-corpus-evals.
  extract_section() { # $1=content-file $2=anchor
    awk -v anchor="$2" '
      function slugify(s) { s=tolower(s); gsub(/[^a-z0-9]+/,"-",s); gsub(/^-+/,"",s); gsub(/-+$/,"",s); return s }
      /^#+[ \t]/ {
        h=$0; sub(/^#+[ \t]+/,"",h); sub(/[ \t]+$/,"",h); slug=slugify(h)
        base=anchor; sub(/-[0-9]+$/,"",base)
        if (insec) exit
        if (slug==anchor || slug==base) { insec=1 }
        next
      }
      insec { print }
    ' "$1"
  }

  NLI_CALLS=0
  NLI_ERRORS=0
  # Sets NLI_VERDICT (SUPPORTED | NOT_SUPPORTED | ERROR) in the caller's shell
  # — deliberately NOT $(...)-style, so the call counters survive.
  nli() { # $1=premise $2=hypothesis
    local body resp verdict
    NLI_CALLS=$((NLI_CALLS + 1))
    body=$(jq -n --arg p "$1" --arg h "$2" --arg m "$NLI_MODEL" '{
      model: $m, temperature: 0, max_tokens: 8,
      messages: [
        {role: "system", content: "You are a strict NLI judge. Answer with exactly one word: SUPPORTED if the premise fully supports every factual claim in the hypothesis, otherwise NOT_SUPPORTED."},
        {role: "user", content: ("Premise:\n" + $p + "\n\nHypothesis:\n" + $h)}
      ]}')
    if ! resp=$(curl -sS --max-time 120 https://openrouter.ai/api/v1/chat/completions \
      -H "Authorization: Bearer $OPENROUTER_API_KEY" \
      -H "Content-Type: application/json" \
      -d "$body"); then
      NLI_ERRORS=$((NLI_ERRORS + 1)); NLI_VERDICT=ERROR; return
    fi
    verdict=$(printf '%s' "$resp" | jq -r '.choices[0].message.content // empty' | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')
    case "$verdict" in
      SUPPORTED) NLI_VERDICT=SUPPORTED ;;
      NOT_SUPPORTED|NOTSUPPORTED) NLI_VERDICT=NOT_SUPPORTED ;;
      *) NLI_ERRORS=$((NLI_ERRORS + 1)); NLI_VERDICT=ERROR ;;
    esac
  }

  record() { # $1=wiki $2=anchor $3=kind $4=index $5=verdict $6=premise $7=hypothesis
    jq -cn --arg wiki "$1" --arg anchor "$2" --arg kind "$3" --arg idx "$4" \
      --arg verdict "$5" --arg model "$NLI_MODEL" \
      --arg psha "$(printf '%s' "$6" | sha256sum | cut -d' ' -f1)" \
      --arg hsha "$(printf '%s' "$7" | sha256sum | cut -d' ' -f1)" \
      '{wiki:$wiki, anchor:$anchor, kind:$kind, index:$idx, verdict:$verdict, model:$model, premise_sha256:$psha, hypothesis_sha256:$hsha}' \
      >> "$JUDGMENTS"
  }

  DECL_TOTAL=0; DECL_JUDGED=0; DECL_SUPPORTED=0; EXTRACT_MISS=0
  CIT_TOTAL=0; CIT_IRRELEVANT=0; CIT_TRUNC=0
  CONTENT_FILE=$(mktemp)
  trap 'rm -f "$CONTENT_FILE"' EXIT

  while IFS= read -r ROW; do
    DECL_TOTAL=$((DECL_TOTAL + 1))
    WIKI=$(printf '%s' "$ROW" | jq -r '.wiki')
    ANCHOR=$(printf '%s' "$ROW" | jq -r '.anchor')
    printf '%s' "$ROW" | jq -r '.content' > "$CONTENT_FILE"
    SECTION=$(extract_section "$CONTENT_FILE" "$ANCHOR")
    NSRC=$(printf '%s' "$ROW" | jq -r '.sources | length')
    if [ -z "${SECTION//[[:space:]]/}" ] || [ "$NSRC" = "0" ]; then
      EXTRACT_MISS=$((EXTRACT_MISS + 1))
      continue
    fi

    ALL_SOURCES=$(printf '%s' "$ROW" | jq -r '.sources[:6] | to_entries | map("[" + ((.key+1)|tostring) + "] " + .value) | join("\n\n")')
    NUSE=$(printf '%s' "$ROW" | jq -r '.sources[:6] | length')
    if [ "$NSRC" -gt 6 ]; then CIT_TRUNC=$((CIT_TRUNC + 1)); fi

    nli "$ALL_SOURCES" "$SECTION"; RECALL_V="$NLI_VERDICT"
    record "$WIKI" "$ANCHOR" recall all "$RECALL_V" "$ALL_SOURCES" "$SECTION"
    if [ "$RECALL_V" = "ERROR" ]; then continue; fi
    DECL_JUDGED=$((DECL_JUDGED + 1))
    if [ "$RECALL_V" = "SUPPORTED" ]; then DECL_SUPPORTED=$((DECL_SUPPORTED + 1)); fi

    i=0
    while [ "$i" -lt "$NUSE" ]; do
      SRC=$(printf '%s' "$ROW" | jq -r --argjson i "$i" '.sources[$i]')
      CIT_TOTAL=$((CIT_TOTAL + 1))
      nli "$SRC" "$SECTION"; ALONE_V="$NLI_VERDICT"
      record "$WIKI" "$ANCHOR" precision_alone "$i" "$ALONE_V" "$SRC" "$SECTION"
      if [ "$ALONE_V" = "NOT_SUPPORTED" ] && [ "$NUSE" -gt 1 ]; then
        REST=$(printf '%s' "$ROW" | jq -r --argjson i "$i" '.sources[:6] | del(.[$i]) | join("\n\n")')
        nli "$REST" "$SECTION"; REST_V="$NLI_VERDICT"
        record "$WIKI" "$ANCHOR" precision_rest "$i" "$REST_V" "$REST" "$SECTION"
        if [ "$REST_V" = "SUPPORTED" ]; then CIT_IRRELEVANT=$((CIT_IRRELEVANT + 1)); fi
      fi
      i=$((i + 1))
    done
  done < <(printf '%s' "$SAMPLE" | jq -c '.[]')

  warden_observe cr25_sampled_declarations "$DECL_TOTAL" "of ${DECL_COUNT:-?} in corpus (cap $MAXD); $EXTRACT_MISS unresolved (anchor/sources), $DECL_JUDGED judged, $CIT_TRUNC citation-truncated at 6"
  warden_observe cr25_nli_calls "$NLI_CALLS" "$NLI_ERRORS errors; model $NLI_MODEL, prompt cr25-nli-v1"

  if [ "$DECL_TOTAL" -gt 0 ] && [ "$EXTRACT_MISS" -le $((DECL_TOTAL / 5)) ]; then
    warden_pass "CR-25 — sampled declarations resolve to sections and cited signals" "$((DECL_TOTAL - EXTRACT_MISS))/$DECL_TOTAL resolved"
  else
    warden_fail "CR-25 — sampled declarations resolve to sections and cited signals" "$EXTRACT_MISS/$DECL_TOTAL unresolved — anchor drift or dangling signalIds; 16-corpus-evals owns that integrity gate"
  fi

  if [ "$DECL_JUDGED" -lt 5 ]; then
    warden_skip "CR-25 — ALCE citation-fidelity floor measured and recorded" "only $DECL_JUDGED declarations judged (<5) — corpus too thin for a floor; restore the full seed"
  elif [ "$NLI_CALLS" -gt 0 ] && [ "$NLI_ERRORS" -le $((NLI_CALLS / 5)) ]; then
    RECALL=$(awk -v s="$DECL_SUPPORTED" -v n="$DECL_JUDGED" 'BEGIN{printf "%.4f", s/n}')
    PRECISION=$(awk -v x="$CIT_IRRELEVANT" -v n="$CIT_TOTAL" 'BEGIN{ if (n==0) printf "%.4f", 1; else printf "%.4f", 1 - x/n }')
    warden_observe cr25_alce_citation_recall "$RECALL" "$DECL_SUPPORTED/$DECL_JUDGED sections supported by their full citation set"
    warden_observe cr25_alce_citation_precision "$PRECISION" "$CIT_IRRELEVANT/$CIT_TOTAL citations irrelevant (ALCE rule)"
    warden_observe cr25_human_check_frame "$JUDGMENTS" "sample >=20% of these judgments for the register's human check (out-of-band)"
    if [ "$DECL_TOTAL" -lt "${DECL_COUNT:-0}" ] || [ "$CIT_TRUNC" -gt 0 ]; then
      # A deterministic prefix of the corpus is not the register's dataset —
      # a truncated sample records advisory numbers but cannot become the
      # binding CR-24 floor, so nothing is persisted to warden state.
      warden_skip "CR-25 — ALCE citation-fidelity floor measured and recorded" "advisory only: caps truncated the register's dataset ($DECL_TOTAL/${DECL_COUNT:-?} declarations sampled, $CIT_TRUNC declaration(s) citation-truncated at 6) — floor NOT persisted; set WARDEN_CR25_MAX_DECLS >= ${DECL_COUNT:-?} and re-run to bind"
    else
      warden_save_state cr25_alce_recall_floor "$RECALL"
      warden_save_state cr25_alce_precision_floor "$PRECISION"
      warden_save_state cr25_alce_floor_provenance "run=$WARDEN_RUN_ID model=$NLI_MODEL n_decl=$DECL_JUDGED n_cit=$CIT_TOTAL"
      warden_pass "CR-25 — ALCE citation-fidelity floor measured and recorded" "recall=$RECALL precision=$PRECISION over $DECL_JUDGED declarations / $CIT_TOTAL citations (full corpus: ${DECL_COUNT:-?} declarations) — the CR-24 non-inferiority floor, no threshold by design"
    fi
  else
    warden_fail "CR-25 — ALCE citation-fidelity floor measured and recorded" "$NLI_ERRORS/$NLI_CALLS NLI calls failed (>20%) — floor not trustworthy; check OpenRouter availability and re-run"
  fi
fi

Shape (note for the next author)

Read-only against robin_ci; ride it in the same targeted invocation as 16-corpus-evals (bash .warden/run.sh --destructive 16-corpus-evals 26-cr-25) so the corpus is the fixture, not a local demo — in a default all-plans run 18-issue-337 resets robin_ci between the two. The measurement lands in three places on purpose: observations (trend line across runs, every run), warden state (cr25_alce_* — the future CR-24 plan compares engine renders against these exact keys; written only by a full-coverage run, never by a capped one), and the per-judgment JSONL (the 20% human-check frame; hashes bind each judgment to the exact premise/hypothesis text reconstructable from the DB). The NLI prompt is versioned cr25-nli-v1 in the observations — change the prompt, bump the version, and expect the floor to move: compare floors only within a prompt version and (per the companion §11.3) hold the judge model fixed when CR-24 eventually compares renders against this floor.