Skip to content

Commit 1c5bcdd

Browse files
ipasechnikovclaude
andcommitted
Speed up error-resolution flow: triage, defer/loinc/resolve wrappers, auto LOINC candidates, ready-to-run swap suggestion
- triage.sh: batch-classify active errors into suggested actions (auto-swap / fhir-422 / sender-missing / loinc-lookup / parsing-defer / aidbox-reject). - defer.sh / loinc-search.sh / resolve-mapping.sh: wrap the curl + jq boilerplate previously inlined in the skill docs. - inspect-error.sh: for per-1 constraint, emit ready-to-run wire-preprocessor.ts command; for observation-code-loinc tasks, auto-run ValueSet/\$expand on each localDisplay so the picker has LOINC candidates without another call. - Skill docs updated to point at the new scripts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 662afea commit 1c5bcdd

6 files changed

Lines changed: 230 additions & 17 deletions

File tree

.claude/skills/check-errors/SKILL.md

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ Emits a markdown table of active errors plus a deferred-count reminder. If outpu
1717

1818
If the user asked specifically about deferred messages, use `scripts/errors/list-errors.sh --deferred`.
1919

20+
For a classified action plan across all active errors (recommended when there are ≥3):
21+
22+
```sh
23+
scripts/errors/triage.sh
24+
```
25+
26+
Prints each error with a suggested class (auto-swap / fhir-422 / sender-missing / loinc-lookup / parsing-defer / aidbox-reject / other) and next command. Read-only — user still approves each fix.
27+
2028
Ask: **"Which error would you like me to investigate?"** Skip `deferred` rows unless the user explicitly asks.
2129

2230
## Step 2: Inspect one
@@ -27,7 +35,7 @@ scripts/errors/inspect-error.sh <id>
2735

2836
Emits: status, type, sender, full error, unmapped codes (if present), raw HL7v2 saved to `/tmp/hl7v2-<id>.hl7`, and an `hl7v2-inspect` overview for `parsing_error`/`conversion_error`. **You do not need to curl the resource yourself.**
2937

30-
For `HTTP 422` conversion errors the script also prints the **current values** of each candidate HL7v2 field, so you typically don't need an additional `hl7v2-inspect --field` call. For `code_mapping_error` with any `observation-code-loinc` task, peer OBX rows are dumped automatically — use them (code, unit, ref range) to pick the right LOINC.
38+
For `HTTP 422` conversion errors the script also prints the **current values** of each candidate HL7v2 field, so you typically don't need an additional `hl7v2-inspect --field` call. For `per-1` (reversed period) it also emits a **ready-to-run** `wire-preprocessor.ts` command — just copy/run it. For `code_mapping_error` with any `observation-code-loinc` task, peer OBX rows are dumped automatically AND LOINC candidates are fetched via ValueSet/$expand on each localDisplay — you usually have everything needed to pick the right LOINC without another call.
3139

3240
Pick the playbook below by the `Status` line from Step 2.
3341

@@ -38,7 +46,7 @@ Pick the playbook below by the `Status` line from Step 2.
3846
1. From the inspect overview, identify what's wrong: missing MSH, invalid encoding chars (MSH-1/2), truncated segments, wrong line endings, embedded binary.
3947
2. **No code/config fix.** Sender must correct the message format. Defer = resolved on our side — do it:
4048
```sh
41-
curl -sf -X POST 'http://localhost:3000/defer/<id>'
49+
scripts/errors/defer.sh <id>
4250
```
4351
3. Only mark for retry if the sender has already fixed and will resend with the same ID.
4452

@@ -63,7 +71,7 @@ Do **not** run `hl7v2-inspect --values` unless the mapping is ambiguous. Diagnos
6371
2. Suggest fixes in this order:
6472
- Best: sender populates the missing field — explain what's needed; defer = park it out of active queue pending sender action:
6573
```sh
66-
curl -sf -X POST 'http://localhost:3000/defer/<id>'
74+
scripts/errors/defer.sh <id>
6775
```
6876
- Workaround: add a preprocessor in `src/v2-to-fhir/preprocessor-registry.ts` + `config/hl7v2-to-fhir.json`
6977
- Last resort: relax config (make a segment optional) — warn about the tradeoff
@@ -92,28 +100,21 @@ If a **new** preprocessor function is needed, write it manually first:
92100

93101
Inspect output lists each unmapped code with `localCode`, `localDisplay`, `localSystem`, `mappingType`, and `taskId`. Use the printed `taskId` for the resolve call.
94102

95-
1. For `observation-code-loinc`, search LOINC via Aidbox's ValueSet/$expand (the `/api/terminology/*` path does not exist):
103+
1. For `observation-code-loinc`, `inspect-error.sh` already dumps LOINC candidates via ValueSet/$expand. For an ad-hoc search:
96104
```sh
97-
SECRET=$(awk -F': ' '/^[[:space:]]*BOX_ROOT_CLIENT_SECRET:/ {print $2}' docker-compose.yaml)
98-
TERM=$(printf '%s' '<term>' | jq -sRr @uri)
99-
curl -sf -u "root:$SECRET" "http://localhost:8080/fhir/ValueSet/\$expand?url=http://loinc.org/vs&filter=${TERM}&count=10" \
100-
| jq '.expansion.contains[] | {code, display}'
105+
scripts/errors/loinc-search.sh '<term>' [--count N]
101106
```
102-
If the LOINC package is not loaded locally, `.expansion.contains` will be empty — fall back to domain knowledge and cite the LOINC concept by name (e.g. peer OBX codes in the same message, unit of measure, and specimen type).
107+
If no matches, the LOINC package isn't loaded — fall back to domain knowledge and cite the LOINC concept by name (e.g. peer OBX codes in the same message, unit of measure, and specimen type).
103108
2. For `patient-class`, `obr-status`, `obx-status`, etc.: look up valid target values in `src/code-mapping/mapping-types.ts`.
104109
3. Present suggestions with confidence: "High: `2823-3` → LOINC `2823-3` (Potassium)" vs "Needs review: `GLU` → `2345-7` (Glucose) — verify with lab".
105110
4. If ambiguous / needs domain expertise → defer = park it out of active queue pending sender action:
106111
```sh
107-
curl -sf -X POST 'http://localhost:3000/defer/<id>'
112+
scripts/errors/defer.sh <id>
108113
```
109-
5. After approval, resolve via the app API. Endpoint expects **form-urlencoded** body with `resolvedCode` and `resolvedDisplay` (not JSON). A `302` redirect to `/unmapped-codes?saved=...&replayed=N` signals success:
114+
5. After approval, resolve via the app API. Message is auto-requeued.
110115
```sh
111-
curl -s -X POST 'http://localhost:3000/api/mapping/tasks/<taskId>/resolve' \
112-
--data-urlencode 'resolvedCode=<target>' \
113-
--data-urlencode 'resolvedDisplay=<target display>' \
114-
-D - -o /dev/null
116+
scripts/errors/resolve-mapping.sh <taskId> <resolvedCode> <resolvedDisplay>
115117
```
116-
Message is auto-requeued.
117118
118119
### `sending_error` — Aidbox rejected the FHIR bundle
119120
@@ -157,8 +158,12 @@ scripts/errors/status.sh <id>
157158
| Script | Purpose |
158159
|---|---|
159160
| `scripts/errors/list-errors.sh` | Active errors + deferred reminder (markdown table). |
160-
| `scripts/errors/inspect-error.sh <id>` | Full diagnosis: resource fields, saved raw file, hl7v2-inspect overview, 422 candidates w/ values, peer OBX for LOINC tasks. |
161+
| `scripts/errors/triage.sh` | Batch-classify all active errors → suggested action per row. Read-only. |
162+
| `scripts/errors/inspect-error.sh <id>` | Full diagnosis: resource fields, saved raw file, hl7v2-inspect overview, 422 candidates w/ values + ready-to-run wire command for per-1, peer OBX + auto LOINC candidates for LOINC tasks. |
161163
| `scripts/errors/status.sh <id>` | Concise `{id,status,type,error,sender}` for a single message. |
162164
| `scripts/errors/verify-retry.sh <id>` | After a fix: verify → mark-for-retry → reprocess → status. Aborts if not supported. |
165+
| `scripts/errors/defer.sh <id>` | Defer a message (park out of active queue) + print status. |
166+
| `scripts/errors/loinc-search.sh <term>` | LOINC ValueSet/$expand wrapper. Prints `code — display` lines. |
167+
| `scripts/errors/resolve-mapping.sh <taskId> <code> <display>` | Resolve a mapping task (form-urlencoded POST) + auto-requeue. |
163168
| `scripts/errors/list-preprocessors.sh` | Available preprocessors w/ JSDoc summary. |
164169
| `scripts/errors/wire-preprocessor.ts` | Wire a preprocessor into `config.ts` + `hl7v2-to-fhir.json` atomically. Idempotent. |

scripts/errors/defer.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
# Defer an IncomingHL7v2Message — parks it out of active queue pending sender action.
3+
#
4+
# Usage: scripts/errors/defer.sh <message-id>
5+
set -e
6+
7+
if [ -z "$1" ]; then
8+
echo "Usage: $0 <message-id>" >&2
9+
exit 2
10+
fi
11+
ID="$1"
12+
13+
PROJECT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
14+
curl -sf -X POST "http://localhost:3000/defer/$ID" >/dev/null
15+
"$PROJECT_DIR/scripts/errors/status.sh" "$ID"

scripts/errors/inspect-error.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ if [ "$STATUS" = "conversion_error" ] && printf '%s' "$ERROR_TEXT" | grep -q 'HT
101101
case "$CONSTRAINT_ID" in
102102
per-1)
103103
echo "- Constraint \`per-1\`: period start must be <= end. Likely: sender reversed start/end fields. Candidate fix: \`swap-if-reversed\` preprocessor with params \`{ a, b }\` pointing at the two date fields."
104+
SUGGEST_SWAP=1
104105
;;
105106
ref-1)
106107
echo "- Constraint \`ref-1\`: SHALL have a contained resource or a reference (not both / need one). Likely: empty or dangling reference target."
@@ -185,6 +186,24 @@ if [ "$STATUS" = "conversion_error" ] && printf '%s' "$ERROR_TEXT" | grep -q 'HT
185186
fi
186187
done
187188
fi
189+
# Suggest a ready-to-run wire-preprocessor command when the fix is obvious
190+
# (per-1 reversed dates: exactly 2 fields in the same segment).
191+
if [ "${SUGGEST_SWAP:-0}" = "1" ]; then
192+
PAIR=$(printf '%s\n' "$FIELDS_TO_SHOW" | sort -u)
193+
PAIR_CNT=$(printf '%s\n' "$PAIR" | grep -c .)
194+
if [ "$PAIR_CNT" = "2" ]; then
195+
SEG=$(printf '%s\n' "$PAIR" | head -1 | cut -d- -f1)
196+
SEG2=$(printf '%s\n' "$PAIR" | tail -1 | cut -d- -f1)
197+
if [ "$SEG" = "$SEG2" ]; then
198+
A=$(printf '%s\n' "$PAIR" | head -1 | cut -d- -f2)
199+
B=$(printf '%s\n' "$PAIR" | tail -1 | cut -d- -f2)
200+
# Normalize msg type "ADT_A01^ADT_A01" -> "ADT-A01" for config key
201+
MSGKEY=$(printf '%s' "$TYPE" | cut -d'^' -f1 | tr '_' '-')
202+
echo " Ready-to-run fix:"
203+
echo " bun scripts/errors/wire-preprocessor.ts $MSGKEY $SEG $A swap-if-reversed '{\"a\":$A,\"b\":$B}'"
204+
fi
205+
fi
206+
fi
188207
else
189208
echo " (no matching row in IG segment CSVs for path \`$PATH_TAIL\`)"
190209
fi
@@ -215,5 +234,24 @@ if [ "$STATUS" = "code_mapping_error" ] && [ "$UNMAPPED" != "[]" ]; then
215234
echo '```'
216235
"$PROJECT_DIR/scripts/hl7v2-inspect.sh" "$TMP" --segment OBX --values 2>&1 || true
217236
echo '```'
237+
238+
# Auto-run LOINC search on each observation-code-loinc task's localDisplay
239+
# so common cases (Creatinine, Sodium, ...) are answered without another call.
240+
echo
241+
echo "### LOINC candidates (from ValueSet/\$expand)"
242+
printf '%s' "$JSON" | jq -r '
243+
. as $root |
244+
.unmappedCodes[]? |
245+
. as $u |
246+
(($u.mappingTask.reference // "") | sub("^Task/"; "")) as $tid |
247+
([$root.entries[]? | select(.resourceType=="Task" and .id==$tid)] | first) as $t |
248+
select($t.code.coding[0].code == "observation-code-loinc") |
249+
"\($u.localCode // "")\t\($u.localDisplay // "")\t\($tid)"
250+
' | while IFS=$'\t' read -r LCODE LDISPLAY LTID; do
251+
[ -z "$LDISPLAY" ] && continue
252+
echo "- \`$LCODE\` ($LDISPLAY) — task \`$LTID\`:"
253+
"$PROJECT_DIR/scripts/errors/loinc-search.sh" "$LDISPLAY" --count 5 2>/dev/null \
254+
| sed 's/^/ /' || echo " (lookup failed)"
255+
done
218256
fi
219257
fi

scripts/errors/loinc-search.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
# Search LOINC via Aidbox ValueSet/$expand. Prints top matches as "code — display".
3+
# Wraps the auth + URI encode + jq boilerplate.
4+
#
5+
# Usage: scripts/errors/loinc-search.sh <term> [--count N]
6+
set -e
7+
8+
if [ -z "$1" ]; then
9+
echo "Usage: $0 <term> [--count N]" >&2
10+
exit 2
11+
fi
12+
13+
TERM="$1"; shift
14+
COUNT=10
15+
while [ $# -gt 0 ]; do
16+
case "$1" in
17+
--count) COUNT="$2"; shift ;;
18+
*) echo "Unknown arg: $1" >&2; exit 2 ;;
19+
esac
20+
shift
21+
done
22+
23+
PROJECT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
24+
SECRET=$(awk -F': ' '/^[[:space:]]*BOX_ROOT_CLIENT_SECRET:/ {print $2}' "$PROJECT_DIR/docker-compose.yaml")
25+
if [ -z "$SECRET" ]; then
26+
echo "ERROR: BOX_ROOT_CLIENT_SECRET missing from docker-compose.yaml" >&2
27+
exit 1
28+
fi
29+
30+
ENC=$(printf '%s' "$TERM" | jq -sRr @uri)
31+
RESP=$(curl -sf -u "root:$SECRET" \
32+
"http://localhost:8080/fhir/ValueSet/\$expand?url=http://loinc.org/vs&filter=${ENC}&count=${COUNT}")
33+
34+
N=$(printf '%s' "$RESP" | jq -r '.expansion.contains | length // 0')
35+
if [ "$N" = "0" ]; then
36+
echo "(no matches — LOINC package may not be loaded; fall back to domain knowledge)"
37+
exit 0
38+
fi
39+
40+
printf '%s' "$RESP" | jq -r '.expansion.contains[] | "\(.code) — \(.display)"'

scripts/errors/resolve-mapping.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash
2+
# Resolve an unmapped code mapping task. Auto-requeues the message.
3+
# Wraps the form-urlencoded POST + 302 parsing.
4+
#
5+
# Usage: scripts/errors/resolve-mapping.sh <taskId> <resolvedCode> <resolvedDisplay>
6+
set -e
7+
8+
if [ $# -lt 3 ]; then
9+
echo "Usage: $0 <taskId> <resolvedCode> <resolvedDisplay>" >&2
10+
exit 2
11+
fi
12+
13+
TASK_ID="$1"
14+
CODE="$2"
15+
DISPLAY="$3"
16+
17+
RESP=$(curl -s -X POST "http://localhost:3000/api/mapping/tasks/$TASK_ID/resolve" \
18+
--data-urlencode "resolvedCode=$CODE" \
19+
--data-urlencode "resolvedDisplay=$DISPLAY" \
20+
-D - -o /dev/null)
21+
22+
STATUS=$(printf '%s' "$RESP" | awk 'NR==1 {print $2}')
23+
LOCATION=$(printf '%s' "$RESP" | awk -F': ' 'tolower($1)=="location" {print $2}' | tr -d '\r')
24+
25+
if [ "$STATUS" = "302" ]; then
26+
echo "resolved: $CODE ($DISPLAY)"
27+
echo "redirect: $LOCATION"
28+
else
29+
echo "ERROR: expected 302, got $STATUS" >&2
30+
printf '%s\n' "$RESP" >&2
31+
exit 1
32+
fi

scripts/errors/triage.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/bin/bash
2+
# Batch-classify all active errors into suggested actions.
3+
# Read-only — prints a plan. User still confirms each fix.
4+
#
5+
# Classifications:
6+
# auto-swap conversion_error w/ HTTP 422 per-1 (reversed dates)
7+
# fhir-422 conversion_error w/ HTTP 422 (other constraint — needs preprocessor)
8+
# sender-missing conversion_error w/ "required but missing" (defer-candidate)
9+
# loinc-lookup code_mapping_error w/ observation-code-loinc
10+
# code-mapping code_mapping_error other (check src/code-mapping/mapping-types.ts)
11+
# parsing-defer parsing_error (sender fix → defer)
12+
# aidbox-reject sending_error (check Aidbox health)
13+
# other unrecognized — inspect manually
14+
#
15+
# Usage: scripts/errors/triage.sh
16+
set -e
17+
18+
PROJECT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
19+
SECRET=$(awk -F': ' '/^[[:space:]]*BOX_ROOT_CLIENT_SECRET:/ {print $2}' "$PROJECT_DIR/docker-compose.yaml")
20+
if [ -z "$SECRET" ]; then
21+
echo "ERROR: BOX_ROOT_CLIENT_SECRET missing from docker-compose.yaml" >&2
22+
exit 1
23+
fi
24+
25+
ERROR_STATUSES="parsing_error,conversion_error,code_mapping_error,sending_error"
26+
ELEMENTS="id,status,type,error,sendingApplication,sendingFacility"
27+
28+
JSON=$(curl -sf -u "root:$SECRET" \
29+
"http://localhost:8080/fhir/IncomingHL7v2Message?status=$ERROR_STATUSES&_sort=-_lastUpdated&_count=50&_elements=$ELEMENTS")
30+
N=$(printf '%s' "$JSON" | jq -r '(.entry // []) | length')
31+
32+
if [ "$N" = "0" ]; then
33+
echo "No active errors."
34+
exit 0
35+
fi
36+
37+
echo "**Triage plan ($N active errors):**"
38+
echo
39+
echo "| # | Class | Type | ID | Suggested action |"
40+
echo "|---|-------|------|----|------------------|"
41+
42+
printf '%s' "$JSON" | jq -r '
43+
.entry | to_entries[] |
44+
.key as $i | .value.resource as $r |
45+
"\($i + 1)\t\($r.status // "")\t\($r.type // "")\t\($r.id)\t\($r.error // "" | gsub("[\r\n]+"; " "))"
46+
' | while IFS=$'\t' read -r IDX STATUS TYPE ID ERR; do
47+
case "$STATUS" in
48+
conversion_error)
49+
if printf '%s' "$ERR" | grep -q "HTTP 422"; then
50+
if printf '%s' "$ERR" | grep -q "per-1"; then
51+
CLASS="auto-swap"
52+
ACTION="\`inspect-error.sh $ID\` → run emitted \`wire-preprocessor.ts\` line → \`verify-retry.sh\`"
53+
else
54+
CLASS="fhir-422"
55+
ACTION="\`inspect-error.sh $ID\` → pick preprocessor from candidates"
56+
fi
57+
elif printf '%s' "$ERR" | grep -q "required but missing"; then
58+
CLASS="sender-missing"
59+
ACTION="likely \`defer.sh $ID\` (sender must populate field)"
60+
else
61+
CLASS="conv-other"
62+
ACTION="\`inspect-error.sh $ID\`"
63+
fi
64+
;;
65+
code_mapping_error)
66+
CLASS="loinc-lookup"
67+
ACTION="\`inspect-error.sh $ID\` (auto LOINC candidates) → \`resolve-mapping.sh <taskId> <code> <display>\`"
68+
;;
69+
parsing_error)
70+
CLASS="parsing-defer"
71+
ACTION="\`defer.sh $ID\` (sender must fix malformed message)"
72+
;;
73+
sending_error)
74+
CLASS="aidbox-reject"
75+
ACTION="check \`curl http://localhost:8080/health\`\`inspect-error.sh $ID\`"
76+
;;
77+
*)
78+
CLASS="other"
79+
ACTION="\`inspect-error.sh $ID\`"
80+
;;
81+
esac
82+
echo "| $IDX | $CLASS | $TYPE | \`$ID\` | $ACTION |"
83+
done

0 commit comments

Comments
 (0)