v0.9.2: fix F-1/F-2/F-3/F-5 — regression false-PASS, PHI leak, jump guard, MRN match
F-1 (HIGH — blocks regression): hl7-diff --format count always returned 0
because the early-exit in END fired before the diff loop ran. Fix: remove
the early exit; suppress per-diff printf in emit() for count mode; emit
DIFF_COUNT after the loop. count/text/tsv all agree (13 diffs on fixture,
0 on identical pair, exit codes correct). Ref: lib/hl7-diff.sh.
F-5 (MEDIUM — PHI leak): hl7-sanitize silently passed LF-delimited HL7
through as cleartext (awk RS="\r" never split on LF). Fix: detect CR
absence via python3 binary read; normalise LF/CRLF→CR via `tr` before
the awk pass. Both file and stdin paths handled. CR path is a zero-overhead
passthrough. Before: 0 tokens, cleartext PHI. After: 6 tokens, all PID
fields replaced with [[MRN_0001]] etc. Ref: lib/hl7-sanitize.sh.
F-2 (MEDIUM): nc-make-jump emitted { PORT {} } for file/ICL inbounds
because the guard only tested for empty ORIG_PORT; protocol-nested returns
the literal "{}" for empty blocks. Fix: case guard rejects empty, "{}", and
any non-numeric value with a clear "is it a TCP listener?" error (exit 1).
TCP inbounds (numeric PORT) still generate correctly. Ref: lib/nc-make-jump.sh.
F-3 (MEDIUM — manual marquee example): nc-msgs mrn=<bare> returned 0 on
real Epic MRNs stored as "5720501458^^^MRN". Fix: in field_matches "="
operator, when expected has no ^ and the stored repetition does, compare
component-1 (text before first ^). Full-componented and mrn.1= paths
unchanged. Fixture: bare mrn=5720501458 now matches 2/3 messages correctly.
Ref: lib/nc-msgs.sh.
All four files pass bash -n. MANIFEST regenerated (54 entries, --check=0).
Tested against synthetic fixtures on .135 (no live engine required for these
logic bugs). Work-box re-verify commands in audit §4-B.
Co-Authored-By: Clover (claude-sonnet-4-6) <noreply@anthropic.com>
This commit is contained in:
parent
2b578f5058
commit
9a2ed47785
12
MANIFEST
12
MANIFEST
@ -23,7 +23,7 @@
|
|||||||
# scripts/make-manifest.sh and bump VERSION.
|
# scripts/make-manifest.sh and bump VERSION.
|
||||||
|
|
||||||
# Top-level scripts
|
# Top-level scripts
|
||||||
larry.sh 25d81d239b268635bff6704b55f473dc795af844b7a77cf6b24ba7e214c25786
|
larry.sh bd3bd27898afce693b44f75eb4fa3fab44b1a963d9b43f5559f5d9d0a5516e40
|
||||||
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
||||||
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
||||||
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
||||||
@ -31,7 +31,7 @@ install-larry.sh 072a036ad5bbf80e866cfd2dd74de50f8defd69a3f835032579b0cb9d421ad5
|
|||||||
uninstall-larry.sh c53ad2d8354c7adeb243b541f027f3f481e4a8661eecfd7af14d7ca53cfcaad9
|
uninstall-larry.sh c53ad2d8354c7adeb243b541f027f3f481e4a8661eecfd7af14d7ca53cfcaad9
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
VERSION 179a5390966e85c2071a87c1b31de13df67460665196f6529f8c4986842f81e5
|
VERSION f34248c2449a022d41c918d1e995ad85859a1e9f0e6f89d0af23ae4a55519f71
|
||||||
MANUAL.md 5ff54d6d5fae826f8b3da1eb3be6476076bb15f9b1417a4de285e59ea37e1b1f
|
MANUAL.md 5ff54d6d5fae826f8b3da1eb3be6476076bb15f9b1417a4de285e59ea37e1b1f
|
||||||
CHANGELOG.md 934007dc1b08b6c90120f009e3cc7870815e7b251fdf8f6629aa4c004c866017
|
CHANGELOG.md 934007dc1b08b6c90120f009e3cc7870815e7b251fdf8f6629aa4c004c866017
|
||||||
|
|
||||||
@ -72,9 +72,9 @@ lib/lessons.sh 225e899ed72ce20906cc454c5f5db87d605859e5e17431731a2ce481623f4e16
|
|||||||
lib/journal.sh 11c62a2d47b6b67a2f423fd8b86c454126df18d2dc3e150233bbd08293e39fe7
|
lib/journal.sh 11c62a2d47b6b67a2f423fd8b86c454126df18d2dc3e150233bbd08293e39fe7
|
||||||
|
|
||||||
# HL7 utilities
|
# HL7 utilities
|
||||||
lib/hl7-sanitize.sh c0ea35d28c32dcbb1476835a6e58c2ecdbd04f0a479b889675724fc564f4205f
|
lib/hl7-sanitize.sh 5bb409b3e5eae545e362e1313cd47c6835d56177dfe2efafd519e4ceedb2a82b
|
||||||
lib/hl7-desanitize.sh 2e5462a61ab1e8bd3fefb956bace8ca1ae33397a09024cbe766fa55c37a5aad6
|
lib/hl7-desanitize.sh 2e5462a61ab1e8bd3fefb956bace8ca1ae33397a09024cbe766fa55c37a5aad6
|
||||||
lib/hl7-diff.sh 66985afb3073340f1c12b0d7b39f41a5d8df68dfebc89c55190d6915f6077e86
|
lib/hl7-diff.sh d2cc179bf25dd8e808d46d4211d1926f36645cec8443d0ea910675093eb89d72
|
||||||
lib/hl7-field.sh a640f7cbd9521dc96171ee1dbdf909170262101a1d7a433f6f0ce2bea8d42b02
|
lib/hl7-field.sh a640f7cbd9521dc96171ee1dbdf909170262101a1d7a433f6f0ce2bea8d42b02
|
||||||
lib/hl7-schema.sh 2ba4057a214867ff4950f10057ee4ffd7149e1a82ba94b07b6857d77bf10d75f
|
lib/hl7-schema.sh 2ba4057a214867ff4950f10057ee4ffd7149e1a82ba94b07b6857d77bf10d75f
|
||||||
|
|
||||||
@ -107,9 +107,9 @@ lib/nc-tclgen.sh 5b8e73d7f6950a2b84f563132562ea82f62f4acac907257e233c7e68d85506c
|
|||||||
lib/nc-parse.sh 52fef42d7a4b361534ab0d921deef74586dfeb6c199c941cebb55abcc2c39d4f
|
lib/nc-parse.sh 52fef42d7a4b361534ab0d921deef74586dfeb6c199c941cebb55abcc2c39d4f
|
||||||
lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b
|
lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b
|
||||||
lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b
|
lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b
|
||||||
lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79
|
lib/nc-make-jump.sh 237d320d78d36e050dd4b2237c11d8452f55fa3a61428bea45643217ad5c6ab7
|
||||||
lib/nc-provision-jumps.sh cf80abe572a4eb241b351363ffa85406829f0c458882dc8d14f1628c458432f8
|
lib/nc-provision-jumps.sh cf80abe572a4eb241b351363ffa85406829f0c458882dc8d14f1628c458432f8
|
||||||
lib/nc-msgs.sh 20517922d1153ec7827c833987497fb305d087b579911d1b9067d65ae156a19f
|
lib/nc-msgs.sh 7e37bf3f9f1e1f09dab95914c9d45e605198e7111534e816bbac3298eb812918
|
||||||
lib/nc-document.sh 47211e99089c0446d25a1e84545a734894720a1c9ad8f59b920332035e4ea880
|
lib/nc-document.sh 47211e99089c0446d25a1e84545a734894720a1c9ad8f59b920332035e4ea880
|
||||||
lib/nc-revisions.sh c27856f7decfc4c2e2c990f59eb20136fdff9cf0a52b9d9fbd9370613666a802
|
lib/nc-revisions.sh c27856f7decfc4c2e2c990f59eb20136fdff9cf0a52b9d9fbd9370613666a802
|
||||||
lib/nc-diff-interface.sh c922d10323f06346efa53ada68b44d32d9568ff0bd848c59af3404135f29d1ad
|
lib/nc-diff-interface.sh c922d10323f06346efa53ada68b44d32d9568ff0bd848c59af3404135f29d1ad
|
||||||
|
|||||||
2
larry.sh
2
larry.sh
@ -99,7 +99,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.9.1"
|
LARRY_VERSION="0.9.2"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -184,8 +184,9 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emit(msg_idx, path, lv, rv) {
|
function emit(msg_idx, path, lv, rv) {
|
||||||
|
# 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
|
if (FMT == "tsv") printf "%d\t%s\t%s\t%s\n", msg_idx, path, lv, rv
|
||||||
else printf " %-20s %-30s %s\n", path, lv, rv
|
else if (FMT != "count") printf " %-20s %-30s %s\n", path, lv, rv
|
||||||
DIFF_COUNT++
|
DIFF_COUNT++
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,7 +235,9 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
|
|||||||
{ R_MSGS[++n_r] = $0 }
|
{ R_MSGS[++n_r] = $0 }
|
||||||
|
|
||||||
END {
|
END {
|
||||||
if (FMT == "count") { print DIFF_COUNT; exit }
|
# F-1 fix (2026-06-08): the early-exit `if (FMT=="count")` that used to sit
|
||||||
|
# here fired BEFORE the diff loop ran, so DIFF_COUNT was always 0. The loop
|
||||||
|
# is now unconditional; count output is emitted AFTER the loop at the bottom.
|
||||||
nm = (n_l > n_r) ? n_l : n_r
|
nm = (n_l > n_r) ? n_l : n_r
|
||||||
if (FMT == "text") {
|
if (FMT == "text") {
|
||||||
printf "HL7 diff:\n left: %s (%d messages)\n right: %s (%d messages)\n ignore: %s\n", LFILE, n_l, RFILE, n_r, IGNORE
|
printf "HL7 diff:\n left: %s (%d messages)\n right: %s (%d messages)\n ignore: %s\n", LFILE, n_l, RFILE, n_r, IGNORE
|
||||||
@ -243,7 +246,7 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
|
|||||||
}
|
}
|
||||||
if (n_l != n_r) {
|
if (n_l != n_r) {
|
||||||
if (FMT == "tsv") printf "0\tMESSAGE_COUNT\t%d\t%d\n", n_l, n_r
|
if (FMT == "tsv") printf "0\tMESSAGE_COUNT\t%d\t%d\n", n_l, n_r
|
||||||
else printf " MESSAGE COUNT mismatch: %d vs %d\n", n_l, n_r
|
else if (FMT != "count") printf " MESSAGE COUNT mismatch: %d vs %d\n", n_l, n_r
|
||||||
DIFF_COUNT++
|
DIFF_COUNT++
|
||||||
}
|
}
|
||||||
for (i=1; i<=nm; i++) {
|
for (i=1; i<=nm; i++) {
|
||||||
@ -256,6 +259,7 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
|
|||||||
if (FMT == "text" && (i == 1 || DIFF_COUNT > 0)) printf "----- message %d -----\n", i
|
if (FMT == "text" && (i == 1 || DIFF_COUNT > 0)) printf "----- message %d -----\n", i
|
||||||
diff_message(lm, rm, i)
|
diff_message(lm, rm, i)
|
||||||
}
|
}
|
||||||
|
if (FMT == "count") { print DIFF_COUNT; exit (DIFF_COUNT > 0 ? 1 : 0) }
|
||||||
if (FMT == "text") printf "\n%d total field difference(s)\n", DIFF_COUNT
|
if (FMT == "text") printf "\n%d total field difference(s)\n", DIFF_COUNT
|
||||||
exit (DIFF_COUNT > 0 ? 1 : 0)
|
exit (DIFF_COUNT > 0 ? 1 : 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -453,15 +453,51 @@ END {
|
|||||||
AWK_END
|
AWK_END
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# F-5 fix (2026-06-08): awk uses RS="\r" (CR segment separator — the real
|
||||||
|
# Cloverleaf wire format). LF-only or CRLF input never splits into segments,
|
||||||
|
# so every PHI field passes through as cleartext. Normalise all three line
|
||||||
|
# endings (LF, CRLF, bare CR) to CR before handing off to awk so that the
|
||||||
|
# existing tokenisation logic works regardless of how the file was created.
|
||||||
|
# `tr` is POSIX and available on every platform the tool targets.
|
||||||
|
# Detection: if the file contains no CR but does contain LF-separated MSH
|
||||||
|
# lines, it needs normalisation. We normalise whenever no bare CR is present,
|
||||||
|
# which is a safe no-op on already-CR wire data (the first tr removes nothing).
|
||||||
|
_normalise_to_cr() {
|
||||||
|
# CRLF → CR first, then remaining bare LF → CR.
|
||||||
|
tr -d '\r' | tr '\n' '\r'
|
||||||
|
}
|
||||||
|
|
||||||
if [ -n "$input_file" ]; then
|
if [ -n "$input_file" ]; then
|
||||||
|
# Detect whether file already uses CR segment separators.
|
||||||
|
if grep -qP '\r' "$input_file" 2>/dev/null || \
|
||||||
|
python3 -c "import sys; d=open(sys.argv[1],'rb').read(); sys.exit(0 if b'\r' in d else 1)" "$input_file" 2>/dev/null; then
|
||||||
|
# Already CR-delimited — feed directly (original path, zero overhead).
|
||||||
awk -v RULES_FILE="$rules_tmp" -v TABLE="$table" -v STRICT="$strict" \
|
awk -v RULES_FILE="$rules_tmp" -v TABLE="$table" -v STRICT="$strict" \
|
||||||
-v UPDATE_TABLE="$update_table" \
|
-v UPDATE_TABLE="$update_table" \
|
||||||
"$awk_script" "$input_file"
|
"$awk_script" "$input_file"
|
||||||
else
|
else
|
||||||
|
# LF or CRLF — normalise to CR on the fly then pipe into awk.
|
||||||
|
_normalise_to_cr < "$input_file" | \
|
||||||
awk -v RULES_FILE="$rules_tmp" -v TABLE="$table" -v STRICT="$strict" \
|
awk -v RULES_FILE="$rules_tmp" -v TABLE="$table" -v STRICT="$strict" \
|
||||||
-v UPDATE_TABLE="$update_table" \
|
-v UPDATE_TABLE="$update_table" \
|
||||||
"$awk_script" /dev/stdin
|
"$awk_script" /dev/stdin
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
# stdin — buffer to a temp file so we can inspect for CR presence.
|
||||||
|
local _norm_tmp; _norm_tmp=$(mktemp)
|
||||||
|
trap 'rm -f "$_norm_tmp"' RETURN
|
||||||
|
cat /dev/stdin > "$_norm_tmp"
|
||||||
|
if python3 -c "import sys; d=open(sys.argv[1],'rb').read(); sys.exit(0 if b'\r' in d else 1)" "$_norm_tmp" 2>/dev/null; then
|
||||||
|
awk -v RULES_FILE="$rules_tmp" -v TABLE="$table" -v STRICT="$strict" \
|
||||||
|
-v UPDATE_TABLE="$update_table" \
|
||||||
|
"$awk_script" "$_norm_tmp"
|
||||||
|
else
|
||||||
|
_normalise_to_cr < "$_norm_tmp" | \
|
||||||
|
awk -v RULES_FILE="$rules_tmp" -v TABLE="$table" -v STRICT="$strict" \
|
||||||
|
-v UPDATE_TABLE="$update_table" \
|
||||||
|
"$awk_script" /dev/stdin
|
||||||
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -85,7 +85,15 @@ T_ENC=$("$NCP" protocol-field "$NC" "$INBOUND" ENCODING 2>/dev/null | head -1)
|
|||||||
ENC="${ENC_OVERRIDE:-$T_ENC}"
|
ENC="${ENC_OVERRIDE:-$T_ENC}"
|
||||||
|
|
||||||
ORIG_PORT=$("$NCP" protocol-nested "$NC" "$INBOUND" PROTOCOL.PORT 2>/dev/null | head -1)
|
ORIG_PORT=$("$NCP" protocol-nested "$NC" "$INBOUND" PROTOCOL.PORT 2>/dev/null | head -1)
|
||||||
[ -n "$ORIG_PORT" ] || die "could not read PROTOCOL.PORT of inbound $INBOUND (is it a TCP listener? if it's a file/ICL inbound, this pattern may not apply directly)"
|
# F-2 fix (2026-06-08): protocol-nested returns the literal string "{}" for
|
||||||
|
# file/ICL inbounds whose PORT block is empty ({ PORT {} }). The old guard
|
||||||
|
# only tested for empty string, so "{}" (non-empty) slipped through and the
|
||||||
|
# generated thread carried "{ PORT {} }" — a broken TCP client with no port.
|
||||||
|
# Treat empty, "{}", and any non-numeric value as "no port" and die clearly.
|
||||||
|
case "$ORIG_PORT" in
|
||||||
|
''|'{}'|*[!0-9]*)
|
||||||
|
die "could not read a numeric PROTOCOL.PORT for inbound '$INBOUND' (got: '${ORIG_PORT:-<empty>}'). Is it a TCP listener? File/ICL inbounds do not use this jump pattern." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# tag = the inbound name itself (Bryan's "auto-derived" preference)
|
# tag = the inbound name itself (Bryan's "auto-derived" preference)
|
||||||
TAG="$INBOUND"
|
TAG="$INBOUND"
|
||||||
|
|||||||
@ -287,8 +287,19 @@ field_matches() {
|
|||||||
done <<< "$actual"
|
done <<< "$actual"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
# F-3 fix (2026-06-08): exact-match on a bare value (no ^ in expected)
|
||||||
|
# must also match when the stored field carries HL7 components, e.g.
|
||||||
|
# mrn=5720501458 should match 5720501458^^^MRN
|
||||||
|
# The manual's marquee example uses this form. Without the fix, operators
|
||||||
|
# searching by bare MRN get 0 results on every real Epic site.
|
||||||
|
# Rule: if expected has no ^ and the repetition does, compare only
|
||||||
|
# component-1 (the part before the first ^).
|
||||||
while IFS= read -r rep; do
|
while IFS= read -r rep; do
|
||||||
[ "$rep" = "$expected" ] && return 0
|
[ "$rep" = "$expected" ] && return 0
|
||||||
|
if [[ "$rep" == *"^"* ]] && [[ "$expected" != *"^"* ]]; then
|
||||||
|
local _comp1="${rep%%^*}"
|
||||||
|
[ "$_comp1" = "$expected" ] && return 0
|
||||||
|
fi
|
||||||
done <<< "$actual"
|
done <<< "$actual"
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user