cloverleaf-larry/lib/nc-document.sh

982 lines
50 KiB
Bash
Executable File

#!/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 <thread> [site] (e.g. ADTto_CodaMetrix ancout)
# nc-document.sh <site>/<thread> (v1 node form)
# SYSTEM/PATTERN nc-document.sh --name <pattern> (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/<site>/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 <thread> [site] [options]
# nc-document.sh <site>/<thread> [options]
# nc-document.sh --name <pattern> [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
# --inbound-systems P path to the curated inbound-systems lookup TSV (default:
# $LARRY_HOME/inbound-systems.tsv, then the shipped seed).
# Maps a feed thread name / port:<n> to the external sender
# name used in the Message Flow "From" row; on no match the
# tool falls back to the honest generic "Epic (process X)".
# --strict-delivery SYSTEM mode only: tighten the delivery gate. A matching
# thread counts as a delivery ONLY if it has a real
# downstream endpoint (non-empty PROTOCOL.HOST/PORT) or is
# OUTBOUNDONLY=1 — excludes reply-only outbounds that a
# broad --name pattern would otherwise sweep in.
# -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; }
# ─────────────────────────────────────────────────────────────────────────────
# Inbound-systems lookup config (Bryan-curated). Maps a feed/source thread (by
# name, or by port:<n>) to the human name of the external upstream sender. Used
# to label the "From"/feed row deterministically. Resolution order:
# 1. $LARRY_HOME/inbound-systems.tsv (the live, user-curated config)
# 2. inbound-systems.tsv next to lib/ (the shipped seed default)
# Override with --inbound-systems PATH (parsed below).
# ─────────────────────────────────────────────────────────────────────────────
_inbound_systems_file() {
if [ -n "${INBOUND_SYSTEMS_FILE:-}" ]; then
[ -f "$INBOUND_SYSTEMS_FILE" ] && { printf '%s' "$INBOUND_SYSTEMS_FILE"; return 0; }
return 0 # explicit path that doesn't exist → no lookup, honest fallback
fi
local c
for c in "${LARRY_HOME:-$HOME/.larry}/inbound-systems.tsv" \
"$LIB_DIR/../inbound-systems.tsv" \
"$LIB_DIR/inbound-systems.tsv"; do
[ -f "$c" ] && { printf '%s' "$c"; return 0; }
done
return 0
}
# Look up an upstream system label by feed thread name and/or port. Args: name port.
# Emits the curated label on stdout, or nothing if no entry matches (caller falls
# back to the honest generic label). Thread-name key wins over port key.
_lookup_inbound_system() {
local fname="$1" fport="$2" cfg
cfg=$(_inbound_systems_file)
[ -n "$cfg" ] || return 0
[ -f "$cfg" ] || return 0
awk -F'\t' -v name="$fname" -v port="$fport" '
/^[[:space:]]*#/ { next }
/^[[:space:]]*$/ { next }
{
key=$1; val=$2
gsub(/^[[:space:]]+|[[:space:]]+$/,"",key)
gsub(/^[[:space:]]+|[[:space:]]+$/,"",val)
if (key=="" || val=="") next
if (name!="" && key==name) { byname=val }
else if (port!="" && key=="port:" port) { byport=val }
}
END { if (byname!="") print byname; else if (byport!="") print byport }
' "$cfg"
}
# ─────────────────────────────────────────────────────────────────────────────
# Arg parsing
# ─────────────────────────────────────────────────────────────────────────────
PATTERN=""
THREAD_ARG=""
SITE_ARG=""
HCIROOT_OVERRIDE=""
OUT=""
TITLE=""
POC_VENDOR=""
POC_INTERNAL=""
STATUS=""
ESCALATION=""
OPEN_ITEMS=""
NOTES=""
WANT_APPENDIX=1
STRICT_DELIVERY=0
INBOUND_SYSTEMS_FILE="${INBOUND_SYSTEMS_FILE:-}"
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 ;;
--strict-delivery) STRICT_DELIVERY=1 ;;
--inbound-systems) shift; INBOUND_SYSTEMS_FILE="${1:-}" ;;
-h|--help) sed -n '2,71p' "$NC_SELF" | sed 's/^# \{0,1\}//'; exit 0 ;;
--*) die "unknown flag: $1" ;;
*) POSITIONAL+=("$1") ;;
esac
shift
done
# Positional shapes (single-thread mode):
# <thread> thread only
# <thread> <site> thread + site
# <site>/<thread> 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 <thread> [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 <thread>. 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):
# <DEST>\037<TRXID>\037<TYPE>\037<XLATE>\037<PRE>\037<POST>\037<PROCS>\037<WILDCARD>\037<ENABLED>
# 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 <name> }` 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.
# DEST has TWO forms (mirror nc-parse.sh index parser):
# single { DEST <thread> } -> dest = the one thread
# list { DEST {a b c} } -> dest = space-joined list (multi-dest)
# The caller treats `dest` as a whitespace-separated SET when matching a delivery.
if (match($0, /\{ DEST \{[^}]*\} \}/)) { # list form FIRST (more specific)
v=$0; sub(/^.*\{ DEST \{/,"",v); sub(/\} \}.*$/,"",v)
gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); dest=v
}
else 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 <prefix>_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 <proc>.tcl under $HCIROOT/<site>/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=<abs path or empty>
# COMMENTS<TAB>... (one per matched comment, capped)
# FIELDS=<space-joined sorted-unique>
# MATCHES=<space-joined sorted-unique>
# TABLES=<space-joined sorted-unique>
# DISP=<space-joined sorted-unique>
# CONDS<TAB>... (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<TAB>... 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_<digits>(_<digits>) 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 <TABLE> ... / 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<n;i++) printf "%s%s", (i?" ":""), a[i]; printf "\n"
delete a
printf "MATCHES="; n=0; for (k in matches) { a[n++]=k }
asort_keys(a, n); for (i=0;i<n;i++) printf "%s%s", (i?" ":""), a[i]; printf "\n"
delete a
printf "TABLES="; n=0; for (k in tables) { a[n++]=k }
asort_keys(a, n); for (i=0;i<n;i++) printf "%s%s", (i?" ":""), a[i]; printf "\n"
delete a
printf "DISP="; n=0; for (k in disp) { a[n++]=k }
asort_keys(a, n); for (i=0;i<n;i++) printf "%s%s", (i?" ":""), a[i]; printf "\n"
for (i=1;i<=ncomm;i++) printf "COMMENT\t%s\n", comm[i]
for (i=1;i<=ncond;i++) printf "COND\t%s\n", cond[i]
}
# portable insertion sort (no gawk asort dependency — works with mawk/BWK awk)
function asort_keys(arr, n, i, j, tmp) {
for (i=1;i<n;i++){ tmp=arr[i]; j=i-1; while (j>=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 <procname> <bits-file>
_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"
}
# ─────────────────────────────────────────────────────────────────────────────
# ★ XLATE-INTERNAL FILTERING & FAN-OUT EXTRACTION (Bryan).
#
# A Cloverleaf .xlt is a TCL-style nested-brace program of `{ { OP <op> } ... }`
# statements. THREE op types change the message COUNT and MUST be called out:
# OP SUPPRESS → the message (or the in-progress output) is DROPPED. FILTERING.
# OP SEND → emit a COPY of the current output mid-translation. FAN-OUT
# (message cloned / multiplied — one input yields >1 output).
# OP CONTINUE → keep translating after a SEND (the companion that makes the
# fan-out produce a *second* distinct message). FAN-OUT.
#
# These usually live inside an `OP IF` whose `{ COND {...} }` is the governing
# condition, in either the THENBODY or ELSEBODY branch. We parse the brace
# structure deterministically (pure awk, NO API) and emit one record per action:
# SUPPRESS<TAB><branch><TAB><condition>
# SEND<TAB><branch><TAB><condition>
# CONTINUE<TAB><branch><TAB><condition>
# where <branch> is when/when-not/unconditional and <condition> is the nearest
# enclosing IF's COND (empty for a top-level/unconditional action).
# ─────────────────────────────────────────────────────────────────────────────
_locate_xlate() { # site xlatename(.xlt) → abs path or empty
local site="$1" xl="$2" base p i
[ -z "$xl" ] && return 0
base="${xl%.xlt}"
# 1) home site Xlate/
p="$ROOT/$site/Xlate/$base.xlt"
[ -f "$p" ] && { printf '%s' "$p"; return 0; }
# 2) any site (deterministic order — first wins)
for ((i=0; i<${#SITE_NAMES[@]}; i++)); do
p="$ROOT/${SITE_NAMES[$i]}/Xlate/$base.xlt"
[ -f "$p" ] && { printf '%s' "$p"; return 0; }
done
return 0
}
# Parse one .xlt; emit ACTION<TAB>BRANCH<TAB>COND records (one per suppress/send/
# continue). Pure awk, brace-depth + IF-frame stack; no API, no \b metachar.
_xlate_actions() { # xltfile
local f="$1"
[ -n "$f" ] || return 0
[ -f "$f" ] || return 0
awk '
BEGIN { depth=0; sp=0; pend_cond=""; pend_at=-1 }
# frame stack: at each THENBODY/ELSEBODY open we push {cond,branch,depth}
function push(c,b,d){ sp++; fcond[sp]=c; fbr[sp]=b; fdep[sp]=d }
function curcond(){ return (sp>0)? fcond[sp] : "" }
function curbr(){ return (sp>0)? fbr[sp] : "uncond" }
{
line=$0
# capture a COND for the IF whose body is about to open. The COND line is
# `{ COND {<expr>} }` (expr may itself contain braces/spaces).
if (match(line, /\{ COND \{/)) {
c=line
sub(/^[[:space:]]*\{ COND \{/,"",c) # drop up to the inner {
sub(/\} \}[[:space:]]*$/,"",c) # drop the trailing } }
gsub(/^[[:space:]]+|[[:space:]]+$/,"",c)
pend_cond=c
}
# branch openers — push a frame carrying the pending COND.
if (line ~ /\{ THENBODY \{/) push(pend_cond, "when", depth)
if (line ~ /\{ ELSEBODY \{/) push(pend_cond, "when-not", depth)
# the action ops. They are their own `{ { OP X } }` statement lines.
if (line ~ /\{ OP SUPPRESS \}/) printf "SUPPRESS\t%s\t%s\n", curbr(), curcond()
if (line ~ /\{ OP SEND \}/) printf "SEND\t%s\t%s\n", curbr(), curcond()
if (line ~ /\{ OP CONTINUE \}/) printf "CONTINUE\t%s\t%s\n", curbr(), curcond()
# update brace depth AFTER processing the line, then pop any frames whose
# body has now closed (depth dropped back to at-or-below the frame depth).
no=gsub(/\{/,"{",line); ncl=gsub(/\}/,"}",line)
depth += no - ncl
while (sp>0 && depth <= fdep[sp]) sp--
}
' "$f"
}
# Render a human "Xlate filtering & fan-out" block for one xlate, from its action
# records. Emits markdown bullet lines on stdout (empty output if none).
# _xlate_filter_block <xlatename> <xltfile>
_xlate_filter_block() {
local xl="$1" f="$2" recs
[ -n "$f" ] || return 0
[ -f "$f" ] || return 0
recs=$(_xlate_actions "$f")
[ -n "$recs" ] || return 0
local sup=0 fan=0 line act br cond
while IFS=$'\t' read -r act br cond; do
[ -z "$act" ] && continue
case "$act" in
SUPPRESS) sup=$((sup+1)) ;;
SEND|CONTINUE) fan=$((fan+1)) ;;
esac
done <<< "$recs"
# header line
printf '_Xlate `%s` changes the message count:_\n\n' "$xl"
while IFS=$'\t' read -r act br cond; do
[ -z "$act" ] && continue
local when=""
case "$br" in
when) [ -n "$cond" ] && when=" when \`$cond\`" ;;
when-not) [ -n "$cond" ] && when=" when NOT \`$cond\`" ;;
*) when=" unconditionally" ;;
esac
case "$act" in
SUPPRESS)
printf -- '- **FILTERING — message SUPPRESSED (dropped)**%s.\n' "$when" ;;
SEND)
printf -- '- **FAN-OUT — message CLONED / multiplied here** (`OP SEND` emits an extra output copy mid-translation)%s.\n' "$when" ;;
CONTINUE)
printf -- '- **FAN-OUT — translation CONTINUES after a send** (`OP CONTINUE`, the companion that yields a second distinct message)%s.\n' "$when" ;;
esac
done <<< "$recs"
printf '\n'
}
# ─────────────────────────────────────────────────────────────────────────────
# 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)
# Fallback: if `sources` yields nothing, the authoritative nc-paths chain's
# PENULTIMATE node IS the local routing thread that DESTs to this delivery
# (last node = the delivery itself). Strip its "site/" prefix. This keeps the
# route/xlate breakdown working even when the one-hop `sources` primitive
# misses (e.g. a same-process inbound the source-scan can't see).
if [ -z "$route_thr" ] && [ -n "$chain" ]; then
# collect the NODES (skip the --> / ==> arrow tokens); the penultimate node is
# the local routing thread. Keep only a node that lives in THIS thread's site.
local _penult; _penult=$(printf '%s' "$chain" | awk '
{ last2=""; last1=""
for (i=1;i<=NF;i++) if ($i!="-->" && $i!="==>") { last2=last1; last1=$i }
print last2 }')
if [ -n "$_penult" ]; then
local _pn_site="${_penult%%/*}" _pn_thr="${_penult#*/}"
[ "$_pn_site" = "$site" ] && route_thr="$_pn_thr"
fi
fi
# --- 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 may be a single thread OR a space-joined multi-dest list { DEST {a b c} }
# — match if THIS outbound (`$ob`) is among the route's destinations.
_dest_hit=0
for _d in $dest; do [ "$_d" = "$ob" ] && { _dest_hit=1; break; }; done
[ "$_dest_hit" = "1" ] || 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#*/}"
# ── upstream feed label (Bryan-curated inbound-systems lookup, deterministic).
# Resolve the feed-root thread's inbound PORT, then consult the lookup keyed
# on the feed thread name and/or port:<n>. On a hit we use the curated
# external-sender name; on NO hit we fall back to the honest generic label.
local feed_port="" feed_label=""
if [ -n "$feed_site" ] && [ -n "$feed_thr" ]; then
local feed_nc; feed_nc=$(_nc_for_site "$feed_site")
if [ -n "$feed_nc" ]; then
feed_port=$(_clean "$("$NCP" protocol-nested "$feed_nc" "$feed_thr" PROTOCOL.PORT 2>/dev/null | head -1)")
fi
fi
feed_label=$(_lookup_inbound_system "$feed_thr" "$feed_port")
# ── 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
# ── ★ xlate-internal filtering / fan-out (Bryan). Locate the route's .xlt and
# parse it for SUPPRESS (filtering) and SEND/CONTINUE (cloning/fan-out).
local xlate_block="" xlate_path="" xlate_sup=0 xlate_fan=0
if [ -n "$r_xlate" ]; then
xlate_path=$(_locate_xlate "$site" "$r_xlate")
[ -z "$xlate_path" ] && xlate_path=$(_locate_xlate "$feed_site" "$r_xlate")
if [ -n "$xlate_path" ]; then
xlate_block=$(_xlate_filter_block "$r_xlate" "$xlate_path")
local _acts; _acts=$(_xlate_actions "$xlate_path")
xlate_sup=$(printf '%s\n' "$_acts" | grep -c '^SUPPRESS' 2>/dev/null || true)
xlate_fan=$(printf '%s\n' "$_acts" | grep -cE '^(SEND|CONTINUE)' 2>/dev/null || true)
[ -z "$xlate_sup" ] && xlate_sup=0
[ -z "$xlate_fan" ] && xlate_fan=0
fi
fi
# ─────────────────────────── 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"
# ★ call out xlate-internal filtering / fan-out inline in the prose.
if [ "${xlate_sup:-0}" -gt 0 ] && [ "${xlate_fan:-0}" -gt 0 ]; then
printf ', which both **suppresses (filters)** and **clones (fans out)** messages internally (see "Xlate filtering & fan-out" below)'
elif [ "${xlate_sup:-0}" -gt 0 ]; then
printf ', which **suppresses (drops/filters)** some messages internally (see "Xlate filtering & fan-out" below)'
elif [ "${xlate_fan:-0}" -gt 0 ]; then
printf ', which **clones / fans out** messages internally (see "Xlate filtering & fan-out" below)'
fi
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
# ★ Xlate filtering & fan-out subsection (only when the xlate actually changes
# the message count). Deterministic parse of the .xlt — NO API.
if [ -n "$xlate_block" ]; then
printf '#### Xlate filtering & fan-out\n\n'
printf '%s\n\n' "$xlate_block"
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.
# "From" prefers the Bryan-curated inbound-systems label (deterministic, NOT
# guessed); when no curated entry matches we fall back to the honest generic
# "Epic (process <name>)" so the doc never fabricates a sender.
local feed_from
if [ -n "$feed_label" ]; then
feed_from="$feed_label"
else
feed_from="$(printf 'Epic (process `%s`)' "${in_pname:-${dproc:-ADT}}")"
fi
printf '| Epic | feed | Raw Epic feed entering the integrator | %s | `%s` |\n' \
"$feed_from" "${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`%s\n' "${r_xlate:-}" \
"$( if [ "${xlate_sup:-0}" -gt 0 ] || [ "${xlate_fan:-0}" -gt 0 ]; then
printf ' — internal: %d suppress (filter), %d send/continue (fan-out)' "${xlate_sup:-0}" "${xlate_fan:-0}"
fi )"
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
# ── optional stricter gate (Vera Minor-2): a real delivery additionally needs
# a downstream endpoint (non-empty PROTOCOL.HOST or PORT) OR OUTBOUNDONLY=1.
# Without this, a broad --name can sweep in reply-only outbounds that have
# no downstream target. (Default OFF — preserves prior behavior.)
if [ "$STRICT_DELIVERY" = "1" ]; then
s_host=$(_clean "$("$NCP" protocol-nested "$nc" "$prot" PROTOCOL.HOST 2>/dev/null | head -1)")
s_port=$(_clean "$("$NCP" protocol-nested "$nc" "$prot" PROTOCOL.PORT 2>/dev/null | head -1)")
s_obonly=$("$NCP" protocol-field "$nc" "$prot" OUTBOUNDONLY 2>/dev/null | head -1)
if [ -z "$s_host" ] && [ -z "$s_port" ] && [ "$s_obonly" != "1" ]; then
continue # reply-only outbound — not a real delivery
fi
fi
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._\n\n' "$PATTERN"
else
printf '_Cloverleaf interface documentation for `%s`._\n\n' "$THREAD_ARG"
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
} | out_target
if [ -n "$OUT" ]; then
printf 'nc-document: wrote %s (%d delivery section(s))\n' "$OUT" "${#TARGETS[@]}" >&2
fi