#!/usr/bin/env bash # nc-document.sh — document a Cloverleaf INTERFACE end-to-end as a PLAIN-TEXT # knowledge entry in Bryan's confirmed Legacy "ADT Messages" template. # # OUTPUT IS PLAIN TEXT BY DEFAULT (no markdown). Bryan shares the generated doc # in OneNote, which does NOT render markdown — `#`, `**bold**`, `| pipe tables |`, # `---` rules and backticks all show up as literal junk. So the default render is # clean plain text: UPPERCASE headings (underlined with a dashes line), no bold, # no backticks, no pipe tables, no `---` rules. It reads cleanly in any editor and # pastes straight into OneNote. # # 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 (always inline in # each interface's description) ARE the deliverable; the raw proc TCL appendix is # OPT-IN via --raw-tcl. When larry runs WITH the API the model transparently # polishes the 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, which stay INLINE in the description by default. # - Message Flow = one block per hop (Epic feed → Cloverleaf routing → Final # Delivery), built from nc-paths.sh (the route-chain enumerator). # DEFAULT render = a label:value block per hop (reads in ANY # font, zero setup). With --onenote-table this becomes TAB- # separated rows (header + one row per hop) you paste into # OneNote and turn into a real table via Insert > Table. # - 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. Same DEFAULT/--onenote-table behavior. # - ★ 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 INLINE in the Description (always on). # - Raw proc TCL appendix — OPT-IN behind --raw-tcl (off by default). # # 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 text path (default: stdout) # --title TITLE doc title (default: derived from thread/name) # --onenote-table render the tabular sections (Message Flow, Delivery # breakdown) as TAB-separated rows (header row + one data # row per record, real \t between cells, NO leading/trailing # pipes) for paste-into-OneNote → Insert > Table. The # DEFAULT (without this flag) renders them as indented # label:value blocks that read in any font. Non-tabular # sections (Title/Context/Description) stay plain text in # both modes. # --raw-tcl also emit the raw proc-source appendix (verbatim TCL of # every referenced UPOC proc). OFF by default — the readable # extracted UPOC bits stay inline in each description; only # the verbatim appendix is gated behind this flag. # --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 # --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: 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:) 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="" # Raw TCL appendix is OPT-IN (--raw-tcl). The readable extracted UPOC bits stay # inline in each description regardless; only the verbatim appendix is gated. WANT_APPENDIX=0 # Tabular sections render as label:value blocks by default; --onenote-table emits # TAB-separated rows instead (header + one row per record) for OneNote paste. ONENOTE_TABLE=0 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:-}" ;; --onenote-table) ONENOTE_TABLE=1 ;; --raw-tcl) WANT_APPENDIX=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 ;; # back-compat no-op (appendix is now off by default) --strict-delivery) STRICT_DELIVERY=1 ;; --inbound-systems) shift; INBOUND_SYSTEMS_FILE="${1:-}" ;; -h|--help) sed -n '2,92p' "$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 } # ───────────────────────────────────────────────────────────────────────────── # PLAIN-TEXT RENDER HELPERS (v0.8.24). The whole doc is plain text so it pastes # cleanly into OneNote (which does NOT render markdown). No `#`/`##`, no # `**bold**`, no backticks, no `---` rules, no `| pipe tables |`. # ───────────────────────────────────────────────────────────────────────────── TAB=$(printf '\t') # A top-level heading: UPPERCASE text underlined with a full-width dashes line. _h1() { # text local t; t=$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]') printf '%s\n' "$t" printf '%s\n\n' "$(printf '%*s' "${#t}" '' | tr ' ' '=')" } # A section heading: UPPERCASE text underlined with dashes. _h2() { # text local t; t=$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]') printf '%s\n' "$t" printf '%s\n\n' "$(printf '%*s' "${#t}" '' | tr ' ' '-')" } # A sub-heading: UPPERCASE text on its own line, no underline (keeps it light). _h3() { # text printf '%s\n\n' "$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" } # A label:value line for a key/value bullet block. Pads the label to a common # width so values line up in a monospaced view (and still reads fine elsewhere). # _kv