Skip to content

Codebase review: implement all P0–P3 findings - #4

Merged
langhorst merged 26 commits into
claude/go-integration-platform-plan-amhaw5from
claude/waggle-codebase-review-8x1bh0
Sep 15, 2026
Merged

langhorst merged 26 commits into
claude/go-integration-platform-plan-amhaw5from
claude/waggle-codebase-review-8x1bh0

Conversation

@langhorst

Copy link
Copy Markdown
Owner

Adds docs/CODE_REVIEW.md (a principal-engineer review of the repository at f2d084d) and implements every finding in it, one commit per finding group, in the order the review recommends.

25 commits, 93 files changed. Each commit passed gofmt, go vet, golangci-lint run, and go test -race ./... before it was made.

Security (P0)

  • Authentication on the management surface. Bearer token or basic auth (token as password), constant-time comparison, 127.0.0.1:8420 as the default listen address, and a hard config error when a non-loopback address is used without auth. Cross-site requests are rejected via Sec-Fetch-Site/Origin, which also closes the DNS-rebinding path to the reload endpoint.
  • Script editor confinement. PUT /api/scripts was an arbitrary file write under the channels directory. It now writes only .js files actually referenced by loaded channels, resolves symlinks, writes atomically, and bounds the request body.
  • Server hardening. Read/read-header/idle timeouts, a max header size, per-response write deadlines, a cap on concurrent SSE streams, bounded list limits, and generic internal-error responses that no longer leak internals.
  • Outbound HTTP routing. A http.path metadata override could carry a scheme and host and redirect a send to an arbitrary server; it must now be a path.

Correctness (P1)

  • Parser stack overflow. Deeply nested JSON or XML under the default body limit triggered a fatal Go stack overflow that recover() cannot catch. Both parsers now cap container nesting at 512 levels.
  • Ignored persistence errors. Every Recorder call on the hot path discarded its error, producing queued deliveries with empty payloads. Store failures now fail the message.
  • Pause/Start deadlock. The channel no longer holds its status mutex across Source.Stop/Start; lifecycle and state locks are separate.
  • Unbounded intake. One pipeline goroutine per channel over a bounded buffer (maxPending, default 256) replaces a goroutine per inbound message, giving the source real backpressure.
  • HTTP metadata collision. Inbound HTTP context was written to the same keys the outbound sender reads as routing overrides. Keys are now defined in one package and inbound context lives under source.*.
  • File reader delivery contract. Transient failures no longer move files to the error directory; the reader gained an ack mode, a hold timeout, and race-free moves with a copy fallback across filesystems.
  • Format fixes. Set splits values into components and repetitions instead of writing them literally, JSON containers with existing content are guarded, and XML namespace prefixes, charset decoding, and whitespace-only text are handled correctly. Adapter durations now require units, sends are cancellable, and node depth is O(1).

Design (P2)

  • hl7v2 and astm were ~85% copy-paste; both are now declarations over one delimited format parameterized by a Spec.
  • The two TCP listeners and two TCP senders share one tcp.Server/tcp.Client, and AckMode is defined once.
  • format.DataType takes typed values and exposes segment-relative resolution.
  • The JSON API, web UI, and TUI share one set of engine view models; sentinel errors replace string matching, and UI errors are surfaced instead of swallowed.
  • The TUI attaches to a running daemon over the HTTP API and SSE stream instead of booting a second engine against the same database.
  • Config normalization is separated from validation, adapter types are checked at load, and failed starts are reported.
  • The store gained a read pool, wake-on-enqueue workers, a unique queue index, and logged prune errors.

Tests and tooling (P3)

  • One shared testutil harness replaces six ad-hoc end-to-end setups; tests now run the shipped example channels.
  • Timing-based assertions removed, coverage gaps closed, and fuzzing strengthened to a fixed-point property. That found delimiter combinations that broke round-tripping, so Delims.Validate now rejects duplicate, alphanumeric, and control-character delimiters.
  • CI workflow, .golangci.yml, and a make lint target (gofmt + vet + tidy check + golangci-lint) wired into make check.

Verification

Full suite passes three consecutive times under -race; lint clean; 15s of fuzzing clean for both hl7v2 and astm.

Note for deployers

The daemon now refuses to start on a non-loopback address without auth configured, and examples/daemon.yaml ships a placeholder token. Anyone deploying from the examples needs to set a real one.

Base is claude/go-integration-platform-plan-amhaw5, the repository's current default branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U


Generated by Claude Code

Ranked findings (security, correctness, design, tests/tooling) with
file:line references and a suggested order of work.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…ents and dead code

- go mod tidy: the five direct dependencies were marked // indirect and
  go.sum lacked entries tidy expects.
- .gitignore: data/ and out/ were root-anchored, so `make run` (which
  writes examples/data and examples/out) dirtied the tree.
- Remove "phase N" comments left over from the original build plan.
- mllp-listener: the empty if on a framing error is now a warning log.
- Remove unused astm1381 soh constant, Duration.MarshalYAML, and the tui
  max helper that shadowed the builtin.
- Document astm-sender maxRetries as an attempt count.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
- errorlint: compare io.EOF with errors.Is; wrap errors with %w in
  http-sender.
- gosec: TLS 1.2 minimum for the http-sender CA pool; render web UI
  badges through html/template instead of Sprintf into template.HTML;
  ReadHeaderTimeout on the API server.
- noctx: listeners bind through net.ListenConfig with the run context;
  store migrations use the Context variants.
- staticcheck/unparam/unused: pathID no longer takes a constant key,
  drop the unused messagesQuery type, do not call t.Fatal from the SSE
  test's feeder goroutine, remove an empty branch in a test.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
CI runs go mod tidy -diff, golangci-lint, and make check (gofmt, vet,
race tests) on pushes to main and on pull requests. make lint now
includes tidy-check and golangci so the local gate matches CI.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…s-site requests

The API is the daemon's full-control surface (channel lifecycle, message
replay, script editing) and it served every route anonymously on all
interfaces. Now:

- daemon.yaml gains an auth block: auth.token is accepted as a Bearer
  token and as the basic-auth password with any user name, so the same
  secret works for API clients and the browser prompt; basicUser and
  basicPassword add a dedicated login; auth.disabled opts out.
- The default listen address is 127.0.0.1:8420, and binding a
  non-loopback address without credentials is a config error.
- One middleware wraps the whole route table, static assets included,
  with constant-time credential comparison.
- State-changing requests carrying a browser Origin must match the
  request Host, and Sec-Fetch-Site must be same-origin. Basic auth is
  ambient, so without this a page on any origin could drive the daemon.

Tests cover each credential form, every route class, the disabled
switch, the cross-site guard, and the config policy table.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
ScriptsRoot is the channels directory, so the old prefix check let a
client PUT any file under it, channel YAML included, and have the reload
action apply it. A path now qualifies only when it is a .js file that a
loaded channel references and it still resolves under the root after
symlinks are followed. The root itself, traversal, unreferenced files,
and symlinks pointing outside are rejected with one generic error; the
reason is logged rather than echoed.

The body is read through MaxBytesReader with io.ReadAll: an oversized
upload answers 413, a truncated one 400, and neither reaches disk. The
file is replaced via temp file and rename so the hot-reload watcher can
never compile a partial write.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…ric internal errors

- API server: ReadTimeout, IdleTimeout, MaxHeaderBytes alongside the
  existing ReadHeaderTimeout. WriteTimeout would cut SSE streams, so the
  auth middleware sets a per-response write deadline and exempts the
  event endpoints.
- http-listener: WriteTimeout sized to cover the destination-mode hold,
  a configurable idleTimeout (default 60s), MaxHeaderBytes.
- SSE: concurrent streams are capped (MaxEventStreams, default 64) with
  503 + Retry-After beyond that.
- limit is validated and clamped to 500 on the message and DLQ lists;
  before_id is validated; bad values answer 400 instead of being
  silently ignored.
- Internal errors are logged and answered with a generic message so
  filesystem paths and SQL detail stay out of response bodies.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
A script could write an absolute URL into meta['http.path'] and
ResolveReference would send the whole request there. The path is now
rejected as a permanent error when it carries a scheme, host,
userinfo, or a protocol-relative prefix; the destination host stays
the operator's YAML decision. Successful response bodies are drained
through a 1 MiB limit rather than read to completion.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
Both parsers recurse once per nesting level. A payload consisting of
nothing but open brackets or open tags, well under the 10 MiB default
body limit, overflowed the goroutine stack; that is a fatal runtime
error, not a panic, so nothing could recover the process. Nesting past
MaxDepth is now a parse error, which routes the message to the invalid
message channel like any other malformed input. Tests cover the limit
boundary and the megabyte-of-brackets shape of the original crash.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
Pause held c.mu while calling Source.Stop. The MLLP and ASTM listeners
stop by waiting for their connection goroutines, and a connection that
had just read a frame was inside deliver, whose first action is to lock
c.mu. Each side waited on the other and the channel was wedged for good.

Lifecycle operations now serialize on their own mutex, which is held
across adapter calls; c.mu guards only the status fields and is never
held while calling into an adapter. A message that arrives during the
pause transition is accepted and processed, since the transport has
already read it and must answer. The regression test hangs on the old
code and passes now.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…nored

Every Recorder call after Record discarded its error. The worst case:
SetDestinationState(QUEUED, payload) failed, Enqueue succeeded, and the
worker read the queue row, found no payload behind it, and sent empty
bytes. Now:

- A failed SetState/SetTransformed routes the message to the invalid
  message channel and answers AE with a persistence-failure text, so a
  destination-ACK sender retries rather than treating it as accepted.
- A channel-level serialize failure is an error, not a message marked
  TRANSFORMED with no payload stored.
- The QUEUED record is checked before Enqueue; if it fails the delivery
  is recorded as ERROR and never queued.
- Store failures on outcome records (SENT, FILTERED, ERROR) are logged
  with full context rather than dropped.
- Head reports a queue row with no recorded payload as Orphaned and the
  worker dead-letters it instead of sending nothing.

Tests inject failures at each seam and assert nothing is sent or
enqueued.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
deliver spawned a goroutine per message, all serializing on a mutex. A
fast MLLP sender in immediate-ACK mode got its ACK as soon as Record
returned and kept sending, so goroutines accumulated without limit.

Each channel now runs a single pipeline goroutine draining a buffered
channel of recorded messages (maxPending, default 256). When the buffer
is full the source adapter blocks inside deliver, which delays its
transport ACK: a burst becomes backpressure on the sender. Stop stops
the source, refuses new intake, waits for handoffs in flight, closes
the buffer, and lets the pipeline drain it before adapters close, so
nothing accepted is lost. Inject shares the same admission path and
works on a paused channel (replay bypasses the source). This also
removes the wg.Add/Wait race between Inject and Stop.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…urce.*

Metadata keys are a protocol between adapters, scripts, the queue, and
the engine, none of which import each other, and they were string
literals scattered across six packages. internal/meta now defines them.

The http-listener stamped http.method and http.path on every inbound
message, the exact keys http-sender reads as per-message routing
overrides, so an http-listener to http-sender channel silently replayed
the inbound request's path and method against the outbound base URL.
Inbound context now lives under source.http.* (method, path, query
parameters, content type); the outbound http.* keys remain script-set
only. A listener test asserts every inbound key is namespaced and a
sender test asserts inbound context does not route.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…race-free

file-reader moved a file to the error directory whenever deliver
returned an error, but that error means "not accepted right now"
(channel paused, store busy), so a transient refusal became permanent
loss. The file now stays in place and is offered again next poll.

The reader gains ackMode like the network sources: destination mode
waits for the pipeline outcome and moves a rejected file to the error
directory, an accepted one to processed. Rename errors are no longer
discarded: a cross-device move falls back to copy-and-remove, and a
file that still cannot be moved is remembered so it is not redelivered
every poll. The collision-suffix loop spun forever when the target
directory was not a directory; it now stops on any Stat error.

file-writer fsyncs before publishing and publishes with os.Link, which
fails on an existing name instead of replacing a file another channel
wrote between the existence check and the rename.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…explicit shutdown status, O(1) node depth

- adapter.Duration rejects bare numbers: holdTimeout: 30 used to mean
  30ns and passed every zero check. Negative durations are rejected too.
- http-listener: the handoff to the engine runs under a non-cancellable
  context so Stop cannot fail a Record mid-insert; a request held for
  the pipeline outcome now answers 202 with the message id on Stop
  instead of an implicit empty 200; a client that disconnects releases
  its handler.
- mllp-sender gains a write deadline (writeTimeout, default 10s), and
  both TCP senders honour context cancellation by expiring the
  connection deadline, so shutdown no longer waits on a stalled peer.
- Delimited trees record each node's structural level in Kind, so
  Value finds it in O(1) instead of searching from the root; getAll
  over 4000 OBX segments drops from about a second to a millisecond.
  Golden trees regenerated.
- response.setAck from a destination-level script reaches the source
  ACK; it was lost with the per-destination copy.
- Script logger bindings use the injected logger.
- Retention pruning is nudged onto its own goroutine instead of running
  a DELETE on the pipeline's write path every 100 inserts.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…L namespace/charset/whitespace fixes

Delimited formats (HL7 v2, ASTM):
- Set splits its value on the separators below the addressed level the
  way the parser splits wire text, so set('PID-5', 'DOE^JOHN') builds
  components and set(get(x)) round-trips. Separators at or above the
  level stay literal and are escaped on the wire. This is the Mirth
  behaviour scripts expect; before, every such write produced \S\.
- Writing below MSH-1/MSH-2 (H-1/H-2) is rejected instead of corrupting
  the header.

JSON:
- Set never silently replaces a populated container: a key step on a
  non-empty array descends into element 0 (what get reads), an index
  step on a non-empty object is an error.
- Bracketed keys use one escape grammar in both directions: Flatten
  emits JSON escapes and Resolve now decodes them, so every flattened
  path resolves.
- Number leaves are validated against the JSON number grammar rather
  than json.Valid, which accepted any JSON value.

XML:
- xml:lang and xml:space serialize again (the reserved xml prefix is
  mapped back from its namespace URI).
- ISO-8859-1 and Windows-1252 declarations are transcoded and the
  declaration rewritten to UTF-8, which is what Serialize emits.
- Whitespace-only simple content is a value (<a> </a> keeps its space);
  only formatting between child elements is dropped.
- Set with a first step naming anything but the document element fails
  at Set instead of appending a second root that Serialize rejects.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…channels

internal/testutil replaces six copies of the same setup (temp store,
script engine, engine, channel from YAML, poll the store) and three
differently named wait helpers with one fixture: NewFixture,
StartChannelYAML, StartExample, ListenAddr, Eventually, WaitEvent,
WaitMessageState, WaitForDestinationState, Getter, AckingReceiver.
The engine's end-to-end tests move to package engine_test so they can
import it, and adapter.Addresser replaces the concrete listener type
assertions.

StartExample loads the real examples/channels/*.yaml with only listen
and peer addresses rewritten, so drift between the examples and the
tests is now caught. It caught one: astm-to-mllp.yaml sent results to
adt-to-csv's listener, whose filter drops anything but ADT, so under
make run every bridged result was FILTERED. It now points at
mllp-to-astm's listener as the README describes.

Two sleep-based negative assertions ("no output appeared for the
filtered message") now wait for the message's FILTERED event instead,
which marks the end of its pipeline.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
… astm become declarations

hl7v2 and astm were about 85% the same code: header delimiter
extraction, tree delimiter recovery, parse, serialize, path parsing,
escape decoding and encoding, and all six DataType methods, each
copied with a different segment-name length and delimiter order.

delimited.Format now implements format.DataType from a Spec that
names the genuine differences: the name pattern and length, which
segments are headers, the default delimiters, the order of the
encoding characters in the header (HL7 "^~\&" is
component/repetition/escape/subcomponent, ASTM "\^&" is
repetition/component/escape), and the escape-sequence table. The
path regexp is derived from the spec, so the subcomponent level
exists exactly when the format has one.

hl7v2 and astm shrink to their Spec plus DataType delegation; the
hl7v2 escape file is gone. Every existing format, script, and
adapter test passes unchanged.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
The MLLP and ASTM listeners carried byte-identical lifecycle code
(bind, accept loop, per-connection goroutine with AfterFunc close,
Stop with WaitGroup) and the two senders the same lazy-connect,
drop-on-error, deadline-merging client. internal/adapter/tcp now
holds a Server and a Client used by both; the protocol handling stays
in each adapter.

AckMode, its validation, the accepting-code test (AA/CA), and the
destination-mode hold (AwaitDecision with Decided/TimedOut/Canceled
outcomes) move to the adapter package. The four inbound adapters
(MLLP, ASTM, HTTP, file) share them, which also settles two
inconsistencies: the ASTM listener now accepts CA like the others,
and both listeners gain a configurable writeTimeout (default 10s)
where one had a hard-coded 5s and the other none. The MLLP listener
gains an optional idleTimeout. Listener handoffs run under a
non-cancellable context so Stop cannot fail a Record mid-insert.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…he interface

Two optional interfaces papered over gaps in format.DataType.
TypedSetter existed because Set took a string while JSON needs to keep
JS numbers and booleans typed; SegmentJoiner existed because the
script engine built segment-relative paths with an HL7-shaped default
join ("SEG-rel") that XML overrode. The default was wrong for the
formats that did not override it: on a CSV row handle row.get('3')
errored with an invalid path, and on a JSON object handle
obj.get('family') silently returned nothing.

Set now takes a Go scalar (string, bool, nil, numeric) for every
format; JSON keeps the type and the text formats store format.String
of it. ResolveFrom and SetFrom join the interface and each format
defines its own relative dialect: field paths on an HL7/ASTM segment,
a column number on a CSV row, a key path on a JSON object or element,
element steps on an XML element. The script engine's segment handles
use them directly. A cross-format test drives segment handles through
CSV, JSON, XML, and HL7 scripts.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…rors; surfaced UI errors

The channel list with counts, the script reference list, the message
stage list, and the lifecycle action switch were each written two or
three times across the JSON handlers, the HTML handlers, and the TUI.
They now live in engine: ChannelSummaries/ChannelSummary (with
Received/Sent/Errors/Filtered/Queued accessors), ScriptRefs, Stages,
and Lifecycle. ErrUnknownChannel and ErrUnknownAction replace string
matching on error text for status codes; script.ErrNotLoaded likewise.

Web UI: templates render into a buffer so a template error is a clean
500 instead of a truncated 200; a failed channel action or requeue is
shown in the refreshed table instead of only logged; store failures no
longer render as "no messages"; the message page distinguishes not
found from a store error; the DLQ page checks the channel exists; the
message list paginates with an "older messages" link. The TUI reads
the same summaries and stages instead of recomputing them.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…mands

waggle tui booted a second full engine on the same database, channel
directory, and adapters as the daemon, so running it alongside the
daemon double-processed file inputs and fought for listen ports. It now
observes a running daemon through HTTPBackend: the JSON API for
channels, messages, trees, and diffs, and the SSE stream for events
(reconnecting with backoff and delivering a Resync after a gap). The
address and token default to the daemon config; -addr and -token
override them. GET /api/channels/{id} is added for single-row refresh.

The model no longer does backend I/O inside Update: every load is a
tea.Cmd that returns a typed message, stale results for a view the
operator has left are dropped, and a slow daemon stalls a command
rather than the screen. Tests drive the commands to completion
synchronously; an integration test runs the backend against a real
engine behind a real API server.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…; report failed starts

Validate filled in defaults (name, destination dataType) as a side
effect; Normalize now does that and Validate is pure. Adapter types
were only checked when the engine built the channel while data types
were checked at load; both are now checked at load against the
registries, so a typo in a channel file fails with the list of
registered types before anything starts. The daemon logs how many
channels failed to start instead of discarding the errors.

With the TUI attached over HTTP, the daemon is the only place an
engine is bootstrapped, so there is nothing left to extract.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
… logged prune errors

- Reads (message lists, details, counts, queue head) go through a
  separate read-only connection pool; writes keep their single
  serialized connection. WAL mode lets the two run concurrently, so
  UI queries no longer queue behind pipeline inserts. A test holds a
  write transaction open and shows a read completing alongside it.
- Each destination queue has a wake channel that Enqueue and Requeue
  signal; workers block on it and only fall back to polling (now 1s)
  for retry deadlines. With N destinations that removes 4N idle
  queries per second.
- A unique index on destination_queue(destination_id, message_id)
  makes the one-pending-delivery invariant the database's.
- Retention prune errors are logged instead of discarded.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
…n fuzzing; reject unusable delimiters

Flakiness:
- The hot-reload test polls for the recorded compile error instead of
  sleeping past the watcher interval.
- The ASTM duplicate-frame test proves the count is final by sending a
  second session rather than sleeping; the http-listener stop test waits
  for the handoff signal instead of 100ms; handler-goroutine results are
  passed over channels or atomics rather than shared variables.
- Closed-port tests use an address that was just released instead of
  port 1.
- Table-driven error cases run as named subtests in sorted order; a no-op
  backoff assertion now checks the cap.
- Golden trees live in each format's own testdata.

Coverage, each a behaviour the README promised without a test: a paused
channel keeps draining its queue; reload applies rewritten YAML and
refuses a changed id; enabled: false stays stopped; a queued delivery
survives an engine restart; destination-ACK with a queued destination
answers before delivery; the insert-count retention trigger; maxAttempts
-1 retrying past many failures; SSE per-channel filtering and resync
pass-through; Stop/Start restart of every inbound adapter; MLLP framing
violations and oversize frames dropping the connection.

The hl7v2 and astm fuzz targets now require the canonical form to be a
fixed point (same tree, same bytes). That found two inputs whose
delimiters cannot round-trip: duplicate delimiter roles, and digit
delimiters that appear inside \X0F\-style escape bodies. Both specs
forbid alphanumeric delimiters; the parser now rejects duplicate,
alphanumeric, and terminator delimiters at parse and serialize time.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
The review now says up front that its findings have been implemented on
this branch and that its line references describe the original commit.
README's Development section lists make lint, what it runs, and that it
needs golangci-lint installed.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CDAe95EyukJTfQD3SePK6U
@langhorst
langhorst merged commit 88c063b into claude/go-integration-platform-plan-amhaw5 Sep 15, 2026
1 check passed
@langhorst
langhorst deleted the claude/waggle-codebase-review-8x1bh0 branch September 15, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants