cloverleaf-larry/lib/nc-table.sh
Bryan Johnson a0502e2ec6 v0.4.2: operational layer — engine ctrl, tables CRUD, xlate viz, smat-diff, create-thread, tclgen
Seven new lib tools — covers the remaining Bryan-requested gaps.

lib/nc-engine.sh
  - Cloverleaf process control. Wraps shipped binaries (hcienginestop,
    hcienginerun, hcienginerestart, hciengineroutetest). Every action
    is Y/N confirmed AND journaled into engine-actions.tsv.
  - Subcommands: stop, start, bounce/restart, status, resend-ib,
    resend-ob, route-test, testxlate, tpstest.

lib/nc-status.sh
  - Runtime status, v1-modelled. Subcommands: sites, threads, not-up,
    connections, queued, raw. Auto-discovers hcienginestat / tstat /
    connstatus binaries; falls back to file-presence heuristics.

lib/nc-table.sh
  - Read+CRUD for .tbl lookup tables. Subcommands: list, show, pairs
    (→csv/tsv), lookup, reverse-lookup, add, delete, create, replace.
  - All modifications journal-backed. Composes csv-to-table /
    table-to-csv for format conversion.

lib/nc-xlate.sh
  - Visualize .xlt files. Parses the TCL nested-block ops format.
    Subcommands: list, show, ops (TSV), tree (ASCII flow), summary
    (counts + segments + tables touched), diff (cross-xlate).
  - Confirmed working against Epic_ADT_CodaMetrix.xlt: identified
    12 PATHCOPY + 1 COPY ops across MSH/EVN/PID/PV1/PV2/PD1/ZPD/ZPV/
    AL1/GT1/IN1/IN2.

lib/nc-smat-diff.sh
  - Cross-env smat content diff. Samples N msgs from each side,
    pairs by configurable HL7 field (default MSH.10 = control ID),
    hl7-diffs each pair with --ignore MSH.7. Outputs per-pair reports
    + master _summary.md with paired/A-only/B-only counts.

lib/nc-create-thread.sh
  - High-level: create a new protocol + optionally splice a route from
    an existing thread to the new one. Both writes journal-backed.
    Confirmed end-to-end: created to_metrics_test outbound + routed
    IB_ADT_muxS → to_metrics_test via journal entries 001+002.

lib/nc-tclgen.sh
  - TCL UPOC scaffolding from intent. Templates: tps-presc, tps-postsc,
    tps-iclkill, xlate-helper, trxid, ack, field-rewrite. Produces
    clean syntax-correct TCL ready to edit.

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

224 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# nc-table.sh — read and modify Cloverleaf lookup tables (.tbl files).
# Every modification goes through the journal (snapshot + diff + atomic write).
#
# Subcommands:
# list [--site SITE] list .tbl files
# show <name> [--site SITE] dump the file
# pairs <name> [--site SITE] [--format csv|tsv] input→output pairs only
# lookup <name> <input> find output for input
# reverse-lookup <name> <output> find input(s) for output
# add <name> <input> <output> [--site SITE] add or update a row (journaled)
# delete <name> <input> [--site SITE] delete a row by input (journaled)
# create <name> --from-csv FILE [opts] create a new table from CSV
# replace <name> --from-csv FILE [opts] replace contents (journaled)
#
# Find tables at: $HCISITEDIR/tables/<name>.tbl (or $HCIROOT/Tables/ shared)
set -o pipefail
NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
JOURNAL="$LIB_DIR/journal.sh"
C2T="$LIB_DIR/csv-to-table.sh"
T2C="$LIB_DIR/table-to-csv.sh"
die() { printf 'nc-table: %s\n' "$*" >&2; exit 1; }
[ -f "$JOURNAL" ] && . "$JOURNAL"
locate_table() {
local name="$1" site="${2:-${HCISITE:-}}"
# Strip .tbl if user gave it
name="${name%.tbl}"
for d in \
"${HCIROOT:-}/$site/tables" \
"${HCIROOT:-}/$site/Tables" \
"${HCIROOT:-}/Tables"; do
[ -f "$d/${name}.tbl" ] && { printf '%s\n' "$d/${name}.tbl"; return 0; }
done
return 1
}
cmd_list() {
local site="${HCISITE:-}"
while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; esac; shift; done
for d in "${HCIROOT:-}/$site/tables" "${HCIROOT:-}/$site/Tables" "${HCIROOT:-}/Tables"; do
[ -d "$d" ] || continue
find "$d" -maxdepth 1 -name '*.tbl' -type f 2>/dev/null | sort
done | sort -u
}
cmd_show() {
local name="$1"; shift
local site="${HCISITE:-}"
while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; esac; shift; done
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
cat "$f"
}
cmd_pairs() {
local name="$1"; shift
local site="${HCISITE:-}"
local fmt="csv"
while [ $# -gt 0 ]; do case "$1" in
--site) shift; site="$1" ;;
--format) shift; fmt="$1" ;;
esac; shift; done
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
case "$fmt" in
csv) "$T2C" --with-header "$f" ;;
tsv) "$T2C" --with-header --delim $'\t' "$f" ;;
*) die "bad --format" ;;
esac
}
cmd_lookup() {
local name="$1" input="$2"
local site="${HCISITE:-}"
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
"$T2C" "$f" | awk -F',' -v target="$input" '
BEGIN { found=0 }
{
# CSV unquote (basic)
v=$1; gsub(/^"|"$/, "", v); gsub(/""/, "\"", v)
if (v == target) { o=$2; gsub(/^"|"$/, "", o); gsub(/""/, "\"", o); print o; found=1; exit }
}
END { if (!found) exit 1 }
'
}
cmd_reverse_lookup() {
local name="$1" output="$2"
local site="${HCISITE:-}"
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
"$T2C" "$f" | awk -F',' -v target="$output" '
{
v=$2; gsub(/^"|"$/, "", v); gsub(/""/, "\"", v)
if (v == target) { i=$1; gsub(/^"|"$/, "", i); gsub(/""/, "\"", i); print i }
}
'
}
# Modification helpers (journal-backed)
modify_via_csv() {
local name="$1" csv_path="$2" site="$3" mode="$4" # mode = add|delete|replace|create
local action="$mode-table"
local target
# Determine target path
if [ "$mode" = "create" ]; then
[ -n "$site" ] || die "create: --site required"
[ -d "${HCIROOT}/$site/tables" ] || mkdir -p "${HCIROOT}/$site/tables"
target="${HCIROOT}/$site/tables/${name%.tbl}.tbl"
[ ! -e "$target" ] || die "table already exists: $target (use replace if you want to overwrite)"
else
target=$(locate_table "$name" "$site") || die "no such table: $name"
fi
local new; new=$(mktemp)
"$C2T" --has-header --out "$new" < "$csv_path" 2>/dev/null \
|| "$C2T" --has-header "$csv_path" > "$new"
if declare -f journal_write >/dev/null 2>&1; then
journal_write "$target" "$new"
else
# No journal — direct write with simple backup
[ -f "$target" ] && cp -p "$target" "${target}.larry-bak.$(date +%s)"
mv "$new" "$target"
echo "(no journal available; backup at ${target}.larry-bak.<ts>)"
fi
rm -f "$new"
}
cmd_add() {
local name="$1" input="$2" output="$3"; shift 3
local site="${HCISITE:-}"
while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; esac; shift; done
[ -n "$name" ] && [ -n "$input" ] && [ -n "$output" ] || die "usage: add NAME INPUT OUTPUT"
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
# Read current pairs, replace existing input row or append, then rewrite
local tmp_csv; tmp_csv=$(mktemp)
{
"$T2C" --with-header "$f"
# If the new pair wasn't already in the existing data, it'll be added below
} > "$tmp_csv"
local awk_script='
BEGIN { added=0 }
NR==1 { print; next }
{
n=split($0, c, ",")
v=c[1]; gsub(/^"|"$/, "", v); gsub(/""/, "\"", v)
if (v == NEW_IN) {
printf "%s,%s\n", NEW_IN, NEW_OUT
added=1
} else { print }
}
END { if (!added) printf "%s,%s\n", NEW_IN, NEW_OUT }
'
local newcsv; newcsv=$(mktemp)
awk -v NEW_IN="$input" -v NEW_OUT="$output" "$awk_script" "$tmp_csv" > "$newcsv"
modify_via_csv "$name" "$newcsv" "$site" "add"
rm -f "$tmp_csv" "$newcsv"
}
cmd_delete() {
local name="$1" input="$2"; shift 2
local site="${HCISITE:-}"
while [ $# -gt 0 ]; do case "$1" in --site) shift; site="$1" ;; esac; shift; done
[ -n "$name" ] && [ -n "$input" ] || die "usage: delete NAME INPUT"
local f; f=$(locate_table "$name" "$site") || die "no such table: $name"
local tmp_csv; tmp_csv=$(mktemp)
"$T2C" --with-header "$f" \
| awk -F',' -v IN="$input" '
NR==1 { print; next }
{
v=$1; gsub(/^"|"$/, "", v); gsub(/""/, "\"", v)
if (v == IN) next
print
}
' > "$tmp_csv"
modify_via_csv "$name" "$tmp_csv" "$site" "delete"
rm -f "$tmp_csv"
}
cmd_create() {
local name="$1"; shift
local from_csv=""
local site="${HCISITE:-}"
while [ $# -gt 0 ]; do case "$1" in
--from-csv) shift; from_csv="$1" ;;
--site) shift; site="$1" ;;
esac; shift; done
[ -f "$from_csv" ] || die "create: --from-csv FILE required"
modify_via_csv "$name" "$from_csv" "$site" "create"
}
cmd_replace() {
local name="$1"; shift
local from_csv=""
local site="${HCISITE:-}"
while [ $# -gt 0 ]; do case "$1" in
--from-csv) shift; from_csv="$1" ;;
--site) shift; site="$1" ;;
esac; shift; done
[ -f "$from_csv" ] || die "replace: --from-csv FILE required"
modify_via_csv "$name" "$from_csv" "$site" "replace"
}
SUB="${1:-list}"
case "$SUB" in
list) shift; cmd_list "$@" ;;
show) shift; [ $# -ge 1 ] || die "usage: show NAME"; cmd_show "$@" ;;
pairs) shift; [ $# -ge 1 ] || die "usage: pairs NAME"; cmd_pairs "$@" ;;
lookup) shift; [ $# -ge 2 ] || die "usage: lookup NAME INPUT"; cmd_lookup "$@" ;;
reverse-lookup) shift; [ $# -ge 2 ] || die "usage: reverse-lookup NAME OUTPUT"; cmd_reverse_lookup "$@" ;;
add) shift; cmd_add "$@" ;;
delete) shift; cmd_delete "$@" ;;
create) shift; cmd_create "$@" ;;
replace) shift; cmd_replace "$@" ;;
help|-h|--help) sed -n '2,20p' "$NC_SELF" ;;
*) die "unknown subcommand: $SUB" ;;
esac