#!/usr/bin/env bash
#
# contrast-check - agora_theme - task T-608.
#
# Computes the WCAG contrast ratio of every foreground/background pair the
# colour token file DECLARES, and fails if any of them is below the threshold
# that pair declares for itself. T-606 wrote css/tokens.css specifically so that
# this script needs a regular expression rather than a CSS grammar; that file's
# header calls the shape below "the T-608 contract" and this script is the other
# half of it. Neither may be relaxed without amending the other.
#
# ---------------------------------------------------------------------------
# THE FORMULA, AND WHERE IT COMES FROM.
#
# WCAG 2.x relative luminance, read at source from
# https://www.w3.org/WAI/GL/wiki/Relative_luminance (the WCAG 2.x glossary entry
# for `relative luminance`, cross-referenced against
# https://www.w3.org/TR/WCAG22/#dfn-contrast-ratio for the ratio itself):
#
#   L = 0.2126 * R + 0.7152 * G + 0.0722 * B
#   where for each channel c = C8bit / 255:
#     if c <= 0.03928  then  channel = c / 12.92
#     else                   channel = ((c + 0.055) / 1.055) ^ 2.4
#
#   contrast ratio = (L1 + 0.05) / (L2 + 0.05), L1 the LIGHTER of the two.
#
# ON 0.03928 VERSUS 0.04045. The W3C page carries an erratum: the threshold that
# IEC 61966-2-1 actually specifies is 0.04045, and WCAG's 0.03928 is a rounding
# of it that the working group chose not to correct. The normative WCAG text
# says 0.03928, so 0.03928 is what this script uses.
#
# The difference cannot change any verdict here, and that is arithmetic rather
# than an assurance. The two thresholds disagree only for a channel value c with
# 0.03928 < c <= 0.04045. Every value this script can ever see is an 8-bit hex
# literal, so c is n/255 for an integer n, and:
#     n = 10  ->  10/255 = 0.039216  which is <= 0.03928   (linear branch, both)
#     n = 11  ->  11/255 = 0.043137  which is >  0.04045   (gamma branch, both)
# There is no 8-bit value between them. The band is EMPTY for this input domain,
# so no runtime check guards it: a check that cannot fire is decoration, and this
# repository has enough ways to go green already. The proof lives here instead.
#
# ---------------------------------------------------------------------------
# WHAT IT ASSERTS. A checker that only computes ratios goes green while the file
# it reads rots underneath it, so the arithmetic is assertion 7 of nine:
#
#   1. tokens parsed > 0            an empty scope proves nothing (I-007)
#   2. pairs parsed  > 0            `0 pairs checked - 0 below threshold` is a
#                                   FAILURE, not a pass (I-028). This is the
#                                   single most likely way this script rots into
#                                   decoration: delete the pair lines and the
#                                   arithmetic has nothing to be wrong about.
#   3. no duplicate token name      last-wins is how a palette silently becomes
#                                   a different palette
#   4. every @pair names two DEFINED tokens
#   5. every token is named by at least one @pair - an unpaired colour is a
#                                   colour whose contrast nobody checked
#   6. every declared minimum is one of WCAG's two thresholds, 4.5 or 3
#                                   (SC 1.4.3 text / SC 1.4.11 non-text). Without
#                                   this, dimming a token and editing its `4.5`
#                                   down to `1.2` passes, which would make the
#                                   whole script optional
#   7. every pair's computed ratio >= its declared minimum       <- the arithmetic
#   8. no malformed token line and no malformed @pair line. A line that misses
#                                   the strict shape by one character would
#                                   otherwise VANISH from the parse rather than
#                                   fail it, and a vanished pair is a pair nobody
#                                   checks. This is the same hole as 5, arriving
#                                   through a typo instead of an omission
#   9. no second palette anywhere beside it - see below
#
# ---------------------------------------------------------------------------
# ASSERTION 9, AND WHY A SECOND PALETTE IS NOT A FEATURE REQUEST.
#
# This script keys tokens by NAME in a flat map. A dark palette re-declares every
# token name inside a media query, so a flat parse keeps whichever declaration it
# read last and then reports a clean run having checked ONE palette out of TWO -
# a false green of exactly the species this project keeps catching. T-606 chose
# not to ship one for that reason, in writing, in its own header.
#
# Two guards, because the second palette can arrive through two doors:
#   (a) inside this file       -> assertion 3 catches it: 18 duplicate names.
#   (b) in a sibling stylesheet -> a redeclaration there escapes 3 entirely, so
#       every other *.css beside the token file is scanned for a token
#       DECLARATION of its own.
# An explicit `@media ... prefers-color-scheme` at-rule fails either way, with a
# message that says why, so whoever adds one is told what it breaks rather than
# left guessing at a duplicate-name finding.
#
# The at-rule is matched, never the STRING (I-018: match the definition, never
# the mention). css/tokens.css line 45 contains the words `prefers-color-scheme`
# in prose explaining that there is no such block; a string match would report a
# finding against the very comment that documents the rule.
#
# NOTE FOR T-610. Guard (b) fails if the base stylesheet declares an
# `--agora-color-*` custom property of its own - a component alias such as
# `--agora-color-table-rule: var(--agora-color-border)` is a finding here. That
# is deliberate and it is T-606's rule, not a new one: "Every token defined in
# exactly one file." Non-colour custom properties (`--agora-space-1`, and so on)
# are untouched - only the `--agora-color-` prefix is claimed.
#
# WHAT IS OUT OF SCOPE. Colour LITERALS in other stylesheets are T-610's
# invariant, not this one's. This script cares that no other file DECLARES a
# token; whether another file hard-codes `#c00` is a different assertion with a
# different dirty case. Two scripts, two failure messages, neither guessing at
# the other's job.
#
# ---------------------------------------------------------------------------
# THE DIRTY CASE (T-608's signed criterion, second half).
#
#   cp -r css "$T/css" && sed -i 's/--agora-color-text-inverse: #fff;/...dimmed.../' \
#     "$T/css/tokens.css" && tests/bin/contrast-check "$T/css/tokens.css"
#
# The optional argument exists FOR THAT and for nothing else: it lets the dirty
# case run against a COPY instead of mutating a tracked file. The gate calls this
# script with no argument at all. The sibling-stylesheet scan follows the
# argument - the palette directory is the token file's own directory - so a copy
# is checked exactly the way the original is.
#
# `--agora-color-text-inverse` is the token the dirty case dims, and the choice
# is measured rather than aesthetic: it appears in EXACTLY ONE @pair, so dimming
# it produces `1 below threshold` and the criterion's stated number is reachable.
# Most tokens do not have that property - `--agora-color-border` appears in three
# pairs and dimming it prints `3 below threshold`, which is correct behaviour and
# the wrong demonstration. `grep -oE '^/\* @pair (--\S+) (--\S+)' css/tokens.css`
# prints the per-token counts if the palette changes and the choice needs redoing.
#
# ---------------------------------------------------------------------------
# Contract:
#   scope:    the token file, plus every sibling *.css in its directory
#   scanned:  <N> file(s)   - N must be > 0 or this script exits 1
#   tokens parsed / pairs parsed  - both printed, both must be > 0
#   closing:  "<N> pairs checked - 0 below threshold"   (U+2014 EM DASH)
#   exit 0 iff structural findings == 0 AND pairs below threshold == 0
#
# THE CLOSING LINE USES AN EM DASH. T-608's signed success criterion, in the site
# template's specs/002-base-and-theme/tasks.md, reads: "Prints `N pairs checked -
# 0 below threshold` with `N` stated". That separator was read AS BYTES from the
# file - `342 200 224`, U+2014 - rather than by eye, the same way gate-a-theme.sh
# established its own. Nobody should tidy it into a hyphen without amending the
# signed criterion first.
#
# House rules for tests/bin/ (T-321(c)): `wc -l` never `grep -c` for a compared
# value; numeric guards defaulted ONLY where zero means FAIL; grep rc >= 2 is an
# error, never "no match"; never `-F` with `-i`.
#
# Usage: tests/bin/contrast-check [TOKEN_FILE]   (run from anywhere; it cd's to
#        the repo root, and resolves a relative TOKEN_FILE against it)

set -u
# No `set -e`: a finding must be recorded and counted, not abort the run.

# shellcheck disable=SC1007
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
# shellcheck disable=SC1007
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
cd "$REPO_ROOT" || { echo "FATAL: cannot cd to repo root $REPO_ROOT"; exit 1; }

C_BAD=""; C_OK=""; C_DIM=""; C_OFF=""
if [ -t 1 ]; then
  C_BAD=$(printf '\033[31m'); C_OK=$(printf '\033[32m')
  C_DIM=$(printf '\033[2m');  C_OFF=$(printf '\033[0m')
fi

fatal() { printf 'FATAL: %s\n' "$1" >&2; exit 1; }

# ------------------------------------------------------- grep's third state --
# I-027. `grep` has THREE exit states: 0 = matched, 1 = no match, >= 2 = grep
# itself FAILED. Inferring "no findings" from "no output" turns every grep error
# into a green run. Assertion 9's two scans are the only greps that decide
# anything here, and each one's status is read.
GREP_ERR_FILE=""
assert_grep_ok() {
  [ -s "$GREP_ERR_FILE" ] || return 0
  printf '\nFATAL: the scan did not complete - grep failed:\n' >&2
  sed 's/^/  /' "$GREP_ERR_FILE" >&2
  printf 'Nothing was scanned: this run has NO result, which is not a clean result. (I-027)\n' >&2
  exit 1
}

# ---------------------------------------------------------------- preflight --
# I-026: EXERCISE the tool, do not merely locate it. `command -v awk` succeeding
# says nothing about whether this host's awk can raise a float to 2.4, which is
# the one thing this script cannot work without. A `^` that silently returned 0
# would make every luminance 0, every ratio 1.0, and every pair a finding - loud.
# A `^` that silently returned its left operand would make every ratio wrong and
# some pairs pass. The probe compares an ACTUAL value against a known one.
command -v awk >/dev/null 2>&1 || fatal "required tool not found: awk"
AWK_PROBE=$(awk 'BEGIN { printf "%.6f", ((0.5 + 0.055) / 1.055) ^ 2.4 }' 2>&1)
[ "$AWK_PROBE" = '0.214041' ] || \
  fatal "awk cannot compute ((0.5+0.055)/1.055)^2.4 - expected 0.214041, got [$AWK_PROBE]. Every ratio below would be wrong, so nothing is reported."

TOKEN_FILE=${1:-css/tokens.css}
[ -e "$TOKEN_FILE" ] || fatal "token file not found: $TOKEN_FILE (empty scope)"
[ -f "$TOKEN_FILE" ] || fatal "token file is not a regular file: $TOKEN_FILE"
[ -r "$TOKEN_FILE" ] || fatal "token file is not readable: $TOKEN_FILE"
[ -s "$TOKEN_FILE" ] || fatal "token file is empty: $TOKEN_FILE (I-007: an empty scope proves nothing)"

# shellcheck disable=SC1007
PALETTE_DIR=$(CDPATH= cd -- "$(dirname -- "$TOKEN_FILE")" && pwd) || fatal "cannot resolve the directory of $TOKEN_FILE"
TOKEN_ABS=$PALETTE_DIR/$(basename -- "$TOKEN_FILE")

RECORDS_FILE=$(mktemp -t contrast-check.XXXXXX) || fatal "cannot create a temp file"
GREP_ERR_FILE=$(mktemp -t contrast-check-grep.XXXXXX) || fatal "cannot create a temp file"
trap 'rm -f "$RECORDS_FILE" "$GREP_ERR_FILE"' EXIT

# --------------------------------------------------------- the parse + math --
# One awk pass. Everything it decides is emitted as a tab-separated record and
# counted in the shell, so no number below is a variable the parser could have
# forgotten to update.
#
#   T <name> <hex> <line>                      a token, accepted
#   P <fg> <bg> <min> <ratio> <verdict> <note> a pair, computed
#   F <line> <reason>                          a structural finding
#
# The hex character classes are written out longhand rather than with `{3}` and
# `{6}`: ERE interval expressions are optional in POSIX awk and mawk 1.3.3 does
# not have them. Nothing here needs to find out which awk the runner ships.
awk -v OFS='\t' '
  function hex_digit(ch,   i) {
    i = index("0123456789abcdef", ch)
    return i - 1
  }
  # Expand a 3-digit literal by doubling each digit, per CSS Color: #abc == #aabbcc.
  # T-606 records WHY the short form must survive here: stylelint-config-standard
  # sets color-hex-length: short, core extends it, and the job is blocking - so
  # `#ffffff` is a lint ERROR and the parser is what adapts.
  function expand(hex,   h) {
    h = substr(hex, 2)
    if (length(h) == 3) return substr(h,1,1) substr(h,1,1) substr(h,2,1) substr(h,2,1) substr(h,3,1) substr(h,3,1)
    return h
  }
  function chan(v,   c) {
    c = v / 255
    if (c <= 0.03928) return c / 12.92
    return ((c + 0.055) / 1.055) ^ 2.4
  }
  function lum(hex,   h, r, g, b) {
    h = expand(hex)
    r = hex_digit(substr(h,1,1)) * 16 + hex_digit(substr(h,2,1))
    g = hex_digit(substr(h,3,1)) * 16 + hex_digit(substr(h,4,1))
    b = hex_digit(substr(h,5,1)) * 16 + hex_digit(substr(h,6,1))
    return 0.2126 * chan(r) + 0.7152 * chan(g) + 0.0722 * chan(b)
  }
  function ratio(a, b,   l1, l2, t) {
    l1 = lum(a); l2 = lum(b)
    if (l1 < l2) { t = l1; l1 = l2; l2 = t }
    return (l1 + 0.05) / (l2 + 0.05)
  }
  function finding(ln, why) { print "F", ln, why }

  {
    line = $0
    sub(/\r$/, "", line)

    # ---- a pair line -----------------------------------------------------
    # Anchored at the start of the line. The header prose of the token file
    # contains the word @pair while explaining the shape, so the DEFINITION is
    # discriminated from the MENTION by requiring a `--` token reference on the
    # same line (I-018). An indented or otherwise malformed pair line therefore
    # FAILS instead of vanishing from the parse.
    if (line ~ /@pair/ && line ~ /--/) {
      if (line ~ /^\/\* @pair --[^ ]+ --[^ ]+ [0-9]+(\.[0-9]+)? .+ \*\/$/) {
        body = substr(line, 10)
        body = substr(body, 1, length(body) - 3)
        n = split(body, f, / +/)
        pn++
        pfg[pn] = f[1]; pbg[pn] = f[2]; pair_min[pn] = f[3]; pair_line[pn] = NR
        note = ""
        for (i = 4; i <= n; i++) note = note (i > 4 ? " " : "") f[i]
        pair_note[pn] = note
      } else {
        finding(NR, "malformed @pair line - it names tokens but does not match the declared shape `/* @pair FG BG MIN NOTE */`, so it would be silently DROPPED from the parse rather than checked")
      }
      next
    }

    # ---- a token declaration ---------------------------------------------
    # Same discrimination: `--agora-color-` immediately followed by a name and a
    # colon is a DECLARATION; the same string inside prose is not.
    if (line ~ /--agora-color-[a-z0-9-]*[ \t]*:/) {
      if (line ~ /^  --agora-color-[a-z0-9-]+: (#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]|#[0-9a-f][0-9a-f][0-9a-f]);$/) {
        eq = index(line, ":")
        name = substr(line, 3, eq - 3)
        hex = substr(line, eq + 2)
        sub(/;$/, "", hex)
        if (name in tok) {
          finding(NR, "duplicate token " name " - first declared on line " token_line[name] ". A flat name-to-value parse keeps ONE of the two, so a redeclaration silently changes the palette this script believes it checked. The FIRST value is kept here and the run fails.")
        } else {
          tok[name] = hex; token_line[name] = NR; tn++
          print "T", name, hex, NR
        }
      } else {
        finding(NR, "malformed token declaration - a token value must be exactly two spaces, the name, `: `, a BARE 3- or 6-digit lowercase hex literal and `;`. This line would be silently DROPPED from the parse, and a dropped token is a colour nobody checked (T-606 contract rules 1 and 2)")
      }
      next
    }
  }

  END {
    # ---- assertion 4: every pair names two defined tokens; then 6, then 7.
    for (i = 1; i <= pn; i++) {
      bad = 0
      if (!(pfg[i] in tok)) { finding(pair_line[i], "@pair names an undefined foreground token " pfg[i]); bad = 1 }
      if (!(pbg[i] in tok)) { finding(pair_line[i], "@pair names an undefined background token " pbg[i]); bad = 1 }
      used[pfg[i]] = 1; used[pbg[i]] = 1
      if (bad) continue

      # assertion 6. WCAG has two thresholds and this file may declare only
      # those two: 4.5 for text (SC 1.4.3) and 3 for large text and non-text
      # (SC 1.4.11). An unrecognised minimum is evaluated against the STRICTER
      # of the two, so editing `4.5` down to something convenient cannot buy a
      # pass while the finding is being ignored.
      m = pair_min[i] + 0
      if (m != 4.5 && m != 3) {
        finding(pair_line[i], "declared minimum " pair_min[i] " is not a WCAG threshold - only 4.5 (SC 1.4.3, text) and 3 (SC 1.4.11, large text and non-text) are allowed. Evaluated against 4.5 for this run.")
        m = 4.5
      }

      r = ratio(tok[pfg[i]], tok[pbg[i]])
      # assertion 7. Compared UNROUNDED: 4.4996 is below 4.5, and rounding it to
      # 4.50 before the comparison would be a weakening dressed as tidiness. The
      # printed value is rounded; the compared value is not.
      print "P", pfg[i], pbg[i], pair_min[i], sprintf("%.2f", r), (r >= m ? "ok" : "BELOW"), pair_note[i]
    }

    # ---- assertion 5: every token is named by at least one pair.
    for (name in tok) {
      if (!(name in used)) {
        finding(token_line[name], "token " name " is declared but named by NO @pair - an unpaired colour is a colour whose contrast nobody checked. Declare its pairs, or delete the token.")
      }
    }
    print "C", "tokens", tn + 0
    print "C", "pairs", pn + 0
  }
' "$TOKEN_ABS" > "$RECORDS_FILE"
AWK_RC=$?
[ "$AWK_RC" -eq 0 ] || fatal "awk exited $AWK_RC while parsing $TOKEN_FILE - nothing was computed, so nothing is reported (I-029)."

# --------------------------------- assertion 9: no second palette beside it --
# Two scans, both with grep's third state read. `find -maxdepth 1` rather than a
# glob so an empty directory is an empty list instead of a literal `*.css`.
SIBLINGS=0
SIBLING_LIST=""
SECOND_PALETTE_FILE=$(mktemp -t contrast-check-9.XXXXXX) || fatal "cannot create a temp file"
trap 'rm -f "$RECORDS_FILE" "$GREP_ERR_FILE" "$SECOND_PALETTE_FILE"' EXIT

scan9() { # <file> <ere> <reason-prefix>
  _out=$(grep -nE "$2" -- "$1" 2>/dev/null)
  _rc=$?
  if [ "$_rc" -ge 2 ]; then
    printf 'grep exit %s on %s while matching: %s\n' "$_rc" "$1" "$2" >> "$GREP_ERR_FILE"
    return
  fi
  [ "$_rc" -eq 0 ] || return 0
  while IFS= read -r hit; do
    [ -n "$hit" ] || continue
    printf '%s\t%s\t%s\n' "${1#"$REPO_ROOT"/}" "${hit%%:*}" "$3" >> "$SECOND_PALETTE_FILE"
  done <<HITS
$_out
HITS
}

while IFS= read -r css; do
  [ -n "$css" ] || continue
  # The at-rule, not the string: `@media` ... `prefers-color-scheme` before the
  # opening brace. The token file's own prose says the words and must not match.
  scan9 "$css" '@media[^{]*prefers-color-scheme' \
    'a prefers-color-scheme at-rule - a second palette re-declares every token name, and this script keys tokens by name in a FLAT map: it would keep one palette, check it, and report clean. Supporting dark mode honestly means keying the token map by scope, which is an amendment to T-608 and not a drop-in (T-606 header).'
  [ "$css" = "$TOKEN_ABS" ] && continue
  SIBLINGS=$((SIBLINGS + 1))
  SIBLING_LIST="$SIBLING_LIST ${css#"$REPO_ROOT"/}"
  scan9 "$css" '^[[:space:]]*--agora-color-[a-z0-9-]+[[:space:]]*:' \
    'declares an --agora-color-* token outside the token file - every token is defined in exactly ONE file (T-606 header), and a declaration here escapes the duplicate-name check entirely.'
done <<CSS_FILES
$(find "$PALETTE_DIR" -maxdepth 1 -type f -name '*.css' 2>/dev/null | sort)
CSS_FILES

assert_grep_ok

# ------------------------------------------------------------------ report --
TOKENS=$(awk -F'\t' '$1 == "C" && $2 == "tokens" { print $3 }' "$RECORDS_FILE")
PAIRS=$(awk -F'\t' '$1 == "C" && $2 == "pairs" { print $3 }' "$RECORDS_FILE")
BELOW=$(awk -F'\t' '$1 == "P" && $6 == "BELOW"' "$RECORDS_FILE" | wc -l | tr -d '[:space:]')
COMPUTED=$(awk -F'\t' '$1 == "P"' "$RECORDS_FILE" | wc -l | tr -d '[:space:]')
STRUCT_AWK=$(awk -F'\t' '$1 == "F"' "$RECORDS_FILE" | wc -l | tr -d '[:space:]')
STRUCT_9=0
[ -s "$SECOND_PALETTE_FILE" ] && STRUCT_9=$(wc -l < "$SECOND_PALETTE_FILE" | tr -d '[:space:]')
STRUCT=$((STRUCT_AWK + STRUCT_9))
SCANNED=$((1 + SIBLINGS))

printf '\ncontrast-check - WCAG contrast of every declared colour pair (T-608)\n\n'
printf 'scope:       %s\n' "${TOKEN_ABS#"$REPO_ROOT"/}"
printf 'siblings:    %s stylesheet(s) scanned for a second palette:%s\n' \
  "$SIBLINGS" "${SIBLING_LIST:- none}"
printf 'scanned:     %s file(s)\n' "$SCANNED"
printf 'formula:     WCAG 2.x relative luminance, threshold 0.03928, exponent 2.4 - www.w3.org/WAI/GL/wiki/Relative_luminance\n'
printf 'thresholds:  4.5 text (SC 1.4.3) - 3 large text and non-text (SC 1.4.11)\n'
printf 'exclusions:  none - prose naming a token or prefers-color-scheme is out of scope by construction, not excluded (I-018)\n'
printf 'tokens parsed: %s\n' "${TOKENS:-0}"
printf 'pairs parsed:  %s\n' "${PAIRS:-0}"

if [ -s "$RECORDS_FILE" ] && [ "$COMPUTED" -gt 0 ]; then
  printf '\n  %-30s %-30s %6s %6s  %s\n' 'foreground' 'background' 'min' 'ratio' 'verdict'
  printf '  %s\n' '--------------------------------------------------------------------------------------------'
  awk -F'\t' -v ok="$C_OK" -v bad="$C_BAD" -v off="$C_OFF" '
    $1 == "P" {
      col = ($6 == "ok" ? ok "ok" off : bad "BELOW" off)
      printf "  %-30s %-30s %6s %6s  %s\n", $2, $3, $4, $5, col
    }' "$RECORDS_FILE"
fi

if [ "$STRUCT" -gt 0 ]; then
  printf '\nstructural findings (%s):\n' "$STRUCT"
  awk -F'\t' -v bad="$C_BAD" -v off="$C_OFF" -v f="${TOKEN_ABS#"$REPO_ROOT"/}" '
    $1 == "F" { printf "  %s%s:%s%s  %s\n", bad, f, $2, off, $3 }' "$RECORDS_FILE"
  if [ -s "$SECOND_PALETTE_FILE" ]; then
    awk -F'\t' -v bad="$C_BAD" -v off="$C_OFF" \
      '{ printf "  %s%s:%s%s  %s\n", bad, $1, $2, off, $3 }' "$SECOND_PALETTE_FILE"
  fi
fi

printf '\nstructural findings:    %s\n' "$STRUCT"
printf 'pairs below threshold:  %s\n' "$BELOW"

# T-321(b): defaulted because zero means FAIL for both of these - the safe
# direction of I-031. Do not copy the `:-0` to a guard where zero means PASS.
if [ "${TOKENS:-0}" -eq 0 ]; then
  printf '\n%s0 pairs checked — 0 below threshold%s\n' "$C_BAD" "$C_OFF"
  printf 'FAILURE: 0 tokens parsed - an empty scope proves nothing (I-007). Either the file lost its\n'
  printf 'declarations or the parse contract in its header changed without this script changing with it.\n'
  exit 1
fi

if [ "${PAIRS:-0}" -eq 0 ]; then
  printf '\n%s0 pairs checked — 0 below threshold%s\n' "$C_BAD" "$C_OFF"
  printf 'FAILURE: 0 pairs parsed. That line reads like a pass and is not one (I-028): with no @pair\n'
  printf 'lines there is nothing for the arithmetic to be wrong about, and %s token(s) are unchecked.\n' "$TOKENS"
  exit 1
fi

if [ "$STRUCT" -gt 0 ] || [ "$BELOW" -gt 0 ]; then
  printf '\n%s%s pairs checked — %s below threshold%s\n' "$C_BAD" "$PAIRS" "$BELOW" "$C_OFF"
  [ "$STRUCT" -gt 0 ] && printf 'FAILURE: %s structural finding(s) as well - see above.\n' "$STRUCT"
  exit 1
fi

printf '\n%s%s pairs checked — %s below threshold%s\n' "$C_OK" "$PAIRS" "$BELOW" "$C_OFF"
printf '%s%s%s\n' "$C_DIM" \
  'every declared pair meets the threshold it declares; every token is paired; one palette.' "$C_OFF"
exit 0
