#!/usr/bin/env bash
# PMOS portable kit — pmos-log.sh (D58): the human builder's one-command state writer.
# Appends ONE validated JSON line to the right pmos/state/ file. Mirrors the hosted rules:
# verdict enum (save_eval), pm_label enum (record_acceptance), disposition enum + the D57
# evidence-required-for-terminal rule (record_outcome_check). Append-only by construction — this
# script only ever `>>`s; a correction is a NEW record. No dependencies beyond POSIX + python3
# (used solely for safe JSON string escaping).
#
#   pmos-log.sh run      <run_id> <initiative_id> [human_corrections] [model]
#   pmos-log.sh eval     <run_id> <initiative_id> <score 0..1> <PASS|PASS-WITH-FINDINGS|FAIL> <rubric_path> [evaluator_model]
#   pmos-log.sh accept   <run_id> <initiative_id> <pass|fail> [note]
#   pmos-log.sh outcome  <initiative_id> <validated|refuted|inconclusive> [evidence]
#   pmos-log.sh friction <note...>
set -euo pipefail

KIT="${KIT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
STATE="$KIT/state"
fail() { echo "pmos-log: ERROR — $*" >&2; exit 1; }
[ -d "$STATE" ] || fail "state dir not found at $STATE (run from a vendored kit, or set KIT_ROOT)."

js() { python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"; }  # safe JSON string
nonempty() { # nonempty <label> <value> — ids must not be blank (hosted tools reject them too)
  [ -n "$(printf '%s' "$2" | tr -d '[:space:]')" ] || fail "$1 must not be empty."
}
today="$(date -u +%Y-%m-%d)"

append() { # append <file> <json-line>
  printf '%s\n' "$2" >> "$STATE/$1"
  echo "✅ appended to state/$1:"
  echo "   $2"
}

cmd="${1:-}"; shift || true
case "$cmd" in
  run)
    [ $# -ge 2 ] || fail "usage: run <run_id> <initiative_id> [human_corrections] [model]"
    nonempty run_id "$1"; nonempty initiative_id "$2"
    corr="${3:-0}"
    printf '%s' "$corr" | grep -qE '^[0-9]+$' || fail "human_corrections must be a non-negative integer, got '$corr'."
    # optional builder-model provenance (which model produced the run). Omitted when absent — the
    # metric layer treats a missing model as 'unknown'. if-form, not `&&`: an empty $4 under set -e
    # would abort. Keeps calibration honest across model switches (harness-model-agnosticism).
    model_field=""
    if [ -n "${4:-}" ]; then model_field=",\"model\":$(js "$4")"; fi
    append runs.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"human_corrections\":$corr,\"date\":\"$today\"$model_field}"
    ;;
  eval)
    # SKIPPED is a verdict this writer must be able to record.
    #
    # The enum here was PASS|PASS-WITH-FINDINGS|FAIL, while AGENTS.md tells every adopter: "a gate
    # that was skipped is recorded as skipped — never leave a missing eval to be read as a pass."
    # An adopter following that instruction hit `fail` and had no way to comply except by hand-
    # appending, which is the drift this writer exists to prevent. PMOS-self's own ledger carries 16
    # SKIPPED rows; the kit could not have written one of them.
    #
    # A skip is NOT a soft pass: it takes no score (a skip has no score to give), it requires a
    # stated reason — an unexplained skip is indistinguishable from a forgotten one — and it leaves
    # the verification the run owes still owing.
    if [ "${4:-}" = "SKIPPED" ] || [ "${3:-}" = "SKIPPED" ]; then
      # tolerated both argument positions: `eval <run> <init> SKIPPED <reason>` is the natural form
      # when there is no score to give, and refusing it on argument order would just push people
      # back to hand-appending.
      if [ "${3:-}" = "SKIPPED" ]; then skip_reason="${4:-}"; skip_rubric="${5:-}"; skip_model="${6:-}"
      else skip_reason="${5:-}"; skip_rubric="${6:-}"; skip_model="${7:-}"; fi
      nonempty run_id "$1"; nonempty initiative_id "$2"
      [ -n "$skip_reason" ] || fail "SKIPPED needs a reason: eval <run_id> <initiative_id> SKIPPED <reason> [rubric_path] [evaluator_model]"
      rubric_field=""
      if [ -n "$skip_rubric" ]; then rubric_field=",\"rubric_path\":$(js "$skip_rubric")"; fi
      emodel_field=""
      if [ -n "$skip_model" ]; then emodel_field=",\"evaluator_model\":$(js "$skip_model")"; fi
      append evals.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"score\":null,\"verdict\":\"SKIPPED\",\"skipped_reason\":$(js "$skip_reason")$rubric_field,\"date\":\"$today\"$emodel_field}"
      exit 0
    fi
    [ $# -ge 5 ] || fail "usage: eval <run_id> <initiative_id> <score 0..1> <PASS|PASS-WITH-FINDINGS|FAIL> <rubric_path> [evaluator_model]
       or: eval <run_id> <initiative_id> SKIPPED <reason> [rubric_path] [evaluator_model]"
    nonempty run_id "$1"; nonempty initiative_id "$2"
    printf '%s' "$3" | grep -qE '^(0?\.[0-9]+|0|1(\.0+)?)$' || fail "score must be 0..1 written as 0, 1, N.N or .N — got '$3'."
    case "$4" in PASS|PASS-WITH-FINDINGS|FAIL) ;; *) fail "verdict must be PASS | PASS-WITH-FINDINGS | FAIL | SKIPPED, got '$4'." ;; esac
    score="$3"; case "$score" in .*) score="0$score" ;; esac  # ".5" -> "0.5": bare .5 is not valid JSON
    [ -f "$KIT/$5" ] || [ -f "$5" ] || fail "rubric_path '$5' not found (relative to the kit or the repo)."
    # optional evaluator-model provenance (which model rendered the verdict). Calibration (κ) is a
    # property of the evaluator MODEL, so metrics.py segments κ by it; a missing value counts as
    # 'unknown'. if-form, not `&&` (empty $6 under set -e aborts).
    emodel_field=""
    if [ -n "${6:-}" ]; then emodel_field=",\"evaluator_model\":$(js "$6")"; fi
    append evals.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"score\":$score,\"verdict\":$(js "$4"),\"rubric_path\":$(js "$5"),\"date\":\"$today\"$emodel_field}"
    ;;
  accept)
    [ $# -ge 3 ] || fail "usage: accept <run_id> <initiative_id> <pass|fail> [note]"
    nonempty run_id "$1"; nonempty initiative_id "$2"
    case "$3" in pass|fail) ;; *) fail "pm_label must be pass | fail, got '$3'." ;; esac
    acc=true; [ "$3" = fail ] && acc=false
    append acceptances.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"accepted\":$acc,\"pm_label\":$(js "$3"),\"note\":$(js "${4:-}"),\"date\":\"$today\"}"
    ;;
  outcome)
    [ $# -ge 2 ] || fail "usage: outcome <initiative_id> <validated|refuted|inconclusive> [evidence]"
    nonempty initiative_id "$1"
    case "$2" in validated|refuted|inconclusive) ;; *) fail "disposition must be validated | refuted | inconclusive, got '$2'." ;; esac
    ev="${3:-}"
    if [ "$2" != "inconclusive" ] && [ -z "$(printf '%s' "$ev" | tr -d '[:space:]')" ]; then
      fail "evidence is REQUIRED for a validated/refuted disposition (D57: no rubber-stamps; use 'inconclusive' for an honest non-answer)."
    fi
    append outcome_checks.jsonl "{\"initiative_id\":$(js "$1"),\"disposition\":$(js "$2"),\"evidence\":$(js "$ev"),\"date\":\"$today\"}"
    ;;
  friction)
    [ $# -ge 1 ] || fail "usage: friction <note...>"
    printf -- '- %s %s #friction\n' "$today" "$*" >> "$STATE/discoveries.md"
    echo "✅ appended to state/discoveries.md: - $today $* #friction"
    ;;
  *)
    fail "unknown command '${cmd:-}' — one of: run | eval | accept | outcome | friction (see AGENTS-HUMAN.md)."
    ;;
esac
