v0.8.21: interface document tool — <thread>/<system> document. Legacy ADT-Messages template (flow via nc_paths, Platform|Action|Description|From|To, per-delivery breakdown); deterministic API-free UPOC-bits extraction (comments/HL7 fields/event matches/table/disposition) + raw-TCL appendix; LLM polishes to prose only when API present. Verified on the real 24-site integrator (ADTto_CodaMetrix, codametrix system, PeriWatch UPOC proof).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bryan Johnson 2026-05-28 11:51:28 -07:00
parent 9364c7edeb
commit 474a0710a4
6 changed files with 758 additions and 184 deletions

View File

@ -4,6 +4,84 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here.
Versioning is loose-semver; bumps trigger the in-process self-update on every Versioning is loose-semver; bumps trigger the in-process self-update on every
running client via `LARRY_BASE_URL` + `MANIFEST`. running client via `LARRY_BASE_URL` + `MANIFEST`.
## v0.8.21 — 2026-05-28
Interface **`document`** tool rebuilt (`lib/nc-document.sh`) — documents a
Cloverleaf interface end-to-end in Bryan's confirmed Legacy "ADT Messages"
template. Deterministic, pure bash+awk, **API-FREE** (the whole tool runs
identically on an API-blocked host like Gundersen — no python, no `.pyz`, no
network).
**Two modes.**
- SINGLE INTERFACE: `nc-document.sh <thread> [site]` (or `<site>/<thread>`),
e.g. `ADTto_CodaMetrix ancout` — one fully-detailed interface section.
- SYSTEM/PATTERN: `nc-document.sh --name <pattern>`, e.g. `--name codametrix`
one section per matching DELIVERY (outbound) thread across all sites. A delivery
is any thread that is NOT an inbound listener (`ISSERVER=1`) and NOT an ICL/file
inbound router (`OBWORKASIB=1`), so it catches both `OUTBOUNDONLY=1` threads and
bidirectional `Xto_*` deliveries (e.g. `DFTto_codaMetrix`, `OUTBOUNDONLY=0`).
**Per-interface output (Legacy template):**
- **Title** = the interface / message type.
- **Description** prose — what the messages are, the TRXID filter that selects the
delivery, where translation happens (xlate vs raw), seeded from the surfaced
UPOC bits.
- **Message Flow** table `Platform | Action | Description | From | To` — Epic feed
→ Cloverleaf routing → Final Delivery, one row per hop. The routing row uses
`nc-paths.sh` and adapts its wording to whether the chain crosses a site
boundary (cross-site `destination`-block hop `==>` vs intra-site DATAXLATE route
`-->`).
- **Delivery breakdown** — Flow chain; how-received (inbound `PROTOCOL`
TYPE/HOST/PORT/ISSERVER + `ICLSERVERPORT`); inbound TRXID/TPS proc
(`DATAFORMAT.PROC`); the route's TRXID filter + WILDCARD; route TYPE;
PREPROCS/POSTPROCS; XLATE; destination host:port / process / type.
**★ Deterministic UPOC-bits extraction (the key feature).** For every referenced
UPOC proc (inbound TRXID/TPS proc + each route's PRE/POST/PROCS), locate its
`.tcl` under `$HCIROOT/<site>/tclprocs/` (home site first, then any site) and
surface — with NO API — into the Description: (1) the proc's **comments** (header +
inline `#` filter notes), (2) **HL7 fields** referenced (dotted `PID.8` + the
underscore `PV1_3_3` form normalized to dotted), (3) literal **event-code matches**
(`A01 A02 A03 …`, boundary-checked), (4) **table lookups** (`tbllookup` / `.tbl`,
e.g. `PeriCalm_Loc`), (5) **disposition** (CONTINUE/KILL/return → "pass matching /
kill non-matching"). Rendered compactly, e.g.:
`UPOC Epic_PeriCalm_ADT_pass — … · fields: PV1.45 PID.8 · matches: A02 A03 · table: PeriCalm_Loc · disposition: pass matching / kill non-matching`.
The **raw proc TCL** is included verbatim in a plainly-labelled `## Referenced
proc source` appendix for audit — with **no "summarize by hand / on an API box"
marker** (the surfaced bits ARE the content).
**LLM polish (enrichment, NOT in the bash tool).** The bash tool calls no API. The
`nc_document` tool schema now instructs the model, when run WITH the API, to
transparently polish the surfaced UPOC bits into smoother filter prose in the
Description (no marker, no special mechanism). On API-blocked hosts the
deterministic bits + appendix ARE the deliverable.
**Portability fixes baked in:**
- All extraction awk is `\b`-free (BSD/BWK awk on macOS + mawk on Windows Git-Bash
silently match nothing on `\b`); token boundaries use explicit char-class scans.
- Internal records are `\037`(US)-delimited, not TAB — bash `read` with a TAB IFS
collapses CONSECUTIVE empty fields and was silently shifting columns when an
ICL/file inbound has empty HOST/PORT/ISSERVER. Inbound facts are read into named
globals for the same reason.
- Route parser walks the real DATAXLATE depth map (route sub-blocks at depth 3,
DEST/TYPE/XLATE at depth 6, inner `{ PROCS <name> }` at depth 8), so per-route
TRXID/TYPE/XLATE/PREPROCS extraction is exact.
**Wiring.** `tool_nc_document` (larry.sh) now takes `thread`/`name`/`site`; the
`nc_document` tool schema documents single-thread + system modes and the
UPOC-bit-polish instruction. `larry tools nc-document` drives the same script
standalone (no API).
**Verified on the REAL integrator** (`HCIROOT=/tmp/clvf_realtest/integrator`, the
24-site QA env): generated `ADTto_CodaMetrix ancout` (matches Larry's verified
prototype — flow `mux/ADTfr_epic_964700 --> mux/OB_ADT_ancS ==> ancout/IB_ADT_muxS
--> ancout/ADTto_CodaMetrix`, inbound proc `trxId_IB_ADT_muxS`, TYPE xlate, XLATE
`Epic_ADT_CodaMetrix.xlt`, dest `172.31.23.2:39500` process ADT); the `codametrix`
system doc (2 deliveries: the ADT feed + the intra-site `DFTto_codaMetrix` DFT
feed); and the PeriWatch route, proving the UPOC-bits extraction surfaces real
content from `Epic_PeriCalm_ADT_pass` (comments, fields, `A02`/`A03` matches,
`PeriCalm_Loc` table, pass/kill disposition). `bash -n` clean; TOOLS_JSON valid.
## v0.8.20 — 2026-05-28 ## v0.8.20 — 2026-05-28
Route-chain tracer (`lib/nc-paths.sh`) REARCHITECTED for the real integrator: Route-chain tracer (`lib/nc-paths.sh`) REARCHITECTED for the real integrator:

View File

@ -23,21 +23,21 @@
# scripts/make-manifest.sh and bump VERSION. # scripts/make-manifest.sh and bump VERSION.
# Top-level scripts # Top-level scripts
larry.sh 20b68e650ff9a94a15f7745334fe0dc0f913da2c6d4c2b92388202c951d0d171 larry.sh ebbe42c5b4236737d8e3b02b4a19fd58e7877b67362c9ac3a729aac89cce0cd7
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
install-larry.sh e97da4e12a0d8863ca18d79b12f6c4294c72fa6d4b11dffeab66504236bb4eb1 install-larry.sh e97da4e12a0d8863ca18d79b12f6c4294c72fa6d4b11dffeab66504236bb4eb1
# Metadata # Metadata
VERSION 9bb2e455df78105b99303d11d1de0401d94142ff3fadc8e37bcba6c0c4d59914 VERSION 14f2df7b94315d4dcd8adba946a2421fe03b0e18f69cdc48fa45d527e13a5536
MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde
CHANGELOG.md 73f32366662b55ddc16cb937f0e6a4d0f4cd99181e8717ab9938d80b60984db6 CHANGELOG.md e1078bf774ea4137f1b4810bc8d875572059d854ffc04e559d9e57b2450b76bc
# Agent personas (system-prompt overlays) # Agent personas (system-prompt overlays)
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1 agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
agents/clover.md d1bbfd6cc4642c2bff6e15dcbdf051d71b063b3fe29e0be97d17b3180d3c7ac5 agents/clover.md d1bbfd6cc4642c2bff6e15dcbdf051d71b063b3fe29e0be97d17b3180d3c7ac5
agents/cloverleaf-cheatsheet.md 95c3bc52eaae92dff548702b0a0461ccba6ac6d8b410196c45ca59f28d0b3477 agents/cloverleaf-cheatsheet.md 35801c8d6b2ea67ac3ea828a11f611d1a716dee05f1db096a19d7c86b69c1734
agents/regress.md bb05ed1439b1e35d6e9799e32d683bfab166472c72115c1f02757e227c74e42f agents/regress.md bb05ed1439b1e35d6e9799e32d683bfab166472c72115c1f02757e227c74e42f
# Cygwin/MobaXterm CR-taint defense primitives (sourced by every tool) # Cygwin/MobaXterm CR-taint defense primitives (sourced by every tool)
@ -102,7 +102,7 @@ lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b
lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b
lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79 lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79
lib/nc-msgs.sh 729e2d6c9159e83fa177fc6b982e48ed8453a9743477cc90afdd3cd4ec7e620c lib/nc-msgs.sh 729e2d6c9159e83fa177fc6b982e48ed8453a9743477cc90afdd3cd4ec7e620c
lib/nc-document.sh 1f95082df3a88086868e5c159dddd4fd4019b706dbe1e48f0d7500eb9cd6c063 lib/nc-document.sh a643fddd1c71f0c8871c2bedd393c7ba3a5dceaa6d34e43d5f37cd9dd3985f5d
lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324 lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324
lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f
lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a

View File

@ -1 +1 @@
0.8.20 0.8.21

View File

@ -24,6 +24,7 @@ Two kinds of capability:
| `nc_paths(thread, site, [all], [site_only])` | **"trace the FULL route chain / what feeds X / the whole path / downstream + upstream"** — deterministic DFS path enumerator, output `SITE THREAD HOPS PATH`. Intra-site hops follow DATAXLATE DEST; **cross-site links are via named `destination` blocks** (a `DEST` naming a destination block resolves to its `{ SITE } { THREAD }`; the `PORT` corroborates). The whole route graph is parsed once into memory and walked with O(1) lookups. For `--up`, THREAD = feeder ROOT and the queried thread is the terminus. **Use this instead of repeated `nc_destinations`/`nc_sources`, grep, or read_file** for ANY path / chain / route-tracing question. | | `nc_paths(thread, site, [all], [site_only])` | **"trace the FULL route chain / what feeds X / the whole path / downstream + upstream"** — deterministic DFS path enumerator, output `SITE THREAD HOPS PATH`. Intra-site hops follow DATAXLATE DEST; **cross-site links are via named `destination` blocks** (a `DEST` naming a destination block resolves to its `{ SITE } { THREAD }`; the `PORT` corroborates). The whole route graph is parsed once into memory and walked with O(1) lookups. For `--up`, THREAD = feeder ROOT and the queried thread is the terminus. **Use this instead of repeated `nc_destinations`/`nc_sources`, grep, or read_file** for ANY path / chain / route-tracing question. |
| `nc_xlate_refs(netconfig, [name])` | "what .xlt files are referenced?" — all or scoped to one protocol | | `nc_xlate_refs(netconfig, [name])` | "what .xlt files are referenced?" — all or scoped to one protocol |
| `nc_find_inbound(netconfig, mode, format)` | "which threads are inbound?" — modes: `tcp-listen` (real upstream-client listeners, ISSERVER=1), `icl-or-file` (OBWORKASIB=1 internal mux/file inbounds), `all`. formats: tsv, jsonl, table | | `nc_find_inbound(netconfig, mode, format)` | "which threads are inbound?" — modes: `tcp-listen` (real upstream-client listeners, ISSERVER=1), `icl-or-file` (OBWORKASIB=1 internal mux/file inbounds), `all`. formats: tsv, jsonl, table |
| `nc_document(thread \| name, [site], [out])` | **"document this interface / system end-to-end"** — emits the Legacy "ADT Messages" markdown template (Title, Description, Message Flow table `Platform\|Action\|Description\|From\|To`, per-delivery breakdown) for ONE interface (`thread`, e.g. `ADTto_CodaMetrix`) or a whole SYSTEM (`name` pattern, e.g. `codametrix` → one section per matching delivery thread across sites). Deterministic, pure bash, **API-FREE** (runs on API-blocked boxes). ★ For every referenced UPOC proc it surfaces the proc's **comments, HL7 fields, matched event codes, table lookups, and disposition** into the Description, and includes the **raw proc TCL** in a `## Referenced proc source` appendix. **When you get this output WITH the API, polish those surfaced UPOC bits into smooth filter prose in the Description — do NOT invent facts, just smooth what's surfaced.** On an API-blocked host the deterministic bits + appendix ARE the deliverable (no "summarize by hand" marker). |
### NetConfig modification — generate, then write via `write_file` (Y/N gated) ### NetConfig modification — generate, then write via `write_file` (Y/N gated)

View File

@ -78,7 +78,7 @@ set -o pipefail
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Config # Config
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
LARRY_VERSION="0.8.20" LARRY_VERSION="0.8.21"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -4083,10 +4083,22 @@ tool_larry_rollback_list() {
} }
tool_nc_document() { tool_nc_document() {
local pattern="$1" out_path="${2:-}" hciroot="${3:-${HCIROOT:-}}" # SINGLE-THREAD mode: pass `thread` (+ optional `site`) — documents ONE interface
local title="${4:-}" status="${5:-}" poc_internal="${6:-}" poc_vendor="${7:-}" escalation="${8:-}" open_items="${9:-}" notes="${10:-}" # in the Legacy "ADT Messages" template with the full per-delivery breakdown +
# deterministic UPOC-bits extraction. SYSTEM mode: pass `name` (a pattern) — one
# section per matching delivery thread across sites. Exactly one of thread/name.
local thread="$1" name="$2" site="$3" out_path="${4:-}" hciroot="${5:-${HCIROOT:-}}"
local title="${6:-}" status="${7:-}" poc_internal="${8:-}" poc_vendor="${9:-}" escalation="${10:-}" open_items="${11:-}" notes="${12:-}"
_lib_err_if_missing || return _lib_err_if_missing || return
local args=(--name "$pattern") [ -n "$thread" ] || [ -n "$name" ] \
|| { echo "ERROR: nc_document needs either thread (single interface) or name (system pattern)"; return 1; }
local args=()
if [ -n "$thread" ]; then
args+=(--thread "$thread")
[ -n "$site" ] && args+=(--site "$site")
else
args+=(--name "$name")
fi
[ -n "$hciroot" ] && args+=(--hciroot "$hciroot") [ -n "$hciroot" ] && args+=(--hciroot "$hciroot")
[ -n "$out_path" ] && args+=(--out "$out_path") [ -n "$out_path" ] && args+=(--out "$out_path")
[ -n "$title" ] && args+=(--title "$title") [ -n "$title" ] && args+=(--title "$title")
@ -4165,7 +4177,8 @@ execute_tool() {
"$(J '.field // ""')" "$(J '.value // ""')" \ "$(J '.field // ""')" "$(J '.value // ""')" \
"$(J '.limit // 10')" "$(J '.format // "text"')" \ "$(J '.limit // 10')" "$(J '.format // "text"')" \
"$(J '.sitedir // ""')" "$(J '.db // ""')" ;; "$(J '.sitedir // ""')" "$(J '.db // ""')" ;;
nc_document) tool_nc_document "$(J '.name')" "$(J '.out // ""')" "$(J '.hciroot // ""')" \ nc_document) tool_nc_document "$(J '.thread // ""')" "$(J '.name // ""')" "$(J '.site // ""')" \
"$(J '.out // ""')" "$(J '.hciroot // ""')" \
"$(J '.title // ""')" "$(J '.status // ""')" \ "$(J '.title // ""')" "$(J '.status // ""')" \
"$(J '.poc_internal // ""')" "$(J '.poc_vendor // ""')" \ "$(J '.poc_internal // ""')" "$(J '.poc_vendor // ""')" \
"$(J '.escalation // ""')" "$(J '.open_items // ""')" \ "$(J '.escalation // ""')" "$(J '.open_items // ""')" \
@ -4224,7 +4237,7 @@ TOOLS_JSON=$(cat <<'TOOLS_END'
{"name":"nc_tclproc_refs","description":"List every TCL proc name referenced from a protocol block (or from the whole NetConfig if name is omitted). Pulls from DATAFORMAT.PROC, PREPROCS.PROCS, POSTPROCS.PROCS, etc. Unique sorted.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string"},"name":{"type":"string","description":"Optional. Scope to one protocol."}},"required":["netconfig"]}}, {"name":"nc_tclproc_refs","description":"List every TCL proc name referenced from a protocol block (or from the whole NetConfig if name is omitted). Pulls from DATAFORMAT.PROC, PREPROCS.PROCS, POSTPROCS.PROCS, etc. Unique sorted.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string"},"name":{"type":"string","description":"Optional. Scope to one protocol."}},"required":["netconfig"]}},
{"name":"hl7_field","description":"Extract a specific HL7 v2 field from a message. field_path = SEG[.FIELD[.COMPONENT[.SUBCOMPONENT]]]. Examples: PID.3 (MRN), PID.18 (account number), MSH.7 (timestamp), MSH.9.2 (event code, like A08), PID.5 (patient name with components). Multiple repetitions are returned one per line. Native v3, no v1/v2 dependency.","input_schema":{"type":"object","properties":{"message":{"type":"string","description":"Raw HL7 message text. Segments separated by \\r."},"field_path":{"type":"string","description":"Field path like PID.3 or MSH.9.2"}},"required":["message","field_path"]}}, {"name":"hl7_field","description":"Extract a specific HL7 v2 field from a message. field_path = SEG[.FIELD[.COMPONENT[.SUBCOMPONENT]]]. Examples: PID.3 (MRN), PID.18 (account number), MSH.7 (timestamp), MSH.9.2 (event code, like A08), PID.5 (patient name with components). Multiple repetitions are returned one per line. Native v3, no v1/v2 dependency.","input_schema":{"type":"object","properties":{"message":{"type":"string","description":"Raw HL7 message text. Segments separated by \\r."},"field_path":{"type":"string","description":"Field path like PID.3 or MSH.9.2"}},"required":["message","field_path"]}},
{"name":"nc_msgs","description":"Query Cloverleaf smat (SQLite!) databases for messages from a thread. Filters: time range, exact HL7 field match. Native v3 — reads smatdb directly with sqlite3 -ascii, no hcidbdump/dbExtract needed. Format text shows messages line-by-line with metadata; count returns just the count; json returns structured data. Operates on LOCAL smatdbs; for a remote env's smatdb, use ssh_pull_smat first (sampled mode is cheaper than pulling the whole DB).","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"Thread name. The .smatdb file under $HCISITEDIR/exec/processes/*/<thread>.smatdb is auto-located unless db is given."},"after":{"type":"string","description":"Time-after filter. Accepts \"3 days ago\", \"2026-05-20 14:30:00\", \"2026-05-20\", or a unix timestamp."},"before":{"type":"string","description":"Time-before filter, same formats as after."},"field":{"type":"string","description":"HL7 field path for exact-match filter, e.g. PID.18 or MSH.10."},"value":{"type":"string","description":"Value the field must equal. Use with field. Repeatable filters not supported via this single tool call — chain calls if you need multi-field AND."},"limit":{"type":"integer","description":"Max messages to return. Default 10."},"format":{"type":"string","enum":["text","json","count","raw"],"description":"text = human-readable with metadata; count = just the number; json = structured; raw = raw bytes separated by 0x1c."},"sitedir":{"type":"string","description":"Override $HCISITEDIR for thread-to-db location."},"db":{"type":"string","description":"Explicit .smatdb path; overrides auto-locate."}},"required":["thread"]}}, {"name":"nc_msgs","description":"Query Cloverleaf smat (SQLite!) databases for messages from a thread. Filters: time range, exact HL7 field match. Native v3 — reads smatdb directly with sqlite3 -ascii, no hcidbdump/dbExtract needed. Format text shows messages line-by-line with metadata; count returns just the count; json returns structured data. Operates on LOCAL smatdbs; for a remote env's smatdb, use ssh_pull_smat first (sampled mode is cheaper than pulling the whole DB).","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"Thread name. The .smatdb file under $HCISITEDIR/exec/processes/*/<thread>.smatdb is auto-located unless db is given."},"after":{"type":"string","description":"Time-after filter. Accepts \"3 days ago\", \"2026-05-20 14:30:00\", \"2026-05-20\", or a unix timestamp."},"before":{"type":"string","description":"Time-before filter, same formats as after."},"field":{"type":"string","description":"HL7 field path for exact-match filter, e.g. PID.18 or MSH.10."},"value":{"type":"string","description":"Value the field must equal. Use with field. Repeatable filters not supported via this single tool call — chain calls if you need multi-field AND."},"limit":{"type":"integer","description":"Max messages to return. Default 10."},"format":{"type":"string","enum":["text","json","count","raw"],"description":"text = human-readable with metadata; count = just the number; json = structured; raw = raw bytes separated by 0x1c."},"sitedir":{"type":"string","description":"Override $HCISITEDIR for thread-to-db location."},"db":{"type":"string","description":"Explicit .smatdb path; overrides auto-locate."}},"required":["thread"]}},
{"name":"nc_document","description":"Generate a complete markdown knowledge entry for a Cloverleaf subsystem identified by a name pattern. Walks every NetConfig under $HCIROOT, gathers config + sources + destinations + xlates + tclprocs for every matching thread, composes a markdown doc with placeholder context sections (Vendor POC, Internal Owner, Status, Escalation, Open items, Notes). Returns the doc text and (if out is given) writes it to that path.","input_schema":{"type":"object","properties":{"name":{"type":"string","description":"Case-insensitive substring/regex to match protocol names. e.g. 'codametrix', 'epic_adt', '3M'."},"out":{"type":"string","description":"Optional output file path. Convention: $LARRY_HOME/knowledge/<system>.md."},"hciroot":{"type":"string","description":"Override $HCIROOT for the NetConfig scan."},"title":{"type":"string","description":"Doc title. Default derived from name."},"status":{"type":"string","description":"System status fill-in (production/test/decommissioning/...)."},"poc_internal":{"type":"string","description":"Internal owner fill-in."},"poc_vendor":{"type":"string","description":"Vendor POC fill-in."},"escalation":{"type":"string","description":"Escalation path fill-in."},"open_items":{"type":"string","description":"Open items / known issues fill-in. Can be multi-line, will be inserted as-is."},"notes":{"type":"string","description":"Freeform notes fill-in."}},"required":["name"]}}, {"name":"nc_document","description":"Document a Cloverleaf INTERFACE end-to-end as a native markdown knowledge entry in Bryan's confirmed Legacy 'ADT Messages' template (Title; Description prose; Message Flow table Platform|Action|Description|From|To with one row per hop Epic-feed → Cloverleaf-routing → Final-Delivery built from the nc_paths route chain; per-delivery breakdown: inbound PROTOCOL TYPE/HOST/PORT/ISSERVER + inbound TRXID/TPS proc, route TRXID filter + TYPE + PREPROCS/POSTPROCS, XLATE, destination host:port/process). EVERYTHING THIS TOOL EMITS IS DETERMINISTIC, PURE BASH, AND API-FREE — it runs identically on an API-blocked host and never calls a model. ★ KEY FEATURE — for every referenced UPOC proc it locates the .tcl under $HCIROOT/<site>/tclprocs/ and DETERMINISTICALLY surfaces, into the Description, a compact bit-line: the proc's COMMENTS (the author's own filter notes), the HL7 FIELDS it references (PID.8, PV1.45, EVN.1…), the MATCHED literal event codes (A01 A02 A03…), TABLE lookups (e.g. PeriCalm_Loc), and the DISPOSITION (pass vs kill). The raw proc TCL is included verbatim in a '## Referenced proc source' appendix for audit (NO 'summarize by hand' marker — the surfaced bits ARE the content). ★ WHEN YOU (the model, running WITH the API) GET THIS TOOL'S OUTPUT: transparently POLISH those surfaced UPOC bits into smoother, human-readable filter descriptions inside the Description prose (e.g. turn 'fields: PV1.45 PID.8 · matches: A01 A02 A03 · table: Pericalm_Loc · disposition: kill non-matching' into 'passes only A01/A02/A03 admit/transfer events for female patients whose location is in the Pericalm_Loc table, killing everything else'). Do NOT invent facts not present in the surfaced bits; just smooth them. On an API-blocked host the deterministic bit-lines + appendix ARE the deliverable. MODES: (a) SINGLE INTERFACE — set thread (the delivery/outbound thread, e.g. 'ADTto_CodaMetrix'; optionally site to disambiguate) → one fully-detailed interface section. (b) SYSTEM/PATTERN — set name (case-insensitive substring/regex, e.g. 'codametrix') → one section per matching delivery thread across ALL sites. Give EXACTLY ONE of thread or name. Returns the doc text and (if out is given) writes it there.","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"SINGLE-INTERFACE mode: the delivery (outbound) thread to document, e.g. 'ADTto_CodaMetrix' or 'ADTto_periwatch'. Accepts a bare thread name or a 'site/thread' node. Give this OR name, not both."},"name":{"type":"string","description":"SYSTEM mode: case-insensitive substring/regex matching delivery thread names across all sites, e.g. 'codametrix', 'periwatch', 'epic_adt'. One section per match. Give this OR thread, not both."},"site":{"type":"string","description":"Home site of the thread (the NetConfig's parent dir). Optional — disambiguates a thread name present in multiple sites."},"out":{"type":"string","description":"Optional output file path. Convention: $LARRY_HOME/knowledge/<system>.md."},"hciroot":{"type":"string","description":"Override $HCIROOT for the NetConfig scan."},"title":{"type":"string","description":"Doc title. Default derived from thread/name."},"status":{"type":"string","description":"System status fill-in (production/test/decommissioning/...)."},"poc_internal":{"type":"string","description":"Internal owner fill-in."},"poc_vendor":{"type":"string","description":"Vendor POC fill-in."},"escalation":{"type":"string","description":"Escalation path fill-in."},"open_items":{"type":"string","description":"Open items / known issues fill-in. Can be multi-line, will be inserted as-is."},"notes":{"type":"string","description":"Freeform notes fill-in."}},"required":[]}},
{"name":"nc_find","description":"Cross-site thread search. Native v3 replacement for v1 tbn/tbp/tbh/tbpr/where. Walks every NetConfig under $HCIROOT and returns matching threads with site, port, host, process, direction, file, line. Modes: name=partial name match (like tbn); port=exact port (like tbp); host=substring on host (like tbh); process=substring on PROCESSNAME (like tbpr); where=exact name match across all sites (like the v1 `<thread> where`); xlate=threads referencing a specific .xlt; tclproc=threads referencing a specific TCL proc.","input_schema":{"type":"object","properties":{"mode":{"type":"string","enum":["name","port","host","process","where","xlate","tclproc"],"description":"Search mode."},"query":{"type":"string","description":"Query value: partial name, port number, host substring, process name, exact thread name, xlate filename, or tclproc name."},"format":{"type":"string","enum":["table","tsv","jsonl"],"description":"Output format. Default table."},"hciroot":{"type":"string","description":"Override $HCIROOT."}},"required":["mode","query"]}}, {"name":"nc_find","description":"Cross-site thread search. Native v3 replacement for v1 tbn/tbp/tbh/tbpr/where. Walks every NetConfig under $HCIROOT and returns matching threads with site, port, host, process, direction, file, line. Modes: name=partial name match (like tbn); port=exact port (like tbp); host=substring on host (like tbh); process=substring on PROCESSNAME (like tbpr); where=exact name match across all sites (like the v1 `<thread> where`); xlate=threads referencing a specific .xlt; tclproc=threads referencing a specific TCL proc.","input_schema":{"type":"object","properties":{"mode":{"type":"string","enum":["name","port","host","process","where","xlate","tclproc"],"description":"Search mode."},"query":{"type":"string","description":"Query value: partial name, port number, host substring, process name, exact thread name, xlate filename, or tclproc name."},"format":{"type":"string","enum":["table","tsv","jsonl"],"description":"Output format. Default table."},"hciroot":{"type":"string","description":"Override $HCIROOT."}},"required":["mode","query"]}},
{"name":"nc_insert_protocol","description":"Insert a new protocol block into a NetConfig file. ALL WRITES GO THROUGH THE JOURNAL — original is snapshotted, diff is saved, the file is atomically replaced. Use larry_rollback_list to view, larry-rollback.sh CLI to undo. mode=end appends; mode=after needs anchor=existing-protocol-name; mode=before needs anchor.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string","description":"Target NetConfig file path."},"block":{"type":"string","description":"The full protocol block text (starting with 'protocol NAME {' and ending with '}'). Get this from nc_make_jump output."},"mode":{"type":"string","enum":["end","after","before"],"description":"Insertion position. Default end."},"anchor":{"type":"string","description":"For mode=after|before: existing protocol name to position relative to."}},"required":["netconfig","block"]}}, {"name":"nc_insert_protocol","description":"Insert a new protocol block into a NetConfig file. ALL WRITES GO THROUGH THE JOURNAL — original is snapshotted, diff is saved, the file is atomically replaced. Use larry_rollback_list to view, larry-rollback.sh CLI to undo. mode=end appends; mode=after needs anchor=existing-protocol-name; mode=before needs anchor.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string","description":"Target NetConfig file path."},"block":{"type":"string","description":"The full protocol block text (starting with 'protocol NAME {' and ending with '}'). Get this from nc_make_jump output."},"mode":{"type":"string","enum":["end","after","before"],"description":"Insertion position. Default end."},"anchor":{"type":"string","description":"For mode=after|before: existing protocol name to position relative to."}},"required":["netconfig","block"]}},

View File

@ -1,39 +1,81 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# nc-document.sh — generate a v3 native markdown knowledge entry for a Cloverleaf # nc-document.sh — document a Cloverleaf INTERFACE end-to-end as a native markdown
# subsystem identified by a name pattern. Walks every NetConfig under $HCIROOT # knowledge entry in Bryan's confirmed Legacy "ADT Messages" template.
# (or a passed-in list), gathers config + flow + xlates + tclprocs, composes a #
# markdown doc with placeholder context sections for humans to fill. # 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: # Usage:
# nc-document.sh <thread> [site] [options]
# nc-document.sh <site>/<thread> [options]
# nc-document.sh --name <pattern> [options] # nc-document.sh --name <pattern> [options]
# #
# --name PATTERN case-insensitive substring/regex to match protocol names # --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 # --hciroot DIR defaults to $HCIROOT
# --netconfigs PATHS colon-separated explicit NetConfig list (overrides --hciroot scan)
# --out PATH output markdown path (default: stdout) # --out PATH output markdown path (default: stdout)
# --title TITLE doc title (default: derived from --name) # --title TITLE doc title (default: derived from thread/name)
# --poc-vendor TXT Vendor POC content # --poc-vendor TXT Vendor POC content
# --poc-internal TXT Internal Owner content # --poc-internal TXT Internal Owner content
# --status TXT e.g. production / test / decommissioning # --status TXT e.g. production / test / decommissioning
# --escalation TXT Escalation path text # --escalation TXT Escalation path text
# --open-items TXT Open items text (bulleted by you if multi-line) # --open-items TXT Open items text
# --notes TXT freeform additional notes # --notes TXT freeform additional notes
# # --no-appendix omit the raw proc-source appendix
# Any --poc/-status/--escalation/--open-items/--notes that you OMIT becomes an # -h | --help this help
# empty placeholder section in the doc, ready for someone to fill.
set -u set -u
set -o pipefail set -o pipefail
NC_SELF="$0" NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
NCP="$LIB_DIR/nc-parse.sh" NCP="$LIB_DIR/nc-parse.sh"
NCI="$LIB_DIR/nc-inbound.sh" NCPATHS="$LIB_DIR/nc-paths.sh"
die() { printf 'nc-document: %s\n' "$*" >&2; exit 1; } die() { printf 'nc-document: %s\n' "$*" >&2; exit 1; }
# ─────────────────────────────────────────────────────────────────────────────
# Arg parsing
# ─────────────────────────────────────────────────────────────────────────────
PATTERN="" PATTERN=""
THREAD_ARG=""
SITE_ARG=""
HCIROOT_OVERRIDE="" HCIROOT_OVERRIDE=""
NETCONFIGS_OVERRIDE=""
OUT="" OUT=""
TITLE="" TITLE=""
POC_VENDOR="" POC_VENDOR=""
@ -42,45 +84,84 @@ STATUS=""
ESCALATION="" ESCALATION=""
OPEN_ITEMS="" OPEN_ITEMS=""
NOTES="" NOTES=""
WANT_APPENDIX=1
POSITIONAL=()
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--name) shift; PATTERN="$1" ;; --name) shift; PATTERN="${1:-}" ;;
--hciroot) shift; HCIROOT_OVERRIDE="$1" ;; --thread) shift; THREAD_ARG="${1:-}" ;;
--netconfigs) shift; NETCONFIGS_OVERRIDE="$1" ;; --site) shift; SITE_ARG="${1:-}" ;;
--out) shift; OUT="$1" ;; --hciroot) shift; HCIROOT_OVERRIDE="${1:-}" ;;
--title) shift; TITLE="$1" ;; --out) shift; OUT="${1:-}" ;;
--poc-vendor) shift; POC_VENDOR="$1" ;; --title) shift; TITLE="${1:-}" ;;
--poc-internal) shift; POC_INTERNAL="$1" ;; --poc-vendor) shift; POC_VENDOR="${1:-}" ;;
--status) shift; STATUS="$1" ;; --poc-internal) shift; POC_INTERNAL="${1:-}" ;;
--escalation) shift; ESCALATION="$1" ;; --status) shift; STATUS="${1:-}" ;;
--open-items) shift; OPEN_ITEMS="$1" ;; --escalation) shift; ESCALATION="${1:-}" ;;
--notes) shift; NOTES="$1" ;; --open-items) shift; OPEN_ITEMS="${1:-}" ;;
-h|--help) sed -n '2,25p' "$NC_SELF"; exit 0 ;; --notes) shift; NOTES="${1:-}" ;;
-*) die "unknown flag: $1" ;; --no-appendix) WANT_APPENDIX=0 ;;
*) die "extra arg: $1" ;; -h|--help) sed -n '2,72p' "$NC_SELF" | sed 's/^# \{0,1\}//'; exit 0 ;;
--*) die "unknown flag: $1" ;;
*) POSITIONAL+=("$1") ;;
esac esac
shift shift
done done
[ -n "$PATTERN" ] || die "missing --name PATTERN" # Positional shapes (single-thread mode):
[ -z "$TITLE" ] && TITLE="$(printf '%s' "$PATTERN" | tr '[:upper:]' '[:lower:]')" # <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."
# Determine the NetConfig list
NCONFIGS=()
if [ -n "$NETCONFIGS_OVERRIDE" ]; then
IFS=':' read -ra NCONFIGS <<< "$NETCONFIGS_OVERRIDE"
else
ROOT="${HCIROOT_OVERRIDE:-${HCIROOT:-}}" ROOT="${HCIROOT_OVERRIDE:-${HCIROOT:-}}"
[ -n "$ROOT" ] || die "no \$HCIROOT and no --hciroot; pass one or set the env var" [ -n "$ROOT" ] || die "no \$HCIROOT and no --hciroot; pass one or set the env var"
[ -d "$ROOT" ] || die "hciroot not a directory: $ROOT" [ -d "$ROOT" ] || die "hciroot not a directory: $ROOT"
while IFS= read -r nc; do
NCONFIGS+=("$nc")
done < <(find "$ROOT" -maxdepth 2 -name NetConfig -type f 2>/dev/null)
fi
[ ${#NCONFIGS[@]} -gt 0 ] || die "no NetConfig files found"
# Emit to OUT or stdout # ─────────────────────────────────────────────────────────────────────────────
# 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() { out_target() {
if [ -n "$OUT" ]; then if [ -n "$OUT" ]; then
mkdir -p "$(dirname "$OUT")" 2>/dev/null mkdir -p "$(dirname "$OUT")" 2>/dev/null
@ -90,145 +171,546 @@ out_target() {
fi fi
} }
# Gather all matching protocols across all NetConfigs # ─────────────────────────────────────────────────────────────────────────────
declare -a MATCHES # strip a leading "{" / trailing "}" / empty-brace marker from a scalar value
for nc in "${NCONFIGS[@]}"; do # ─────────────────────────────────────────────────────────────────────────────
site=$(basename "$(dirname "$nc")") _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 while IFS= read -r prot; do
[ -z "$prot" ] && continue [ -z "$prot" ] && continue
MATCHES+=("$site|$nc|$prot") 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 < <("$NCP" list-protocols "$nc" 2>/dev/null | grep -i -- "$PATTERN" || true)
done done
[ "${#TARGETS[@]}" -gt 0 ] || die "no delivery (outbound) threads matching \"$PATTERN\" under $ROOT"
if [ ${#MATCHES[@]} -eq 0 ]; then [ -z "$TITLE" ] && TITLE="$(printf '%s' "$PATTERN" | tr '[:upper:]' '[:lower:]')"
printf 'No protocols matching "%s" found in %d NetConfig(s).\n' "$PATTERN" "${#NCONFIGS[@]}" >&2
exit 2
fi fi
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Compose markdown # Compose the document
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
{ {
printf '# %s — Cloverleaf System Knowledge Entry\n\n' "$TITLE" printf '# %s\n\n' "$TITLE"
printf '_Auto-generated by Larry-Anywhere v3 nc-document.sh on %s. Auto-derived facts are below; context fields are for humans to fill or refine._\n\n' "$(date -Iseconds 2>/dev/null || date)" 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 '## Context\n\n'
printf -- '- **Vendor POC:** %s\n' "${POC_VENDOR:-_(unfilled — add vendor contact name + email/phone)_}" printf -- '- **Vendor POC:** %s\n' "${POC_VENDOR:-_(unfilled)_}"
printf -- '- **Internal Owner:** %s\n' "${POC_INTERNAL:-_(unfilled — add the internal owner / engineer)_}" printf -- '- **Internal Owner:** %s\n' "${POC_INTERNAL:-_(unfilled)_}"
printf -- '- **Status:** %s\n' "${STATUS:-_(unfilled — production / test / decommissioning / on hold)_}" printf -- '- **Status:** %s\n' "${STATUS:-_(unfilled — production / test / decommissioning)_}"
printf -- '- **Escalation:** %s\n' "${ESCALATION:-_(unfilled — on-call path, ticket queue, etc.)_}" printf -- '- **Escalation:** %s\n' "${ESCALATION:-_(unfilled)_}"
printf '\n### Open items\n' printf '\n'
if [ -n "$OPEN_ITEMS" ]; then if [ -n "$OPEN_ITEMS" ]; then printf '### Open items\n%s\n\n' "$OPEN_ITEMS"; fi
printf '%s\n\n' "$OPEN_ITEMS" if [ -n "$NOTES" ]; then printf '### Notes\n%s\n\n' "$NOTES"; fi
else
printf '_(unfilled — add open items / known issues / pending work)_\n\n'
fi
printf '### Notes\n'
if [ -n "$NOTES" ]; then
printf '%s\n\n' "$NOTES"
else
printf '_(unfilled — add any free-form context)_\n\n'
fi
# ─── Threads inventory ─── # one section per delivery thread
# v0.7.5: tr -cd '0-9' instead of tr -d ' ' — Cygwin wc.exe CR-taint would for line in "${TARGETS[@]}"; do
# otherwise crash `printf '%d'` with "invalid number".
printf '## Threads (%d matched in %d site(s))\n\n' "${#MATCHES[@]}" "$(printf '%s\n' "${MATCHES[@]}" | awk -F'|' '{print $1}' | sort -u | wc -l | tr -cd '0-9')"
printf '| Site | Thread | Process | Direction | Port | Host | Type |\n'
printf '|---|---|---|---|---|---|---|\n'
for line in "${MATCHES[@]}"; do
IFS='|' read -r site nc prot <<< "$line" IFS='|' read -r site nc prot <<< "$line"
pname=$("$NCP" protocol-field "$nc" "$prot" PROCESSNAME 2>/dev/null | head -1) document_thread "$prot" "$site" "$nc"
obib=$("$NCP" protocol-field "$nc" "$prot" OBWORKASIB 2>/dev/null | head -1)
outonly=$("$NCP" protocol-field "$nc" "$prot" OUTBOUNDONLY 2>/dev/null | head -1)
ptype=$("$NCP" protocol-nested "$nc" "$prot" PROTOCOL.TYPE 2>/dev/null | head -1)
phost=$("$NCP" protocol-nested "$nc" "$prot" PROTOCOL.HOST 2>/dev/null | head -1)
pport=$("$NCP" protocol-nested "$nc" "$prot" PROTOCOL.PORT 2>/dev/null | head -1)
isserver=$("$NCP" protocol-nested "$nc" "$prot" PROTOCOL.ISSERVER 2>/dev/null | head -1)
direction="?"
[ "$isserver" = "1" ] && direction="inbound (TCP listener)"
[ "$obib" = "1" ] && [ "$direction" = "?" ] && direction="inbound (ICL/file)"
[ "$outonly" = "1" ] && [ "$direction" = "?" ] && direction="outbound"
phost_clean=$(printf '%s' "$phost" | sed 's/^{}$//')
pport_clean=$(printf '%s' "$pport" | sed 's/^{}$//')
printf '| `%s` | `%s` | `%s` | %s | %s | %s | %s |\n' \
"$site" "$prot" "${pname:-?}" "$direction" "${pport_clean:-}" "${phost_clean:-}" "${ptype:-?}"
done
printf '\n'
# ─── Per-thread detail ───
for line in "${MATCHES[@]}"; do
IFS='|' read -r site nc prot <<< "$line"
printf '## `%s` (site: `%s`)\n\n' "$prot" "$site"
printf '### Sources (what feeds this thread)\n\n'
sources=$("$NCP" sources "$nc" "$prot" 2>/dev/null)
if [ -n "$sources" ]; then
printf '%s\n' "$sources" | awk '{print "- `" $0 "`"}'
else
printf '_(none found in `%s`; may be fed via TCP from outside, or from another site via ICL)_\n' "$site"
fi
printf '\n'
printf '### Destinations (where this thread routes to)\n\n'
dests=$("$NCP" destinations "$nc" "$prot" 2>/dev/null)
if [ -n "$dests" ]; then
printf '%s\n' "$dests" | awk '{print "- `" $0 "`"}'
else
printf '_(no DEST entries in DATAXLATE block)_\n'
fi
printf '\n'
printf '### Xlates referenced\n\n'
xlates=$("$NCP" xlate-refs "$nc" "$prot" 2>/dev/null)
if [ -n "$xlates" ]; then
printf '%s\n' "$xlates" | awk -v site="$site" '{print "- `" site "/Xlate/" $0 "`"}'
else
printf '_(no xlates — pass-through or raw routing only)_\n'
fi
printf '\n'
printf '### TCL procs referenced\n\n'
tcls=$("$NCP" tclproc-refs "$nc" "$prot" 2>/dev/null)
if [ -n "$tcls" ]; then
printf '%s\n' "$tcls" | awk -v site="$site" '{print "- `" site "/tclprocs/" $0 ".tcl`"}'
else
printf '_(no TCL procs referenced)_\n'
fi
printf '\n'
done done
# ─── Sources outside the matched set (the "fed by" landscape) ─── # Appendix — raw proc source (plainly labelled; NO "summarize" marker)
printf '## Adjacent threads (the network this subsystem talks to)\n\n' if [ "$WANT_APPENDIX" = "1" ] && [ "${#APPENDIX_LIST[@]}" -gt 0 ]; then
printf '_All threads that **either feed** matched threads **or are fed by** matched threads. These are the immediate operational neighbors._\n\n' printf '## Referenced proc source\n\n'
printf '| Site | Thread | Relationship to matched set |\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'
printf '|---|---|---|\n' for rec in "${APPENDIX_LIST[@]}"; do
declare -A SEEN IFS='|' read -r asite aproc apath <<< "$rec"
for line in "${MATCHES[@]}"; do printf '### `%s` (site `%s`)\n\n' "$aproc" "$asite"
IFS='|' read -r site nc prot <<< "$line" if [ -n "$apath" ] && [ -f "$apath" ]; then
while IFS= read -r src; do printf '_Source: `%s`_\n\n' "$apath"
key="$site|$src" printf '```tcl\n'
[ -n "${SEEN[$key]:-}" ] && continue cat "$apath"
SEEN[$key]=1 printf '\n```\n\n'
printf '| `%s` | `%s` | feeds `%s` |\n' "$site" "$src" "$prot" else
done < <("$NCP" sources "$nc" "$prot" 2>/dev/null) printf '_(proc `%s.tcl` not found under any site tclprocs/)_\n\n' "$aproc"
while IFS= read -r dst; do fi
key="$site|$dst"
[ -n "${SEEN[$key]:-}" ] && continue
SEEN[$key]=1
printf '| `%s` | `%s` | receives from `%s` |\n' "$site" "$dst" "$prot"
done < <("$NCP" destinations "$nc" "$prot" 2>/dev/null)
done done
printf '\n' fi
# ─── Footer ───
printf '---\n\n' printf '---\n\n'
printf '_Generated: %s · NetConfigs scanned: %d · Pattern: `%s`_\n' \ printf '_Generated: %s · sites scanned: %d · %s_\n' \
"$(date -Iseconds 2>/dev/null || date)" "${#NCONFIGS[@]}" "$PATTERN" "$(date -Iseconds 2>/dev/null || date)" "${#SITE_NCS[@]}" \
"$( [ -n "$PATTERN" ] && printf 'pattern: `%s`' "$PATTERN" || printf 'thread: `%s`' "$THREAD_ARG" )"
} | out_target } | out_target
[ -n "$OUT" ] && printf 'nc-document: wrote %s (%d matched threads across %d site(s))\n' \ if [ -n "$OUT" ]; then
"$OUT" "${#MATCHES[@]}" "$(printf '%s\n' "${MATCHES[@]}" | awk -F'|' '{print $1}' | sort -u | wc -l | tr -cd '0-9')" >&2 printf 'nc-document: wrote %s (%d delivery section(s))\n' "$OUT" "${#TARGETS[@]}" >&2
fi