#!/usr/bin/env bash # nc-document.sh — document a Cloverleaf INTERFACE end-to-end as a native markdown # knowledge entry in Bryan's confirmed Legacy "ADT Messages" template. # # Two modes: # SINGLE THREAD nc-document.sh [site] (e.g. ADTto_CodaMetrix ancout) # nc-document.sh / (v1 node form) # SYSTEM/PATTERN nc-document.sh --name (e.g. --name codametrix) # → one section per matching destination thread, across sites. # # Everything emitted by THIS tool is DETERMINISTIC, PURE BASH+AWK, and API-FREE. # It runs identically on an API-blocked host (e.g. Gundersen). It never calls an # LLM and never reaches the network. The deterministic UPOC-bits + raw proc TCL # appendix ARE the deliverable; when larry runs WITH the API the model transparently # polishes those surfaced bits into smoother prose in the Description — that is # normal agent behavior, NOT a mechanism in this script. # # ───────────────────────────────────────────────────────────────────────────── # WHAT GETS DOCUMENTED, per interface (one delivery = one outbound thread): # - Title = the interface / message type. # - Description = prose: what the messages are, what the filters key on, where # translation happens, how it's fed — seeded from the surfaced # UPOC bits. # - Message Flow = a table (Platform | Action | Description | From | To), one row # per hop: Epic feed → Cloverleaf cross-site routing → Final # Delivery. Built from nc-paths.sh (the route-chain enumerator). # - Per-delivery breakdown: # inbound PROTOCOL TYPE/HOST/PORT/ISSERVER + inbound TRXID/TPS proc, # the route's TRXID filter + TYPE + PREPROCS/POSTPROCS + XLATE, # destination host:port / process. # - ★ DETERMINISTIC UPOC-BITS — for each referenced proc, locate its .tcl under # $HCIROOT//tclprocs/ and extract (no API): # 1. comments (header + inline `#` lines — the author's own filter notes) # 2. HL7 fields referenced (PID.8, PV1.45, EVN.1, …) # 3. conditions + literal values (matched event-code lists A01/A02/…, etc.) # 4. table lookups (.tbl / table names, e.g. PeriCalm_Loc) # 5. disposition (CONTINUE / KILL / return — pass vs kill) # Rendered compactly into the Description. # - Raw proc TCL in a plain appendix (`## Referenced proc source`). NO "summarize # by hand / on an API box" marker — the extracted bits are the content. # # Usage: # nc-document.sh [site] [options] # nc-document.sh / [options] # nc-document.sh --name [options] # # --name PATTERN SYSTEM mode: case-insensitive substring/regex over thread # names; one interface section per matching OUTBOUND thread. # --thread NAME single-thread mode (alternative to the positional form) # --site NAME home site of the thread (disambiguates a multi-site name) # --hciroot DIR defaults to $HCIROOT # --out PATH output markdown path (default: stdout) # --title TITLE doc title (default: derived from thread/name) # --poc-vendor TXT Vendor POC content # --poc-internal TXT Internal Owner content # --status TXT e.g. production / test / decommissioning # --escalation TXT Escalation path text # --open-items TXT Open items text # --notes TXT freeform additional notes # --no-appendix omit the raw proc-source appendix # -h | --help this help set -u set -o pipefail NC_SELF="$0" LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" NCP="$LIB_DIR/nc-parse.sh" NCPATHS="$LIB_DIR/nc-paths.sh" die() { printf 'nc-document: %s\n' "$*" >&2; exit 1; } # ───────────────────────────────────────────────────────────────────────────── # Arg parsing # ───────────────────────────────────────────────────────────────────────────── PATTERN="" THREAD_ARG="" SITE_ARG="" HCIROOT_OVERRIDE="" OUT="" TITLE="" POC_VENDOR="" POC_INTERNAL="" STATUS="" ESCALATION="" OPEN_ITEMS="" NOTES="" WANT_APPENDIX=1 POSITIONAL=() while [ $# -gt 0 ]; do case "$1" in --name) shift; PATTERN="${1:-}" ;; --thread) shift; THREAD_ARG="${1:-}" ;; --site) shift; SITE_ARG="${1:-}" ;; --hciroot) shift; HCIROOT_OVERRIDE="${1:-}" ;; --out) shift; OUT="${1:-}" ;; --title) shift; TITLE="${1:-}" ;; --poc-vendor) shift; POC_VENDOR="${1:-}" ;; --poc-internal) shift; POC_INTERNAL="${1:-}" ;; --status) shift; STATUS="${1:-}" ;; --escalation) shift; ESCALATION="${1:-}" ;; --open-items) shift; OPEN_ITEMS="${1:-}" ;; --notes) shift; NOTES="${1:-}" ;; --no-appendix) WANT_APPENDIX=0 ;; -h|--help) sed -n '2,72p' "$NC_SELF" | sed 's/^# \{0,1\}//'; exit 0 ;; --*) die "unknown flag: $1" ;; *) POSITIONAL+=("$1") ;; esac shift done # Positional shapes (single-thread mode): # thread only # thread + site # / v1 node form (nc-paths output feeds back in) if [ -z "$THREAD_ARG" ] && [ "${#POSITIONAL[@]}" -ge 1 ]; then THREAD_ARG="${POSITIONAL[0]}"; fi if [ -z "$SITE_ARG" ] && [ "${#POSITIONAL[@]}" -ge 2 ]; then SITE_ARG="${POSITIONAL[1]}"; fi case "$THREAD_ARG" in */*) _ss="${THREAD_ARG%%/*}"; _st="${THREAD_ARG#*/}" if [ -n "$_ss" ] && [ -n "$_st" ]; then THREAD_ARG="$_st"; SITE_ARG="$_ss"; fi ;; esac [ -n "$PATTERN" ] || [ -n "$THREAD_ARG" ] || \ die "give a [site] (single-thread mode) OR --name PATTERN (system mode). Try --help." ROOT="${HCIROOT_OVERRIDE:-${HCIROOT:-}}" [ -n "$ROOT" ] || die "no \$HCIROOT and no --hciroot; pass one or set the env var" [ -d "$ROOT" ] || die "hciroot not a directory: $ROOT" # ───────────────────────────────────────────────────────────────────────────── # Site discovery: site name → NetConfig path (two parallel arrays, bash-3.2 safe). # ───────────────────────────────────────────────────────────────────────────── SITE_NAMES=() SITE_NCS=() while IFS= read -r nc; do [ -f "$nc" ] || continue SITE_NAMES+=("$(basename "$(dirname "$nc")")") SITE_NCS+=("$nc") done < <(find "$ROOT" -maxdepth 2 -name NetConfig -type f 2>/dev/null | sort) [ "${#SITE_NCS[@]}" -gt 0 ] || die "no NetConfig files found under $ROOT" _nc_for_site() { # site → NetConfig path (first match) local want="$1" i for ((i=0; i<${#SITE_NAMES[@]}; i++)); do [ "${SITE_NAMES[$i]}" = "$want" ] && { printf '%s' "${SITE_NCS[$i]}"; return 0; } done return 1 } # Locate the first site whose NetConfig declares . Emits "site". _locate_thread() { local want="$1" i nc for ((i=0; i<${#SITE_NAMES[@]}; i++)); do nc="${SITE_NCS[$i]}" if "$NCP" list-protocols "$nc" 2>/dev/null | grep -qxF -- "$want"; then printf '%s' "${SITE_NAMES[$i]}"; return 0 fi done return 1 } # ───────────────────────────────────────────────────────────────────────────── # Output sink # ───────────────────────────────────────────────────────────────────────────── out_target() { if [ -n "$OUT" ]; then mkdir -p "$(dirname "$OUT")" 2>/dev/null cat > "$OUT" else cat fi } # ───────────────────────────────────────────────────────────────────────────── # strip a leading "{" / trailing "}" / empty-brace marker from a scalar value # ───────────────────────────────────────────────────────────────────────────── _clean() { printf '%s' "$1" | sed 's/^{}$//; s/^{//; s/}$//'; } # ───────────────────────────────────────────────────────────────────────────── # ROUTE EXTRACTION (deterministic, pure awk). # # Walk a thread's DATAXLATE block and emit ONE record per route, fields delimited # by the UNIT SEPARATOR (\037 — a NON-whitespace char, so bash `read` does NOT # collapse consecutive empty fields the way it would with TAB/space): # \037\037\037\037
\037\037\037\037
# where PRE/POST/PROCS are space-joined proc-name lists (the actual UPOC procs).
# A DATAXLATE route is a depth-2 sub-block; within it ROUTE_DETAILS (depth 3) holds
# DEST/TYPE/XLATE and the PREPROCS/POSTPROCS/PROCS nested blocks; TRXID/WILDCARD/
# ROUTE_ENABLED sit at the route level (depth 2). Empty {} values are skipped.
# ─────────────────────────────────────────────────────────────────────────────
_routes_of() {  # nc thread → US-delimited route records
  local nc="$1" thr="$2"
  # Depth map (from the real integrator): DATAXLATE opens 0->2; each ROUTE opens
  # into depth 3 (first route: a bare `{` 2->3; later routes: `} {` 3->3). At the
  # ROUTE level (depth 3) sit TRXID / WILDCARD / ROUTE_ENABLED. ROUTE_DETAILS opens
  # 3->5; DEST/TYPE/XLATE sit at depth 6; PREPROCS/POSTPROCS/PROCS open 6->8 and the
  # inner `{ PROCS  }` sits at depth 8. A new route boundary is any line that
  # ENTERS or RE-ENTERS a depth-3 sub-block (the `{` or `} {` separator lines).
  "$NCP" route-block "$nc" "$thr" 2>/dev/null | awk -v US="$(printf '\037')" '
    BEGIN { depth=0; route=0; pp_mode="" }
    function flush() {
      if (route) {
        printf "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n",
          dest, US, trxid, US, rtype, US, xlate, US, pre, US, post, US, procs, US, wild, US, enabled
      }
      dest=""; trxid=""; rtype=""; xlate=""; pre=""; post=""; procs=""; wild=""; enabled=""
    }
    {
      line=$0
      no=gsub(/\{/,"{",line); nc_=gsub(/\}/,"}",line)
      prev=depth; depth += no - nc_
      stripped=$0; sub(/^[[:space:]]+/,"",stripped); sub(/[[:space:]]+$/,"",stripped)

      # ROUTE BOUNDARY: a line that is exactly `{` (first route, prev 2 -> 3) or
      # `} {` (close prior + open next, depth stays 3). Both land us at depth 3.
      if ((prev==2 && depth==3 && stripped=="{") || (stripped=="} {" && depth==3)) {
        flush(); route=1; next
      }

      if (!route) next

      # route-level scalars live at depth 3 (prev==3 before any brace change)
      if (prev==3) {
        if (match($0, /\{ TRXID .* \}/))            { v=$0; sub(/^[[:space:]]+\{ TRXID /,"",v); sub(/ \}[[:space:]]*$/,"",v); trxid=v }
        if (match($0, /\{ WILDCARD [A-Za-z]+ \}/))  { v=$0; sub(/^[[:space:]]+\{ WILDCARD /,"",v); sub(/ \}[[:space:]]*$/,"",v); wild=v }
        if (match($0, /\{ ROUTE_ENABLED [0-9]+ \}/)){ v=$0; sub(/^[[:space:]]+\{ ROUTE_ENABLED /,"",v); sub(/ \}[[:space:]]*$/,"",v); enabled=v }
      }
      # ROUTE_DETAILS scalars (DEST/TYPE/XLATE) — each on its own line at depth 6
      if (match($0, /\{ DEST [A-Za-z0-9_]+ \}/))   { v=$0; sub(/^.*\{ DEST /,"",v);  sub(/ \}.*$/,"",v); dest=v }
      if (match($0, /\{ TYPE [A-Za-z0-9_]+ \}/))   { v=$0; sub(/^.*\{ TYPE /,"",v);  sub(/ \}.*$/,"",v); rtype=v }
      if (match($0, /\{ XLATE [A-Za-z0-9_.]+ \}/)) { v=$0; sub(/^.*\{ XLATE /,"",v); sub(/ \}.*$/,"",v); xlate=v }

      # enter a PREPROCS / POSTPROCS / (bare) PROCS block; the inner PROCS line
      # carries the actual proc name(s).
      if ($0 ~ /\{ PREPROCS \{$/)       pp_mode="pre"
      else if ($0 ~ /\{ POSTPROCS \{$/) pp_mode="post"
      else if ($0 ~ /\{ PROCS \{$/)     pp_mode="procs"
      # the inner PROCS line: { PROCS name } | { PROCS {a b} } | { PROCS {} }
      if (pp_mode != "" && match($0, /\{ PROCS /)) {
        v=$0; sub(/^[[:space:]]+\{ PROCS /,"",v); sub(/[[:space:]]*\}[[:space:]]*$/,"",v)
        gsub(/[{}]/,"",v); gsub(/^[[:space:]]+|[[:space:]]+$/,"",v)
        if (v != "") {
          if (pp_mode=="pre")        pre = (pre=="" ? v : pre " " v)
          else if (pp_mode=="post")  post= (post=="" ? v : post " " v)
          else                       procs=(procs=="" ? v : procs " " v)
        }
        pp_mode=""
      }
    }
    END { flush() }
  '
}

# Inbound how-received facts. We read each field into a NAMED variable directly
# (NOT positional TSV — bash `read` with a single-char IFS collapses CONSECUTIVE
# empty fields, which silently shifts columns when HOST/PORT/ISSERVER are empty on
# an ICL/file inbound). Caller passes a prefix; we set _TYPE etc. via
# globals. Robust and order-independent.
_inbound_facts() {  # nc thread
  local nc="$1" thr="$2"
  IN_TYPE=$("$NCP" protocol-nested "$nc" "$thr" PROTOCOL.TYPE 2>/dev/null | head -1)
  IN_HOST=$(_clean "$("$NCP" protocol-nested "$nc" "$thr" PROTOCOL.HOST 2>/dev/null | head -1)")
  IN_PORT=$(_clean "$("$NCP" protocol-nested "$nc" "$thr" PROTOCOL.PORT 2>/dev/null | head -1)")
  IN_ISSERVER=$("$NCP" protocol-nested "$nc" "$thr" PROTOCOL.ISSERVER 2>/dev/null | head -1)
  IN_ICLPORT=$(_clean "$("$NCP" protocol-field "$nc" "$thr" ICLSERVERPORT 2>/dev/null | head -1)")
  IN_PROC=$("$NCP" protocol-nested "$nc" "$thr" DATAFORMAT.PROC 2>/dev/null | head -1)
  IN_PNAME=$("$NCP" protocol-field "$nc" "$thr" PROCESSNAME 2>/dev/null | head -1)
}

# ─────────────────────────────────────────────────────────────────────────────
# ★ DETERMINISTIC UPOC-BITS EXTRACTION (the key feature).
#
# Locate .tcl under $HCIROOT//tclprocs/ (then any site as a fallback)
# and extract, with NO API:
#   COMMENTS   — header + inline `#` lines (the author's own filter notes), cleaned.
#   FIELDS     — HL7 field accessors / segment-field tokens (PID.8, PV1_3_3, EVN.1,
#                getHL7Field … "PV1" N, replaceHL7Field … SEG N).
#   MATCHES    — literal HL7 event/trigger codes referenced (A01 A02 … A53).
#   CONDS      — `if`/condition lines and other literal comparison values.
#   TABLES     — tbllookup / .tbl table names (e.g. PeriCalm_Loc).
#   DISP       — dispositions: CONTINUE / KILL / return (pass vs kill).
# Output is a small set of key=value lines on stdout (one fact-list per key):
#   TCLFILE=
#   COMMENTS... (one per matched comment, capped)
#   FIELDS=
#   MATCHES=
#   TABLES=
#   DISP=
#   CONDS... (one per condition line, capped)
# ─────────────────────────────────────────────────────────────────────────────
_find_tcl() {  # site proc → abs path or empty
  local site="$1" proc="$2" p
  [ -z "$proc" ] && return 0
  # 1) home site
  p="$ROOT/$site/tclprocs/$proc.tcl"
  [ -f "$p" ] && { printf '%s' "$p"; return 0; }
  # 2) any site (deterministic order — first wins)
  local i
  for ((i=0; i<${#SITE_NAMES[@]}; i++)); do
    p="$ROOT/${SITE_NAMES[$i]}/tclprocs/$proc.tcl"
    [ -f "$p" ] && { printf '%s' "$p"; return 0; }
  done
  return 0
}

# Extract the bits from a single .tcl file. Emits the key=value / key... stream.
_upoc_bits() {  # tclfile
  local f="$1"
  [ -n "$f" ] || return 0
  [ -f "$f" ] || return 0
  # PORTABILITY: this awk uses NO `\b` word-boundary metachar — BSD/BWK awk
  # (macOS) and mawk do not support it (it silently matches nothing). Token
  # boundaries are enforced by explicit char-class scanning instead.
  awk '
    BEGIN {
      ncomm=0; ncond=0; MAXCOMM=24; MAXCOND=18
      # segment ids we recognise (for the underscore PV1_3_3 form and seg N form)
      segs="MSH EVN PID PV1 PV2 OBR OBX ORC NK1 IN1 GT1 ZPD ZID MRG DG1 AL1 SCH RGS AIS AIL AIP MSA ZIN"
      nseg=split(segs, SEG, " "); for (i=1;i<=nseg;i++) ISSEG[SEG[i]]=1
    }
    function addset(arr, key) { if (key != "" && !(key in arr)) arr[key]=1 }
    # is char c an identifier char (so a token boundary is a NON-identifier char)?
    function idc(ch) { return (ch ~ /[A-Za-z0-9_]/) }
    {
      line=$0
      # ---- comments: lines whose first non-space char is # ----
      c=line; sub(/^[[:space:]]+/,"",c)
      if (c ~ /^#/) {
        t=c; sub(/^#+[[:space:]]*/,"",t); gsub(/[[:space:]]+$/,"",t)
        if (t != "" && t !~ /^[#=*_-]+$/) { if (ncomm < MAXCOMM) comm[++ncomm]=t }
      }

      # ---- HL7 field accessors: dotted form PID.8 / PV1.45 / EVN.1 / MSH.9.1 ----
      s=line
      while (match(s, /[A-Z][A-Z][A-Z0-9]\.[0-9]+(\.[0-9]+)?/)) {
        tok=substr(s, RSTART, RLENGTH); addset(fields, tok); s=substr(s, RSTART+RLENGTH)
      }
      # underscore form PV1_3_3 / EVN_5_8 / PID_8 -> normalize to dotted. We scan
      # for SEG_(_) where SEG is a known segment id, enforcing a
      # boundary by requiring the char before SEG to be non-identifier.
      s=line
      while (match(s, /[A-Z][A-Z][A-Z0-9]_[0-9]+(_[0-9]+)?/)) {
        st=RSTART; ln=RLENGTH; tok=substr(s, st, ln)
        before = (st==1) ? "" : substr(s, st-1, 1)
        seg3=substr(tok,1,3)
        if ((before=="" || !idc(before)) && (seg3 in ISSEG)) {
          d=tok; gsub(/_/,".",d); addset(fields, d)
        }
        s=substr(s, st+ln)
      }
      # replaceHL7Field/getHL7Field on a NAMED segment + numeric field: SEG N
      s=line
      while (match(s, /(MSH|EVN|PID|PV1|PV2|OBR|OBX|ORC|MSA|DG1|AL1|NK1|IN1)[[:space:]]+[0-9]+/)) {
        st=RSTART; ln=RLENGTH; tok=substr(s, st, ln)
        before=(st==1)?"":substr(s, st-1, 1)
        # only when the line is an HL7 field op (avoids matching arbitrary "PV1 6")
        if ((before=="" || !idc(before)) && line ~ /(get|replace)HL7Field/) {
          gsub(/[[:space:]]+/,".",tok); addset(fields, tok)
        }
        s=substr(s, st+ln)
      }

      # ---- literal HL7 trigger/event codes A01..A99 (boundary-checked, no \b) ----
      s=line
      while (match(s, /A[0-9][0-9]/)) {
        st=RSTART; ln=RLENGTH; tok=substr(s, st, ln)
        before=(st==1)?"":substr(s, st-1, 1)
        afterpos=st+ln; after=(afterpos>length(s))?"":substr(s, afterpos, 1)
        if ((before=="" || !idc(before)) && (after=="" || !idc(after))) addset(matches, tok)
        s=substr(s, st+ln)
      }

      # ---- table lookups: tbllookup  ...  /  word.tbl ----
      if (match(line, /tbllookup[[:space:]]+[A-Za-z_][A-Za-z0-9_]*/)) {
        tb=substr(line,RSTART,RLENGTH); sub(/tbllookup[[:space:]]+/,"",tb); addset(tables, tb)
      }
      s=line
      while (match(s, /[A-Za-z_][A-Za-z0-9_]*\.tbl/)) {
        tok=substr(s, RSTART, RLENGTH); sub(/\.tbl$/,"",tok); addset(tables, tok); s=substr(s, RSTART+RLENGTH)
      }

      # ---- dispositions: plain substring match, boundary not needed (these are
      #      distinct all-caps tokens in TCL disposition code) ----
      if (line ~ /CONTINUE/) addset(disp, "CONTINUE")
      if (line ~ /KILL/)     addset(disp, "KILL")
      if (line ~ /disp/ && line ~ /ERROR/) addset(disp, "ERROR")
      if (c ~ /^return([[:space:]]|$)/ || line ~ /return "\{/) addset(disp, "return")

      # ---- condition lines: TCL `if {...}` carrying a comparison ----
      ct=line; sub(/^[[:space:]]+/,"",ct); gsub(/[[:space:]]+$/,"",ct)
      if (ct ~ /^(if|elseif|\} elseif|switch)([[:space:]]|\{)/ && ct ~ /(==|!=|<|>|lcontain|cequal|string)/) {
        if (ncond < MAXCOND) cond[++ncond]=ct
      }
    }
    END {
      # FIELDS / MATCHES / TABLES / DISP: sorted-unique, space-joined
      printf "FIELDS="; n=0; for (k in fields) { a[n++]=k }
      asort_keys(a, n); for (i=0;i=0 && arr[j]>tmp){ arr[j+1]=arr[j]; j-- } arr[j+1]=tmp }
    }
  ' "$f"
}

# Compose the compact one-line UPOC summary for the Description, from a bits stream.
#   _upoc_oneline  
_upoc_oneline() {
  local proc="$1" bf="$2"
  local fields matches tables disp comments
  fields=$(awk -F= '/^FIELDS=/{sub(/^FIELDS=/,"");print}' "$bf")
  matches=$(awk -F= '/^MATCHES=/{sub(/^MATCHES=/,"");print}' "$bf")
  tables=$(awk -F= '/^TABLES=/{sub(/^TABLES=/,"");print}' "$bf")
  disp=$(awk -F= '/^DISP=/{sub(/^DISP=/,"");print}' "$bf")
  comments=$(awk -F'\t' '/^COMMENT\t/{print $2}' "$bf" \
    | grep -iE 'pass|filter|block|only|kill|continue|drop|route|female|newborn|discharge|location|purpose|determine' \
    | grep -ivE '^(name|author|date|args|returns|upoc type|revision|notes)\b' \
    | head -3 \
    | awk 'NR==1{printf "%s",$0;next}{printf " · %s",$0}END{print ""}')
  local out="UPOC \`$proc\`"
  [ -n "$comments" ] && out="$out — $comments"
  [ -n "$fields" ]   && out="$out · fields: $fields"
  [ -n "$matches" ]  && out="$out · matches: $matches"
  [ -n "$tables" ]   && out="$out · table: $tables"
  if [ -n "$disp" ]; then
    case "$disp" in
      *KILL*CONTINUE*|*CONTINUE*KILL*) out="$out · disposition: pass matching / kill non-matching" ;;
      *KILL*)      out="$out · disposition: kill non-matching" ;;
      *CONTINUE*)  out="$out · disposition: pass matching" ;;
      *)           out="$out · disposition: $disp" ;;
    esac
  fi
  printf '%s\n' "$out"
}

# ─────────────────────────────────────────────────────────────────────────────
# Build the doc section for ONE outbound (delivery) thread.
#   $1 = outbound thread name   $2 = its home site   $3 = its NetConfig
# Emits markdown to stdout. Appends raw proc TCL paths to the global APPENDIX_PROCS
# set (printed once at the end).
# ─────────────────────────────────────────────────────────────────────────────
declare -A APPENDIX_SEEN
APPENDIX_LIST=()   # "site|proc|abs-path" records

_register_appendix() {  # site proc
  local site="$1" proc="$2" key="$site|$proc" p
  [ -z "$proc" ] && return 0
  [ -n "${APPENDIX_SEEN[$key]:-}" ] && return 0
  APPENDIX_SEEN[$key]=1
  p=$(_find_tcl "$site" "$proc")
  APPENDIX_LIST+=("$site|$proc|$p")
}

document_thread() {
  local ob="$1" site="$2" nc="$3"

  # --- the full route chain (flow) via nc-paths ---
  local chain
  chain=$("$NCPATHS" "$site/$ob" --hciroot "$ROOT" --format v1 2>/dev/null | head -1)

  # --- destination (the outbound thread's delivery endpoint) ---
  local dtype dhost dport dproc
  dtype=$("$NCP" protocol-nested "$nc" "$ob" PROTOCOL.TYPE 2>/dev/null | head -1)
  dhost=$(_clean "$("$NCP" protocol-nested "$nc" "$ob" PROTOCOL.HOST 2>/dev/null | head -1)")
  dport=$(_clean "$("$NCP" protocol-nested "$nc" "$ob" PROTOCOL.PORT 2>/dev/null | head -1)")
  dproc=$("$NCP" protocol-field "$nc" "$ob" PROCESSNAME 2>/dev/null | head -1)

  # --- find the SOURCE (routing) thread: who DESTs to this outbound, same site ---
  local route_thr=""
  while IFS= read -r s; do
    [ -z "$s" ] && continue
    route_thr="$s"; break
  done < <("$NCP" sources "$nc" "$ob" 2>/dev/null)

  # --- the specific route (TRXID/TYPE/XLATE/PRE/POST) that targets this outbound ---
  local r_trxid="" r_type="" r_xlate="" r_pre="" r_post="" r_procs="" r_wild="" r_enabled=""
  local US; US=$(printf '\037')
  if [ -n "$route_thr" ]; then
    while IFS="$US" read -r dest trxid rtype xlate pre post procs wild enabled; do
      [ "$dest" = "$ob" ] || continue
      r_trxid="$trxid"; r_type="$rtype"; r_xlate="$xlate"
      r_pre="$pre"; r_post="$post"; r_procs="$procs"; r_wild="$wild"; r_enabled="$enabled"
      break
    done < <(_routes_of "$nc" "$route_thr")
  fi

  # --- inbound how-received facts for the routing thread (the local inbound) ---
  local in_type="" in_host="" in_port="" in_isserver="" in_iclport="" in_proc="" in_pname=""
  if [ -n "$route_thr" ]; then
    IN_TYPE=""; IN_HOST=""; IN_PORT=""; IN_ISSERVER=""; IN_ICLPORT=""; IN_PROC=""; IN_PNAME=""
    _inbound_facts "$nc" "$route_thr"
    in_type="$IN_TYPE"; in_host="$IN_HOST"; in_port="$IN_PORT"; in_isserver="$IN_ISSERVER"
    in_iclport="$IN_ICLPORT"; in_proc="$IN_PROC"; in_pname="$IN_PNAME"
  fi

  # --- the feed root (Epic-side) from the chain ---
  local feed_root feed_site feed_thr
  feed_root="${chain%% *}"          # first node "site/thread"
  feed_site="${feed_root%%/*}"; feed_thr="${feed_root#*/}"

  # ── UPOC bits for every proc this delivery touches (inbound TRXID/TPS proc +
  #    the route's PRE/POST/PROCS). Collect bits files for the Description and
  #    register the raw TCL for the appendix.
  local upoc_lines=() bf proc
  for proc in $in_proc $r_pre $r_post $r_procs; do
    [ -z "$proc" ] && continue
    local tcl; tcl=$(_find_tcl "$site" "$proc")
    [ -z "$tcl" ] && tcl=$(_find_tcl "$feed_site" "$proc")
    if [ -n "$tcl" ]; then
      bf=$(mktemp); _upoc_bits "$tcl" > "$bf"
      upoc_lines+=("$(_upoc_oneline "$proc" "$bf")")
      rm -f "$bf"
    else
      upoc_lines+=("UPOC \`$proc\` — _(proc .tcl not found under any site's tclprocs/)_")
    fi
    _register_appendix "$site" "$proc"
  done

  # ─────────────────────────── render the section ───────────────────────────
  printf '## %s\n\n' "$ob"

  # Description (prose seeded from deterministic facts; the model polishes this
  # into smoother prose when run WITH the API — no marker here).
  printf '### Description\n\n'
  {
    printf 'The **%s** interface delivers messages to `%s`' "$ob" "$ob"
    [ -n "$dhost" ] && printf ' on **%s' "$dhost"
    [ -n "$dport" ] && printf ':%s' "$dport"
    [ -n "$dhost" ] && printf '**'
    [ -n "$dproc" ] && printf ' (process `%s`)' "$dproc"
    printf '.'
    if [ -n "$route_thr" ]; then
      printf ' Routing and filtering happen on the inbound thread `%s`' "$route_thr"
      [ -n "$in_proc" ] && printf ', whose inbound TRXID/TPS proc `%s` assigns the transaction id that the routes key on' "$in_proc"
      printf '.'
    fi
    if [ -n "$r_trxid" ]; then
      printf ' This delivery is selected by the TRXID filter `%s`' "$r_trxid"
      [ "$r_wild" = "ON" ] && printf ' (wildcard match)'
      printf '.'
    fi
    if [ -n "$r_xlate" ]; then
      printf ' Translation is done by the xlate `%s`' "$r_xlate"
      printf '.'
    elif [ "$r_type" = "raw" ]; then
      printf ' Messages are passed **raw** (no translation).'
    fi
    printf '\n\n'
  }
  if [ "${#upoc_lines[@]}" -gt 0 ]; then
    printf 'Filter / translation logic (surfaced deterministically from the referenced UPOC procs):\n\n'
    local l
    for l in "${upoc_lines[@]}"; do printf -- '- %s\n' "$l"; done
    printf '\n'
  fi

  # Message Flow table. The middle "routing" row's wording adapts to whether the
  # chain actually crosses a site boundary (a `==>` hop): cross-site routing goes
  # via a named destination block; an intra-site chain is a local DATAXLATE route.
  local is_cross=0 route_desc
  case "$chain" in *' ==> '*) is_cross=1 ;; esac
  if [ "$is_cross" = "1" ]; then
    route_desc="Cross-site route via destination block; inbound \`${route_thr:-?}\` keys TRXID and routes per delivery"
  else
    route_desc="Intra-site DATAXLATE route; inbound \`${route_thr:-?}\` keys TRXID and routes per delivery"
  fi
  printf '### Message Flow\n\n'
  printf '| Platform | Action | Description | From | To |\n'
  printf '|---|---|---|---|---|\n'
  # Row 1: Epic feed — From = the upstream system/process, To = the engine feed thread.
  printf '| Epic | feed | Raw Epic feed entering the integrator | Epic (process `%s`) | `%s` |\n' \
    "${in_pname:-${dproc:-ADT}}" "${feed_root:-—}"
  # Row 2: Cloverleaf routing (the chain itself)
  printf '| Cloverleaf%s | message routing | %s | `%s` | `%s` |\n' \
    "$( [ "$is_cross" = "1" ] && printf ' (cross-site)' || printf '' )" \
    "$route_desc" "${chain:-—}" "${route_thr:-—}"
  # Row 3: Final delivery
  printf '| Final Delivery | outbound to %s | TRXID `%s` → TYPE `%s`%s | `%s` | `%s`%s%s |\n' \
    "${dproc:-vendor}" "${r_trxid:-—}" "${r_type:-—}" \
    "$( [ -n "$r_xlate" ] && printf ', xlate `%s`' "$r_xlate" )" \
    "${route_thr:-—}" "$ob" \
    "$( [ -n "$dhost" ] && printf ' → %s' "$dhost" )" \
    "$( [ -n "$dport" ] && printf ':%s' "$dport" )"
  printf '\n'

  # Per-delivery breakdown
  printf '### Delivery breakdown — `%s`\n\n' "$ob"
  printf -- '- **Flow:** `%s`\n' "${chain:-—}"
  printf -- '- **How received (inbound `%s`):** PROTOCOL TYPE `%s`' "${route_thr:-?}" "${in_type:-—}"
  [ -n "$in_host" ] && printf ' · HOST `%s`' "$in_host"
  [ -n "$in_port" ] && printf ' · PORT `%s`' "$in_port"
  [ -n "$in_isserver" ] && printf ' · ISSERVER `%s`' "$in_isserver"
  [ -n "$in_iclport" ] && printf ' · ICLSERVERPORT `%s`' "$in_iclport"
  printf '\n'
  printf -- '- **Inbound TRXID/TPS proc:** `%s`\n' "${in_proc:-—}"
  printf -- '- **Route TRXID filter:** `%s`%s\n' "${r_trxid:-—}" "$( [ "$r_wild" = "ON" ] && printf ' (WILDCARD ON)' )"
  printf -- '- **Route TYPE:** `%s`\n' "${r_type:-—}"
  printf -- '- **UPOC PREPROCS:** `%s`\n' "${r_pre:-—}"
  printf -- '- **UPOC POSTPROCS:** `%s`\n' "${r_post:-—}"
  printf -- '- **XLATE:** `%s`\n' "${r_xlate:-—}"
  printf -- '- **Destination:** `%s`%s%s · process `%s` · TYPE `%s`\n' \
    "${dhost:-—}" "$( [ -n "$dport" ] && printf ':%s' "$dport" )" "" "${dproc:-—}" "${dtype:-—}"
  printf '\n'
}

# ─────────────────────────────────────────────────────────────────────────────
# Resolve the set of OUTBOUND (delivery) threads to document.
#   single-thread mode: the one thread (resolve its site).
#   system mode: every thread (across sites) matching --name that is OUTBOUNDONLY
#                (a delivery endpoint); if a matched thread is NOT outbound (e.g. an
#                inbound router) we still document its OUTBOUND children that match.
# Emits "site|nc|thread" records.
# ─────────────────────────────────────────────────────────────────────────────
TARGETS=()

if [ -n "$THREAD_ARG" ]; then
  local_site="$SITE_ARG"
  if [ -z "$local_site" ]; then
    local_site=$(_locate_thread "$THREAD_ARG") || die "thread not found in any site under $ROOT: $THREAD_ARG"
  fi
  nc=$(_nc_for_site "$local_site") || die "no NetConfig for site: $local_site"
  "$NCP" list-protocols "$nc" 2>/dev/null | grep -qxF -- "$THREAD_ARG" \
    || die "thread '$THREAD_ARG' not found in site '$local_site'"
  TARGETS+=("$local_site|$nc|$THREAD_ARG")
  [ -z "$TITLE" ] && TITLE="$THREAD_ARG"
else
  # system / pattern mode: document each matching DELIVERY (outbound) thread. A
  # delivery endpoint is a thread that is NOT an inbound TCP listener (ISSERVER!=1)
  # and NOT an ICL/file inbound router (OBWORKASIB!=1) — i.e. it has an outbound
  # client connection to a downstream system. This covers both pure OUTBOUNDONLY=1
  # threads (e.g. ADTto_CodaMetrix) and bidirectional `Xto_*` deliveries whose
  # OUTBOUNDONLY=0 (e.g. DFTto_codaMetrix). Inbound listeners/routers are skipped
  # (they are documented as the "how received" leg of their deliveries).
  for ((i=0; i<${#SITE_NAMES[@]}; i++)); do
    site="${SITE_NAMES[$i]}"; nc="${SITE_NCS[$i]}"
    while IFS= read -r prot; do
      [ -z "$prot" ] && continue
      isserver=$("$NCP" protocol-nested "$nc" "$prot" PROTOCOL.ISSERVER 2>/dev/null | head -1)
      obib=$("$NCP" protocol-field "$nc" "$prot" OBWORKASIB 2>/dev/null | head -1)
      [ "$isserver" = "1" ] && continue       # inbound listener — not a delivery
      [ "$obib" = "1" ] && continue            # ICL/file inbound router — not a delivery
      TARGETS+=("$site|$nc|$prot")
    done < <("$NCP" list-protocols "$nc" 2>/dev/null | grep -i -- "$PATTERN" || true)
  done
  [ "${#TARGETS[@]}" -gt 0 ] || die "no delivery (outbound) threads matching \"$PATTERN\" under $ROOT"
  [ -z "$TITLE" ] && TITLE="$(printf '%s' "$PATTERN" | tr '[:upper:]' '[:lower:]')"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Compose the document
# ─────────────────────────────────────────────────────────────────────────────
{
  printf '# %s\n\n' "$TITLE"
  if [ -n "$PATTERN" ]; then
    printf '_Cloverleaf interface documentation for the `%s` system — one section per matching delivery thread. Auto-generated by Larry-Anywhere nc-document.sh (deterministic, API-free) on %s._\n\n' \
      "$PATTERN" "$(date -Iseconds 2>/dev/null || date)"
  else
    printf '_Cloverleaf interface documentation for `%s`. Auto-generated by Larry-Anywhere nc-document.sh (deterministic, API-free) on %s._\n\n' \
      "$THREAD_ARG" "$(date -Iseconds 2>/dev/null || date)"
  fi

  # Context block (human fill-ins kept from prior versions)
  printf '## Context\n\n'
  printf -- '- **Vendor POC:** %s\n'    "${POC_VENDOR:-_(unfilled)_}"
  printf -- '- **Internal Owner:** %s\n' "${POC_INTERNAL:-_(unfilled)_}"
  printf -- '- **Status:** %s\n'         "${STATUS:-_(unfilled — production / test / decommissioning)_}"
  printf -- '- **Escalation:** %s\n'     "${ESCALATION:-_(unfilled)_}"
  printf '\n'
  if [ -n "$OPEN_ITEMS" ]; then printf '### Open items\n%s\n\n' "$OPEN_ITEMS"; fi
  if [ -n "$NOTES" ];      then printf '### Notes\n%s\n\n' "$NOTES"; fi

  # one section per delivery thread
  for line in "${TARGETS[@]}"; do
    IFS='|' read -r site nc prot <<< "$line"
    document_thread "$prot" "$site" "$nc"
  done

  # Appendix — raw proc source (plainly labelled; NO "summarize" marker)
  if [ "$WANT_APPENDIX" = "1" ] && [ "${#APPENDIX_LIST[@]}" -gt 0 ]; then
    printf '## Referenced proc source\n\n'
    printf '_Raw TCL of every UPOC proc referenced above (the deterministic UPOC bits in each Description are extracted from these — included verbatim for audit)._\n\n'
    for rec in "${APPENDIX_LIST[@]}"; do
      IFS='|' read -r asite aproc apath <<< "$rec"
      printf '### `%s` (site `%s`)\n\n' "$aproc" "$asite"
      if [ -n "$apath" ] && [ -f "$apath" ]; then
        printf '_Source: `%s`_\n\n' "$apath"
        printf '```tcl\n'
        cat "$apath"
        printf '\n```\n\n'
      else
        printf '_(proc `%s.tcl` not found under any site tclprocs/)_\n\n' "$aproc"
      fi
    done
  fi

  printf '---\n\n'
  printf '_Generated: %s · sites scanned: %d · %s_\n' \
    "$(date -Iseconds 2>/dev/null || date)" "${#SITE_NCS[@]}" \
    "$( [ -n "$PATTERN" ] && printf 'pattern: `%s`' "$PATTERN" || printf 'thread: `%s`' "$THREAD_ARG" )"
} | out_target

if [ -n "$OUT" ]; then
  printf 'nc-document: wrote %s (%d delivery section(s))\n' "$OUT" "${#TARGETS[@]}" >&2
fi