cloverleaf-larry/lib/nc-document.sh
Bryan Johnson e08f030df5 v0.3.0: initial release of Larry-Anywhere
Portable AI agent for Cloverleaf integration work. Pure bash + curl + jq.
Zero dependency on v1 wrapper scripts or v2 cloverleaf-tools.pyz.

27 native Anthropic tools:

NetConfig parsing (read)
  nc_list_protocols, nc_list_processes, nc_protocol_block,
  nc_protocol_field, nc_protocol_nested, nc_protocol_summary,
  nc_destinations, nc_sources, nc_xlate_refs, nc_tclproc_refs

NetConfig modification (journal-backed writes with rollback)
  nc_insert_protocol, nc_add_route, larry_rollback_list

Workflows
  nc_find_inbound, nc_make_jump (3-thread jump pattern), nc_find
  (tbn/tbp/tbh/tbpr/where replacements), nc_document, nc_diff_interface,
  nc_regression

Messages
  hl7_field, nc_msgs (smat is SQLite!), hl7_diff (with --ignore MSH.7)

File system
  read_file, list_dir, grep_files, glob_files, write_file, bash_exec

Validated against a 22-site real Cloverleaf test install. Five worked
examples end-to-end: jump-thread generation, smat MRN search, system
documentation, interface+connected diff, HL7-aware regression diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 09:46:20 -07:00

233 lines
9.3 KiB
Bash
Executable File

#!/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.
#
# Usage:
# nc-document.sh --name <pattern> [options]
#
# --name PATTERN case-insensitive substring/regex to match protocol names
# --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)
# --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)
# --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.
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"
die() { printf 'nc-document: %s\n' "$*" >&2; exit 1; }
PATTERN=""
HCIROOT_OVERRIDE=""
NETCONFIGS_OVERRIDE=""
OUT=""
TITLE=""
POC_VENDOR=""
POC_INTERNAL=""
STATUS=""
ESCALATION=""
OPEN_ITEMS=""
NOTES=""
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" ;;
esac
shift
done
[ -n "$PATTERN" ] || die "missing --name PATTERN"
[ -z "$TITLE" ] && TITLE="$(printf '%s' "$PATTERN" | tr '[:upper:]' '[:lower:]')"
# 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"
# Emit to OUT or stdout
out_target() {
if [ -n "$OUT" ]; then
mkdir -p "$(dirname "$OUT")" 2>/dev/null
cat > "$OUT"
else
cat
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
if [ ${#MATCHES[@]} -eq 0 ]; then
printf 'No protocols matching "%s" found in %d NetConfig(s).\n' "$PATTERN" "${#NCONFIGS[@]}" >&2
exit 2
fi
# ─────────────────────────────────────────────────────────────────────────────
# Compose markdown
# ─────────────────────────────────────────────────────────────────────────────
{
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 '## 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'
fi
# ─── Threads inventory ───
printf '## Threads (%d matched in %d site(s))\n\n' "${#MATCHES[@]}" "$(printf '%s\n' "${MATCHES[@]}" | awk -F'|' '{print $1}' | sort -u | wc -l | tr -d ' ')"
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"
} | 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 -d ' ')" >&2