flowscope 0.7.0

Passive flow & session tracking for packet capture (runtime-free, cross-platform)
Documentation
# flowscope 0.6 — Plan of record

**Date:** 2026-05-22
**Source:** [`docs/feedback-2026-05-22-netring.md`](./feedback-2026-05-22-netring.md)
— integration feedback from netring 0.14.0.
**Backward compatibility:** breaking changes are *in scope*. Pre-1.0
policy ([`plans/INDEX.md`](../plans/INDEX.md)) allows it; `netring`
updates in lockstep and `CHANGELOG.md` carries migration recipes.

---

## 1. Verdict

The netring feedback is high-quality and almost entirely actionable.
The author scoped each item, ranked it, and was honest about
prioritization ("if I were prioritizing flowscope 0.5"). We adopt
most of it, with one substantive disagreement (item 6) and two
deferrals (items 2 and 10) explained below.

The 0.4 series prioritized *common-case ergonomics* (annotation-free
drivers, one-expression offline pipelines, time-aware parser traits)
and traded away an advanced-use surface — driver per-flow user state
— without realizing the main consumer relied on it. 0.6 corrects
that and rounds out the convenience layer.

---

## 2. What ships in 0.6 (eight plans)

| Plan | Item(s) | Change | Breaking? | Effort |
|------|---------|--------|-----------|--------|
| [`38`]../plans/38-driver-state-restore.md | #6 (opt. A) | Restore `S` on the drivers via split constructors | yes | M |
| [`39`]../plans/39-tracker-convenience.md | #3 + #1 | `FlowTracker::finish()` + `sweep_with_parsers` / `sweep_with_datagram_parsers` | no | S |
| [`43`]../plans/43-anomaly-event-split.md | #4 | Split `Anomaly { key: Option<K> }` into `FlowAnomaly` + `TrackerAnomaly` on both `FlowEvent` and `SessionEvent` | yes | M |
| [`44`]../plans/44-reassembler-watermark-threshold.md | #9 | `BufferedReassembler::with_high_watermark_threshold` + `AnomalyKind::ReassemblerHighWatermark` | no | S–M |
| [`50`]../plans/50-as-packet-view.md | #5 | `AsPacketView` trait + blanket `From<&T>` for `PacketView` | minor | S |
| [`58`]../plans/58-driver-factory-ctor.md | #7 | `FlowSessionDriver::with_factory` / `FlowDatagramDriver::with_factory` | no | S |
| [`59`]../plans/59-test-helpers-parsers.md | #8 | `flowscope::test_helpers::{NoopSessionParser, NoopDatagramParser, EchoSessionParser}` | no | S |
| [`60`]../plans/60-l7-umbrella-and-doc-note.md | #11 + #12 | `l7` umbrella feature; intra-doc-link note for re-exporters | no | trivial |
| [`61`]../plans/61-feature-matrix-ci.md | (not in feedback) | CI matrix of partial-feature builds (catches latent cfg dead-code) | no | trivial |

Sequencing:
- **First batch (no inter-dependencies):** 38, 39, 50, 60, 61.
- **Second batch (depends on 38 for the `S` reintroduction):** 58
  (driver `with_factory` must thread through the restored `S`).
- **Independent:** 43, 44, 59.

---

## 3. The one substantive disagreement — item #6

The netring author lays out two options for the dropped driver-`S`
parameter and "leans toward B + #1" (keep drivers lean, ship a
documented `FlowTracker` recipe + the `sweep_with_parsers` helper).

**We adopt option A instead** — restore `S` on the drivers with a
split-constructor design so the common `S = ()` path stays
annotation-free. Reasoning:

1. **The "rare combination" assumption was wrong.** Plan 32's removal
   was justified as "a driver carrying both a per-flow parser AND
   per-flow user state is a rare combination not worth taxing every
   signature for." netring — the main consumer crate — is exactly
   that combination. The cost/benefit math is upside-down: every
   call site that *doesn't* need `S` already pays nothing under the
   split-constructor approach (option A from plan 32's review), and
   netring would drop ~300 LoC of custom driver clone.

2. **Option A doesn't reintroduce the original wart.** The original
   problem with `S` was that Rust type-parameter defaults don't
   participate in inference, so every `FlowDriver::new(...)` site
   had to annotate `<_, _, ()>`. Option A pins the common
   constructors on `impl<E, F> FlowDriver<E, F, ()>` — inference is
   bounded by the impl block, no annotation needed. The fix that
   plan 32 chose (option B, drop `S`) was one of two equally valid
   ways to solve the inference problem; option A would have also
   solved it without losing the advanced-use surface.

3. **The "documented workaround" path scales badly.** Option B
   plus #1 leaves every advanced consumer to recreate
   reassembler-dispatch, parser-clone bookkeeping, and on-tick
   choreography. Even with a `sweep_with_parsers` helper, the
   *track-time* feed-bytes-to-parser path stays manual. The drivers
   exist precisely to absorb that boilerplate; making them
   inaccessible to anyone who needs per-flow state is the wrong
   trade.

We still ship #1 (`sweep_with_parsers`) — it's useful for genuine
direct-tracker use and it's small. But the *motivation* shifts from
"netring needs this because the drivers don't support `S`" to
"FlowTracker direct-use should be ergonomic in its own right."

This decision is a partial reversal of plan 32. The CHANGELOG entry
for plan 38 calls it out explicitly so the rationale survives in
the published record.

---

## 4. What we defer

- **#2 — `FlowTracker::with_auto_sweep(interval)`.** The packet-clock
  parity argument (live vs offline runs producing different events
  for identical inputs) is real and worth solving. The author
  agrees this "deserves its own RFC rather than landing in a minor."
  We defer to 0.6 once the design questions are settled: how
  out-of-order timestamps interact with the auto-sweep clock, what
  events are returned from an implicit sweep (merged into the
  `track` return? a separate channel?), and whether `with_monotonic_
  timestamps` should be a prerequisite.

- **#10 — `SessionParser::is_done()`.** "I'm satisfied, please close
  this flow cleanly" — symmetric with `is_poisoned()`. The
  motivating case (HTTP/1.0 `Connection: close`) is one where the
  server sends a FIN anyway, so the flow closes naturally. A
  genuine motivating case (a stream-parser that knows protocol
  completion without a network-level FIN) hasn't surfaced. Decline
  pending real demand. Easy to add later — the symmetric `is_poisoned`
  pattern is the precedent.

---

## 5. Items added beyond the feedback

**#61 — Cargo feature-matrix CI.** Caught during the plan-37
implementation: `obs::trace_session_message` was emitting a
`dead_code` warning under `--no-default-features --features dns`,
because its callers (the drivers) are `reassembler`-gated but the
function itself was only `session`-gated. CI runs `--all-features`,
so the warning was latent. A small matrix step (e.g.
`{default, pcap-only, dns-only, all-features}`) catches this class
of latent cfg-warning before users hit it. Tiny workflow change;
included.

No other "while we're in there" additions — the API surface
churned enough in 0.4 + 0.6 (per this plan) that additional
optional features would dilute focus. Sister-crate work
(`flowscope-export`, `flowscope-cli`) stays out of scope per
[`docs/DPI_ARCHITECTURE.md`](./DPI_ARCHITECTURE.md).

---

## 6. Migration & release shape

Plans 38, 43, 50 are breaking. The migration is mechanical and
single-pass; netring updates in one commit per CHANGELOG recipe.

Release version: **0.6.0** (pre-1.0 minor bump per the existing
policy — breaking changes inside a 0.x minor are allowed). Tag
`0.6.0`, no `v` prefix (matches `0.1.0` … `0.5.0`).

**Acceptance bar for the 0.6 series:** netring 0.15 (the lockstep
update) can drop both its custom-driver path (~300 LoC, eliminated
by plan 38) and its hand-rolled noop/echo parsers across five test
files (eliminated by plan 59). The `if let Some(k) = key` boilerplate
on every anomaly-routing path disappears (plan 43). Offline-DPI
consumers can feed a foreign packet type with a 3-line trait impl
(plan 50).