v0.8.27: nc-revisions — NetConfig change-history / revision diff
New tool: show how a thread/system/site changed over time by diffing Cloverleaf NetConfig revision snapshots, annotated with who saved each and when. Handles the non-zero-padded NetConfig<TS> revision dirs by parsing the prologue date into a sortable key; scopes diffs to the requested thread/system via nc-parse. Flags: <thread>[.<site>], --system, --site, --format timeline|diff, --limit, --since. Wired as nc_revisions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
111be2c744
commit
5214d87a04
55
CHANGELOG.md
55
CHANGELOG.md
@ -4,6 +4,61 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here.
|
|||||||
Versioning is loose-semver; bumps trigger the in-process self-update on every
|
Versioning is loose-semver; bumps trigger the in-process self-update on every
|
||||||
running client via `LARRY_BASE_URL` + `MANIFEST`.
|
running client via `LARRY_BASE_URL` + `MANIFEST`.
|
||||||
|
|
||||||
|
## v0.8.27 — 2026-05-28
|
||||||
|
|
||||||
|
**★ NEW TOOL: `nc-revisions.sh` — NetConfig change-history / revision-diff.**
|
||||||
|
Shows how a THREAD, a multi-thread SYSTEM, or a WHOLE SITE changed over time by
|
||||||
|
diffing Cloverleaf's own per-save NetConfig revision SNAPSHOTS, annotated with
|
||||||
|
WHO saved each revision and WHEN. Deterministic, pure bash+awk, NO API, NO
|
||||||
|
network — runs identically on an API-blocked host.
|
||||||
|
|
||||||
|
- **Revision storage (verified on the real integrator).** Each save snapshots
|
||||||
|
the full NetConfig into `$HCIROOT/<site>/revisions/NetConfig<TS>/NetConfig`,
|
||||||
|
where `<TS>` is the save time as `M D YYYY H M S` with NO zero-padding (so
|
||||||
|
`NetConfig5212025121420` = 5/21/2025 12:14:20). Because the components are
|
||||||
|
un-padded the dir NAME is NOT lexically sortable across months/hours, so we do
|
||||||
|
NOT sort on it. WHO + DATE come from the snapshot's NetConfig PROLOGUE
|
||||||
|
(`who:` / `date:`, TAB-separated, leading-space-indented; the block is
|
||||||
|
`prologue … end_prologue`). The `date:` string (`Month DD, YYYY H:MM:SS AM/PM
|
||||||
|
TZ`) is parsed into a sortable key `YYYYMMDDHHMMSS` so the timeline is in true
|
||||||
|
chronological order. The live `<site>/NetConfig` is included last as
|
||||||
|
`(current)`; a secondary key (snapshot dir-timestamp; `~` sentinel for live)
|
||||||
|
breaks ties when two saves share the same last-editor prologue stamp.
|
||||||
|
|
||||||
|
- **Scoped, not whole-file.** The diff/summary is reduced to the relevant
|
||||||
|
protocol-block(s) via `nc-parse.sh` (single thread, or every name-matching
|
||||||
|
thread for `--system`), so it is NOT the whole 10k-line NetConfig unless
|
||||||
|
`--site` (whole-NetConfig scope).
|
||||||
|
|
||||||
|
- **Flags.** `--format timeline` (default) = plain-text who/when/summary, one
|
||||||
|
revision per block, summary counting routing threads added/removed/modified
|
||||||
|
in scope between consecutive revisions; `--format diff` = the scoped unified
|
||||||
|
diff between consecutive revisions, one block per transition. `--limit N`
|
||||||
|
(most-recent N), `--since <date>` (YYYY-MM-DD or YYYYMMDD). Invocation:
|
||||||
|
`nc-revisions <thread>` (defaults to `$HCISITE`), `<thread>.<site>`,
|
||||||
|
`<site>/<thread>`, `--system <pat> [--site S]`, or `--site <site>`.
|
||||||
|
|
||||||
|
- **Plain text + control-byte safety.** Output matches the v0.8.24/26 OneNote
|
||||||
|
conventions: no markdown. Timeline runs through `_sanitize_ctl`
|
||||||
|
(unconditional strip — human-readable artifact); `--format diff` through the
|
||||||
|
tty-gated `_sanitize_ctl_tty` (raw, byte-identical on a pipe/redirect so a
|
||||||
|
downstream consumer of the diff sees exact bytes). `--help` sed range stops at
|
||||||
|
the end of the comment header (no code leak).
|
||||||
|
|
||||||
|
- **Wired into `larry.sh`.** Added to the `tools` registry, the `tool_nc_revisions`
|
||||||
|
dispatcher, the `execute_tool` case, and a full `nc_revisions` LLM-tool schema
|
||||||
|
mirroring the other NetConfig tools.
|
||||||
|
|
||||||
|
Proved on the real test integrator (`HCIROOT=/tmp/clvf_realtest/integrator`):
|
||||||
|
`nc-revisions ADTto_uds.ancout2` rendered an 8-revision timeline spanning
|
||||||
|
Apr 2024 → Nov 2025 (correctly ordered across months despite un-padded dir
|
||||||
|
names), surfacing a `~1 modified` host change and the eventual `-1 removed` in
|
||||||
|
the live config; `--format diff` showed the scoped one-line `{ HOST 10.33.176.5 }
|
||||||
|
→ { HOST 10.34.48.11 }` change. `--system codametrix --site ancout --format diff`
|
||||||
|
showed `ADTto_CodaMetrix` added then its `{ PORT 8000 } → { PORT 39500 }` and
|
||||||
|
inbound-archive (`INFILE`/`INSAVE`) edits by BRYJOHN. `bash -n` clean on all
|
||||||
|
touched files; MANIFEST regenerated and `--check` passes.
|
||||||
|
|
||||||
## v0.8.26 — 2026-05-28
|
## v0.8.26 — 2026-05-28
|
||||||
|
|
||||||
**★ HARDENING: extend the v0.8.25 control-byte sanitize across the whole tool
|
**★ HARDENING: extend the v0.8.25 control-byte sanitize across the whole tool
|
||||||
|
|||||||
7
MANIFEST
7
MANIFEST
@ -23,16 +23,16 @@
|
|||||||
# scripts/make-manifest.sh and bump VERSION.
|
# scripts/make-manifest.sh and bump VERSION.
|
||||||
|
|
||||||
# Top-level scripts
|
# Top-level scripts
|
||||||
larry.sh 7fccca0d10a0a742d66efd21da703d780c8359411995cf69925123575b14321c
|
larry.sh 4de38e7e33507a8c8fde539cb29c031eabbec508674bf51dd9f441e536a76509
|
||||||
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
||||||
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
||||||
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
||||||
install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423
|
install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
VERSION 6520a3a0746d8a2969ca4c76db2109929b36882541fcdbe3fb6de1718903d97f
|
VERSION 0ba962133bcfa080a96ec6b746ae1a4eb7816576618aa6c88d2c14506258715b
|
||||||
MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde
|
MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde
|
||||||
CHANGELOG.md 0d7a88d389d6723ee2dd289e5d143d4ada8f232ca0df43c392be9bca856f70b6
|
CHANGELOG.md 3506542eac6c71d4cb29391b234f3fd30aec3853a5f5122b5bceb73d11079f46
|
||||||
|
|
||||||
# Agent personas (system-prompt overlays)
|
# Agent personas (system-prompt overlays)
|
||||||
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
|
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
|
||||||
@ -103,6 +103,7 @@ lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a9
|
|||||||
lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79
|
lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79
|
||||||
lib/nc-msgs.sh 20517922d1153ec7827c833987497fb305d087b579911d1b9067d65ae156a19f
|
lib/nc-msgs.sh 20517922d1153ec7827c833987497fb305d087b579911d1b9067d65ae156a19f
|
||||||
lib/nc-document.sh 47211e99089c0446d25a1e84545a734894720a1c9ad8f59b920332035e4ea880
|
lib/nc-document.sh 47211e99089c0446d25a1e84545a734894720a1c9ad8f59b920332035e4ea880
|
||||||
|
lib/nc-revisions.sh c27856f7decfc4c2e2c990f59eb20136fdff9cf0a52b9d9fbd9370613666a802
|
||||||
lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324
|
lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324
|
||||||
lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f
|
lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f
|
||||||
lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a
|
lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a
|
||||||
|
|||||||
44
larry.sh
44
larry.sh
@ -78,7 +78,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.8.26"
|
LARRY_VERSION="0.8.27"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -355,6 +355,7 @@ nc-make-jump.sh|Generate the 3-thread "jump" pattern for cross-environment data
|
|||||||
nc-tclgen.sh|Generate annotated TCL UPOC scaffolding (skeletons for common Cloverleaf proc patterns)
|
nc-tclgen.sh|Generate annotated TCL UPOC scaffolding (skeletons for common Cloverleaf proc patterns)
|
||||||
nc-document.sh|Generate a markdown knowledge entry documenting a Cloverleaf subsystem/interface
|
nc-document.sh|Generate a markdown knowledge entry documenting a Cloverleaf subsystem/interface
|
||||||
#Diff & regression
|
#Diff & regression
|
||||||
|
nc-revisions.sh|NetConfig change-history / revision-diff: how a thread, system, or whole site changed over time across Cloverleaf's NetConfig revision snapshots, annotated with WHO saved each revision and WHEN (from the prologue). Default plain-text timeline; --format diff for the scoped unified diff between consecutive revisions. Usage: nc-revisions <thread>[.<site>] | --system <pat> [--site S] | --site <site> [--format timeline|diff] [--limit N] [--since DATE]
|
||||||
nc-diff-interface.sh|Diff one Cloverleaf interface across two environments
|
nc-diff-interface.sh|Diff one Cloverleaf interface across two environments
|
||||||
nc-smat-diff.sh|Diff smat (message-archive) content across two environments
|
nc-smat-diff.sh|Diff smat (message-archive) content across two environments
|
||||||
nc-regression.sh|End-to-end regression test orchestrator between two Cloverleaf environments
|
nc-regression.sh|End-to-end regression test orchestrator between two Cloverleaf environments
|
||||||
@ -4114,6 +4115,43 @@ tool_nc_document() {
|
|||||||
"$LARRY_LIB_DIR/nc-document.sh" "${args[@]}" 2>&1
|
"$LARRY_LIB_DIR/nc-document.sh" "${args[@]}" 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# nc_revisions — NetConfig change-history / revision-diff. Diffs Cloverleaf's own
|
||||||
|
# per-save NetConfig revision SNAPSHOTS (under $HCIROOT/<site>/revisions/) for the
|
||||||
|
# requested scope — a single thread, a multi-thread system (case-insensitive name
|
||||||
|
# match), or the whole site — and annotates each revision with WHO saved it and
|
||||||
|
# WHEN (parsed from the NetConfig prologue). Deterministic, API-free. Default
|
||||||
|
# format=timeline (plain-text who/when/summary, one revision per block);
|
||||||
|
# format=diff emits the scoped unified diff between consecutive revisions. The
|
||||||
|
# scope is reduced to the relevant protocol-block(s) via nc-parse.sh so the diff
|
||||||
|
# is NOT the whole 10k-line NetConfig unless --site. --limit keeps the most-recent
|
||||||
|
# N; --since filters by date (YYYY-MM-DD or YYYYMMDD).
|
||||||
|
tool_nc_revisions() {
|
||||||
|
local thread="$1" system="$2" site="$3" fmt="${4:-timeline}" limit="${5:-0}"
|
||||||
|
local since="${6:-}" hciroot="${7:-${HCIROOT:-}}"
|
||||||
|
_lib_err_if_missing || return
|
||||||
|
[ -n "$thread" ] || [ -n "$system" ] || [ -n "$site" ] \
|
||||||
|
|| { echo "ERROR: nc_revisions needs one of: thread, system (+site), or site"; return 1; }
|
||||||
|
local args=()
|
||||||
|
if [ -n "$thread" ]; then
|
||||||
|
args+=(--thread "$thread")
|
||||||
|
[ -n "$site" ] && args+=(--site "$site")
|
||||||
|
elif [ -n "$system" ]; then
|
||||||
|
args+=(--system "$system")
|
||||||
|
[ -n "$site" ] && args+=(--site "$site")
|
||||||
|
else
|
||||||
|
args+=(--site "$site")
|
||||||
|
fi
|
||||||
|
case "$fmt" in
|
||||||
|
timeline|diff) args+=(--format "$fmt") ;;
|
||||||
|
"") args+=(--format timeline) ;;
|
||||||
|
*) echo "ERROR: unknown nc_revisions format: $fmt (timeline|diff)"; return 1 ;;
|
||||||
|
esac
|
||||||
|
case "$limit" in ''|0) : ;; *[!0-9]*) : ;; *) args+=(--limit "$limit") ;; esac
|
||||||
|
[ -n "$since" ] && args+=(--since "$since")
|
||||||
|
[ -n "$hciroot" ] && args+=(--hciroot "$hciroot")
|
||||||
|
"$LARRY_LIB_DIR/nc-revisions.sh" "${args[@]}" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
tool_nc_diff_interface() {
|
tool_nc_diff_interface() {
|
||||||
local interface="$1" left="$2" right="$3" out_path="${4:-}" include_tables="${5:-0}"
|
local interface="$1" left="$2" right="$3" out_path="${4:-}" include_tables="${5:-0}"
|
||||||
local left_label="${6:-}" right_label="${7:-}" depth="${8:-1}"
|
local left_label="${6:-}" right_label="${7:-}" depth="${8:-1}"
|
||||||
@ -4188,6 +4226,9 @@ execute_tool() {
|
|||||||
"$(J '.notes // ""')" \
|
"$(J '.notes // ""')" \
|
||||||
"$(J '.onenote_table // 0' | sed "s/false/0/;s/true/1/")" \
|
"$(J '.onenote_table // 0' | sed "s/false/0/;s/true/1/")" \
|
||||||
"$(J '.raw_tcl // 0' | sed "s/false/0/;s/true/1/")" ;;
|
"$(J '.raw_tcl // 0' | sed "s/false/0/;s/true/1/")" ;;
|
||||||
|
nc_revisions) tool_nc_revisions "$(J '.thread // ""')" "$(J '.system // ""')" "$(J '.site // ""')" \
|
||||||
|
"$(J '.format // "timeline"')" "$(J '.limit // 0')" \
|
||||||
|
"$(J '.since // ""')" "$(J '.hciroot // ""')" ;;
|
||||||
nc_find) tool_nc_find "$(J '.mode')" "$(J '.query')" "$(J '.format // "table"')" "$(J '.hciroot // ""')" ;;
|
nc_find) tool_nc_find "$(J '.mode')" "$(J '.query')" "$(J '.format // "table"')" "$(J '.hciroot // ""')" ;;
|
||||||
nc_insert_protocol) tool_nc_insert_protocol "$(J '.netconfig')" "$(J '.block')" "$(J '.mode // "end"')" "$(J '.anchor // ""')" ;;
|
nc_insert_protocol) tool_nc_insert_protocol "$(J '.netconfig')" "$(J '.block')" "$(J '.mode // "end"')" "$(J '.anchor // ""')" ;;
|
||||||
nc_add_route) tool_nc_add_route "$(J '.netconfig')" "$(J '.protocol_name')" "$(J '.route')" ;;
|
nc_add_route) tool_nc_add_route "$(J '.netconfig')" "$(J '.protocol_name')" "$(J '.route')" ;;
|
||||||
@ -4244,6 +4285,7 @@ TOOLS_JSON=$(cat <<'TOOLS_END'
|
|||||||
{"name":"nc_msgs","description":"Query Cloverleaf smat (SQLite!) databases for messages from a thread. Filters: time range, exact HL7 field match. Native v3 — reads smatdb directly with sqlite3 -ascii, no hcidbdump/dbExtract needed. Format text shows messages line-by-line with metadata; count returns just the count; json returns structured data. Operates on LOCAL smatdbs; for a remote env's smatdb, use ssh_pull_smat first (sampled mode is cheaper than pulling the whole DB).","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"Thread name. The .smatdb file under $HCISITEDIR/exec/processes/*/<thread>.smatdb is auto-located unless db is given."},"after":{"type":"string","description":"Time-after filter. Accepts \"3 days ago\", \"2026-05-20 14:30:00\", \"2026-05-20\", or a unix timestamp."},"before":{"type":"string","description":"Time-before filter, same formats as after."},"field":{"type":"string","description":"HL7 field path for exact-match filter, e.g. PID.18 or MSH.10."},"value":{"type":"string","description":"Value the field must equal. Use with field. Repeatable filters not supported via this single tool call — chain calls if you need multi-field AND."},"limit":{"type":"integer","description":"Max messages to return. Default 10."},"format":{"type":"string","enum":["text","json","count","raw"],"description":"text = human-readable with metadata; count = just the number; json = structured; raw = raw bytes separated by 0x1c."},"sitedir":{"type":"string","description":"Override $HCISITEDIR for thread-to-db location."},"db":{"type":"string","description":"Explicit .smatdb path; overrides auto-locate."}},"required":["thread"]}},
|
{"name":"nc_msgs","description":"Query Cloverleaf smat (SQLite!) databases for messages from a thread. Filters: time range, exact HL7 field match. Native v3 — reads smatdb directly with sqlite3 -ascii, no hcidbdump/dbExtract needed. Format text shows messages line-by-line with metadata; count returns just the count; json returns structured data. Operates on LOCAL smatdbs; for a remote env's smatdb, use ssh_pull_smat first (sampled mode is cheaper than pulling the whole DB).","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"Thread name. The .smatdb file under $HCISITEDIR/exec/processes/*/<thread>.smatdb is auto-located unless db is given."},"after":{"type":"string","description":"Time-after filter. Accepts \"3 days ago\", \"2026-05-20 14:30:00\", \"2026-05-20\", or a unix timestamp."},"before":{"type":"string","description":"Time-before filter, same formats as after."},"field":{"type":"string","description":"HL7 field path for exact-match filter, e.g. PID.18 or MSH.10."},"value":{"type":"string","description":"Value the field must equal. Use with field. Repeatable filters not supported via this single tool call — chain calls if you need multi-field AND."},"limit":{"type":"integer","description":"Max messages to return. Default 10."},"format":{"type":"string","enum":["text","json","count","raw"],"description":"text = human-readable with metadata; count = just the number; json = structured; raw = raw bytes separated by 0x1c."},"sitedir":{"type":"string","description":"Override $HCISITEDIR for thread-to-db location."},"db":{"type":"string","description":"Explicit .smatdb path; overrides auto-locate."}},"required":["thread"]}},
|
||||||
{"name":"nc_document","description":"Document a Cloverleaf INTERFACE end-to-end as a PLAIN-TEXT knowledge entry in Bryan's confirmed Legacy 'ADT Messages' template. OUTPUT IS PLAIN TEXT BY DEFAULT (no markdown) so it pastes cleanly into OneNote, which does NOT render markdown: UPPERCASE headings underlined with dashes, no bold, no backticks, no pipe tables, no '---' rules. Structure: Title; Description prose; Message Flow (one hop per record: Epic-feed → Cloverleaf-routing → Final-Delivery, built from the nc_paths route chain); per-delivery breakdown (inbound PROTOCOL TYPE/HOST/PORT/ISSERVER + inbound TRXID/TPS proc, route TRXID filter + TYPE + PREPROCS/POSTPROCS, XLATE, destination host:port/process). The two tabular sections (Message Flow, Delivery breakdown) render as INDENTED label:value blocks by default (read in any font, zero setup); set onenote_table=true to render them instead as TAB-separated rows (header + one row per record, no leading/trailing pipes) for paste-into-OneNote → Insert > Table. EVERYTHING THIS TOOL EMITS IS DETERMINISTIC, PURE BASH, AND API-FREE — it runs identically on an API-blocked host and never calls a model. ★ KEY FEATURE — for every referenced UPOC proc it locates the .tcl under $HCIROOT/<site>/tclprocs/ and DETERMINISTICALLY surfaces, INLINE in the Description, a compact bit-line: the proc's COMMENTS (the author's own filter notes), the HL7 FIELDS it references (PID.8, PV1.45, EVN.1…), the MATCHED literal event codes (A01 A02 A03…), TABLE lookups (e.g. PeriCalm_Loc), and the DISPOSITION (pass vs kill). Those inline bits are ALWAYS ON. The raw proc TCL appendix is OPT-IN via raw_tcl=true (off by default). ★ WHEN YOU (the model, running WITH the API) GET THIS TOOL'S OUTPUT: transparently POLISH those surfaced UPOC bits into smoother, human-readable filter descriptions inside the Description prose (e.g. turn 'fields: PV1.45 PID.8 · matches: A01 A02 A03 · table: Pericalm_Loc · disposition: kill non-matching' into 'passes only A01/A02/A03 admit/transfer events for female patients whose location is in the Pericalm_Loc table, killing everything else'). Do NOT invent facts not present in the surfaced bits; just smooth them. On an API-blocked host the deterministic bit-lines are the deliverable. MODES: (a) SINGLE INTERFACE — set thread (the delivery/outbound thread, e.g. 'ADTto_CodaMetrix'; optionally site to disambiguate) → one fully-detailed interface section. (b) SYSTEM/PATTERN — set name (case-insensitive substring/regex, e.g. 'codametrix') → one section per matching delivery thread across ALL sites. Give EXACTLY ONE of thread or name. Returns the doc text and (if out is given) writes it there.","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"SINGLE-INTERFACE mode: the delivery (outbound) thread to document, e.g. 'ADTto_CodaMetrix' or 'ADTto_periwatch'. Accepts a bare thread name or a 'site/thread' node. Give this OR name, not both."},"name":{"type":"string","description":"SYSTEM mode: case-insensitive substring/regex matching delivery thread names across all sites, e.g. 'codametrix', 'periwatch', 'epic_adt'. One section per match. Give this OR thread, not both."},"site":{"type":"string","description":"Home site of the thread (the NetConfig's parent dir). Optional — disambiguates a thread name present in multiple sites."},"out":{"type":"string","description":"Optional output file path. Convention: $LARRY_HOME/knowledge/<system>.txt (plain text)."},"hciroot":{"type":"string","description":"Override $HCIROOT for the NetConfig scan."},"title":{"type":"string","description":"Doc title. Default derived from thread/name."},"onenote_table":{"type":"boolean","description":"Render the tabular sections (Message Flow, Delivery breakdown) as TAB-separated rows (header + one data row per record, real tabs, no leading/trailing pipes) for paste-into-OneNote → Insert > Table. Default false = indented label:value blocks. Non-tabular sections stay plain text either way."},"raw_tcl":{"type":"boolean","description":"Also emit the raw proc-source appendix (verbatim TCL of every referenced UPOC proc). Default false — the readable extracted UPOC bits stay inline in each description regardless; only the verbatim appendix is gated behind this."},"status":{"type":"string","description":"System status fill-in (production/test/decommissioning/...)."},"poc_internal":{"type":"string","description":"Internal owner fill-in."},"poc_vendor":{"type":"string","description":"Vendor POC fill-in."},"escalation":{"type":"string","description":"Escalation path fill-in."},"open_items":{"type":"string","description":"Open items / known issues fill-in. Can be multi-line, will be inserted as-is."},"notes":{"type":"string","description":"Freeform notes fill-in."}},"required":[]}},
|
{"name":"nc_document","description":"Document a Cloverleaf INTERFACE end-to-end as a PLAIN-TEXT knowledge entry in Bryan's confirmed Legacy 'ADT Messages' template. OUTPUT IS PLAIN TEXT BY DEFAULT (no markdown) so it pastes cleanly into OneNote, which does NOT render markdown: UPPERCASE headings underlined with dashes, no bold, no backticks, no pipe tables, no '---' rules. Structure: Title; Description prose; Message Flow (one hop per record: Epic-feed → Cloverleaf-routing → Final-Delivery, built from the nc_paths route chain); per-delivery breakdown (inbound PROTOCOL TYPE/HOST/PORT/ISSERVER + inbound TRXID/TPS proc, route TRXID filter + TYPE + PREPROCS/POSTPROCS, XLATE, destination host:port/process). The two tabular sections (Message Flow, Delivery breakdown) render as INDENTED label:value blocks by default (read in any font, zero setup); set onenote_table=true to render them instead as TAB-separated rows (header + one row per record, no leading/trailing pipes) for paste-into-OneNote → Insert > Table. EVERYTHING THIS TOOL EMITS IS DETERMINISTIC, PURE BASH, AND API-FREE — it runs identically on an API-blocked host and never calls a model. ★ KEY FEATURE — for every referenced UPOC proc it locates the .tcl under $HCIROOT/<site>/tclprocs/ and DETERMINISTICALLY surfaces, INLINE in the Description, a compact bit-line: the proc's COMMENTS (the author's own filter notes), the HL7 FIELDS it references (PID.8, PV1.45, EVN.1…), the MATCHED literal event codes (A01 A02 A03…), TABLE lookups (e.g. PeriCalm_Loc), and the DISPOSITION (pass vs kill). Those inline bits are ALWAYS ON. The raw proc TCL appendix is OPT-IN via raw_tcl=true (off by default). ★ WHEN YOU (the model, running WITH the API) GET THIS TOOL'S OUTPUT: transparently POLISH those surfaced UPOC bits into smoother, human-readable filter descriptions inside the Description prose (e.g. turn 'fields: PV1.45 PID.8 · matches: A01 A02 A03 · table: Pericalm_Loc · disposition: kill non-matching' into 'passes only A01/A02/A03 admit/transfer events for female patients whose location is in the Pericalm_Loc table, killing everything else'). Do NOT invent facts not present in the surfaced bits; just smooth them. On an API-blocked host the deterministic bit-lines are the deliverable. MODES: (a) SINGLE INTERFACE — set thread (the delivery/outbound thread, e.g. 'ADTto_CodaMetrix'; optionally site to disambiguate) → one fully-detailed interface section. (b) SYSTEM/PATTERN — set name (case-insensitive substring/regex, e.g. 'codametrix') → one section per matching delivery thread across ALL sites. Give EXACTLY ONE of thread or name. Returns the doc text and (if out is given) writes it there.","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"SINGLE-INTERFACE mode: the delivery (outbound) thread to document, e.g. 'ADTto_CodaMetrix' or 'ADTto_periwatch'. Accepts a bare thread name or a 'site/thread' node. Give this OR name, not both."},"name":{"type":"string","description":"SYSTEM mode: case-insensitive substring/regex matching delivery thread names across all sites, e.g. 'codametrix', 'periwatch', 'epic_adt'. One section per match. Give this OR thread, not both."},"site":{"type":"string","description":"Home site of the thread (the NetConfig's parent dir). Optional — disambiguates a thread name present in multiple sites."},"out":{"type":"string","description":"Optional output file path. Convention: $LARRY_HOME/knowledge/<system>.txt (plain text)."},"hciroot":{"type":"string","description":"Override $HCIROOT for the NetConfig scan."},"title":{"type":"string","description":"Doc title. Default derived from thread/name."},"onenote_table":{"type":"boolean","description":"Render the tabular sections (Message Flow, Delivery breakdown) as TAB-separated rows (header + one data row per record, real tabs, no leading/trailing pipes) for paste-into-OneNote → Insert > Table. Default false = indented label:value blocks. Non-tabular sections stay plain text either way."},"raw_tcl":{"type":"boolean","description":"Also emit the raw proc-source appendix (verbatim TCL of every referenced UPOC proc). Default false — the readable extracted UPOC bits stay inline in each description regardless; only the verbatim appendix is gated behind this."},"status":{"type":"string","description":"System status fill-in (production/test/decommissioning/...)."},"poc_internal":{"type":"string","description":"Internal owner fill-in."},"poc_vendor":{"type":"string","description":"Vendor POC fill-in."},"escalation":{"type":"string","description":"Escalation path fill-in."},"open_items":{"type":"string","description":"Open items / known issues fill-in. Can be multi-line, will be inserted as-is."},"notes":{"type":"string","description":"Freeform notes fill-in."}},"required":[]}},
|
||||||
|
|
||||||
|
{"name":"nc_revisions","description":"NetConfig CHANGE-HISTORY / REVISION-DIFF tracer. Shows how a THREAD, a multi-thread SYSTEM, or a WHOLE SITE changed over time by diffing Cloverleaf's own per-save NetConfig revision SNAPSHOTS (the dirs Cloverleaf writes under $HCIROOT/<site>/revisions/NetConfig<timestamp>/, each holding a full NetConfig), annotated with WHO saved each revision and WHEN — parsed from each snapshot's NetConfig prologue (who: / date:). USE THIS — not grep_files / read_file / bash_exec over the revisions dir — for ANY of: 'who changed X and when', 'what changed in this thread/site over time', 'revision history', 'when did the port/host change', 'show me the change history', 'diff the revisions', 'audit the NetConfig edits'. DETERMINISTIC, PURE BASH, API-FREE — runs identically on an API-blocked host. Revisions are ordered chronologically by the prologue date (NOT by the un-padded dir name, which is not lexically sortable); the LIVE NetConfig is included last as '(current)'. SCOPE is reduced to the relevant protocol-block(s) via nc-parse so the diff is NOT the whole 10k-line NetConfig unless site-wide. TWO FORMATS: format=timeline (DEFAULT) = a plain-text who/when/summary table, one revision per block, where the summary counts routing threads added/removed/modified in scope between consecutive revisions; format=diff = the actual unified diff of the scoped NetConfig section between consecutive revisions (oldest→newest), one block per transition. MODES (give exactly one scope): (a) single THREAD — set thread (a bare name resolved in site / $HCISITE, or 'thread.site', or a 'site/thread' node); (b) SYSTEM — set system (case-insensitive name pattern, e.g. 'codametrix') plus site, one scoped diff per matching thread folded together; (c) whole SITE — set ONLY site (omit thread/system) to track the entire NetConfig. --limit keeps the most-recent N revisions; --since filters to revisions on/after a date (YYYY-MM-DD or YYYYMMDD). Resolves the site under $HCIROOT (or pass hciroot).","input_schema":{"type":"object","properties":{"thread":{"type":"string","description":"SINGLE-THREAD mode: the thread/protocol to track, e.g. 'ADTto_CodaMetrix'. Accepts a bare name (resolved in site or $HCISITE), 'thread.site', or a 'site/thread' node. Give this OR system OR site-only."},"system":{"type":"string","description":"SYSTEM mode: case-insensitive name pattern matching one or more threads (e.g. 'codametrix', 'periwatch'); the scoped diff folds every matching thread's block together. Pair with site."},"site":{"type":"string","description":"The site (NetConfig's parent dir / revisions dir owner). Required for system mode and for whole-site mode (set site and OMIT thread/system to track the entire NetConfig). For single-thread mode it disambiguates / overrides $HCISITE."},"format":{"type":"string","enum":["timeline","diff"],"description":"timeline (default) = plain-text who/when/summary table, one revision per block. diff = the scoped unified diff between consecutive revisions, one block per transition."},"limit":{"type":"integer","description":"Only the most-recent N revisions. 0/omitted = all."},"since":{"type":"string","description":"Only revisions on/after this date. YYYY-MM-DD or YYYYMMDD."},"hciroot":{"type":"string","description":"Override $HCIROOT for site/revisions discovery."}},"required":[]}},
|
||||||
{"name":"nc_find","description":"Cross-site thread search. Native v3 replacement for v1 tbn/tbp/tbh/tbpr/where. Walks every NetConfig under $HCIROOT and returns matching threads with site, port, host, process, direction, file, line. Modes: name=partial name match (like tbn); port=exact port (like tbp); host=substring on host (like tbh); process=substring on PROCESSNAME (like tbpr); where=exact name match across all sites (like the v1 `<thread> where`); xlate=threads referencing a specific .xlt; tclproc=threads referencing a specific TCL proc.","input_schema":{"type":"object","properties":{"mode":{"type":"string","enum":["name","port","host","process","where","xlate","tclproc"],"description":"Search mode."},"query":{"type":"string","description":"Query value: partial name, port number, host substring, process name, exact thread name, xlate filename, or tclproc name."},"format":{"type":"string","enum":["table","tsv","jsonl"],"description":"Output format. Default table."},"hciroot":{"type":"string","description":"Override $HCIROOT."}},"required":["mode","query"]}},
|
{"name":"nc_find","description":"Cross-site thread search. Native v3 replacement for v1 tbn/tbp/tbh/tbpr/where. Walks every NetConfig under $HCIROOT and returns matching threads with site, port, host, process, direction, file, line. Modes: name=partial name match (like tbn); port=exact port (like tbp); host=substring on host (like tbh); process=substring on PROCESSNAME (like tbpr); where=exact name match across all sites (like the v1 `<thread> where`); xlate=threads referencing a specific .xlt; tclproc=threads referencing a specific TCL proc.","input_schema":{"type":"object","properties":{"mode":{"type":"string","enum":["name","port","host","process","where","xlate","tclproc"],"description":"Search mode."},"query":{"type":"string","description":"Query value: partial name, port number, host substring, process name, exact thread name, xlate filename, or tclproc name."},"format":{"type":"string","enum":["table","tsv","jsonl"],"description":"Output format. Default table."},"hciroot":{"type":"string","description":"Override $HCIROOT."}},"required":["mode","query"]}},
|
||||||
{"name":"nc_insert_protocol","description":"Insert a new protocol block into a NetConfig file. ALL WRITES GO THROUGH THE JOURNAL — original is snapshotted, diff is saved, the file is atomically replaced. Use larry_rollback_list to view, larry-rollback.sh CLI to undo. mode=end appends; mode=after needs anchor=existing-protocol-name; mode=before needs anchor.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string","description":"Target NetConfig file path."},"block":{"type":"string","description":"The full protocol block text (starting with 'protocol NAME {' and ending with '}'). Get this from nc_make_jump output."},"mode":{"type":"string","enum":["end","after","before"],"description":"Insertion position. Default end."},"anchor":{"type":"string","description":"For mode=after|before: existing protocol name to position relative to."}},"required":["netconfig","block"]}},
|
{"name":"nc_insert_protocol","description":"Insert a new protocol block into a NetConfig file. ALL WRITES GO THROUGH THE JOURNAL — original is snapshotted, diff is saved, the file is atomically replaced. Use larry_rollback_list to view, larry-rollback.sh CLI to undo. mode=end appends; mode=after needs anchor=existing-protocol-name; mode=before needs anchor.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string","description":"Target NetConfig file path."},"block":{"type":"string","description":"The full protocol block text (starting with 'protocol NAME {' and ending with '}'). Get this from nc_make_jump output."},"mode":{"type":"string","enum":["end","after","before"],"description":"Insertion position. Default end."},"anchor":{"type":"string","description":"For mode=after|before: existing protocol name to position relative to."}},"required":["netconfig","block"]}},
|
||||||
{"name":"nc_add_route","description":"Splice a route entry into an existing protocol's DATAXLATE block. Used to add a new DEST to an inbound's routing (e.g. wiring the OLD inbound to also route to the new linux_<tag>_out jump thread). ALL WRITES GO THROUGH THE JOURNAL.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string"},"protocol_name":{"type":"string","description":"The existing protocol to modify."},"route":{"type":"string","description":"The route entry text (an inner `{ ... }` object with CACHEMSG, ROUTE_DETAILS, TRXID, etc.). Get from nc_make_jump's route_add output."}},"required":["netconfig","protocol_name","route"]}},
|
{"name":"nc_add_route","description":"Splice a route entry into an existing protocol's DATAXLATE block. Used to add a new DEST to an inbound's routing (e.g. wiring the OLD inbound to also route to the new linux_<tag>_out jump thread). ALL WRITES GO THROUGH THE JOURNAL.","input_schema":{"type":"object","properties":{"netconfig":{"type":"string"},"protocol_name":{"type":"string","description":"The existing protocol to modify."},"route":{"type":"string","description":"The route entry text (an inner `{ ... }` object with CACHEMSG, ROUTE_DETAILS, TRXID, etc.). Get from nc_make_jump's route_add output."}},"required":["netconfig","protocol_name","route"]}},
|
||||||
|
|||||||
470
lib/nc-revisions.sh
Executable file
470
lib/nc-revisions.sh
Executable file
@ -0,0 +1,470 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# nc-revisions.sh — NetConfig change-history / revision-diff tool for Larry-Anywhere v3.
|
||||||
|
#
|
||||||
|
# Shows how a THREAD (or a multi-thread SYSTEM, or a whole SITE) changed over
|
||||||
|
# time by diffing Cloverleaf's own NetConfig revision SNAPSHOTS, annotated with
|
||||||
|
# WHO saved each revision and WHEN. Deterministic, pure bash+awk, NO API, NO
|
||||||
|
# network — runs identically on an API-blocked host (e.g. Gundersen).
|
||||||
|
#
|
||||||
|
# HOW CLOVERLEAF STORES REVISIONS (verified on the real integrator):
|
||||||
|
# Every save snapshots the FULL NetConfig into a per-revision directory under
|
||||||
|
# $HCIROOT/<site>/revisions/NetConfig<TS>/
|
||||||
|
# where <TS> is the save timestamp rendered as M D YYYY H M S with NO zero
|
||||||
|
# padding (so `NetConfig5212025121420` = 5/21/2025 12:14:20 and
|
||||||
|
# `NetConfig1126202513418` = 11/26/2025 1:34:18). Because the components are
|
||||||
|
# un-padded the directory NAME is NOT lexically sortable across months/hours,
|
||||||
|
# so we DO NOT sort on it. Each snapshot dir contains a full `NetConfig` file
|
||||||
|
# whose PROLOGUE carries the authoritative author + human-readable date:
|
||||||
|
# prologue
|
||||||
|
# who: <USER>
|
||||||
|
# date: <Month DD, YYYY H:MM:SS AM/PM TZ>
|
||||||
|
# type: net
|
||||||
|
# version: 3.20
|
||||||
|
# end_prologue
|
||||||
|
# (who/date are TAB-separated, leading-space-indented). We parse the prologue
|
||||||
|
# `date:` into a sortable key YYYYMMDDHHMMSS so the timeline is in true
|
||||||
|
# chronological order regardless of the un-padded dir name. The LIVE
|
||||||
|
# $HCIROOT/<site>/NetConfig is included as the most-recent ("(current)") point.
|
||||||
|
#
|
||||||
|
# WHAT EACH ROW SUMMARISES: for the requested scope we extract the relevant
|
||||||
|
# protocol-block(s) from each revision via nc-parse.sh (so the diff is SCOPED —
|
||||||
|
# we never diff the whole 10k-line NetConfig unless --site) and report, between
|
||||||
|
# consecutive revisions, the count of routing threads ADDED / REMOVED / MODIFIED
|
||||||
|
# (a thread whose block body changed). --site widens the scope to the entire
|
||||||
|
# NetConfig (protocol set + per-thread body changes across the whole file).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# nc-revisions.sh <thread> thread in $HCISITE
|
||||||
|
# nc-revisions.sh <thread>.<site> a thread in a specific site
|
||||||
|
# nc-revisions.sh <site>/<thread> v1 node form (nc-paths output feeds in)
|
||||||
|
# nc-revisions.sh --system <pattern> [--site S] a multi-thread system
|
||||||
|
# (case-insensitive name match)
|
||||||
|
# nc-revisions.sh --site <site> the WHOLE NetConfig for one site
|
||||||
|
#
|
||||||
|
# Flags:
|
||||||
|
# --format timeline (DEFAULT) who/when/summary table, one revision per block,
|
||||||
|
# plain text (label:value, aligned) for OneNote paste.
|
||||||
|
# --format diff the actual unified diff of the scoped NetConfig section
|
||||||
|
# between CONSECUTIVE revisions (oldest→newest), one block
|
||||||
|
# per transition.
|
||||||
|
# --limit N only the last N revisions (most recent N).
|
||||||
|
# --since <date> only revisions on/after <date>. Accepts YYYY-MM-DD or
|
||||||
|
# YYYYMMDD (compared against the parsed sortable key).
|
||||||
|
# --thread NAME thread to track (alternative to the positional form).
|
||||||
|
# --system PATTERN multi-thread system mode (see above).
|
||||||
|
# --site NAME the site to operate in (whole-NetConfig scope on its own;
|
||||||
|
# or scopes --thread/--system to a specific site).
|
||||||
|
# --hciroot DIR override $HCIROOT for site discovery.
|
||||||
|
# -h | --help this help.
|
||||||
|
#
|
||||||
|
# PIPE-FIRST: timeline rows and diff blocks are greppable; one revision (or one
|
||||||
|
# transition) per logical block, no decorative noise. The whole output is run
|
||||||
|
# through the shared control-byte sanitiser (lib/cygwin-safe.sh): the timeline
|
||||||
|
# is a human-readable artifact so it strips UNCONDITIONALLY; --format diff is
|
||||||
|
# tty-gated (raw bytes pass through on a pipe/redirect so a downstream consumer
|
||||||
|
# sees byte-identical diff text).
|
||||||
|
#
|
||||||
|
# Exit codes: 0 OK, 1 usage error, 2 not found.
|
||||||
|
set -u
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
NC_SELF="$0"
|
||||||
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
||||||
|
NCP="$LIB_DIR/nc-parse.sh"
|
||||||
|
|
||||||
|
# Shared sanitisers (see lib/cygwin-safe.sh). _sanitize_ctl strips C0 control
|
||||||
|
# bytes unconditionally (timeline = human-readable); _sanitize_ctl_tty strips
|
||||||
|
# only when stdout is a terminal (diff to a pipe stays byte-identical). Degrade
|
||||||
|
# safe to raw passthrough if the lib is somehow unavailable.
|
||||||
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$LIB_DIR/cygwin-safe.sh"
|
||||||
|
else
|
||||||
|
_sanitize_ctl() { cat; }
|
||||||
|
_sanitize_ctl_tty() { cat; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
die() { printf 'nc-revisions: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Arg parsing
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
THREAD=""
|
||||||
|
SYSTEM=""
|
||||||
|
SITE_ARG=""
|
||||||
|
HCIROOT_OVERRIDE=""
|
||||||
|
FORMAT="timeline"
|
||||||
|
LIMIT=0
|
||||||
|
SINCE=""
|
||||||
|
WHOLE_SITE=0
|
||||||
|
POSITIONAL=()
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--thread) shift; THREAD="${1:-}" ;;
|
||||||
|
--system) shift; SYSTEM="${1:-}" ;;
|
||||||
|
--site) shift; SITE_ARG="${1:-}"; WHOLE_SITE=1 ;;
|
||||||
|
--hciroot) shift; HCIROOT_OVERRIDE="${1:-}" ;;
|
||||||
|
--format) shift; FORMAT="${1:-timeline}" ;;
|
||||||
|
--limit) shift; LIMIT="${1:-0}" ;;
|
||||||
|
--since) shift; SINCE="${1:-}" ;;
|
||||||
|
-h|--help) sed -n '2,67p' "$NC_SELF" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
--*) die "unknown flag: $1" ;;
|
||||||
|
*) POSITIONAL+=("$1") ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$FORMAT" in timeline|diff) ;; *) die "bad --format: $FORMAT (timeline|diff)" ;; esac
|
||||||
|
|
||||||
|
# coerce LIMIT to a clean int (CR-safe)
|
||||||
|
LIMIT=$(coerce_int "$LIMIT" 0 2>/dev/null || printf '%s' "$LIMIT")
|
||||||
|
case "$LIMIT" in ''|*[!0-9]*) LIMIT=0 ;; esac
|
||||||
|
|
||||||
|
# Positional shapes for a single thread:
|
||||||
|
# <thread> bare thread (site from --site / $HCISITE)
|
||||||
|
# <thread>.<site> thread.site (cross-site) — split on the LAST dot
|
||||||
|
# <site>/<thread> v1 node form (nc-paths output feeds back in)
|
||||||
|
# An explicit --thread / --system wins; a positional is only consumed when neither
|
||||||
|
# was given.
|
||||||
|
if [ -z "$THREAD" ] && [ -z "$SYSTEM" ] && [ "$WHOLE_SITE" = "0" ] && [ "${#POSITIONAL[@]}" -ge 1 ]; then
|
||||||
|
THREAD="${POSITIONAL[0]}"
|
||||||
|
fi
|
||||||
|
[ "${#POSITIONAL[@]}" -gt 1 ] && die "too many positional args: ${POSITIONAL[*]}"
|
||||||
|
|
||||||
|
# Resolve a site embedded in the thread token.
|
||||||
|
if [ -n "$THREAD" ]; then
|
||||||
|
case "$THREAD" in
|
||||||
|
*/*) # v1 node form site/thread (split on FIRST slash)
|
||||||
|
_s="${THREAD%%/*}"; _t="${THREAD#*/}"
|
||||||
|
if [ -n "$_s" ] && [ -n "$_t" ]; then THREAD="$_t"; [ -z "$SITE_ARG" ] && SITE_ARG="$_s"; fi ;;
|
||||||
|
*.*) # thread.site form (split on LAST dot)
|
||||||
|
_t="${THREAD%.*}"; _s="${THREAD##*.}"
|
||||||
|
if [ -n "$_t" ] && [ -n "$_s" ]; then THREAD="$_t"; [ -z "$SITE_ARG" ] && SITE_ARG="$_s"; fi ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Default the site to $HCISITE when nothing else named it.
|
||||||
|
if [ -z "$SITE_ARG" ]; then SITE_ARG="${HCISITE:-}"; fi
|
||||||
|
|
||||||
|
# Exactly one scope must be determinable.
|
||||||
|
if [ "$WHOLE_SITE" = "1" ] && [ -z "$THREAD" ] && [ -z "$SYSTEM" ]; then
|
||||||
|
: # whole-site scope; SITE_ARG holds the site
|
||||||
|
elif [ -n "$SYSTEM" ]; then
|
||||||
|
:
|
||||||
|
elif [ -n "$THREAD" ]; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
die "no scope given. Try: nc-revisions.sh <thread> | nc-revisions.sh --system <pat> [--site S] | nc-revisions.sh --site <site>"
|
||||||
|
fi
|
||||||
|
[ -n "$SITE_ARG" ] || die "no site resolvable (set \$HCISITE, pass <thread>.<site>, or --site <site>)"
|
||||||
|
|
||||||
|
ROOT="${HCIROOT_OVERRIDE:-${HCIROOT:-}}"
|
||||||
|
[ -n "$ROOT" ] || die "no \$HCIROOT and no --hciroot; pass one or set the env var"
|
||||||
|
[ -d "$ROOT" ] || die "hciroot not a directory: $ROOT"
|
||||||
|
|
||||||
|
SITE_DIR="$ROOT/$SITE_ARG"
|
||||||
|
[ -d "$SITE_DIR" ] || die "site dir not found: $SITE_DIR"
|
||||||
|
REV_DIR="$SITE_DIR/revisions"
|
||||||
|
LIVE_NC="$SITE_DIR/NetConfig"
|
||||||
|
|
||||||
|
US=$'\037' # field separator for the discovered-revisions table
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# PROLOGUE PARSE — extract who + date from a NetConfig's prologue block, and
|
||||||
|
# render a sortable key YYYYMMDDHHMMSS from the human-readable date string.
|
||||||
|
# The prologue (lines 1..end_prologue) carries TAB-separated `who:` / `date:`
|
||||||
|
# fields. Emits exactly: SORTKEY<US>WHO<US>HUMANDATE (one line), or nothing.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
_prologue_facts() { # netconfig_file
|
||||||
|
local nc="$1"
|
||||||
|
[ -f "$nc" ] || return 0
|
||||||
|
awk -v US="$US" '
|
||||||
|
BEGIN {
|
||||||
|
m["January"]="01"; m["February"]="02"; m["March"]="03"; m["April"]="04"
|
||||||
|
m["May"]="05"; m["June"]="06"; m["July"]="07"; m["August"]="08"
|
||||||
|
m["September"]="09"; m["October"]="10"; m["November"]="11"; m["December"]="12"
|
||||||
|
who=""; dh=""
|
||||||
|
}
|
||||||
|
NR==1 && $0 !~ /^prologue/ { exit } # not a prologue-led NetConfig
|
||||||
|
/^[[:space:]]*who:/ { line=$0; sub(/^[[:space:]]*who:[[:space:]]*/,"",line); who=line }
|
||||||
|
/^[[:space:]]*date:/ { line=$0; sub(/^[[:space:]]*date:[[:space:]]*/,"",line); dh=line }
|
||||||
|
/^end_prologue/ { exit }
|
||||||
|
END {
|
||||||
|
# dh looks like: November 26, 2025 1:04:18 PM PST
|
||||||
|
key="00000000000000"
|
||||||
|
if (dh != "") {
|
||||||
|
d=dh
|
||||||
|
gsub(/,/," ",d)
|
||||||
|
n=split(d, f, /[ \t]+/)
|
||||||
|
# f: [1]=Month [2]=Day [3]=Year [4]=H:MM:SS [5]=AM/PM [6]=TZ
|
||||||
|
mon = (f[1] in m) ? m[f[1]] : "00"
|
||||||
|
day = f[2]+0; if (day<10) day="0" day
|
||||||
|
yr = f[3]+0
|
||||||
|
split(f[4], t, ":")
|
||||||
|
hh=t[1]+0; mm=t[2]+0; ss=t[3]+0
|
||||||
|
ap = toupper(f[5])
|
||||||
|
if (ap=="PM" && hh<12) hh+=12
|
||||||
|
if (ap=="AM" && hh==12) hh=0
|
||||||
|
if (hh<10) hh="0" hh
|
||||||
|
if (mm<10) mm="0" mm
|
||||||
|
if (ss<10) ss="0" ss
|
||||||
|
if (yr>0 && mon!="00") key = sprintf("%04d%s%s%s%s%s", yr, mon, day, hh, mm, ss)
|
||||||
|
}
|
||||||
|
printf "%s%s%s%s%s\n", key, US, who, US, dh
|
||||||
|
}
|
||||||
|
' "$nc"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# REVISION DISCOVERY — build a chronologically-ordered list of (sortkey, who,
|
||||||
|
# humandate, label, netconfig-path) records, one per revision PLUS the live
|
||||||
|
# NetConfig. Written to $REVTBL, US-delimited, sorted ascending by sortkey.
|
||||||
|
# Honors --since and --limit (limit keeps the most-recent N).
|
||||||
|
#
|
||||||
|
# TIE-BREAK: the prologue `date:` is the LAST-EDITOR stamp, so two snapshots (and
|
||||||
|
# the live NetConfig copied from the newest snapshot) can share the SAME prologue
|
||||||
|
# date. To keep the order deterministic we carry a SECONDARY key, written as a
|
||||||
|
# leading sort-only column (stripped before $REVTBL):
|
||||||
|
# - snapshots → the dir-name digit string (the actual save timestamp), so two
|
||||||
|
# same-prologue snapshots still order by when they were saved.
|
||||||
|
# - live NetConfig → the sentinel `~~~~~~~~~~~~~~` (C-locale: `~` > any digit),
|
||||||
|
# so the CURRENT state always sorts LAST on a tie.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
REVTBL=$(mktemp)
|
||||||
|
trap 'rm -f "$REVTBL"' EXIT
|
||||||
|
|
||||||
|
_discover_revisions() {
|
||||||
|
local raw; raw=$(mktemp)
|
||||||
|
local d nc facts key who dh rest sec
|
||||||
|
# snapshot revisions
|
||||||
|
if [ -d "$REV_DIR" ]; then
|
||||||
|
for d in "$REV_DIR"/NetConfig*; do
|
||||||
|
[ -d "$d" ] || continue
|
||||||
|
nc="$d/NetConfig"
|
||||||
|
[ -f "$nc" ] || continue
|
||||||
|
facts=$(_prologue_facts "$nc")
|
||||||
|
[ -n "$facts" ] || continue
|
||||||
|
key="${facts%%$US*}"; rest="${facts#*$US}"
|
||||||
|
who="${rest%%$US*}"; dh="${rest#*$US}"
|
||||||
|
# secondary key = the dir-name digit run, zero-padded so a numeric save
|
||||||
|
# timestamp orders correctly as a string.
|
||||||
|
sec=$(basename "$d" | tr -cd '0-9'); sec=$(printf '%014d' "${sec:-0}" 2>/dev/null || printf '%s' "$sec")
|
||||||
|
printf '%s%s%s%s%s%s%s%s%s%s%s\n' \
|
||||||
|
"$key" "$US" "$sec" "$US" "$who" "$US" "$dh" "$US" "$(basename "$d")" "$US" "$nc" >> "$raw"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
# the live (current) NetConfig as the newest point — its secondary key is the
|
||||||
|
# `~` sentinel so it always sorts AFTER a same-prologue-date snapshot.
|
||||||
|
if [ -f "$LIVE_NC" ]; then
|
||||||
|
facts=$(_prologue_facts "$LIVE_NC")
|
||||||
|
if [ -n "$facts" ]; then
|
||||||
|
key="${facts%%$US*}"; rest="${facts#*$US}"
|
||||||
|
who="${rest%%$US*}"; dh="${rest#*$US}"
|
||||||
|
printf '%s%s%s%s%s%s%s%s%s%s%s\n' \
|
||||||
|
"$key" "$US" "~~~~~~~~~~~~~~" "$US" "$who" "$US" "$dh" "$US" "(current)" "$US" "$LIVE_NC" >> "$raw"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --since filter: normalise YYYY-MM-DD / YYYYMMDD to an 8-digit prefix and
|
||||||
|
# compare against the sortkey's leading 8 digits (the date portion).
|
||||||
|
local since8=""
|
||||||
|
if [ -n "$SINCE" ]; then
|
||||||
|
since8=$(printf '%s' "$SINCE" | tr -cd '0-9')
|
||||||
|
since8="${since8:0:8}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# sort ascending by (sortkey, secondary), apply --since, then drop the secondary
|
||||||
|
# column so $REVTBL is the clean 5-field record: key|who|dh|label|nc.
|
||||||
|
LC_ALL=C sort -t"$US" -k1,1 -k2,2 "$raw" \
|
||||||
|
| awk -F"$US" -v US="$US" -v since8="$since8" '
|
||||||
|
{ if (since8=="" || substr($1,1,8) >= since8)
|
||||||
|
print $1 US $3 US $4 US $5 US $6 }
|
||||||
|
' > "$raw.sorted"
|
||||||
|
rm -f "$raw"
|
||||||
|
|
||||||
|
if [ "$LIMIT" -gt 0 ]; then
|
||||||
|
# keep the most-recent LIMIT (tail), preserving ascending order
|
||||||
|
tail -n "$LIMIT" "$raw.sorted" > "$REVTBL"
|
||||||
|
else
|
||||||
|
cp "$raw.sorted" "$REVTBL"
|
||||||
|
fi
|
||||||
|
rm -f "$raw.sorted"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# SCOPED EXTRACTION — emit the relevant NetConfig text for ONE revision so the
|
||||||
|
# diff/summary is scoped to the requested thread/system (NOT the whole file)
|
||||||
|
# unless --site. The extraction is deterministic via nc-parse.sh.
|
||||||
|
# _scoped_text <netconfig> → the protocol-block(s) in scope (or whole file).
|
||||||
|
# For --system we concatenate every matching protocol's block (sorted by name)
|
||||||
|
# so a thread added/removed between revisions shows up as a block appearing/
|
||||||
|
# disappearing in the scoped text.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
_scope_threads() { # netconfig → the in-scope thread names, sorted unique
|
||||||
|
local nc="$1"
|
||||||
|
if [ "$WHOLE_SITE" = "1" ] && [ -z "$THREAD" ] && [ -z "$SYSTEM" ]; then
|
||||||
|
"$NCP" list-protocols "$nc" 2>/dev/null | LC_ALL=C sort -u
|
||||||
|
elif [ -n "$SYSTEM" ]; then
|
||||||
|
"$NCP" list-protocols "$nc" 2>/dev/null | grep -iE -- "$SYSTEM" 2>/dev/null | LC_ALL=C sort -u
|
||||||
|
else
|
||||||
|
# single thread (only if present in this revision)
|
||||||
|
"$NCP" list-protocols "$nc" 2>/dev/null | grep -xF -- "$THREAD"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_scoped_text() { # netconfig → scoped NetConfig text
|
||||||
|
local nc="$1"
|
||||||
|
if [ "$WHOLE_SITE" = "1" ] && [ -z "$THREAD" ] && [ -z "$SYSTEM" ]; then
|
||||||
|
cat "$nc"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local t
|
||||||
|
while IFS= read -r t; do
|
||||||
|
[ -z "$t" ] && continue
|
||||||
|
printf 'protocol %s {\n' "$t"
|
||||||
|
"$NCP" protocol-block "$nc" "$t" 2>/dev/null | sed '1d' # block already starts with the header; re-emit a stable header
|
||||||
|
done < <(_scope_threads "$nc")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Summarise the change between two revisions' in-scope thread SETS + bodies.
|
||||||
|
# Emits a compact phrase: "+N added, -N removed, ~N modified (names…)".
|
||||||
|
# Pure: compares the thread name sets and per-thread block bodies.
|
||||||
|
_summary_between() { # nc_old nc_new
|
||||||
|
local a="$1" b="$2"
|
||||||
|
local ta tb
|
||||||
|
ta=$(mktemp); tb=$(mktemp)
|
||||||
|
_scope_threads "$a" > "$ta"
|
||||||
|
_scope_threads "$b" > "$tb"
|
||||||
|
|
||||||
|
local added removed common t
|
||||||
|
added=$(LC_ALL=C comm -13 "$ta" "$tb")
|
||||||
|
removed=$(LC_ALL=C comm -23 "$ta" "$tb")
|
||||||
|
common=$(LC_ALL=C comm -12 "$ta" "$tb")
|
||||||
|
rm -f "$ta" "$tb"
|
||||||
|
|
||||||
|
local n_add n_rem n_mod=0 mod_names=""
|
||||||
|
n_add=$(printf '%s\n' "$added" | grep -c . 2>/dev/null); [ -z "$n_add" ] && n_add=0
|
||||||
|
n_rem=$(printf '%s\n' "$removed" | grep -c . 2>/dev/null); [ -z "$n_rem" ] && n_rem=0
|
||||||
|
[ -z "$added" ] && n_add=0
|
||||||
|
[ -z "$removed" ] && n_rem=0
|
||||||
|
|
||||||
|
# modified = common thread whose block body differs
|
||||||
|
while IFS= read -r t; do
|
||||||
|
[ -z "$t" ] && continue
|
||||||
|
if ! diff -q <("$NCP" protocol-block "$a" "$t" 2>/dev/null) \
|
||||||
|
<("$NCP" protocol-block "$b" "$t" 2>/dev/null) >/dev/null 2>&1; then
|
||||||
|
n_mod=$((n_mod+1))
|
||||||
|
mod_names="${mod_names:+$mod_names }$t"
|
||||||
|
fi
|
||||||
|
done <<< "$common"
|
||||||
|
|
||||||
|
# detail names (capped so the row stays one block)
|
||||||
|
local add_names rem_names
|
||||||
|
add_names=$(printf '%s\n' "$added" | grep . | head -6 | tr '\n' ' ' | sed 's/ $//')
|
||||||
|
rem_names=$(printf '%s\n' "$removed" | grep . | head -6 | tr '\n' ' ' | sed 's/ $//')
|
||||||
|
local mn; mn=$(printf '%s' "$mod_names" | tr ' ' '\n' | grep . | head -6 | tr '\n' ' ' | sed 's/ $//')
|
||||||
|
|
||||||
|
local parts=""
|
||||||
|
[ "$n_add" -gt 0 ] && parts="${parts:+$parts, }+${n_add} added${add_names:+ ($add_names)}"
|
||||||
|
[ "$n_rem" -gt 0 ] && parts="${parts:+$parts, }-${n_rem} removed${rem_names:+ ($rem_names)}"
|
||||||
|
[ "$n_mod" -gt 0 ] && parts="${parts:+$parts, }~${n_mod} modified${mn:+ ($mn)}"
|
||||||
|
[ -z "$parts" ] && parts="no change in scope"
|
||||||
|
printf '%s' "$parts"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Scope label for the header (human-readable description of what we're tracking).
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
_scope_label() {
|
||||||
|
if [ "$WHOLE_SITE" = "1" ] && [ -z "$THREAD" ] && [ -z "$SYSTEM" ]; then
|
||||||
|
printf 'whole site %s (entire NetConfig)' "$SITE_ARG"
|
||||||
|
elif [ -n "$SYSTEM" ]; then
|
||||||
|
printf 'system "%s" in site %s' "$SYSTEM" "$SITE_ARG"
|
||||||
|
else
|
||||||
|
printf 'thread %s in site %s' "$THREAD" "$SITE_ARG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# RENDER: timeline
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
render_timeline() {
|
||||||
|
local n; n=$(grep -c . "$REVTBL" 2>/dev/null); [ -z "$n" ] && n=0
|
||||||
|
printf 'NetConfig revision history — %s\n' "$(_scope_label)"
|
||||||
|
printf '%s revision(s) found%s\n\n' "$n" "$( [ "$LIMIT" -gt 0 ] && printf ' (last %s)' "$LIMIT" )"
|
||||||
|
if [ "$n" = "0" ]; then
|
||||||
|
printf '(no revisions with a parseable prologue under %s)\n' "$REV_DIR" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# iterate ascending; for each revision (after the first) summarise vs the prior.
|
||||||
|
local prev_nc="" idx=0
|
||||||
|
local key who dh label nc
|
||||||
|
while IFS="$US" read -r key who dh label nc; do
|
||||||
|
[ -z "$nc" ] && continue
|
||||||
|
idx=$((idx+1))
|
||||||
|
printf 'revision %s\n' "$label"
|
||||||
|
printf 'date %s\n' "${dh:-(unknown)}"
|
||||||
|
printf 'who %s\n' "${who:-(unknown)}"
|
||||||
|
if [ -z "$prev_nc" ]; then
|
||||||
|
printf 'changed (baseline — earliest revision in range)\n'
|
||||||
|
else
|
||||||
|
printf 'changed %s\n' "$(_summary_between "$prev_nc" "$nc")"
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
prev_nc="$nc"
|
||||||
|
done < "$REVTBL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# RENDER: diff (unified diff of the scoped section between CONSECUTIVE revisions)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
render_diff() {
|
||||||
|
local n; n=$(grep -c . "$REVTBL" 2>/dev/null); [ -z "$n" ] && n=0
|
||||||
|
printf 'NetConfig revision diff — %s\n' "$(_scope_label)"
|
||||||
|
printf '%s revision(s) found%s\n\n' "$n" "$( [ "$LIMIT" -gt 0 ] && printf ' (last %s)' "$LIMIT" )"
|
||||||
|
if [ "$n" -lt 2 ]; then
|
||||||
|
printf '(need at least 2 revisions to diff; found %s under %s)\n' "$n" "$REV_DIR" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local prev_nc="" prev_lbl="" prev_dh=""
|
||||||
|
local key who dh label nc
|
||||||
|
while IFS="$US" read -r key who dh label nc; do
|
||||||
|
[ -z "$nc" ] && continue
|
||||||
|
if [ -n "$prev_nc" ]; then
|
||||||
|
printf '=== %s (%s) -> %s (%s, by %s) ===\n' \
|
||||||
|
"$prev_lbl" "${prev_dh:-?}" "$label" "${dh:-?}" "${who:-?}"
|
||||||
|
local a b
|
||||||
|
a=$(mktemp); b=$(mktemp)
|
||||||
|
_scoped_text "$prev_nc" > "$a"
|
||||||
|
_scoped_text "$nc" > "$b"
|
||||||
|
# unified diff of the scoped text; label the headers with the revision name.
|
||||||
|
diff -u --label "$prev_lbl" --label "$label" "$a" "$b" 2>/dev/null
|
||||||
|
local rc=$?
|
||||||
|
[ "$rc" = "0" ] && printf '(no change in scope)\n'
|
||||||
|
rm -f "$a" "$b"
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
prev_nc="$nc"; prev_lbl="$label"; prev_dh="$dh"
|
||||||
|
done < "$REVTBL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Drive
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
_discover_revisions
|
||||||
|
|
||||||
|
case "$FORMAT" in
|
||||||
|
timeline)
|
||||||
|
# Human-readable artifact → strip control bytes UNCONDITIONALLY.
|
||||||
|
{ render_timeline; } | _sanitize_ctl
|
||||||
|
exit "${PIPESTATUS[0]}"
|
||||||
|
;;
|
||||||
|
diff)
|
||||||
|
# Diff text may feed a downstream consumer → tty-gated strip (raw on a pipe).
|
||||||
|
{ render_diff; } | _sanitize_ctl_tty
|
||||||
|
exit "${PIPESTATUS[0]}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue
Block a user