v0.9.10: hl7-diff-set (batch route-set compare) + hl7-diff --format text readability (F-8)

- hl7-diff-set: compare a whole set of Windows-vs-Linux regression outputs in one
  command — takes two prefixes (+ optional --dir), auto-discovers routes by glob,
  runs hl7-diff per pair (--framing auto --ignore MSH.7, overridable), reports
  per-route PASS/DIFF/MISSING/EXTRA + summary, exit 0 iff all clean (MISSING and
  EXTRA both count as regressions). No more hand-written for-loop with hardcoded names.
- F-8: hl7-diff --format text now shows SEG.FIELD left='...' right='...' with visible
  (empty)/<absent> tokens so a dropped component (e.g. Linux MSH.9.3 absent vs Windows
  ADT_A02) is obvious without opening the message files; --------- message N header now
  uses the real message index (was a stale counter). tsv/count formats unchanged.

Consolidated by Clover; Vera CODE-PASS (Deliverables/2026-07-23-cloverleaf-larry-v0910-hl7-diff-set.md) — own-fixture verification of all route classifications + the absent-token render + F-7 no-hang. Live-acceptance = Bryan's regression run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Johnson 2026-07-23 11:15:24 -07:00
parent 4a1d2451f3
commit 4828862a8e
8 changed files with 424 additions and 20 deletions

View File

@ -4,6 +4,63 @@ 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.9.10 — 2026-07-23
New: `hl7-diff-set` (`lib/hl7-diff-set.sh` + `bin/hl7-diff-set`, also runnable
as `larry tools hl7-diff-set`) — Bryan-direct ask, the natural finish to
today's regression tooling. Compares a WHOLE SET of paired baseline-vs-
candidate regression output files in one command instead of a hand-written
for-loop with hardcoded route names. Built for `hciroutetest`'s real on-disk
layout: one directory holding paired files named `<prefix>.<route>` where two
prefixes (e.g. `adt_in_regression_out_windows_single` /
`adt_in_regression_out_linux_single`) share a set of route suffixes. Auto-
discovers routes by globbing both prefixes (`<prefix>.*`, portable bash-3.2-
safe glob, no `nullglob`/bash-4-isms); runs the EXISTING `hl7-diff.sh` engine
per pair (not reimplemented) with sane regression defaults (`--framing auto
--ignore MSH.7`, both overridable, plus `--include-fields` pass-through);
reports PASS/DIFF/MISSING/EXTRA per route (a baseline route with no candidate
file, or vice-versa, is reported — never silently skipped, and both count as
non-clean) plus a final summary line (`4 routes: 3 clean, 1 differs`). Exit 0
iff every route is PASS, so it is a single scriptable regression gate.
`--format text` (default) also prints the full `hl7-diff --format text`
detail (indented) for any differing route; `--format tsv` prints one
machine row per route (`route\tstatus\tcount`) with the summary line routed
to stderr so stdout stays clean tsv. Self-tested (4-route mixed fixture: 1
clean, 1 differing with a real absent-component diff, 1 dropped-on-candidate
MISSING, 1 candidate-only EXTRA) under both `bash --version` 5.3.9 and the
real `/bin/bash` 3.2.57 on this Mac — identical output, confirming the
bash-3.2/MobaXterm-safety constraint. Bryan's exact command for his live
single-site directory:
```
hl7-diff-set adt_in_regression_out_windows_single adt_in_regression_out_linux_single \
--dir /opt/cloverleaf/cis2025.01/integrator/adt/data/regression_testing/single
```
Fix: F-8 (tsk-2026-07-23-hl7diff-readability, Bryan-direct live feedback) —
`hl7-diff --format text` printed only the field path plus ONE value per diff
line (`MSH.9.3 ADT_A02`); the right-hand value was always present in the
underlying data (tsv format carried it correctly) but rendered as invisible
trailing whitespace when it was empty, so it looked like only one side was
shown. Confirmed against Bryan's live repro: Windows `MSH-9=ADT^A02^ADT_A02`
vs Linux `MSH-9=ADT^A02``MSH.9.3` is `ADT_A02` on Windows and genuinely
ABSENT (not blank) on Linux (only 2 components). Text mode now labels both
sides explicitly (`MSH.9.3 left='ADT_A02' right='<absent>'`), and
distinguishes an empty-but-present value (`(empty)`) from a component or
subcomponent that does not exist AT ALL on that side (`<absent>` — absence
cascades: a fully-absent field marks every component/subcomponent under it
absent too, regardless of that component's own split count). Also fixed in
the same pass: the `----- message N -----` header used a stale running
`DIFF_COUNT` guess printed BEFORE processing each message, so a clean
message 1 followed by a differing message 2 got the message-2 diffs
mislabeled under a "message 1" header (or no header at all) — the header is
now printed lazily, inside `emit()`, keyed off the real `msg_idx`, right
before that message's first emitted line. tsv/count output formats are
unchanged (still raw left/right values, no display tokens). Self-tested:
2-message fixture (message 1 clean, message 2 differs) now correctly shows
`----- message 2 -----`; separate fixture proves the `(empty)` token for a
genuinely-blank-but-present component. Verified `bash -n` clean and
`make-manifest.sh --check` clean.
## v0.9.9 — 2026-07-23
Fix: REPL leaves the terminal corrupted after exit — Backspace prints a

View File

@ -23,7 +23,7 @@
# scripts/make-manifest.sh and bump VERSION.
# Top-level scripts
larry.sh 8da143113410eaf99aeea14ba12ebb13344b568d76d4711984d4c1dd75cbd4e0
larry.sh 322316fefec54d88f0a0ac346129874195b6de4786739b2aa43e15a4fa3eb206
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
@ -31,9 +31,9 @@ install-larry.sh 19f42e8f6e54eaffffe33e1464a8d245721260aed047270a75987c0b25c936c
uninstall-larry.sh c53ad2d8354c7adeb243b541f027f3f481e4a8661eecfd7af14d7ca53cfcaad9
# Metadata
VERSION 050990d5e9bd0494fa852a4a9ab0d60b91c583a63307920c7887a39e692f9998
MANUAL.md 46cc8650bbd5daed20617dcf8626e0653da25bf2facf6781912087fcb7c43e57
CHANGELOG.md a14b2be47d8db54419551218df02f4d4f4428e638f5ab7de58426c3aa8c11c2e
VERSION cd8f34a6873bdd24cb055e5d97d11fc0dac541a6939c13ef3b700c6bd28344e4
MANUAL.md 7f442b2e3e0bfa7d8cadb588abb3f197dc03bc44f47b84fc5458edba8010ddfa
CHANGELOG.md 4dc1258c1de7ec0d116c881e562140d140d0ca987110c3d2afb060903b4a8cfe
# Agent personas (system-prompt overlays)
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
@ -74,7 +74,8 @@ lib/journal.sh 1c285df3f5677477c7b675c4f03236823005670b338a1781dd0c73d12fd1424f
# HL7 utilities
lib/hl7-sanitize.sh efd333dc3e267cf2280433e53bbb910023d0bc4b30724181b53873b410d83005
lib/hl7-desanitize.sh 2e5462a61ab1e8bd3fefb956bace8ca1ae33397a09024cbe766fa55c37a5aad6
lib/hl7-diff.sh 1d81163869d003bb6528818553417350c1ab885f2f04eea5d2a46a06100e789b
lib/hl7-diff.sh 79e92c5c9326f6dab49673b719259ae46aaa1020b37b8ac91b5a26869f17656b
lib/hl7-diff-set.sh 40d40b1f8e688c584f03bc6efb9645e18daf0fd9081a595b08137d2438619777
lib/hl7-field.sh a640f7cbd9521dc96171ee1dbdf909170262101a1d7a433f6f0ce2bea8d42b02
lib/hl7-schema.sh 2ba4057a214867ff4950f10057ee4ffd7149e1a82ba94b07b6857d77bf10d75f
@ -182,6 +183,7 @@ bin/nc-regression 59a5f25fcea642433613f0933fe06dd9fa25b372e5d130e119cb466a076099
bin/nc-msgs 8a938cefedbb2bad9496c6f84831788b6ac45d34e8b16fc60d8aa47c25adea76
bin/hl7-field cb56984dfc9342563e05cdf0e3514a64df2099d24adeaa1d5b22cc6ce298eb73
bin/hl7-diff 2d965567ea4f737d97ea92313dfacd84b745cea74313d1be565a3234240b76f3
bin/hl7-diff-set b2d6a21aa8ba55b0c6b7ae166e1dda73696e456a8c94ca0cacb686b2ae8cea30
bin/len2nl d73136e353d7964f097b4a50993ba2a3364550e13b7ae5a1b43af00ed8eddbe3
bin/hl7-sanitize 7e2d883b4e49501eccf3db0ef27f4866dc664c3bde51fd6ad2a7804ca7de8e4e
bin/hl7-desanitize 78c5bda12e7374c056fac940ac1889ceca87f19a1e711dc658cc46b63b020925

View File

@ -402,6 +402,17 @@ lib/nc-msgs.sh ADTto_3m --limit 100 --format raw \
Compare two HL7 files (or multi-message dumps) with field-level normalization.
**v0.9.10 (F-8, Bryan-direct live feedback):** `--format text` now labels
BOTH sides on every diff line — `SEG.FIELD left='...' right='...'` — instead
of printing only the left value with the right value rendering invisibly
blank. An empty-but-present value shows as the token `(empty)`; a component
or subcomponent that does not exist AT ALL on that side (the other side has
more components — e.g. left `MSH-9=ADT^A02^ADT_A02` vs right `MSH-9=ADT^A02`,
so `MSH.9.3` is absent, not blank, on the right) shows as `<absent>`. The
`----- message N -----` header is also now keyed off the real message index
(it used to print based on a stale running counter and could mislabel which
message a diff belonged to when an earlier message was clean).
```bash
# Default — ignores MSH.7 (timestamp); shows everything else
lib/hl7-diff.sh left.hl7 right.hl7
@ -423,6 +434,52 @@ lib/hl7-diff.sh --format tsv left.hl7 right.hl7
---
## HL7 diff over a whole route SET (`hl7-diff-set` / `lib/hl7-diff-set.sh`)
One command instead of a hand-written for-loop with hardcoded route names.
Built for `hciroutetest`'s real on-disk layout: one directory holding paired
files named `<prefix>.<route>` — a baseline (e.g. Windows) prefix and a
candidate (e.g. Linux) prefix that share a set of route suffixes:
```
adt_in_regression_out_windows_single.adt_process_out
adt_in_regression_out_linux_single.adt_process_out
adt_in_regression_out_windows_single.lab_adt_out
adt_in_regression_out_linux_single.lab_adt_out
...
```
```bash
# Bryan's real single-site regression directory — the actual command he runs:
hl7-diff-set adt_in_regression_out_windows_single adt_in_regression_out_linux_single \
--dir /opt/cloverleaf/cis2025.01/integrator/adt/data/regression_testing/single
# Override the ignore list / framing (both forward straight to hl7-diff)
hl7-diff-set <baseline-prefix> <candidate-prefix> --dir <path> \
--ignore "MSH.7,MSH.10" --framing len10
# Machine-parseable: one row per route, route<TAB>status<TAB>diff_count.
# The summary line prints to STDERR in this mode, so stdout stays clean tsv —
# recommended for scripting/CI gates.
hl7-diff-set <baseline-prefix> <candidate-prefix> --dir <path> --format tsv
```
Auto-discovers routes by globbing both prefixes (`<prefix>.*`), so a route
present on only ONE side is never silently dropped:
- both files present, 0 diffs → **PASS**
- both files present, N>0 diffs → **DIFF** (`--format text` also prints the
full `hl7-diff --format text` detail, indented, for that route)
- baseline route has no candidate file → **MISSING** (a dropped output IS a
regression — counts as a failure)
- candidate has a route the baseline does not → **EXTRA** (reported, also
counts as non-clean)
Prints a final summary line, e.g. `4 routes: 3 clean, 1 differs`. **Exit 0
iff every route is PASS — non-zero otherwise — so it is a single scriptable
regression gate:** `hl7-diff-set ... --dir ... || echo "regression FAILED"`.
---
## Jump thread generation (`lib/nc-make-jump.sh`)
Generates the 3-thread cross-environment data-replay pattern: `linux_<tag>_out` on OLD, `windows_<tag>_in` + `windows_<tag>_out` in NEW's `server_jump` site. Output is plain TCL text — no file writes.

View File

@ -1 +1 @@
0.9.9
0.9.10

18
bin/hl7-diff-set Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# hl7-diff-set — direct, on-PATH wrapper for lib/hl7-diff-set.sh: diff a WHOLE
# SET of paired Windows-vs-Linux regression output files in one command
# (auto-discovers routes from two shared file prefixes; no hand-written
# for-loop / hardcoded route list needed). Reuses the existing hl7-diff engine
# per pair — same --ignore/--framing semantics, sane regression defaults.
#
# hl7-diff-set adt_in_regression_out_windows_single adt_in_regression_out_linux_single \
# --dir /opt/cloverleaf/cis2025.01/integrator/adt/data/regression_testing/single
#
# (no `larry tools` prefix needed). All args pass straight through; -h/--help
# is handled by the underlying tool.
set -o pipefail
_self="${BASH_SOURCE[0]}"; [ -L "$_self" ] && _self="$(readlink "$_self")"
. "$(cd "$(dirname "$_self")" && pwd)/_nc_common.sh"
lib="$(_nc_resolve_lib)" || { echo "hl7-diff-set: lib/ toolkit not found (set LARRY_LIB_DIR or LARRY_HOME)" >&2; exit 1; }
if [ $# -eq 0 ]; then exec bash "$lib/hl7-diff-set.sh" --help; fi
exec bash "$lib/hl7-diff-set.sh" "$@"

View File

@ -100,7 +100,7 @@ set -o pipefail
# ─────────────────────────────────────────────────────────────────────────────
# Config
# ─────────────────────────────────────────────────────────────────────────────
LARRY_VERSION="0.9.9"
LARRY_VERSION="0.9.10"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
# ─────────────────────────────────────────────────────────────────────────────
@ -445,6 +445,7 @@ nc-msgs.sh|Native smat query: search/inspect archived messages without hcidbdump
#HL7
hl7-field.sh|Extract a field by path (PID.3, MSH.10, PV1-3-4 …) from an HL7 v2 message
hl7-diff.sh|HL7-aware diff with field-level normalization between two messages
hl7-diff-set.sh|Run hl7-diff over a WHOLE SET of paired baseline-vs-candidate regression output files (e.g. Windows vs Linux hciroutetest dumps) in one command — auto-discovers routes from two shared file prefixes, reports PASS/DIFF/MISSING/EXTRA per route + a summary line
len2nl.sh|Convert length-prefixed / MLLP-framed HL7 to newline-readable form
hl7-sanitize.sh|Tokenize PHI fields in HL7 v2 messages ([[CATEGORY_NNNN]] tokens)
hl7-desanitize.sh|Reverse hl7-sanitize: restore original values from a token map

206
lib/hl7-diff-set.sh Executable file
View File

@ -0,0 +1,206 @@
#!/usr/bin/env bash
# hl7-diff-set.sh — run hl7-diff over a WHOLE SET of paired Windows-vs-Linux
# (or any baseline-vs-candidate) regression output files in one command,
# instead of hand-writing a for-loop with hardcoded route names.
#
# Built for hciroutetest's real on-disk layout (Bryan-direct, 2026-07-23):
# one directory holding paired files named <prefix>.<route>, where TWO
# prefixes (a baseline/"windows" set and a candidate/"linux" set) share a set
# of route suffixes after the last dot, e.g.:
# adt_in_regression_out_windows_single.adt_process_out
# adt_in_regression_out_linux_single.adt_process_out
# adt_in_regression_out_windows_single.lab_adt_out
# adt_in_regression_out_linux_single.lab_adt_out
# ...
#
# Usage:
# hl7-diff-set.sh <baseline-prefix> <candidate-prefix>
# [--dir PATH] # directory holding the paired files (default: .)
# [--ignore "FIELD,..."] # forwarded to hl7-diff. Default: MSH.7
# [--include-fields "F,.."] # forwarded to hl7-diff (overrides --ignore for that set)
# [--framing auto|nl|0x1c|len10] # forwarded to hl7-diff. Default: auto
# [--format text|tsv] # how THIS command reports. Default: text
#
# hl7-diff-set.sh adt_in_regression_out_windows_single adt_in_regression_out_linux_single \
# --dir /opt/cloverleaf/cis2025.01/integrator/adt/data/regression_testing/single
#
# What it does:
# 1. Globs "<dir>/<baseline-prefix>.*", derives each route suffix (the part
# after "<baseline-prefix>."), and pairs it with "<dir>/<candidate-prefix>.<suffix>".
# Also globs "<dir>/<candidate-prefix>.*" so a candidate-only route (no
# baseline file) is found too — nothing is silently skipped.
# 2. Runs the existing lib/hl7-diff.sh on every matched pair (this does NOT
# reimplement diffing — same engine, same --ignore/--framing semantics).
# 3. Reports PASS/DIFF/MISSING/EXTRA per route, plus a one-line summary.
#
# Per-route outcomes:
# PASS — both files present, 0 field differences (post-ignore)
# DIFF — both files present, N>0 field differences (--format text also
# shows the full hl7-diff --format text detail, indented)
# MISSING — baseline route has no matching candidate file (a dropped output
# IS a regression — this counts as a failure)
# EXTRA — candidate has a route the baseline does not (reported, also
# counts as non-clean — never silently skipped)
#
# --format text (default) prints a human report to stdout: per-route lines,
# DIFF detail indented beneath, then the summary line.
# --format tsv prints ONE machine row per route to stdout:
# route<TAB>status<TAB>diff_count
# (diff_count is "-" for MISSING/EXTRA, since no diff was actually run) —
# the summary line still prints, to STDERR, so stdout stays clean tsv.
# Recommended for scripting: hl7-diff.sh's OWN v0.9.9 text-mode message-
# header mislabel is fixed as of this release (F-8), but tsv remains the
# safer machine-parseable choice regardless.
#
# Exit codes: 0 = every route PASS (all clean); 1 = at least one route is
# DIFF/MISSING/EXTRA; 2 = usage/input error (bad flag, --dir missing, or
# neither prefix matched any file at all).
set -o pipefail
NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
HL7DIFF="$LIB_DIR/hl7-diff.sh"
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
# shellcheck disable=SC1090,SC1091
. "$LIB_DIR/cygwin-safe.sh"
else
coerce_int() { local r="${1:-}" d="${2:-0}" c; c=$(printf '%s' "$r" | tr -cd '0-9'); printf '%s' "${c:-$d}"; }
fi
die() { printf 'hl7-diff-set: %s\n' "$*" >&2; exit 2; }
DIR="."
IGNORE="MSH.7"
INCLUDE=""
FRAMING="auto"
FORMAT="text"
BASE_PREFIX=""
CAND_PREFIX=""
while [ $# -gt 0 ]; do
case "$1" in
--dir) shift; DIR="$1" ;;
--ignore) shift; IGNORE="$1" ;;
--include-fields) shift; INCLUDE="$1" ;;
--framing) shift; FRAMING="$1" ;;
--format) shift; FORMAT="$1" ;;
-h|--help) sed -n '2,52p' "$NC_SELF"; exit 0 ;;
-*) die "unknown flag: $1" ;;
*)
if [ -z "$BASE_PREFIX" ]; then BASE_PREFIX="$1"
elif [ -z "$CAND_PREFIX" ]; then CAND_PREFIX="$1"
else die "extra arg: $1"
fi
;;
esac
shift
done
[ -x "$HL7DIFF" ] || [ -f "$HL7DIFF" ] || die "backing tool not found: $HL7DIFF"
[ -n "$BASE_PREFIX" ] && [ -n "$CAND_PREFIX" ] || die "usage: hl7-diff-set.sh <baseline-prefix> <candidate-prefix> [--dir PATH] [--ignore FIELDS] [--framing auto|nl|0x1c|len10] [--format text|tsv] (run --help for full usage)"
[ -d "$DIR" ] || die "no such --dir: $DIR"
case "$FORMAT" in text|tsv) ;; *) die "bad --format (want text|tsv)" ;; esac
case "$FRAMING" in auto|nl|0x1c|len10) ;; *) die "bad --framing (want auto|nl|0x1c|len10)" ;; esac
# --- discovery: glob both prefixes, derive route suffixes -------------------
# Portable (bash 3.2 / MobaXterm-safe) glob: a literal no-match pattern stays
# un-expanded, so every candidate is existence-checked with `[ -e ]` before
# use — no reliance on nullglob (bash-4-leaning / shopt-dependent).
_base_suffixes=()
for _f in "$DIR/$BASE_PREFIX".*; do
[ -e "$_f" ] || continue
_bn="$(basename "$_f")"
_base_suffixes+=("${_bn#"$BASE_PREFIX".}")
done
_cand_suffixes=()
for _f in "$DIR/$CAND_PREFIX".*; do
[ -e "$_f" ] || continue
_bn="$(basename "$_f")"
_cand_suffixes+=("${_bn#"$CAND_PREFIX".}")
done
if [ "${#_base_suffixes[@]}" -eq 0 ] && [ "${#_cand_suffixes[@]}" -eq 0 ]; then
die "no files matched '$BASE_PREFIX.*' or '$CAND_PREFIX.*' in $DIR"
fi
# Union of both suffix lists, sorted + de-duped, for a stable/deterministic
# route order regardless of filesystem glob order.
ROUTES="$(printf '%s\n' "${_base_suffixes[@]}" "${_cand_suffixes[@]}" | grep -v '^$' | sort -u)"
# --- run each pair ------------------------------------------------------------
diff_args=(--ignore "$IGNORE" --framing "$FRAMING")
[ -n "$INCLUDE" ] && diff_args+=(--include-fields "$INCLUDE")
n_routes=0 n_pass=0 n_diff=0 n_missing=0 n_extra=0
if [ "$FORMAT" = "text" ]; then
printf 'hl7-diff-set: baseline=%s candidate=%s dir=%s\n' "$BASE_PREFIX" "$CAND_PREFIX" "$DIR"
printf ' ignore: %s framing: %s\n\n' "$IGNORE" "$FRAMING"
fi
while IFS= read -r route; do
[ -z "$route" ] && continue
n_routes=$((n_routes + 1))
bfile="$DIR/$BASE_PREFIX.$route"
cfile="$DIR/$CAND_PREFIX.$route"
if [ ! -f "$bfile" ]; then
n_extra=$((n_extra + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tEXTRA\t-\n' "$route"
else
printf ' %-24s EXTRA (candidate-only route — no baseline file: %s)\n' "$route" "$bfile"
fi
continue
fi
if [ ! -f "$cfile" ]; then
n_missing=$((n_missing + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tMISSING\t-\n' "$route"
else
printf ' %-24s MISSING (no candidate output — dropped route: %s)\n' "$route" "$cfile"
fi
continue
fi
count="$("$HL7DIFF" "${diff_args[@]}" --format count "$bfile" "$cfile" 2>&1)"
count="$(coerce_int "$count" 0)"
if [ "$count" -eq 0 ]; then
n_pass=$((n_pass + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tPASS\t0\n' "$route"
else
printf ' %-24s PASS\n' "$route"
fi
else
n_diff=$((n_diff + 1))
if [ "$FORMAT" = "tsv" ]; then
printf '%s\tDIFF\t%s\n' "$route" "$count"
else
printf ' %-24s DIFF (%s diff(s))\n' "$route" "$count"
"$HL7DIFF" "${diff_args[@]}" --format text "$bfile" "$cfile" 2>&1 | sed 's/^/ /'
printf '\n'
fi
fi
done <<EOF_ROUTES
$ROUTES
EOF_ROUTES
# --- summary ------------------------------------------------------------------
_summary="$n_routes routes: $n_pass clean, $n_diff differs"
[ "$n_missing" -gt 0 ] && _summary="$_summary, $n_missing missing"
[ "$n_extra" -gt 0 ] && _summary="$_summary, $n_extra extra"
if [ "$FORMAT" = "tsv" ]; then
printf '%s\n' "$_summary" >&2
else
printf '%s\n' "$_summary"
fi
if [ "$n_diff" -eq 0 ] && [ "$n_missing" -eq 0 ] && [ "$n_extra" -eq 0 ] && [ "$n_routes" -gt 0 ]; then
exit 0
fi
exit 1

View File

@ -32,8 +32,21 @@
# --ignore MSH.7
#
# Output (text format):
# Per-message header, then one line per differing field:
# SEG.FIELD[.COMP[.SUB]] LEFT_VALUE RIGHT_VALUE
# Per-message header (printed once, right before that message's FIRST
# differing line — a message with no diffs gets no header), then one line
# per differing field, BOTH sides labeled so you never have to guess which
# value came from which file:
# SEG.FIELD[.COMP[.SUB]] left='LEFT_VALUE' right='RIGHT_VALUE'
# An empty-but-present value renders as the literal token (empty); a
# component/subcomponent that does not exist AT ALL on that side (the other
# side has more components, e.g. left MSH-9=ADT^A02^ADT_A02 vs right
# MSH-9=ADT^A02 — right's 3rd component is not merely blank, it is absent)
# renders as <absent>, so "blank" and "not there" are never visually the
# same thing. (F-8, tsk-2026-07-23-hl7diff-readability, Bryan-direct
# live-feedback: previously the right-hand value printed as a genuinely
# blank/whitespace field, which looked like only ONE value was shown per
# diff line — the data was always there in the underlying lv/rv, it just
# rendered invisibly.)
#
# Exit codes: 0 identical (post-ignore), 1 differences found, 2 input error
set -o pipefail
@ -187,8 +200,14 @@ split_messages "$RIGHT" "$TMP_R"
# v0.8.26: pipe the comparison output through the tty-gated sanitizer, then
# propagate ${PIPESTATUS[0]} so awk's exit (0 identical, 1 differs) survives.
# SQ: a literal single-quote character, passed in via -v (OUTSIDE the
# single-quoted awk program text below) so the text-mode left/right quoting
# (F-8) never has to put a bare ' inside the awk source itself -- the whole
# program is one bash '...'-quoted string, and a stray ' in there would
# terminate it early and corrupt everything after (bash has no escape inside
# single quotes).
awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
-v LFILE="$LEFT" -v RFILE="$RIGHT" '
-v LFILE="$LEFT" -v RFILE="$RIGHT" -v SQ="'" '
# F-7 fix (2026-07-23, tsk-2026-07-23-len10, Bryan-blocking, discovered while
# validating len10 self-tests -- PRE-EXISTING since v0.3.0 initial release
# e08f030, unrelated to the len10 change): n, i, ent, arr were used in this
@ -246,7 +265,15 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
return n
}
function compare_field(seg, fidx, lv, rv, msg_idx, n_lc, n_rc, lc, rc, lcomp, rcomp, j, k, n_lsc, n_rsc, lsub, rsub, nm, diffs, ls, rs, ns) {
# F-8 fix (2026-07-23, tsk-2026-07-23-hl7diff-readability, Bryan-direct live
# feedback): lv_absent/rv_absent (new formal params, 6th/7th) tell emit()
# whether a side has NO such component/subcomponent at all (the other side
# has more components -- e.g. left MSH-9=ADT^A02^ADT_A02, right MSH-9=ADT^A02:
# component 3 on the right is ABSENT there, not merely blank) so text mode
# can render <absent> instead of a same-looking-as-blank (empty). Absence
# cascades down: if the whole field is absent, every component and
# subcomponent under it is absent too, regardless of its own split count.
function compare_field(seg, fidx, lv, rv, msg_idx, lv_absent, rv_absent, n_lc, n_rc, lc, rc, lcomp, rcomp, j, k, n_lsc, n_rsc, lsub, rsub, nm, diffs, ls, rs, ns, lc_absent, rc_absent, ls_absent, rs_absent) {
if (lv == rv) return 0
# Try component-level if both have ^
if (lv ~ /\^/ || rv ~ /\^/) {
@ -257,6 +284,8 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
for (j=1; j<=nm; j++) {
lc = (j <= n_lc) ? lcomp[j] : ""
rc = (j <= n_rc) ? rcomp[j] : ""
lc_absent = lv_absent || (j > n_lc)
rc_absent = rv_absent || (j > n_rc)
if (lc == rc) continue
# subcomponent
if (lc ~ /&/ || rc ~ /&/) {
@ -266,14 +295,16 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
for (k=1; k<=ns; k++) {
ls = (k <= n_lsc) ? lsub[k] : ""
rs = (k <= n_rsc) ? rsub[k] : ""
ls_absent = lc_absent || (k > n_lsc)
rs_absent = rc_absent || (k > n_rsc)
if (ls != rs && !ignored(seg, fidx, j, k)) {
emit(msg_idx, seg "." fidx "." j "." k, ls, rs)
emit(msg_idx, seg "." fidx "." j "." k, ls, rs, ls_absent, rs_absent)
diffs++
}
}
} else {
if (!ignored(seg, fidx, j, "")) {
emit(msg_idx, seg "." fidx "." j, lc, rc)
emit(msg_idx, seg "." fidx "." j, lc, rc, lc_absent, rc_absent)
diffs++
}
}
@ -281,16 +312,40 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
return diffs
}
if (!ignored(seg, fidx, "", "")) {
emit(msg_idx, seg "." fidx, lv, rv)
emit(msg_idx, seg "." fidx, lv, rv, lv_absent, rv_absent)
return 1
}
return 0
}
function emit(msg_idx, path, lv, rv) {
# F-8 fix: text mode now (a) labels both sides, left=... right=..., so the
# reader never has to guess which value belongs to which file -- the data
# was always present in lv/rv, it just rendered invisibly when the
# right-hand value happened to be empty; (b) renders an empty-but-present
# value as the literal token (empty) and a component/subcomponent that does
# not exist AT ALL on that side as <absent> (see compare_field), so blank
# and not-there are never visually indistinguishable; (c) prints the
# "----- message N -----" header lazily, right before the first emitted
# line of a message, keyed off the msg_idx actually passed in -- this
# replaces the old preemptive header print in END (which used the STALE
# global DIFF_COUNT to decide whether to print a header for the message
# about to be processed, so a clean message 1 followed by a differing
# message 2 got the message-2 diffs mislabeled under a "----- message 1
# -----" header, or no header at all). tsv/count output is untouched
# (still raw lv/rv). SQ is the single-quote character passed in via -v.
function emit(msg_idx, path, lv, rv, lv_absent, rv_absent, lv_disp, rv_disp) {
# count mode: accumulate only — no per-diff output (final print is in END).
if (FMT == "tsv") printf "%d\t%s\t%s\t%s\n", msg_idx, path, lv, rv
else if (FMT != "count") printf " %-20s %-30s %s\n", path, lv, rv
if (FMT == "tsv") {
printf "%d\t%s\t%s\t%s\n", msg_idx, path, lv, rv
} else if (FMT == "text") {
if (msg_idx != LAST_HDR_MSG) {
printf "----- message %d -----\n", msg_idx
LAST_HDR_MSG = msg_idx
}
lv_disp = (lv_absent == 1) ? "<absent>" : ((lv == "") ? "(empty)" : lv)
rv_disp = (rv_absent == 1) ? "<absent>" : ((rv == "") ? "(empty)" : rv)
printf " %-20s left=%-24s right=%s\n", path, SQ lv_disp SQ, SQ rv_disp SQ
}
DIFF_COUNT++
}
@ -307,7 +362,10 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
lv = (i <= nl) ? lf[i] : ""
rv = (i <= nr) ? rf[i] : ""
field_num = (seg_name == "MSH") ? i : (i - 1)
compare_field(seg_name, field_num, lv, rv, msg_idx)
# F-8: field itself absent on a side (that side segment does not have
# this many pipe-delimited fields) cascades to <absent> for every
# component/subcomponent under it -- see compare_field.
compare_field(seg_name, field_num, lv, rv, msg_idx, (i > nl), (i > nr))
}
}
@ -333,7 +391,11 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
}
}
BEGIN { DIFF_COUNT = 0; n_l = 0; n_r = 0 }
# F-8: LAST_HDR_MSG tracks which message index the text-mode "----- message
# N -----" header was last printed for, so emit() can print it lazily (and
# correctly) right before the first line of that message instead of the
# old preemptive/stale-DIFF_COUNT guess in the loop below.
BEGIN { DIFF_COUNT = 0; n_l = 0; n_r = 0; LAST_HDR_MSG = 0 }
FNR == NR { L_MSGS[++n_l] = $0; next }
{ R_MSGS[++n_r] = $0 }
@ -360,7 +422,8 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
emit(i, "MESSAGE_PRESENCE", (lm != "" ? "present" : "missing"), (rm != "" ? "present" : "missing"))
continue
}
if (FMT == "text" && (i == 1 || DIFF_COUNT > 0)) printf "----- message %d -----\n", i
# F-8: header printing moved into emit() (lazy, keyed by the real
# msg_idx) -- no preemptive print here anymore. See emit()/LAST_HDR_MSG.
diff_message(lm, rm, i)
}
if (FMT == "count") { print DIFF_COUNT; exit (DIFF_COUNT > 0 ? 1 : 0) }