From 474a0710a4e3153b0e00a68505bfc5fac08c21bf Mon Sep 17 00:00:00 2001 From: Bryan Johnson Date: Thu, 28 May 2026 11:51:28 -0700 Subject: [PATCH] =?UTF-8?q?v0.8.21:=20interface=20document=20tool=20?= =?UTF-8?q?=E2=80=94=20/=20document.=20Legacy=20ADT-Messag?= =?UTF-8?q?es=20template=20(flow=20via=20nc=5Fpaths,=20Platform|Action|Des?= =?UTF-8?q?cription|From|To,=20per-delivery=20breakdown);=20deterministic?= =?UTF-8?q?=20API-free=20UPOC-bits=20extraction=20(comments/HL7=20fields/e?= =?UTF-8?q?vent=20matches/table/disposition)=20+=20raw-TCL=20appendix;=20L?= =?UTF-8?q?LM=20polishes=20to=20prose=20only=20when=20API=20present.=20Ver?= =?UTF-8?q?ified=20on=20the=20real=2024-site=20integrator=20(ADTto=5FCodaM?= =?UTF-8?q?etrix,=20codametrix=20system,=20PeriWatch=20UPOC=20proof).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 78 +++ MANIFEST | 10 +- VERSION | 2 +- agents/cloverleaf-cheatsheet.md | 1 + larry.sh | 25 +- lib/nc-document.sh | 826 +++++++++++++++++++++++++------- 6 files changed, 758 insertions(+), 184 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 680739e..e8a2022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 [site]` (or `/`), + e.g. `ADTto_CodaMetrix ancout` — one fully-detailed interface section. +- SYSTEM/PATTERN: `nc-document.sh --name `, 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//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 }` 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 Route-chain tracer (`lib/nc-paths.sh`) REARCHITECTED for the real integrator: diff --git a/MANIFEST b/MANIFEST index 4540a16..3858f11 100644 --- a/MANIFEST +++ b/MANIFEST @@ -23,21 +23,21 @@ # scripts/make-manifest.sh and bump VERSION. # Top-level scripts -larry.sh 20b68e650ff9a94a15f7745334fe0dc0f913da2c6d4c2b92388202c951d0d171 +larry.sh ebbe42c5b4236737d8e3b02b4a19fd58e7877b67362c9ac3a729aac89cce0cd7 larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 install-larry.sh e97da4e12a0d8863ca18d79b12f6c4294c72fa6d4b11dffeab66504236bb4eb1 # Metadata -VERSION 9bb2e455df78105b99303d11d1de0401d94142ff3fadc8e37bcba6c0c4d59914 +VERSION 14f2df7b94315d4dcd8adba946a2421fe03b0e18f69cdc48fa45d527e13a5536 MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde -CHANGELOG.md 73f32366662b55ddc16cb937f0e6a4d0f4cd99181e8717ab9938d80b60984db6 +CHANGELOG.md e1078bf774ea4137f1b4810bc8d875572059d854ffc04e559d9e57b2450b76bc # Agent personas (system-prompt overlays) agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1 agents/clover.md d1bbfd6cc4642c2bff6e15dcbdf051d71b063b3fe29e0be97d17b3180d3c7ac5 -agents/cloverleaf-cheatsheet.md 95c3bc52eaae92dff548702b0a0461ccba6ac6d8b410196c45ca59f28d0b3477 +agents/cloverleaf-cheatsheet.md 35801c8d6b2ea67ac3ea828a11f611d1a716dee05f1db096a19d7c86b69c1734 agents/regress.md bb05ed1439b1e35d6e9799e32d683bfab166472c72115c1f02757e227c74e42f # 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-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79 lib/nc-msgs.sh 729e2d6c9159e83fa177fc6b982e48ed8453a9743477cc90afdd3cd4ec7e620c -lib/nc-document.sh 1f95082df3a88086868e5c159dddd4fd4019b706dbe1e48f0d7500eb9cd6c063 +lib/nc-document.sh a643fddd1c71f0c8871c2bedd393c7ba3a5dceaa6d34e43d5f37cd9dd3985f5d lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324 lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a diff --git a/VERSION b/VERSION index d757c24..9f75b28 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.20 +0.8.21 diff --git a/agents/cloverleaf-cheatsheet.md b/agents/cloverleaf-cheatsheet.md index 9007c5a..50193aa 100644 --- a/agents/cloverleaf-cheatsheet.md +++ b/agents/cloverleaf-cheatsheet.md @@ -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_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_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) diff --git a/larry.sh b/larry.sh index 11d2244..9cc6d99 100755 --- a/larry.sh +++ b/larry.sh @@ -78,7 +78,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.8.20" +LARRY_VERSION="0.8.21" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" # ───────────────────────────────────────────────────────────────────────────── @@ -4083,10 +4083,22 @@ tool_larry_rollback_list() { } tool_nc_document() { - local pattern="$1" out_path="${2:-}" hciroot="${3:-${HCIROOT:-}}" - local title="${4:-}" status="${5:-}" poc_internal="${6:-}" poc_vendor="${7:-}" escalation="${8:-}" open_items="${9:-}" notes="${10:-}" + # SINGLE-THREAD mode: pass `thread` (+ optional `site`) — documents ONE interface + # 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 - 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 "$out_path" ] && args+=(--out "$out_path") [ -n "$title" ] && args+=(--title "$title") @@ -4165,7 +4177,8 @@ execute_tool() { "$(J '.field // ""')" "$(J '.value // ""')" \ "$(J '.limit // 10')" "$(J '.format // "text"')" \ "$(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 '.poc_internal // ""')" "$(J '.poc_vendor // ""')" \ "$(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":"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/*/.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/.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//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/.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 ` 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"]}}, diff --git a/lib/nc-document.sh b/lib/nc-document.sh index 1195527..cfcf3ca 100755 --- a/lib/nc-document.sh +++ b/lib/nc-document.sh @@ -1,39 +1,81 @@ #!/usr/bin/env bash -# nc-document.sh — generate a v3 native markdown knowledge entry for a Cloverleaf -# subsystem identified by a name pattern. Walks every NetConfig under $HCIROOT -# (or a passed-in list), gathers config + flow + xlates + tclprocs, composes a -# markdown doc with placeholder context sections for humans to fill. +# nc-document.sh — document a Cloverleaf INTERFACE end-to-end as a native markdown +# knowledge entry in Bryan's confirmed Legacy "ADT Messages" template. +# +# Two modes: +# SINGLE THREAD nc-document.sh [site] (e.g. ADTto_CodaMetrix ancout) +# nc-document.sh / (v1 node form) +# SYSTEM/PATTERN nc-document.sh --name (e.g. --name codametrix) +# → one section per matching destination thread, across sites. +# +# Everything emitted by THIS tool is DETERMINISTIC, PURE BASH+AWK, and API-FREE. +# It runs identically on an API-blocked host (e.g. Gundersen). It never calls an +# LLM and never reaches the network. The deterministic UPOC-bits + raw proc TCL +# appendix ARE the deliverable; when larry runs WITH the API the model transparently +# polishes those surfaced bits into smoother prose in the Description — that is +# normal agent behavior, NOT a mechanism in this script. +# +# ───────────────────────────────────────────────────────────────────────────── +# WHAT GETS DOCUMENTED, per interface (one delivery = one outbound thread): +# - Title = the interface / message type. +# - Description = prose: what the messages are, what the filters key on, where +# translation happens, how it's fed — seeded from the surfaced +# UPOC bits. +# - Message Flow = a table (Platform | Action | Description | From | To), one row +# per hop: Epic feed → Cloverleaf cross-site routing → Final +# Delivery. Built from nc-paths.sh (the route-chain enumerator). +# - Per-delivery breakdown: +# inbound PROTOCOL TYPE/HOST/PORT/ISSERVER + inbound TRXID/TPS proc, +# the route's TRXID filter + TYPE + PREPROCS/POSTPROCS + XLATE, +# destination host:port / process. +# - ★ DETERMINISTIC UPOC-BITS — for each referenced proc, locate its .tcl under +# $HCIROOT//tclprocs/ and extract (no API): +# 1. comments (header + inline `#` lines — the author's own filter notes) +# 2. HL7 fields referenced (PID.8, PV1.45, EVN.1, …) +# 3. conditions + literal values (matched event-code lists A01/A02/…, etc.) +# 4. table lookups (.tbl / table names, e.g. PeriCalm_Loc) +# 5. disposition (CONTINUE / KILL / return — pass vs kill) +# Rendered compactly into the Description. +# - Raw proc TCL in a plain appendix (`## Referenced proc source`). NO "summarize +# by hand / on an API box" marker — the extracted bits are the content. # # Usage: +# nc-document.sh [site] [options] +# nc-document.sh / [options] # nc-document.sh --name [options] # -# --name PATTERN 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 -# --netconfigs PATHS colon-separated explicit NetConfig list (overrides --hciroot scan) # --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-internal TXT Internal Owner content # --status TXT e.g. production / test / decommissioning # --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 -# -# Any --poc/-status/--escalation/--open-items/--notes that you OMIT becomes an -# empty placeholder section in the doc, ready for someone to fill. +# --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" -NCI="$LIB_DIR/nc-inbound.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="" -NETCONFIGS_OVERRIDE="" OUT="" TITLE="" POC_VENDOR="" @@ -42,45 +84,84 @@ STATUS="" ESCALATION="" OPEN_ITEMS="" NOTES="" +WANT_APPENDIX=1 +POSITIONAL=() while [ $# -gt 0 ]; do case "$1" in - --name) shift; PATTERN="$1" ;; - --hciroot) shift; HCIROOT_OVERRIDE="$1" ;; - --netconfigs) shift; NETCONFIGS_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" ;; - -h|--help) sed -n '2,25p' "$NC_SELF"; exit 0 ;; - -*) die "unknown flag: $1" ;; - *) die "extra arg: $1" ;; + --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 -[ -n "$PATTERN" ] || die "missing --name PATTERN" -[ -z "$TITLE" ] && TITLE="$(printf '%s' "$PATTERN" | tr '[:upper:]' '[:lower:]')" +# 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 -# Determine the NetConfig list -NCONFIGS=() -if [ -n "$NETCONFIGS_OVERRIDE" ]; then - IFS=':' read -ra NCONFIGS <<< "$NETCONFIGS_OVERRIDE" -else - 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" - 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" +[ -n "$PATTERN" ] || [ -n "$THREAD_ARG" ] || \ + die "give a [site] (single-thread mode) OR --name PATTERN (system mode). Try --help." -# Emit to OUT or stdout +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 @@ -90,145 +171,546 @@ out_target() { fi } -# Gather all matching protocols across all NetConfigs -declare -a MATCHES -for nc in "${NCONFIGS[@]}"; do - site=$(basename "$(dirname "$nc")") - while IFS= read -r prot; do - [ -z "$prot" ] && continue - MATCHES+=("$site|$nc|$prot") - done < <("$NCP" list-protocols "$nc" 2>/dev/null | grep -i -- "$PATTERN" || true) -done +# ───────────────────────────────────────────────────────────────────────────── +# strip a leading "{" / trailing "}" / empty-brace marker from a scalar value +# ───────────────────────────────────────────────────────────────────────────── +_clean() { printf '%s' "$1" | sed 's/^{}$//; s/^{//; s/}$//'; } -if [ ${#MATCHES[@]} -eq 0 ]; then - printf 'No protocols matching "%s" found in %d NetConfig(s).\n' "$PATTERN" "${#NCONFIGS[@]}" >&2 - exit 2 +# ───────────────────────────────────────────────────────────────────────────── +# ROUTE EXTRACTION (deterministic, pure awk). +# +# Walk a thread's DATAXLATE block and emit ONE record per route, fields delimited +# by the UNIT SEPARATOR (\037 — a NON-whitespace char, so bash `read` does NOT +# collapse consecutive empty fields the way it would with TAB/space): +# \037\037\037\037
\037\037\037\037
+# where PRE/POST/PROCS are space-joined proc-name lists (the actual UPOC procs).
+# A DATAXLATE route is a depth-2 sub-block; within it ROUTE_DETAILS (depth 3) holds
+# DEST/TYPE/XLATE and the PREPROCS/POSTPROCS/PROCS nested blocks; TRXID/WILDCARD/
+# ROUTE_ENABLED sit at the route level (depth 2). Empty {} values are skipped.
+# ─────────────────────────────────────────────────────────────────────────────
+_routes_of() {  # nc thread → US-delimited route records
+  local nc="$1" thr="$2"
+  # Depth map (from the real integrator): DATAXLATE opens 0->2; each ROUTE opens
+  # into depth 3 (first route: a bare `{` 2->3; later routes: `} {` 3->3). At the
+  # ROUTE level (depth 3) sit TRXID / WILDCARD / ROUTE_ENABLED. ROUTE_DETAILS opens
+  # 3->5; DEST/TYPE/XLATE sit at depth 6; PREPROCS/POSTPROCS/PROCS open 6->8 and the
+  # inner `{ PROCS  }` sits at depth 8. A new route boundary is any line that
+  # ENTERS or RE-ENTERS a depth-3 sub-block (the `{` or `} {` separator lines).
+  "$NCP" route-block "$nc" "$thr" 2>/dev/null | awk -v US="$(printf '\037')" '
+    BEGIN { depth=0; route=0; pp_mode="" }
+    function flush() {
+      if (route) {
+        printf "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n",
+          dest, US, trxid, US, rtype, US, xlate, US, pre, US, post, US, procs, US, wild, US, enabled
+      }
+      dest=""; trxid=""; rtype=""; xlate=""; pre=""; post=""; procs=""; wild=""; enabled=""
+    }
+    {
+      line=$0
+      no=gsub(/\{/,"{",line); nc_=gsub(/\}/,"}",line)
+      prev=depth; depth += no - nc_
+      stripped=$0; sub(/^[[:space:]]+/,"",stripped); sub(/[[:space:]]+$/,"",stripped)
+
+      # ROUTE BOUNDARY: a line that is exactly `{` (first route, prev 2 -> 3) or
+      # `} {` (close prior + open next, depth stays 3). Both land us at depth 3.
+      if ((prev==2 && depth==3 && stripped=="{") || (stripped=="} {" && depth==3)) {
+        flush(); route=1; next
+      }
+
+      if (!route) next
+
+      # route-level scalars live at depth 3 (prev==3 before any brace change)
+      if (prev==3) {
+        if (match($0, /\{ TRXID .* \}/))            { v=$0; sub(/^[[:space:]]+\{ TRXID /,"",v); sub(/ \}[[:space:]]*$/,"",v); trxid=v }
+        if (match($0, /\{ WILDCARD [A-Za-z]+ \}/))  { v=$0; sub(/^[[:space:]]+\{ WILDCARD /,"",v); sub(/ \}[[:space:]]*$/,"",v); wild=v }
+        if (match($0, /\{ ROUTE_ENABLED [0-9]+ \}/)){ v=$0; sub(/^[[:space:]]+\{ ROUTE_ENABLED /,"",v); sub(/ \}[[:space:]]*$/,"",v); enabled=v }
+      }
+      # ROUTE_DETAILS scalars (DEST/TYPE/XLATE) — each on its own line at depth 6
+      if (match($0, /\{ DEST [A-Za-z0-9_]+ \}/))   { v=$0; sub(/^.*\{ DEST /,"",v);  sub(/ \}.*$/,"",v); dest=v }
+      if (match($0, /\{ TYPE [A-Za-z0-9_]+ \}/))   { v=$0; sub(/^.*\{ TYPE /,"",v);  sub(/ \}.*$/,"",v); rtype=v }
+      if (match($0, /\{ XLATE [A-Za-z0-9_.]+ \}/)) { v=$0; sub(/^.*\{ XLATE /,"",v); sub(/ \}.*$/,"",v); xlate=v }
+
+      # enter a PREPROCS / POSTPROCS / (bare) PROCS block; the inner PROCS line
+      # carries the actual proc name(s).
+      if ($0 ~ /\{ PREPROCS \{$/)       pp_mode="pre"
+      else if ($0 ~ /\{ POSTPROCS \{$/) pp_mode="post"
+      else if ($0 ~ /\{ PROCS \{$/)     pp_mode="procs"
+      # the inner PROCS line: { PROCS name } | { PROCS {a b} } | { PROCS {} }
+      if (pp_mode != "" && match($0, /\{ PROCS /)) {
+        v=$0; sub(/^[[:space:]]+\{ PROCS /,"",v); sub(/[[:space:]]*\}[[:space:]]*$/,"",v)
+        gsub(/[{}]/,"",v); gsub(/^[[:space:]]+|[[:space:]]+$/,"",v)
+        if (v != "") {
+          if (pp_mode=="pre")        pre = (pre=="" ? v : pre " " v)
+          else if (pp_mode=="post")  post= (post=="" ? v : post " " v)
+          else                       procs=(procs=="" ? v : procs " " v)
+        }
+        pp_mode=""
+      }
+    }
+    END { flush() }
+  '
+}
+
+# Inbound how-received facts. We read each field into a NAMED variable directly
+# (NOT positional TSV — bash `read` with a single-char IFS collapses CONSECUTIVE
+# empty fields, which silently shifts columns when HOST/PORT/ISSERVER are empty on
+# an ICL/file inbound). Caller passes a prefix; we set _TYPE etc. via
+# globals. Robust and order-independent.
+_inbound_facts() {  # nc thread
+  local nc="$1" thr="$2"
+  IN_TYPE=$("$NCP" protocol-nested "$nc" "$thr" PROTOCOL.TYPE 2>/dev/null | head -1)
+  IN_HOST=$(_clean "$("$NCP" protocol-nested "$nc" "$thr" PROTOCOL.HOST 2>/dev/null | head -1)")
+  IN_PORT=$(_clean "$("$NCP" protocol-nested "$nc" "$thr" PROTOCOL.PORT 2>/dev/null | head -1)")
+  IN_ISSERVER=$("$NCP" protocol-nested "$nc" "$thr" PROTOCOL.ISSERVER 2>/dev/null | head -1)
+  IN_ICLPORT=$(_clean "$("$NCP" protocol-field "$nc" "$thr" ICLSERVERPORT 2>/dev/null | head -1)")
+  IN_PROC=$("$NCP" protocol-nested "$nc" "$thr" DATAFORMAT.PROC 2>/dev/null | head -1)
+  IN_PNAME=$("$NCP" protocol-field "$nc" "$thr" PROCESSNAME 2>/dev/null | head -1)
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# ★ DETERMINISTIC UPOC-BITS EXTRACTION (the key feature).
+#
+# Locate .tcl under $HCIROOT//tclprocs/ (then any site as a fallback)
+# and extract, with NO API:
+#   COMMENTS   — header + inline `#` lines (the author's own filter notes), cleaned.
+#   FIELDS     — HL7 field accessors / segment-field tokens (PID.8, PV1_3_3, EVN.1,
+#                getHL7Field … "PV1" N, replaceHL7Field … SEG N).
+#   MATCHES    — literal HL7 event/trigger codes referenced (A01 A02 … A53).
+#   CONDS      — `if`/condition lines and other literal comparison values.
+#   TABLES     — tbllookup / .tbl table names (e.g. PeriCalm_Loc).
+#   DISP       — dispositions: CONTINUE / KILL / return (pass vs kill).
+# Output is a small set of key=value lines on stdout (one fact-list per key):
+#   TCLFILE=
+#   COMMENTS... (one per matched comment, capped)
+#   FIELDS=
+#   MATCHES=
+#   TABLES=
+#   DISP=
+#   CONDS... (one per condition line, capped)
+# ─────────────────────────────────────────────────────────────────────────────
+_find_tcl() {  # site proc → abs path or empty
+  local site="$1" proc="$2" p
+  [ -z "$proc" ] && return 0
+  # 1) home site
+  p="$ROOT/$site/tclprocs/$proc.tcl"
+  [ -f "$p" ] && { printf '%s' "$p"; return 0; }
+  # 2) any site (deterministic order — first wins)
+  local i
+  for ((i=0; i<${#SITE_NAMES[@]}; i++)); do
+    p="$ROOT/${SITE_NAMES[$i]}/tclprocs/$proc.tcl"
+    [ -f "$p" ] && { printf '%s' "$p"; return 0; }
+  done
+  return 0
+}
+
+# Extract the bits from a single .tcl file. Emits the key=value / key... stream.
+_upoc_bits() {  # tclfile
+  local f="$1"
+  [ -n "$f" ] || return 0
+  [ -f "$f" ] || return 0
+  # PORTABILITY: this awk uses NO `\b` word-boundary metachar — BSD/BWK awk
+  # (macOS) and mawk do not support it (it silently matches nothing). Token
+  # boundaries are enforced by explicit char-class scanning instead.
+  awk '
+    BEGIN {
+      ncomm=0; ncond=0; MAXCOMM=24; MAXCOND=18
+      # segment ids we recognise (for the underscore PV1_3_3 form and seg N form)
+      segs="MSH EVN PID PV1 PV2 OBR OBX ORC NK1 IN1 GT1 ZPD ZID MRG DG1 AL1 SCH RGS AIS AIL AIP MSA ZIN"
+      nseg=split(segs, SEG, " "); for (i=1;i<=nseg;i++) ISSEG[SEG[i]]=1
+    }
+    function addset(arr, key) { if (key != "" && !(key in arr)) arr[key]=1 }
+    # is char c an identifier char (so a token boundary is a NON-identifier char)?
+    function idc(ch) { return (ch ~ /[A-Za-z0-9_]/) }
+    {
+      line=$0
+      # ---- comments: lines whose first non-space char is # ----
+      c=line; sub(/^[[:space:]]+/,"",c)
+      if (c ~ /^#/) {
+        t=c; sub(/^#+[[:space:]]*/,"",t); gsub(/[[:space:]]+$/,"",t)
+        if (t != "" && t !~ /^[#=*_-]+$/) { if (ncomm < MAXCOMM) comm[++ncomm]=t }
+      }
+
+      # ---- HL7 field accessors: dotted form PID.8 / PV1.45 / EVN.1 / MSH.9.1 ----
+      s=line
+      while (match(s, /[A-Z][A-Z][A-Z0-9]\.[0-9]+(\.[0-9]+)?/)) {
+        tok=substr(s, RSTART, RLENGTH); addset(fields, tok); s=substr(s, RSTART+RLENGTH)
+      }
+      # underscore form PV1_3_3 / EVN_5_8 / PID_8 -> normalize to dotted. We scan
+      # for SEG_(_) where SEG is a known segment id, enforcing a
+      # boundary by requiring the char before SEG to be non-identifier.
+      s=line
+      while (match(s, /[A-Z][A-Z][A-Z0-9]_[0-9]+(_[0-9]+)?/)) {
+        st=RSTART; ln=RLENGTH; tok=substr(s, st, ln)
+        before = (st==1) ? "" : substr(s, st-1, 1)
+        seg3=substr(tok,1,3)
+        if ((before=="" || !idc(before)) && (seg3 in ISSEG)) {
+          d=tok; gsub(/_/,".",d); addset(fields, d)
+        }
+        s=substr(s, st+ln)
+      }
+      # replaceHL7Field/getHL7Field on a NAMED segment + numeric field: SEG N
+      s=line
+      while (match(s, /(MSH|EVN|PID|PV1|PV2|OBR|OBX|ORC|MSA|DG1|AL1|NK1|IN1)[[:space:]]+[0-9]+/)) {
+        st=RSTART; ln=RLENGTH; tok=substr(s, st, ln)
+        before=(st==1)?"":substr(s, st-1, 1)
+        # only when the line is an HL7 field op (avoids matching arbitrary "PV1 6")
+        if ((before=="" || !idc(before)) && line ~ /(get|replace)HL7Field/) {
+          gsub(/[[:space:]]+/,".",tok); addset(fields, tok)
+        }
+        s=substr(s, st+ln)
+      }
+
+      # ---- literal HL7 trigger/event codes A01..A99 (boundary-checked, no \b) ----
+      s=line
+      while (match(s, /A[0-9][0-9]/)) {
+        st=RSTART; ln=RLENGTH; tok=substr(s, st, ln)
+        before=(st==1)?"":substr(s, st-1, 1)
+        afterpos=st+ln; after=(afterpos>length(s))?"":substr(s, afterpos, 1)
+        if ((before=="" || !idc(before)) && (after=="" || !idc(after))) addset(matches, tok)
+        s=substr(s, st+ln)
+      }
+
+      # ---- table lookups: tbllookup  ...  /  word.tbl ----
+      if (match(line, /tbllookup[[:space:]]+[A-Za-z_][A-Za-z0-9_]*/)) {
+        tb=substr(line,RSTART,RLENGTH); sub(/tbllookup[[:space:]]+/,"",tb); addset(tables, tb)
+      }
+      s=line
+      while (match(s, /[A-Za-z_][A-Za-z0-9_]*\.tbl/)) {
+        tok=substr(s, RSTART, RLENGTH); sub(/\.tbl$/,"",tok); addset(tables, tok); s=substr(s, RSTART+RLENGTH)
+      }
+
+      # ---- dispositions: plain substring match, boundary not needed (these are
+      #      distinct all-caps tokens in TCL disposition code) ----
+      if (line ~ /CONTINUE/) addset(disp, "CONTINUE")
+      if (line ~ /KILL/)     addset(disp, "KILL")
+      if (line ~ /disp/ && line ~ /ERROR/) addset(disp, "ERROR")
+      if (c ~ /^return([[:space:]]|$)/ || line ~ /return "\{/) addset(disp, "return")
+
+      # ---- condition lines: TCL `if {...}` carrying a comparison ----
+      ct=line; sub(/^[[:space:]]+/,"",ct); gsub(/[[:space:]]+$/,"",ct)
+      if (ct ~ /^(if|elseif|\} elseif|switch)([[:space:]]|\{)/ && ct ~ /(==|!=|<|>|lcontain|cequal|string)/) {
+        if (ncond < MAXCOND) cond[++ncond]=ct
+      }
+    }
+    END {
+      # FIELDS / MATCHES / TABLES / DISP: sorted-unique, space-joined
+      printf "FIELDS="; n=0; for (k in fields) { a[n++]=k }
+      asort_keys(a, n); for (i=0;i=0 && arr[j]>tmp){ arr[j+1]=arr[j]; j-- } arr[j+1]=tmp }
+    }
+  ' "$f"
+}
+
+# Compose the compact one-line UPOC summary for the Description, from a bits stream.
+#   _upoc_oneline  
+_upoc_oneline() {
+  local proc="$1" bf="$2"
+  local fields matches tables disp comments
+  fields=$(awk -F= '/^FIELDS=/{sub(/^FIELDS=/,"");print}' "$bf")
+  matches=$(awk -F= '/^MATCHES=/{sub(/^MATCHES=/,"");print}' "$bf")
+  tables=$(awk -F= '/^TABLES=/{sub(/^TABLES=/,"");print}' "$bf")
+  disp=$(awk -F= '/^DISP=/{sub(/^DISP=/,"");print}' "$bf")
+  comments=$(awk -F'\t' '/^COMMENT\t/{print $2}' "$bf" \
+    | grep -iE 'pass|filter|block|only|kill|continue|drop|route|female|newborn|discharge|location|purpose|determine' \
+    | grep -ivE '^(name|author|date|args|returns|upoc type|revision|notes)\b' \
+    | head -3 \
+    | awk 'NR==1{printf "%s",$0;next}{printf " · %s",$0}END{print ""}')
+  local out="UPOC \`$proc\`"
+  [ -n "$comments" ] && out="$out — $comments"
+  [ -n "$fields" ]   && out="$out · fields: $fields"
+  [ -n "$matches" ]  && out="$out · matches: $matches"
+  [ -n "$tables" ]   && out="$out · table: $tables"
+  if [ -n "$disp" ]; then
+    case "$disp" in
+      *KILL*CONTINUE*|*CONTINUE*KILL*) out="$out · disposition: pass matching / kill non-matching" ;;
+      *KILL*)      out="$out · disposition: kill non-matching" ;;
+      *CONTINUE*)  out="$out · disposition: pass matching" ;;
+      *)           out="$out · disposition: $disp" ;;
+    esac
+  fi
+  printf '%s\n' "$out"
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Build the doc section for ONE outbound (delivery) thread.
+#   $1 = outbound thread name   $2 = its home site   $3 = its NetConfig
+# Emits markdown to stdout. Appends raw proc TCL paths to the global APPENDIX_PROCS
+# set (printed once at the end).
+# ─────────────────────────────────────────────────────────────────────────────
+declare -A APPENDIX_SEEN
+APPENDIX_LIST=()   # "site|proc|abs-path" records
+
+_register_appendix() {  # site proc
+  local site="$1" proc="$2" key="$site|$proc" p
+  [ -z "$proc" ] && return 0
+  [ -n "${APPENDIX_SEEN[$key]:-}" ] && return 0
+  APPENDIX_SEEN[$key]=1
+  p=$(_find_tcl "$site" "$proc")
+  APPENDIX_LIST+=("$site|$proc|$p")
+}
+
+document_thread() {
+  local ob="$1" site="$2" nc="$3"
+
+  # --- the full route chain (flow) via nc-paths ---
+  local chain
+  chain=$("$NCPATHS" "$site/$ob" --hciroot "$ROOT" --format v1 2>/dev/null | head -1)
+
+  # --- destination (the outbound thread's delivery endpoint) ---
+  local dtype dhost dport dproc
+  dtype=$("$NCP" protocol-nested "$nc" "$ob" PROTOCOL.TYPE 2>/dev/null | head -1)
+  dhost=$(_clean "$("$NCP" protocol-nested "$nc" "$ob" PROTOCOL.HOST 2>/dev/null | head -1)")
+  dport=$(_clean "$("$NCP" protocol-nested "$nc" "$ob" PROTOCOL.PORT 2>/dev/null | head -1)")
+  dproc=$("$NCP" protocol-field "$nc" "$ob" PROCESSNAME 2>/dev/null | head -1)
+
+  # --- find the SOURCE (routing) thread: who DESTs to this outbound, same site ---
+  local route_thr=""
+  while IFS= read -r s; do
+    [ -z "$s" ] && continue
+    route_thr="$s"; break
+  done < <("$NCP" sources "$nc" "$ob" 2>/dev/null)
+
+  # --- the specific route (TRXID/TYPE/XLATE/PRE/POST) that targets this outbound ---
+  local r_trxid="" r_type="" r_xlate="" r_pre="" r_post="" r_procs="" r_wild="" r_enabled=""
+  local US; US=$(printf '\037')
+  if [ -n "$route_thr" ]; then
+    while IFS="$US" read -r dest trxid rtype xlate pre post procs wild enabled; do
+      [ "$dest" = "$ob" ] || continue
+      r_trxid="$trxid"; r_type="$rtype"; r_xlate="$xlate"
+      r_pre="$pre"; r_post="$post"; r_procs="$procs"; r_wild="$wild"; r_enabled="$enabled"
+      break
+    done < <(_routes_of "$nc" "$route_thr")
+  fi
+
+  # --- inbound how-received facts for the routing thread (the local inbound) ---
+  local in_type="" in_host="" in_port="" in_isserver="" in_iclport="" in_proc="" in_pname=""
+  if [ -n "$route_thr" ]; then
+    IN_TYPE=""; IN_HOST=""; IN_PORT=""; IN_ISSERVER=""; IN_ICLPORT=""; IN_PROC=""; IN_PNAME=""
+    _inbound_facts "$nc" "$route_thr"
+    in_type="$IN_TYPE"; in_host="$IN_HOST"; in_port="$IN_PORT"; in_isserver="$IN_ISSERVER"
+    in_iclport="$IN_ICLPORT"; in_proc="$IN_PROC"; in_pname="$IN_PNAME"
+  fi
+
+  # --- the feed root (Epic-side) from the chain ---
+  local feed_root feed_site feed_thr
+  feed_root="${chain%% *}"          # first node "site/thread"
+  feed_site="${feed_root%%/*}"; feed_thr="${feed_root#*/}"
+
+  # ── UPOC bits for every proc this delivery touches (inbound TRXID/TPS proc +
+  #    the route's PRE/POST/PROCS). Collect bits files for the Description and
+  #    register the raw TCL for the appendix.
+  local upoc_lines=() bf proc
+  for proc in $in_proc $r_pre $r_post $r_procs; do
+    [ -z "$proc" ] && continue
+    local tcl; tcl=$(_find_tcl "$site" "$proc")
+    [ -z "$tcl" ] && tcl=$(_find_tcl "$feed_site" "$proc")
+    if [ -n "$tcl" ]; then
+      bf=$(mktemp); _upoc_bits "$tcl" > "$bf"
+      upoc_lines+=("$(_upoc_oneline "$proc" "$bf")")
+      rm -f "$bf"
+    else
+      upoc_lines+=("UPOC \`$proc\` — _(proc .tcl not found under any site's tclprocs/)_")
+    fi
+    _register_appendix "$site" "$proc"
+  done
+
+  # ─────────────────────────── render the section ───────────────────────────
+  printf '## %s\n\n' "$ob"
+
+  # Description (prose seeded from deterministic facts; the model polishes this
+  # into smoother prose when run WITH the API — no marker here).
+  printf '### Description\n\n'
+  {
+    printf 'The **%s** interface delivers messages to `%s`' "$ob" "$ob"
+    [ -n "$dhost" ] && printf ' on **%s' "$dhost"
+    [ -n "$dport" ] && printf ':%s' "$dport"
+    [ -n "$dhost" ] && printf '**'
+    [ -n "$dproc" ] && printf ' (process `%s`)' "$dproc"
+    printf '.'
+    if [ -n "$route_thr" ]; then
+      printf ' Routing and filtering happen on the inbound thread `%s`' "$route_thr"
+      [ -n "$in_proc" ] && printf ', whose inbound TRXID/TPS proc `%s` assigns the transaction id that the routes key on' "$in_proc"
+      printf '.'
+    fi
+    if [ -n "$r_trxid" ]; then
+      printf ' This delivery is selected by the TRXID filter `%s`' "$r_trxid"
+      [ "$r_wild" = "ON" ] && printf ' (wildcard match)'
+      printf '.'
+    fi
+    if [ -n "$r_xlate" ]; then
+      printf ' Translation is done by the xlate `%s`' "$r_xlate"
+      printf '.'
+    elif [ "$r_type" = "raw" ]; then
+      printf ' Messages are passed **raw** (no translation).'
+    fi
+    printf '\n\n'
+  }
+  if [ "${#upoc_lines[@]}" -gt 0 ]; then
+    printf 'Filter / translation logic (surfaced deterministically from the referenced UPOC procs):\n\n'
+    local l
+    for l in "${upoc_lines[@]}"; do printf -- '- %s\n' "$l"; done
+    printf '\n'
+  fi
+
+  # Message Flow table. The middle "routing" row's wording adapts to whether the
+  # chain actually crosses a site boundary (a `==>` hop): cross-site routing goes
+  # via a named destination block; an intra-site chain is a local DATAXLATE route.
+  local is_cross=0 route_desc
+  case "$chain" in *' ==> '*) is_cross=1 ;; esac
+  if [ "$is_cross" = "1" ]; then
+    route_desc="Cross-site route via destination block; inbound \`${route_thr:-?}\` keys TRXID and routes per delivery"
+  else
+    route_desc="Intra-site DATAXLATE route; inbound \`${route_thr:-?}\` keys TRXID and routes per delivery"
+  fi
+  printf '### Message Flow\n\n'
+  printf '| Platform | Action | Description | From | To |\n'
+  printf '|---|---|---|---|---|\n'
+  # Row 1: Epic feed — From = the upstream system/process, To = the engine feed thread.
+  printf '| Epic | feed | Raw Epic feed entering the integrator | Epic (process `%s`) | `%s` |\n' \
+    "${in_pname:-${dproc:-ADT}}" "${feed_root:-—}"
+  # Row 2: Cloverleaf routing (the chain itself)
+  printf '| Cloverleaf%s | message routing | %s | `%s` | `%s` |\n' \
+    "$( [ "$is_cross" = "1" ] && printf ' (cross-site)' || printf '' )" \
+    "$route_desc" "${chain:-—}" "${route_thr:-—}"
+  # Row 3: Final delivery
+  printf '| Final Delivery | outbound to %s | TRXID `%s` → TYPE `%s`%s | `%s` | `%s`%s%s |\n' \
+    "${dproc:-vendor}" "${r_trxid:-—}" "${r_type:-—}" \
+    "$( [ -n "$r_xlate" ] && printf ', xlate `%s`' "$r_xlate" )" \
+    "${route_thr:-—}" "$ob" \
+    "$( [ -n "$dhost" ] && printf ' → %s' "$dhost" )" \
+    "$( [ -n "$dport" ] && printf ':%s' "$dport" )"
+  printf '\n'
+
+  # Per-delivery breakdown
+  printf '### Delivery breakdown — `%s`\n\n' "$ob"
+  printf -- '- **Flow:** `%s`\n' "${chain:-—}"
+  printf -- '- **How received (inbound `%s`):** PROTOCOL TYPE `%s`' "${route_thr:-?}" "${in_type:-—}"
+  [ -n "$in_host" ] && printf ' · HOST `%s`' "$in_host"
+  [ -n "$in_port" ] && printf ' · PORT `%s`' "$in_port"
+  [ -n "$in_isserver" ] && printf ' · ISSERVER `%s`' "$in_isserver"
+  [ -n "$in_iclport" ] && printf ' · ICLSERVERPORT `%s`' "$in_iclport"
+  printf '\n'
+  printf -- '- **Inbound TRXID/TPS proc:** `%s`\n' "${in_proc:-—}"
+  printf -- '- **Route TRXID filter:** `%s`%s\n' "${r_trxid:-—}" "$( [ "$r_wild" = "ON" ] && printf ' (WILDCARD ON)' )"
+  printf -- '- **Route TYPE:** `%s`\n' "${r_type:-—}"
+  printf -- '- **UPOC PREPROCS:** `%s`\n' "${r_pre:-—}"
+  printf -- '- **UPOC POSTPROCS:** `%s`\n' "${r_post:-—}"
+  printf -- '- **XLATE:** `%s`\n' "${r_xlate:-—}"
+  printf -- '- **Destination:** `%s`%s%s · process `%s` · TYPE `%s`\n' \
+    "${dhost:-—}" "$( [ -n "$dport" ] && printf ':%s' "$dport" )" "" "${dproc:-—}" "${dtype:-—}"
+  printf '\n'
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Resolve the set of OUTBOUND (delivery) threads to document.
+#   single-thread mode: the one thread (resolve its site).
+#   system mode: every thread (across sites) matching --name that is OUTBOUNDONLY
+#                (a delivery endpoint); if a matched thread is NOT outbound (e.g. an
+#                inbound router) we still document its OUTBOUND children that match.
+# Emits "site|nc|thread" records.
+# ─────────────────────────────────────────────────────────────────────────────
+TARGETS=()
+
+if [ -n "$THREAD_ARG" ]; then
+  local_site="$SITE_ARG"
+  if [ -z "$local_site" ]; then
+    local_site=$(_locate_thread "$THREAD_ARG") || die "thread not found in any site under $ROOT: $THREAD_ARG"
+  fi
+  nc=$(_nc_for_site "$local_site") || die "no NetConfig for site: $local_site"
+  "$NCP" list-protocols "$nc" 2>/dev/null | grep -qxF -- "$THREAD_ARG" \
+    || die "thread '$THREAD_ARG' not found in site '$local_site'"
+  TARGETS+=("$local_site|$nc|$THREAD_ARG")
+  [ -z "$TITLE" ] && TITLE="$THREAD_ARG"
+else
+  # system / pattern mode: document each matching DELIVERY (outbound) thread. A
+  # delivery endpoint is a thread that is NOT an inbound TCP listener (ISSERVER!=1)
+  # and NOT an ICL/file inbound router (OBWORKASIB!=1) — i.e. it has an outbound
+  # client connection to a downstream system. This covers both pure OUTBOUNDONLY=1
+  # threads (e.g. ADTto_CodaMetrix) and bidirectional `Xto_*` deliveries whose
+  # OUTBOUNDONLY=0 (e.g. DFTto_codaMetrix). Inbound listeners/routers are skipped
+  # (they are documented as the "how received" leg of their deliveries).
+  for ((i=0; i<${#SITE_NAMES[@]}; i++)); do
+    site="${SITE_NAMES[$i]}"; nc="${SITE_NCS[$i]}"
+    while IFS= read -r prot; do
+      [ -z "$prot" ] && continue
+      isserver=$("$NCP" protocol-nested "$nc" "$prot" PROTOCOL.ISSERVER 2>/dev/null | head -1)
+      obib=$("$NCP" protocol-field "$nc" "$prot" OBWORKASIB 2>/dev/null | head -1)
+      [ "$isserver" = "1" ] && continue       # inbound listener — not a delivery
+      [ "$obib" = "1" ] && continue            # ICL/file inbound router — not a delivery
+      TARGETS+=("$site|$nc|$prot")
+    done < <("$NCP" list-protocols "$nc" 2>/dev/null | grep -i -- "$PATTERN" || true)
+  done
+  [ "${#TARGETS[@]}" -gt 0 ] || die "no delivery (outbound) threads matching \"$PATTERN\" under $ROOT"
+  [ -z "$TITLE" ] && TITLE="$(printf '%s' "$PATTERN" | tr '[:upper:]' '[:lower:]')"
 fi
 
 # ─────────────────────────────────────────────────────────────────────────────
-# Compose markdown
+# Compose the document
 # ─────────────────────────────────────────────────────────────────────────────
 {
-  printf '# %s — Cloverleaf System Knowledge Entry\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)"
+  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 — add vendor contact name + email/phone)_}"
-  printf -- '- **Internal Owner:** %s\n' "${POC_INTERNAL:-_(unfilled — add the internal owner / engineer)_}"
-  printf -- '- **Status:** %s\n'        "${STATUS:-_(unfilled — production / test / decommissioning / on hold)_}"
-  printf -- '- **Escalation:** %s\n'    "${ESCALATION:-_(unfilled — on-call path, ticket queue, etc.)_}"
-  printf '\n### Open items\n'
-  if [ -n "$OPEN_ITEMS" ]; then
-    printf '%s\n\n' "$OPEN_ITEMS"
-  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'
+  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
 
-  # ─── Threads inventory ───
-  # v0.7.5: tr -cd '0-9' instead of tr -d ' ' — Cygwin wc.exe CR-taint would
-  # 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"
-    pname=$("$NCP" protocol-field "$nc" "$prot" PROCESSNAME 2>/dev/null | head -1)
-    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
-
-  # ─── Sources outside the matched set (the "fed by" landscape) ───
-  printf '## Adjacent threads (the network this subsystem talks to)\n\n'
-  printf '_All threads that **either feed** matched threads **or are fed by** matched threads. These are the immediate operational neighbors._\n\n'
-  printf '| Site | Thread | Relationship to matched set |\n'
-  printf '|---|---|---|\n'
-  declare -A SEEN
-  for line in "${MATCHES[@]}"; do
-    IFS='|' read -r site nc prot <<< "$line"
-    while IFS= read -r src; do
-      key="$site|$src"
-      [ -n "${SEEN[$key]:-}" ] && continue
-      SEEN[$key]=1
-      printf '| `%s` | `%s` | feeds `%s` |\n' "$site" "$src" "$prot"
-    done < <("$NCP" sources "$nc" "$prot" 2>/dev/null)
-    while IFS= read -r dst; do
-      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
-  printf '\n'
-
-  # ─── Footer ───
   printf '---\n\n'
-  printf '_Generated: %s · NetConfigs scanned: %d · Pattern: `%s`_\n' \
-    "$(date -Iseconds 2>/dev/null || date)" "${#NCONFIGS[@]}" "$PATTERN"
+  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
 
-[ -n "$OUT" ] && printf 'nc-document: wrote %s (%d matched threads across %d site(s))\n' \
-  "$OUT" "${#MATCHES[@]}" "$(printf '%s\n' "${MATCHES[@]}" | awk -F'|' '{print $1}' | sort -u | wc -l | tr -cd '0-9')" >&2
+if [ -n "$OUT" ]; then
+  printf 'nc-document: wrote %s (%d delivery section(s))\n' "$OUT" "${#TARGETS[@]}" >&2
+fi