cloverleaf-larry/lib/nc-smat-diff.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

162 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# nc-smat-diff.sh — diff smat (message archive) content across two environments.
# Different from nc-regression which runs route_test; this just samples actual
# stored messages from each env's smatdb and compares them.
#
# Pairing strategy:
# By default, sample N most-recent messages from each side, then pair by
# MSH.10 (message control ID) — the standard HL7 unique identifier.
# Unmatched IDs are reported as A-only / B-only.
#
# Usage:
# nc-smat-diff.sh <thread> --env-a HCIROOT_A --env-b HCIROOT_B [opts]
#
# Options:
# --site-a SITE site on env-A (default $HCISITE)
# --site-b SITE site on env-B (default same as --site-a)
# --limit N messages to sample per side (default 50)
# --ignore FIELDS hl7-diff --ignore list (default "MSH.7")
# --out DIR output directory for per-msg diffs + summary
# --pair-on FIELD HL7 field to pair messages on (default MSH.10)
# --include-history include SmatHistory archives on both sides
# --after EXPR only messages after this time (e.g. "3 days ago")
set -o pipefail
NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
NCM="$LIB_DIR/nc-msgs.sh"
HL7F="$LIB_DIR/hl7-field.sh"
HL7DIFF="$LIB_DIR/hl7-diff.sh"
die() { printf 'nc-smat-diff: %s\n' "$*" >&2; exit 1; }
THREAD=""
ENV_A=""
ENV_B=""
SITE_A="${HCISITE:-}"
SITE_B=""
LIMIT=50
IGNORE="MSH.7"
OUT=""
PAIR_ON="MSH.10"
INC_HIST=0
AFTER=""
while [ $# -gt 0 ]; do
case "$1" in
--env-a) shift; ENV_A="$1" ;;
--env-b) shift; ENV_B="$1" ;;
--site-a) shift; SITE_A="$1" ;;
--site-b) shift; SITE_B="$1" ;;
--limit) shift; LIMIT="$1" ;;
--ignore) shift; IGNORE="$1" ;;
--out) shift; OUT="$1" ;;
--pair-on) shift; PAIR_ON="$1" ;;
--include-history) INC_HIST=1 ;;
--after) shift; AFTER="$1" ;;
-h|--help) sed -n '2,22p' "$NC_SELF"; exit 0 ;;
-*) die "unknown flag: $1" ;;
*) [ -z "$THREAD" ] && THREAD="$1" || die "extra arg: $1" ;;
esac
shift
done
[ -n "$THREAD" ] || die "missing thread name"
[ -n "$ENV_A" ] && [ -n "$ENV_B" ] || die "--env-a and --env-b required"
[ -n "$SITE_A" ] || die "--site-a required (or set HCISITE)"
[ -z "$SITE_B" ] && SITE_B="$SITE_A"
[ -z "$OUT" ] && OUT=$(mktemp -d)
mkdir -p "$OUT/a" "$OUT/b" "$OUT/diff" 2>/dev/null
dump_side() {
local hciroot="$1" site="$2" target_dir="$3"
local nc_args=("$THREAD" --limit "$LIMIT" --format raw)
[ "$INC_HIST" = "1" ] && nc_args+=(--include-history)
[ -n "$AFTER" ] && nc_args+=(--after "$AFTER")
HCISITEDIR="$hciroot/$site" "$NCM" "${nc_args[@]}" > "$target_dir/all.raw" 2>"$target_dir/err"
# Split into individual messages on 0x1c, and index by pair-on field
awk -v RS=$'\x1c' -v dir="$target_dir" '
NF > 0 || $0 != "" {
n++
fpath = dir "/msg_" sprintf("%05d", n) ".hl7"
printf "%s", $0 > fpath
close(fpath)
}
' "$target_dir/all.raw"
# Build pair-on-field → file index
: > "$target_dir/index.tsv"
for f in "$target_dir"/msg_*.hl7; do
[ -f "$f" ] || continue
local key; key=$("$HL7F" "$PAIR_ON" "$f" 2>/dev/null | head -1)
[ -z "$key" ] && key="(no-$PAIR_ON)"
printf '%s\t%s\n' "$key" "$f" >> "$target_dir/index.tsv"
done
}
printf 'nc-smat-diff:\n thread: %s\n A: %s/%s\n B: %s/%s\n limit: %d ignore: %s pair-on: %s\n out: %s\n\n' \
"$THREAD" "$ENV_A" "$SITE_A" "$ENV_B" "$SITE_B" "$LIMIT" "$IGNORE" "$PAIR_ON" "$OUT" >&2
dump_side "$ENV_A" "$SITE_A" "$OUT/a"
dump_side "$ENV_B" "$SITE_B" "$OUT/b"
A_COUNT=$(wc -l < "$OUT/a/index.tsv" | tr -d ' ')
B_COUNT=$(wc -l < "$OUT/b/index.tsv" | tr -d ' ')
printf 'A: %d msgs B: %d msgs\n\n' "$A_COUNT" "$B_COUNT" >&2
# Pair messages by key
sort -k1 "$OUT/a/index.tsv" > "$OUT/a/sorted.tsv"
sort -k1 "$OUT/b/index.tsv" > "$OUT/b/sorted.tsv"
A_KEYS=$(awk -F'\t' '{print $1}' "$OUT/a/sorted.tsv" | sort -u)
B_KEYS=$(awk -F'\t' '{print $1}' "$OUT/b/sorted.tsv" | sort -u)
SUMMARY="$OUT/_summary.md"
{
printf '# smat diff: thread=%s\n\n' "$THREAD"
printf '- A: `%s/%s` (%d messages sampled)\n' "$ENV_A" "$SITE_A" "$A_COUNT"
printf '- B: `%s/%s` (%d messages sampled)\n' "$ENV_B" "$SITE_B" "$B_COUNT"
printf '- pair-on: `%s`\n' "$PAIR_ON"
printf '- ignore: `%s`\n\n' "$IGNORE"
printf '## Per-pair diffs\n\n'
printf '| %s | diffs | report |\n|---|---|---|\n' "$PAIR_ON"
} > "$SUMMARY"
DIFFS_TOTAL=0
A_ONLY=0
B_ONLY=0
PAIRED=0
while IFS= read -r key; do
[ -z "$key" ] && continue
a_files=$(grep -F "${key} " "$OUT/a/sorted.tsv" | awk -F'\t' '{print $2}' | head -1)
b_files=$(grep -F "${key} " "$OUT/b/sorted.tsv" | awk -F'\t' '{print $2}' | head -1)
if [ -z "$a_files" ] && [ -n "$b_files" ]; then
B_ONLY=$((B_ONLY+1))
echo "| \`$key\` | (only on B) | — |" >> "$SUMMARY"
elif [ -z "$b_files" ] && [ -n "$a_files" ]; then
A_ONLY=$((A_ONLY+1))
echo "| \`$key\` | (only on A) | — |" >> "$SUMMARY"
else
PAIRED=$((PAIRED+1))
report="$OUT/diff/${key//\//_}.md"
cnt=$("$HL7DIFF" --ignore "$IGNORE" --format count "$a_files" "$b_files" 2>/dev/null || echo "?")
{
printf '# msg %s\n\nA: `%s`\nB: `%s`\n\n' "$key" "$a_files" "$b_files"
"$HL7DIFF" --ignore "$IGNORE" "$a_files" "$b_files" 2>/dev/null
} > "$report"
echo "| \`$key\` | $cnt | [report](./diff/$(basename "$report")) |" >> "$SUMMARY"
DIFFS_TOTAL=$((DIFFS_TOTAL + ${cnt:-0}))
fi
done < <(printf '%s\n%s\n' "$A_KEYS" "$B_KEYS" | sort -u)
{
printf '\n## Summary\n\n'
printf '- paired (A and B): %d\n' "$PAIRED"
printf '- A-only: %d\n' "$A_ONLY"
printf '- B-only: %d\n' "$B_ONLY"
printf '- total field differences (post-ignore): %d\n' "$DIFFS_TOTAL"
} >> "$SUMMARY"
printf 'done. Summary: %s\n' "$SUMMARY" >&2
echo "$SUMMARY"