cloverleaf-larry/lib/nc-tclgen.sh
Bryan Johnson d58e4e0ec8 v0.8.28: expose 5 lib-only tools + fix nc-engine arg-parsing crash
Wires nc_status, nc_engine, nc_xlate, nc_smat_diff, nc_tclgen as first-class
LLM tools (all 4 surfaces). nc_engine unlocks TPS testing (hcitps) + the
route-test driver. Fixes a real nc-engine.sh bug surfaced by the exposure:
the dispatcher treated every --flag as taking a value (--dry-run ate the next
token) and a set -u leak from journal.sh crashed start/stop/bounce on bash 3.2;
fixed with set +u + a multi-case parser (no over-shift on bare trailing flags).
Corrects stale CHANGELOG + nc_engine schema text that misstated the bug as live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:18:23 -07:00

293 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# nc-tclgen.sh — generate TCL UPOC scaffolding from intent. Skeletons for
# common Cloverleaf TPS/Xlate use cases. Output is ready-to-edit TCL.
#
# Larry can write your full UPOC for you via prompt; this tool gives you a
# clean, lint-free starting point with the right argument handling and
# boilerplate. Useful when offline or when you want to start from a template.
#
# Subcommands:
# tps-presc <proc> [--description TXT] PreSC TPS proc skeleton
# tps-postsc <proc> [--description TXT] PostSC TPS proc skeleton
# tps-iclkill <proc> [--description TXT] a proc that calls hcitpsmsgkill
# xlate-helper <proc> [--description TXT] xlate helper function skeleton
# trxid <proc> [--description TXT] trxid (routing key) extractor
# ack <proc> [--description TXT] raw ACK generator
# field-rewrite <proc> --segment SEG --field N --to VALUE small field setter
# list-templates
#
# Output: TCL source to stdout (or --out PATH).
set -o pipefail
usage() { sed -n '2,19p' "$0"; exit 0; }
die() { printf 'nc-tclgen: %s\n' "$*" >&2; exit 1; }
header() {
local proc="$1" desc="$2" template="$3"
cat <<EOF
########################################################################
# $proc — $desc
#
# Cloverleaf UPOC: $template
# Generated by larry-anywhere nc-tclgen.sh on $(date -Iseconds 2>/dev/null || date)
# Author: ${USER:-larry-anywhere}
########################################################################
EOF
}
emit_tps_presc() {
local proc="$1" desc="${2:-PreSC handler — runs before service send}"
header "$proc" "$desc" "PreSC"
cat <<EOF
proc $proc {dArgs} {
upvar 1 \$dArgs args
keylget args MODE mode
keylget args MSGID mid
switch -- \$mode {
start { return "" }
run {
# Get the message data
set hl7Data [keylget args MSGDATA]
# TODO: inspect or transform \$hl7Data
# Example: strip CR/LF from segment delimiters
# regsub -all {\r|\n} \$hl7Data " " hl7Data
# Put the (possibly modified) message back into args
keylset args MSGDATA \$hl7Data
return { { CONTINUE } }
}
time { return "" }
shutdown { return "" }
default { return "" }
}
}
EOF
}
emit_tps_postsc() {
local proc="$1" desc="${2:-PostSC handler — runs after route translation}"
header "$proc" "$desc" "PostSC"
cat <<EOF
proc $proc {dArgs} {
upvar 1 \$dArgs args
keylget args MODE mode
keylget args MSGID mid
switch -- \$mode {
start { return "" }
run {
set hl7Data [keylget args MSGDATA]
# TODO: post-xlate processing
# Example: log message control ID
# set msh [lindex [split \$hl7Data "\r"] 0]
# set ctlId [lindex [split \$msh "|"] 9]
# echo "post-xlate \$proc: ctlId=\$ctlId"
keylset args MSGDATA \$hl7Data
return { { CONTINUE } }
}
time { return "" }
shutdown { return "" }
default { return "" }
}
}
EOF
}
emit_tps_iclkill() {
local proc="$1" desc="${2:-Drop message based on condition}"
header "$proc" "$desc" "PreSC (kills msg conditionally)"
cat <<EOF
proc $proc {dArgs} {
upvar 1 \$dArgs args
keylget args MODE mode
switch -- \$mode {
start { return "" }
run {
set hl7Data [keylget args MSGDATA]
# TODO: decide whether to drop. Return KILL to drop, CONTINUE to keep.
# Example: drop if PID-18 (account) is empty.
set segs [split \$hl7Data "\r"]
foreach s \$segs {
if {[string match "PID|*" \$s]} {
set fields [split \$s "|"]
if {[lindex \$fields 18] eq ""} {
echo "$proc: dropping message — empty PID.18"
return { { KILL } }
}
}
}
return { { CONTINUE } }
}
default { return "" }
}
}
EOF
}
emit_xlate_helper() {
local proc="$1" desc="${2:-Xlate helper function}"
header "$proc" "$desc" "Xlate proc"
cat <<EOF
# Xlate-callable helper. Use in an Xlate via:
# { OP COPY } { IN =[proc $proc {input}] } { OUT 0(0).PID(0).#X }
proc $proc {input} {
# TODO: transform \$input into the desired output value.
# Examples:
# - Uppercase: return [string toupper \$input]
# - Default fallback: if {\$input eq ""} { return "UNKNOWN" } else { return \$input }
# - Table lookup: return [tablelookup my_table_name \$input]
return \$input
}
EOF
}
emit_trxid() {
local proc="$1" desc="${2:-Routing-key extractor (returns trxid for routes)}"
header "$proc" "$desc" "TRXID (DATAFORMAT.PROC)"
cat <<EOF
proc $proc {dArgs} {
upvar 1 \$dArgs args
keylget args MSGDATA hl7Data
# TODO: return a string that route-conditions will match (TRXID regex).
# Common pattern: combine MSH.9 event + a tag from PID/PV1.
set segs [split \$hl7Data "\r"]
set msh [lindex \$segs 0]
set mshFields [split \$msh "|"]
set msgType [lindex \$mshFields 8] ;# MSH.9 (ADT^A08)
# Example: include receiving facility (PV1.4) or patient class
set trxid "\$msgType"
keylset args TRXID \$trxid
return { { CONTINUE } }
}
EOF
}
emit_ack() {
local proc="$1" desc="${2:-Generate a raw ACK (AA) for the inbound message}"
header "$proc" "$desc" "ACK generator"
cat <<EOF
proc $proc {dArgs} {
upvar 1 \$dArgs args
keylget args MSGDATA hl7Data
set segs [split \$hl7Data "\r"]
set msh [lindex \$segs 0]
set mshFields [split \$msh "|"]
set sendingApp [lindex \$mshFields 2]
set sendingFac [lindex \$mshFields 3]
set receivingApp [lindex \$mshFields 4]
set receivingFac [lindex \$mshFields 5]
set ctlId [lindex \$mshFields 9]
set ts [clock format [clock seconds] -format "%Y%m%d%H%M%S"]
set ackMsh "MSH|^~\\\\&|\$receivingApp|\$receivingFac|\$sendingApp|\$sendingFac|\$ts||ACK|\$ctlId|P|2.3"
set ackMsa "MSA|AA|\$ctlId"
set ack "\$ackMsh\r\$ackMsa\r"
keylset args MSGDATA \$ack
return { { CONTINUE } }
}
EOF
}
emit_field_rewrite() {
local proc="$1" seg="$2" fnum="$3" to="$4"
local desc="Set $seg.$fnum to '$to' on every message"
header "$proc" "$desc" "PreSC field rewrite"
cat <<EOF
proc $proc {dArgs} {
upvar 1 \$dArgs args
keylget args MODE mode
if {\$mode ne "run"} { return "" }
set hl7Data [keylget args MSGDATA]
set segs [split \$hl7Data "\r"]
set newSegs {}
foreach s \$segs {
if {[string match "$seg|*" \$s]} {
set fields [split \$s "|"]
# $seg.\$fnum maps to list-index N (HL7 segment numbering)
set fields [lreplace \$fields $fnum $fnum "$to"]
set s [join \$fields "|"]
}
lappend newSegs \$s
}
keylset args MSGDATA [join \$newSegs "\r"]
return { { CONTINUE } }
}
EOF
}
list_templates() {
cat <<EOF
Templates:
tps-presc PreSC handler skeleton
tps-postsc PostSC handler skeleton
tps-iclkill Conditional message drop
xlate-helper Xlate-callable helper function
trxid Routing key extractor
ack Raw ACK generator
field-rewrite Set a specific field to a constant on every message
EOF
}
SUB="${1:-help}"
shift 2>/dev/null
OUT_FILE=""
PROC=""
DESC=""
SEG=""; FNUM=""; TO=""
# Parse common flags
while [ $# -gt 0 ]; do
case "$1" in
--description) shift; DESC="$1" ;;
--out) shift; OUT_FILE="$1" ;;
--segment) shift; SEG="$1" ;;
--field) shift; FNUM="$1" ;;
--to) shift; TO="$1" ;;
-h|--help) usage ;;
-*) die "unknown flag: $1" ;;
*) [ -z "$PROC" ] && PROC="$1" || die "extra arg: $1" ;;
esac
shift
done
run_emit() {
if [ -n "$OUT_FILE" ]; then
mkdir -p "$(dirname "$OUT_FILE")" 2>/dev/null
"$@" > "$OUT_FILE"
printf 'wrote %s\n' "$OUT_FILE" >&2
else
"$@"
fi
}
case "$SUB" in
tps-presc) [ -n "$PROC" ] || die "needs PROC name"; run_emit emit_tps_presc "$PROC" "$DESC" ;;
tps-postsc) [ -n "$PROC" ] || die "needs PROC name"; run_emit emit_tps_postsc "$PROC" "$DESC" ;;
tps-iclkill) [ -n "$PROC" ] || die "needs PROC name"; run_emit emit_tps_iclkill "$PROC" "$DESC" ;;
xlate-helper) [ -n "$PROC" ] || die "needs PROC name"; run_emit emit_xlate_helper "$PROC" "$DESC" ;;
trxid) [ -n "$PROC" ] || die "needs PROC name"; run_emit emit_trxid "$PROC" "$DESC" ;;
ack) [ -n "$PROC" ] || die "needs PROC name"; run_emit emit_ack "$PROC" "$DESC" ;;
field-rewrite) [ -n "$PROC" ] && [ -n "$SEG" ] && [ -n "$FNUM" ] && [ -n "$TO" ] \
|| die "needs PROC + --segment SEG --field N --to VALUE"
run_emit emit_field_rewrite "$PROC" "$SEG" "$FNUM" "$TO" ;;
list-templates|list) list_templates ;;
help|-h|--help) usage ;;
*) die "unknown subcommand: $SUB" ;;
esac