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:
parent
4a1d2451f3
commit
4828862a8e
57
CHANGELOG.md
57
CHANGELOG.md
@ -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
|
||||
|
||||
12
MANIFEST
12
MANIFEST
@ -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
|
||||
|
||||
57
MANUAL.md
57
MANUAL.md
@ -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.
|
||||
|
||||
18
bin/hl7-diff-set
Executable file
18
bin/hl7-diff-set
Executable 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" "$@"
|
||||
3
larry.sh
3
larry.sh
@ -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
206
lib/hl7-diff-set.sh
Executable 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
|
||||
@ -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) }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user