Requirements and Signature Model
Use one explicit symbol contract. This design scans Unicode code points after NFC normalization and optional casefold(), applying the identical policy to signatures and input. Offsets are half-open positions in the normalized stream. If consumers require original byte spans, the decoder must preserve a provenance map because normalization and case folding can change length.
Each signature has a stable ID, non-empty pattern, severity, tags, and JSON metadata. Duplicate normalized patterns are allowed and produce separate records; duplicate IDs are rejected. Matches remain ordered, overlapping, nested, and co-terminal. Incremental UTF-8 decoding prevents split characters, while exact streaming NFC additionally requires retaining the unresolved final starter and combining marks or normalizing complete logical records.
| Record | Field | Type | Required | Semantics | Validation |
|---|---|---|---|---|---|
| Signature | id | String | Yes | Stable external identifier | Non-empty and unique per dictionary |
| Signature | pattern | String | Yes | Text compiled into the automaton | Non-empty after normalization |
| Signature | severity | String | Yes | Routing or priority class | Controlled deployment value |
| Signature | tags | List of strings | No | Classification labels | Deduplicate while preserving order |
| Signature | metadata | JSON object | No | Owner, source, revision, response data | JSON-serializable |
| Dictionary | normalization | String | Yes | Unicode normalization policy | NFC here |
| Dictionary | casefold | Boolean | Yes | Unicode case policy | Identical at compile and scan time |
| Match | signature_id | String | Yes | Matched rule identifier | Copied from compiled signature |
| Match | start | Integer | Yes | Inclusive normalized offset | Non-negative |
| Match | end | Integer | Yes | Exclusive normalized offset | Greater than start |
| Match | pattern | String | Yes | Original authored signature | Retained for audit and display |
| Match | severity | String | Yes | Signature priority | Immutable for a dictionary version |
| Match | tags | List of strings | Yes | Signature classifications | Copied without mutation |
| Match | metadata | JSON object | Yes | Application-defined context | Copied or exposed read-only |
Automaton Compilation
Insert normalized signatures into a trie, then traverse it breadth-first to assign failure links. A failure link points to the longest proper suffix represented by another state. Each node inherits terminal outputs from its failure target, so reaching she also reports he. Sparse transitions keep memory proportional to dictionary structure; scanning follows failure links on missing edges.
Serialization captures the normalization policy, signatures, pattern lengths, transitions, failure links, and outputs as a versioned unit. Validate every index when loading and never use a general object serializer for untrusted automata.
Streaming Chunk Boundaries
Two states cross each chunk boundary: the incremental UTF-8 decoder retains incomplete bytes, and the scanner retains its automaton state and normalized position. Never decode chunks independently with replacement characters or reset the automaton. A pattern may begin in one chunk and finish in another.
NFC adds a third boundary concern because a starter in one chunk may compose with combining marks in the next. Scan complete normalized records or retain an unresolved normalization suffix. At end of stream, finalize the decoder, reject malformed trailing bytes according to policy, flush normalization carry, and then complete scanning.
Reporting, Backpressure, and Deduplication
Scanning $N$ symbols costs $O(N+Z)$ for $Z$ outputs. A repetitive stream and nested signatures can overwhelm a slower sink, so place a bounded queue after scanning. On saturation, explicitly pause reads, spill durable batches, aggregate counts, or drop declared low-priority records while recording loss—never grow an unbounded list.
Distinct IDs sharing a pattern remain distinct matches. For retry-safe delivery, use an idempotency key such as (stream_id, dictionary_version, signature_id, start, end). Checkpoint decoder bytes, normalization carry, automaton state, normalized position, and source cursor atomically.
flowchart LR
A["Ordered byte chunks"] --> B["Incremental UTF-8 decoder"]
B --> C["Normalization and offset map"]
C --> D["Stateful Aho-Corasick scan"]
D --> E{"Bounded match queue"}
E -- "capacity" --> F["Batch and deduplicate"]
F --> G["Idempotent sink"]
E -- "high-water mark" --> H["Pause input or spill"]
H --> E
D --> I["Atomic checkpoint"]
Validation and Performance
Compare every streaming run with a batch scan using the same normalization contract. Split multibyte characters and every signature at every possible boundary; include suffix-related duplicates, malformed UTF-8 policies, combining marks, checkpoint restores, and alternating sink backpressure.
Measure compilation separately from scanning. Report pattern bytes, states, transitions, serialized size, compile time, decoded throughput, transitions per symbol, outputs per symbol, queue high-water mark, and sink throughput. Include both low-output corpora and output storms because reporting can dominate automaton work.
Overlap Storm
Compile signatures a, aa, …, a repeated 32 times, all with distinct IDs. Stream 100,000 a symbols using chunk sizes cycling through 1, 7, 64, and 4096 bytes. Position $i$ emits $\min(i+1,32)$ records, for
$$ \frac{32\cdot33}{2}+32(100{,}000-32)=3{,}199{,}504 $$
total matches.
Use a queue bounded at 1,024 records and sink batches of 256. Verify batch and streaming triples are identical; chunk crossings lose no matches; the queue never exceeds capacity; reads pause at the high-water mark; checkpoint replay creates no duplicate sink records under idempotency keys; and metrics distinguish 100,000 consumed symbols from 3,199,504 emitted records. Repeat with reporting disabled and with count-only aggregation to isolate transition throughput from output cost.