717 lines
37 KiB
Bash
Executable File
717 lines
37 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
|
|
# -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> 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
|
|
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"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 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
|