# Files changed · PR \#93 · flambo/atlantis

[View on GitCafe](https://git.cafe/flambo/atlantis/pull/93/diffs)

Repository: [flambo/atlantis](https://git.cafe/flambo/atlantis)

Visibility: public

[feat\(netflow-db\): sync in-process MAAD estimator](https://git.cafe/flambo/atlantis/pull/93.md)

Version: 0

Head commit: 0c0ff5a3e5d90e03f5599b867beb24c4593b22ba

Base commit: 8705e788d07f06ed5502fa1f17ae735e29c5e6ed

Comparison base: 8705e788d07f06ed5502fa1f17ae735e29c5e6ed

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## docs/maad-conformance.md

Status: added; +84; −0

````diff
@@ -1,0 +1,84 @@
+# MAAD conformance
+
+`scripts/local/validate_maad.py` is a small, local Rust-vs-Haskell comparison
+for address sets. It accepts one or more `NAME=PATH` cases, where each path is
+an external file containing one IPv4 address per line. The validator passes the
+same path, unchanged and in the original order, to both implementations:
+
+- Rust: `<rust> maad <path>`
+- Haskell: `<haskell> --input <path> --output - --format json --structure --spectrum --dimensions`
+
+It ignores the oracle's schema/input metadata differences, normalizes Rust's
+`prefixLengths` and the oracle's `metadata.prefix_counts[*].pl` to the same
+prefix-length list, then requires equal total addresses, prefix lengths, row
+counts, and numeric rows. Numeric fields use independent absolute and relative
+tolerances, both defaulting to `1e-10`.
+
+## Pinned oracle
+
+The Haskell oracle is pinned to
+`chris-misa/maad@3ae75363d44e08faacdee186d4ff8906c6ccd06a`. The only source
+change made to that oracle checkout is `deltaQ`: Atlantis uses `1/8` instead
+of the upstream `1/16`. The unpatched checkout is not comparable.
+
+Build the oracle from that checkout by applying exactly that one-line change,
+then run its build script in the supplied Nix shell:
+
+```sh
+git clone https://github.com/chris-misa/maad.git /tmp/maad
+git -C /tmp/maad checkout 3ae75363d44e08faacdee186d4ff8906c6ccd06a
+(cd /tmp/maad && git apply --unidiff-zero <<'PATCH'
+diff --git a/MAAD.hs b/MAAD.hs
+--- a/MAAD.hs
++++ b/MAAD.hs
+@@ -66 +66 @@ deltaQ :: Double
+-deltaQ = 1.0 / 16.0
++deltaQ = 1.0 / 8.0
+PATCH
+)
+(cd /tmp/maad && nix-shell shell.nix --run './compile.sh')
+```
+
+If the checkout's GHC dependencies are already installed, `./compile.sh` can
+be run directly instead.
+
+Build the Rust release binary from this repository:
+
+```sh
+./scripts/build_maad_fast.sh
+```
+
+## Running a comparison
+
+Keep real or private address files outside the repository. They are inputs,
+not fixtures, and must not be committed:
+
+```sh
+python3 scripts/local/validate_maad.py \
+  --rust target/release/netflow-db \
+  --haskell /tmp/maad/MAAD \
+  private-window=/path/to/private/addresses.txt
+```
+
+Add another file as another positional `NAME=PATH` (or repeat `--case`). Use
+`--abs-tol` and `--rel-tol` when a deliberate numerical comparison needs a
+different tolerance. A passing case prints one compact summary; command,
+input, JSON, row-count, metadata, or numeric mismatches return nonzero.
+
+## Known edge cases
+
+The Haskell executable cannot produce JSON for an empty set, a singleton, or a
+set whose every prefix is filtered at the default `/8`--`/24` range: after
+filtering, it calls `foldl1` on an empty list and exits nonzero. Rust returns an
+empty result for these inputs. The validator reports the Haskell command
+failure, so omit such cases from a passing conformance run or treat that
+failure as the expected oracle limitation.
+
+Rust also intentionally treats alpha decreases at or below its `1e-12` grid
+epsilon as non-decreasing when building the spectrum. Consequently, a
+sub-epsilon spectrum turn can produce fewer Rust rows than Haskell; row counts
+remain exact in the validator, so use cases with meaningful spectrum curvature
+when expecting a pass.
+
+There is intentionally no synthetic-data generator, scheduled CI job, or
+persisted report format. Keep sensitive and private address sets local.
````

## scripts/local/validate_maad.py

Status: added; +370; −0

```diff
@@ -1,0 +1,370 @@
+#!/usr/bin/env python3
+"""Compare Rust and Haskell MAAD JSON output for local address-set cases."""
+
+from __future__ import annotations
+
+import argparse
+import ipaddress
+import json
+import math
+import subprocess
+import sys
+from dataclasses import dataclass
+from typing import Any, Iterable
+
+
+DEFAULT_TOLERANCE = 1e-10
+MAX_ERROR_DETAILS = 12
+
+
+@dataclass(frozen=True)
+class Case:
+    name: str
+    path: str
+
+
+class CaseError(Exception):
+    """A user-facing failure for one validation case."""
+
+
+def nonnegative_float(raw: str) -> float:
+    try:
+        value = float(raw)
+    except ValueError as error:
+        raise argparse.ArgumentTypeError(f"not a number: {raw!r}") from error
+    if not math.isfinite(value) or value < 0:
+        raise argparse.ArgumentTypeError("tolerance must be finite and non-negative")
+    return value
+
+
+def parse_case_spec(raw: str) -> Case:
+    name, separator, path = raw.partition("=")
+    if not separator or not name.strip() or not path:
+        raise CaseError(f"case must be NAME=PATH, got {raw!r}")
+    name = name.strip()
+    if any(character.isspace() for character in name):
+        raise CaseError(f"case name must not contain whitespace: {name!r}")
+    return Case(name=name, path=path)
+
+
+def validate_input(path: str) -> None:
+    """Check each non-empty line without rewriting or materializing the file."""
+
+    try:
+        with open(path, "r", encoding="ascii", errors="strict", newline="") as stream:
+            for line_number, raw_line in enumerate(stream, start=1):
+                value = raw_line.strip()
+                if not value:
+                    raise CaseError(f"{path}: line {line_number}: expected an IPv4 address")
+                try:
+                    address = ipaddress.ip_address(value)
+                except ValueError as error:
+                    raise CaseError(
+                        f"{path}: line {line_number}: invalid IPv4 address {value!r}"
+                    ) from error
+                if not isinstance(address, ipaddress.IPv4Address):
+                    raise CaseError(
+                        f"{path}: line {line_number}: expected IPv4, got {value!r}"
+                    )
+    except CaseError:
+        raise
+    except (OSError, UnicodeError) as error:
+        raise CaseError(f"unable to read input {path!r}: {error}") from error
+
+
+def compact_error(text: str) -> str:
+    detail = " ".join(text.split())
+    if len(detail) > 240:
+        return detail[:237] + "..."
+    return detail
+
+
+def run_json(command: list[str], label: str) -> dict[str, Any]:
+    try:
+        completed = subprocess.run(
+            command,
+            capture_output=True,
+            check=False,
+            encoding="utf-8",
+            errors="replace",
+        )
+    except OSError as error:
+        raise CaseError(f"{label} command failed: {error}") from error
+
+    if completed.returncode != 0:
+        detail = compact_error(completed.stderr)
+        suffix = f": {detail}" if detail else ""
+        raise CaseError(f"{label} command exited {completed.returncode}{suffix}")
+
+    try:
+        result = json.loads(completed.stdout, parse_constant=_reject_nonfinite_json)
+    except (TypeError, ValueError) as error:
+        detail = compact_error(completed.stdout)
+        raise CaseError(f"{label} emitted invalid JSON{': ' + detail if detail else ''}") from error
+    if not isinstance(result, dict):
+        raise CaseError(f"{label} JSON root must be an object")
+    return result
+
+
+def _reject_nonfinite_json(value: str) -> None:
+    raise ValueError(f"non-finite JSON value {value}")
+
+
+def exact_integer(value: Any, field: str) -> int:
+    if isinstance(value, bool) or not isinstance(value, int):
+        raise CaseError(f"{field} must be an integer")
+    return value
+
+
+def normalized_prefix_lengths(result: dict[str, Any], label: str) -> tuple[int, ...]:
+    metadata = result.get("metadata")
+    if not isinstance(metadata, dict):
+        raise CaseError(f"{label} metadata must be an object")
+
+    # Rust stores the lengths directly. The oracle retains the same information
+    # as prefix_counts entries; counts are deliberately not part of this shape.
+    if "prefixLengths" in metadata:
+        raw_lengths = metadata["prefixLengths"]
+    elif "prefix_counts" in metadata:
+        raw_counts = metadata["prefix_counts"]
+        if not isinstance(raw_counts, list):
+            raise CaseError(f"{label} metadata.prefix_counts must be an array")
+        raw_lengths = []
+        for index, entry in enumerate(raw_counts):
+            if not isinstance(entry, dict) or "pl" not in entry:
+                raise CaseError(
+                    f"{label} metadata.prefix_counts[{index}] must contain pl"
+                )
+            raw_lengths.append(entry["pl"])
+    else:
+        raise CaseError(f"{label} metadata has no prefix lengths")
+
+    if not isinstance(raw_lengths, list):
+        raise CaseError(f"{label} metadata prefix lengths must be an array")
+    return tuple(
+        exact_integer(value, f"{label} metadata prefix length {index}")
+        for index, value in enumerate(raw_lengths)
+    )
+
+
+def numeric(value: Any, field: str) -> float:
+    if isinstance(value, bool) or not isinstance(value, (int, float)):
+        raise CaseError(f"{field} must be numeric")
+    converted = float(value)
+    if not math.isfinite(converted):
+        raise CaseError(f"{field} must be finite")
+    return converted
+
+
+def close_enough(left: float, right: float, absolute: float, relative: float) -> bool:
+    return abs(left - right) <= absolute + relative * max(abs(left), abs(right))
+
+
+def compare_rows(
+    rust: dict[str, Any],
+    haskell: dict[str, Any],
+    section: str,
+    fields: tuple[str, ...],
+    absolute: float,
+    relative: float,
+) -> list[str]:
+    errors: list[str] = []
+    rust_rows = rust.get(section)
+    haskell_rows = haskell.get(section)
+    if not isinstance(rust_rows, list) or not isinstance(haskell_rows, list):
+        return [f"{section} must be an array in both results"]
+    if len(rust_rows) != len(haskell_rows):
+        errors.append(f"{section} row count rust={len(rust_rows)} haskell={len(haskell_rows)}")
+    for index, (rust_row, haskell_row) in enumerate(zip(rust_rows, haskell_rows)):
+        if not isinstance(rust_row, dict) or not isinstance(haskell_row, dict):
+            errors.append(f"{section}[{index}] must be an object in both results")
+            continue
+        for field in fields:
+            try:
+                rust_value = numeric(rust_row.get(field), f"rust {section}[{index}].{field}")
+                haskell_value = numeric(
+                    haskell_row.get(field), f"haskell {section}[{index}].{field}"
+                )
+            except CaseError as error:
+                errors.append(str(error))
+                continue
+            if not close_enough(rust_value, haskell_value, absolute, relative):
+                errors.append(
+                    f"{section}[{index}].{field} rust={rust_value:.17g} "
+                    f"haskell={haskell_value:.17g}"
+                )
+            if len(errors) >= MAX_ERROR_DETAILS:
+                return errors
+    return errors
+
+
+def compare_results(
+    rust: dict[str, Any],
+    haskell: dict[str, Any],
+    absolute: float,
+    relative: float,
+) -> tuple[list[str], tuple[int, int, int, int, int]]:
+    errors: list[str] = []
+
+    try:
+        rust_metadata = rust["metadata"]
+        haskell_metadata = haskell["metadata"]
+        if not isinstance(rust_metadata, dict) or not isinstance(haskell_metadata, dict):
+            raise CaseError("metadata must be an object in both results")
+        rust_total = exact_integer(rust_metadata.get("totalAddrs"), "rust metadata.totalAddrs")
+        haskell_total = exact_integer(
+            haskell_metadata.get("totalAddrs"), "haskell metadata.totalAddrs"
+        )
+        rust_prefixes = normalized_prefix_lengths(rust, "rust")
+        haskell_prefixes = normalized_prefix_lengths(haskell, "haskell")
+    except (KeyError, CaseError) as error:
+        errors.append(str(error))
+        rust_total = haskell_total = -1
+        rust_prefixes = haskell_prefixes = ()
+
+    if rust_total != haskell_total:
+        errors.append(f"total addresses rust={rust_total} haskell={haskell_total}")
+    if rust_prefixes != haskell_prefixes:
+        errors.append(f"prefix lengths rust={list(rust_prefixes)} haskell={list(haskell_prefixes)}")
+
+    section_fields = {
+        "structure": ("q", "tauTilde", "sd"),
+        "spectrum": ("alpha", "f"),
+        "dimensions": ("q", "dim"),
+    }
+    for section, fields in section_fields.items():
+        errors.extend(compare_rows(rust, haskell, section, fields, absolute, relative))
+        if len(errors) >= MAX_ERROR_DETAILS:
+            break
+
+    counts = (
+        rust_total,
+        len(rust.get("structure", [])) if isinstance(rust.get("structure"), list) else -1,
+        len(rust.get("spectrum", [])) if isinstance(rust.get("spectrum"), list) else -1,
+        len(rust.get("dimensions", [])) if isinstance(rust.get("dimensions"), list) else -1,
+        len(rust_prefixes),
+    )
+    return errors[:MAX_ERROR_DETAILS], counts
+
+
+def case_specs(parser: argparse.ArgumentParser, args: argparse.Namespace) -> list[Case]:
+    raw_cases = [*args.named_cases, *args.cases]
+    if not raw_cases:
+        parser.error("provide at least one NAME=PATH case")
+
+    parsed: list[Case] = []
+    names: set[str] = set()
+    for raw in raw_cases:
+        try:
+            case = parse_case_spec(raw)
+        except CaseError as error:
+            parser.error(str(error))
+        if case.name in names:
+            parser.error(f"duplicate case name: {case.name!r}")
+        names.add(case.name)
+        parsed.append(case)
+    return parsed
+
+
+def build_parser() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description="Compare Rust and Haskell MAAD JSON for local IPv4 address files.",
+        epilog=(
+            "Cases are NAME=PATH; paths are passed unchanged to both binaries. "
+            "Example: %(prog)s --rust target/release/netflow-db "
+            "--haskell /path/to/MAAD sample=/tmp/addresses.txt"
+        ),
+    )
+    parser.add_argument("--rust", required=True, help="Rust netflow-db binary")
+    parser.add_argument("--haskell", required=True, help="Haskell MAAD binary")
+    parser.add_argument(
+        "--abs-tol",
+        "--absolute-tolerance",
+        dest="absolute_tolerance",
+        type=nonnegative_float,
+        default=DEFAULT_TOLERANCE,
+        help=f"absolute numeric tolerance (default: {DEFAULT_TOLERANCE:g})",
+    )
+    parser.add_argument(
+        "--rel-tol",
+        "--relative-tolerance",
+        dest="relative_tolerance",
+        type=nonnegative_float,
+        default=DEFAULT_TOLERANCE,
+        help=f"relative numeric tolerance (default: {DEFAULT_TOLERANCE:g})",
+    )
+    parser.add_argument(
+        "--case",
+        dest="named_cases",
+        action="append",
+        default=[],
+        metavar="NAME=PATH",
+        help="address file case (repeatable; positional cases are also accepted)",
+    )
+    parser.add_argument("cases", nargs="*", metavar="NAME=PATH")
+    return parser
+
+
+def run_case(
+    case: Case,
+    rust_binary: str,
+    haskell_binary: str,
+    absolute: float,
+    relative: float,
+) -> bool:
+    try:
+        validate_input(case.path)
+        rust = run_json([rust_binary, "maad", case.path], "Rust")
+        haskell = run_json(
+            [
+                haskell_binary,
+                "--input",
+                case.path,
+                "--output",
+                "-",
+                "--format",
+                "json",
+                "--structure",
+                "--spectrum",
+                "--dimensions",
+            ],
+            "Haskell",
+        )
+        errors, counts = compare_results(rust, haskell, absolute, relative)
+    except CaseError as error:
+        print(f"{case.name}: FAIL {error}")
+        return False
+
+    if errors:
+        print(f"{case.name}: FAIL")
+        for error in errors:
+            print(f"  {error}")
+        return False
+
+    total, structure_rows, spectrum_rows, dimension_rows, prefix_count = counts
+    print(
+        f"{case.name}: PASS total={total} prefixes={prefix_count} "
+        f"rows=structure:{structure_rows},spectrum:{spectrum_rows},dimensions:{dimension_rows}"
+    )
+    return True
+
+
+def main(argv: Iterable[str] | None = None) -> int:
+    parser = build_parser()
+    args = parser.parse_args(argv)
+    cases = case_specs(parser, args)
+
+    all_passed = True
+    for case in cases:
+        all_passed = run_case(
+            case,
+            args.rust,
+            args.haskell,
+            args.absolute_tolerance,
+            args.relative_tolerance,
+        ) and all_passed
+    return 0 if all_passed else 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
```

## tools/netflow-db/src/maad.rs

Status: modified; +594; −137

```diff
@@ -5,11 +5,73 @@
 use std::net::Ipv4Addr;
 
 const MIN_MAAD_ADDRESSES: usize = 2;
-const SPILLOVER_THRESHOLD: f64 = 0.05;
+const SCHEMA_VERSION: u32 = 2;
-const DELTA_Q: f64 = 1.0 / 8.0;
+const DEFAULT_FULL_THRESHOLD: f64 = 0.05;
+const DEFAULT_Q_STEP: f64 = 1.0 / 8.0;
+const DEFAULT_Q_MIN: f64 = -0.5;
+const DEFAULT_Q_MAX: f64 = 3.5;
+const DEFAULT_MIN_PREFIX_LENGTH: u8 = 8;
+const DEFAULT_MAX_PREFIX_LENGTH: u8 = 24;
+const MAX_PARENT_PREFIX_LENGTH: u8 = 31;
+const MAX_Q_VALUES: usize = 1025;
+const GRID_EPSILON: f64 = 1e-12;
-const MIN_Q: f64 = -0.5;
-const MAX_Q: f64 = 3.5;
 
+/// Configuration for the in-process MAAD estimator.
+#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub struct MaadConfig {
+    pub q_min: f64,
+    pub q_max: f64,
+    pub q_step: f64,
+    pub min_prefix_length: u8,
+    pub max_prefix_length: u8,
+    pub full_threshold: f64,
+}
+
+impl Default for MaadConfig {
+    fn default() -> Self {
+        Self {
+            q_min: DEFAULT_Q_MIN,
+            q_max: DEFAULT_Q_MAX,
+            q_step: DEFAULT_Q_STEP,
+            min_prefix_length: DEFAULT_MIN_PREFIX_LENGTH,
+            max_prefix_length: DEFAULT_MAX_PREFIX_LENGTH,
+            full_threshold: DEFAULT_FULL_THRESHOLD,
+        }
+    }
+}
+
+/// Configuration or input errors returned by the configurable estimator.
+#[derive(Clone, Debug, PartialEq, thiserror::Error)]
+pub enum MaadError {
+    #[error("q bounds must be finite (q_min={q_min}, q_max={q_max})")]
+    NonFiniteQBounds { q_min: f64, q_max: f64 },
+    #[error("q_min must not exceed q_max (q_min={q_min}, q_max={q_max})")]
+    InvalidQBounds { q_min: f64, q_max: f64 },
+    #[error("q_step must be finite and positive (q_step={q_step})")]
+    InvalidQStep { q_step: f64 },
+    #[error(
+        "q range must contain q_max on a uniform q_step grid (q_min={q_min}, q_max={q_max}, q_step={q_step})"
+    )]
+    QRangeNotAligned { q_min: f64, q_max: f64, q_step: f64 },
+    #[error("q grid would contain {requested_values} values; the maximum is {max_values}")]
+    QGridTooLarge {
+        requested_values: f64,
+        max_values: usize,
+    },
+    #[error("required q={q} is not on the configured q grid")]
+    RequiredQNotOnGrid { q: f64 },
+    #[error(
+        "prefix range must satisfy 0 <= min < max <= 31 (min={min_prefix_length}, max={max_prefix_length})"
+    )]
+    InvalidPrefixRange {
+        min_prefix_length: u8,
+        max_prefix_length: u8,
+    },
+    #[error("full_threshold must be finite and in [0, 1) (full_threshold={full_threshold})")]
+    InvalidFullThreshold { full_threshold: f64 },
+}
+
 #[derive(Clone, Debug, PartialEq, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct MaadResult {
@@ -24,6 +86,7 @@
 #[serde(rename_all = "camelCase")]
 pub struct MaadMetadata {
     pub input: &'static str,
+    pub prefix_lengths: Vec<u8>,
     pub min_prefix_length: Option<u8>,
     pub max_prefix_length: Option<u8>,
     pub total_addrs: usize,
@@ -57,35 +120,44 @@
 
 /// Compute MAAD-compatible output from an IPv4 address set.
 pub fn compute(addresses: impl IntoIterator<Item = Ipv4Addr>) -> MaadResult {
+    compute_with_config(addresses, MaadConfig::default())
+        .expect("the default MAAD configuration must be valid")
+}
+
+/// Compute MAAD-compatible output using an explicitly validated configuration.
+pub fn compute_with_config(
+    addresses: impl IntoIterator<Item = Ipv4Addr>,
+    config: MaadConfig,
+) -> Result<MaadResult, MaadError> {
+    let q_values = validate_config(&config)?;
     let mut addresses: Vec<_> = addresses.into_iter().map(u32::from).collect();
     addresses.sort_unstable();
     addresses.dedup();
     if addresses.len() < MIN_MAAD_ADDRESSES {
-        return empty_result(addresses.len());
+        return Ok(empty_result(addresses.len()));
     }
     let counts = build_prefix_counts(&addresses);
-    let min_prefix_length = first_atomic_length(&counts);
+    let prepared = prepare_valid_moments(&counts, &config);
+    if prepared.is_empty() {
+        return Ok(empty_result(addresses.len()));
-    let max_prefix_length = first_spillover_length(&counts);
-    if min_prefix_length > max_prefix_length {
-        return empty_result(addresses.len());
     }
+    let (prefix_lengths, prepared): (Vec<_>, Vec<_>) = prepared.into_iter().unzip();
+    let structure = compute_structure(&prepared, &q_values);
-    let prefix_lengths: Vec<_> = (min_prefix_length..=max_prefix_length).collect();
-    let prepared = prepare_moments(&counts, &prefix_lengths);
-    let structure = compute_structure(&prepared);
-    let spectrum = compute_spectrum(&structure);
+    let spectrum = compute_spectrum(&structure, config.q_step);
     let dimensions = compute_dimensions(&counts, &prefix_lengths, &structure, addresses.len());
-    MaadResult {
+    Ok(MaadResult {
+        schema_version: SCHEMA_VERSION,
-        schema_version: 1,
         metadata: MaadMetadata {
             input: "-",
+            min_prefix_length: prefix_lengths.first().copied(),
+            max_prefix_length: prefix_lengths.last().copied(),
+            prefix_lengths,
-            min_prefix_length: Some(min_prefix_length),
-            max_prefix_length: Some(max_prefix_length),
             total_addrs: addresses.len(),
         },
         structure,
         spectrum,
         dimensions,
-    }
+    })
 }
 
 /// Serialize a computed MAAD result using the established JSON field names.
@@ -96,9 +168,10 @@
 
 fn empty_result(total_addrs: usize) -> MaadResult {
     MaadResult {
+        schema_version: SCHEMA_VERSION,
-        schema_version: 1,
         metadata: MaadMetadata {
             input: "-",
+            prefix_lengths: Vec::new(),
             min_prefix_length: None,
             max_prefix_length: None,
             total_addrs,
@@ -136,62 +209,188 @@
     }
 }
 
+fn validate_config(config: &MaadConfig) -> Result<Vec<f64>, MaadError> {
+    if !config.q_min.is_finite() || !config.q_max.is_finite() {
+        return Err(MaadError::NonFiniteQBounds {
+            q_min: config.q_min,
+            q_max: config.q_max,
+        });
+    }
+    if config.q_min > config.q_max {
+        return Err(MaadError::InvalidQBounds {
+            q_min: config.q_min,
+            q_max: config.q_max,
+        });
+    }
+    if !config.q_step.is_finite() || config.q_step <= 0.0 {
+        return Err(MaadError::InvalidQStep {
+            q_step: config.q_step,
+        });
+    }
+    if config.min_prefix_length >= config.max_prefix_length
+        || config.max_prefix_length > MAX_PARENT_PREFIX_LENGTH
+    {
+        return Err(MaadError::InvalidPrefixRange {
+            min_prefix_length: config.min_prefix_length,
+            max_prefix_length: config.max_prefix_length,
+        });
+    }
+    if !config.full_threshold.is_finite() || !(0.0..1.0).contains(&config.full_threshold) {
+        return Err(MaadError::InvalidFullThreshold {
+            full_threshold: config.full_threshold,
+        });
+    }
+
+    let step_count = (config.q_max - config.q_min) / config.q_step;
+    if !step_count.is_finite() {
+        return Err(MaadError::QRangeNotAligned {
+            q_min: config.q_min,
+            q_max: config.q_max,
+            q_step: config.q_step,
+        });
+    }
+    let rounded_step_count = step_count.round();
+    if (step_count - rounded_step_count).abs() > GRID_EPSILON * rounded_step_count.abs().max(1.0) {
+        return Err(MaadError::QRangeNotAligned {
+            q_min: config.q_min,
+            q_max: config.q_max,
+            q_step: config.q_step,
+        });
+    }
+    let requested_values = rounded_step_count + 1.0;
+    if requested_values > MAX_Q_VALUES as f64 {
+        return Err(MaadError::QGridTooLarge {
+            requested_values,
+            max_values: MAX_Q_VALUES,
+        });
+    }
+
-fn first_atomic_length(counts: &[Vec<(u32, usize)>]) -> u8 {
+    let step_count = rounded_step_count as usize;
-    (1..=32)
-        .find(|&length| counts[length].iter().any(|&(_, count)| count == 1))
-        .map_or(33, |length| length as u8)
+    let mut q_values: Vec<_> = (0..=step_count)
+        .map(|index| config.q_min + index as f64 * config.q_step)
+        .collect();
+    for required_q in [0.0_f64, 2.0] {
+        let tolerance = GRID_EPSILON * required_q.abs().max(1.0);
+        let Some(value) = q_values
+            .iter_mut()
+            .find(|value| (**value - required_q).abs() <= tolerance)
+        else {
+            return Err(MaadError::RequiredQNotOnGrid { q: required_q });
+        };
+        *value = required_q;
+    }
+    Ok(q_values)
 }
 
-fn first_spillover_length(counts: &[Vec<(u32, usize)>]) -> u8 {
-    (1..=32)
-        .find(|&length| {
-            let capacity = 2_f64.powi(32 - length as i32);
-            counts[length]
-                .iter()
+fn is_valid_parent(count: usize, prefix_length: u8, full_threshold: f64) -> bool {
+    count > 1 && (count as f64).log2() / f64::from(32 - prefix_length) < 1.0 - full_threshold
-                .any(|&(_, count)| count as f64 / capacity >= 1.0 - SPILLOVER_THRESHOLD)
-        })
-        .map_or(33, |length| length as u8)
 }
 
-fn prepare_moments(counts: &[Vec<(u32, usize)>], prefix_lengths: &[u8]) -> Vec<PreparedMoment> {
-    prefix_lengths
-        .iter()
-        .map(|&prefix_length| {
-            // A /32 has no child prefix. Treating it as an empty moment extends the
-            // estimator safely to small sparse sets, where both legacy ports indexed /33.
-            let Some(children) = counts.get(usize::from(prefix_length) + 1) else {
-                return PreparedMoment {
-                    parent_counts: Vec::new(),
-                    child_counts: Vec::new(),
-                };
-            };
-            let mut parent_counts = Vec::new();
-            let mut child_counts = Vec::new();
-            let mut next_child = 0;
-            for &(prefix, count) in &counts[usize::from(prefix_length)] {
-                if count <= 1 {
-                    continue;
-                }
-                let first_child = prefix << 1;
-                while next_child < children.len() && children[next_child].0 < first_child {
-                    next_child += 1;
-                }
-                let mut child_counts_for_parent = Vec::with_capacity(2);
-                while next_child < children.len() && children[next_child].0 <= first_child | 1 {
-                    child_counts_for_parent.push(children[next_child].1);
-                    next_child += 1;
-                }
-                if !child_counts_for_parent.is_empty() {
-                    parent_counts.push(count);
-                    child_counts.push(child_counts_for_parent);
-                }
+fn prepare_valid_moments(
+    counts: &[Vec<(u32, usize)>],
+    config: &MaadConfig,
+) -> Vec<(u8, PreparedMoment)> {
+    let mut prepared = Vec::new();
+    let mut path_allowed = vec![true; counts[0].len()];
+
+    for prefix_length in 0..=config.max_prefix_length {
+        let parents = &counts[usize::from(prefix_length)];
+        let children = &counts[usize::from(prefix_length) + 1];
+
+        if prefix_length >= config.min_prefix_length {
+            let moment = prepare_moment_at_length(
+                parents,
+                children,
+                &path_allowed,
+                prefix_length,
+                config.full_threshold,
+            );
+            if !moment.parent_counts.is_empty() {
+                prepared.push((prefix_length, moment));
             }
+        }
+
+        if prefix_length < config.max_prefix_length {
+            path_allowed = propagate_allowed_paths(
+                parents,
+                children,
+                &path_allowed,
+                prefix_length,
+                config.full_threshold,
+            );
+        }
+    }
+
+    prepared
+}
+
+fn prepare_moment_at_length(
+    parents: &[(u32, usize)],
+    children: &[(u32, usize)],
+    path_allowed: &[bool],
+    prefix_length: u8,
+    full_threshold: f64,
+) -> PreparedMoment {
+    let mut parent_counts = Vec::new();
+    let mut child_counts = Vec::new();
+    let mut next_child = 0;
+
+    for (parent_index, &(prefix, count)) in parents.iter().enumerate() {
+        let first_child = prefix << 1;
+        while next_child < children.len() && children[next_child].0 < first_child {
+            next_child += 1;
+        }
+        let child_start = next_child;
+        while next_child < children.len() && children[next_child].0 <= first_child | 1 {
+            next_child += 1;
+        }
+        if path_allowed[parent_index]
+            && is_valid_parent(count, prefix_length, full_threshold)
+            && child_start < next_child
+        {
+            parent_counts.push(count);
+            child_counts.push(
+                children[child_start..next_child]
+                    .iter()
+                    .map(|&(_, child_count)| child_count)
+                    .collect(),
+            );
+        }
+    }
+
+    PreparedMoment {
+        parent_counts,
+        child_counts,
+    }
+}
+
+fn propagate_allowed_paths(
+    parents: &[(u32, usize)],
+    children: &[(u32, usize)],
+    path_allowed: &[bool],
+    prefix_length: u8,
+    full_threshold: f64,
+) -> Vec<bool> {
+    let mut child_path_allowed = Vec::with_capacity(children.len());
+    let mut next_child = 0;
+
+    for (parent_index, &(prefix, count)) in parents.iter().enumerate() {
+        let first_child = prefix << 1;
+        while next_child < children.len() && children[next_child].0 < first_child {
+            next_child += 1;
+        }
+        let child_start = next_child;
+        while next_child < children.len() && children[next_child].0 <= first_child | 1 {
+            next_child += 1;
+        }
+        let is_branch = next_child - child_start == 2;
+        let allowed = path_allowed[parent_index]
+            && (!is_branch || is_valid_parent(count, prefix_length, full_threshold));
+        child_path_allowed.extend(std::iter::repeat_n(allowed, next_child - child_start));
+    }
+
+    debug_assert_eq!(child_path_allowed.len(), children.len());
+    child_path_allowed
-            PreparedMoment {
-                parent_counts,
-                child_counts,
-            }
-        })
-        .collect()
 }
 
 fn one_moment(prepared: &PreparedMoment, powers: &[f64]) -> (f64, f64) {
@@ -221,7 +420,10 @@
     (this_z.log2() - next_z.log2(), d2)
 }
 
+fn compute_structure(prepared: &[PreparedMoment], q_values: &[f64]) -> Vec<StructureRow> {
+    if prepared.is_empty() {
+        return Vec::new();
-fn compute_structure(prepared: &[PreparedMoment]) -> Vec<StructureRow> {
+    }
     let max_count = prepared
         .iter()
         .flat_map(|moment| {
@@ -233,9 +435,10 @@
         .copied()
         .max()
         .unwrap_or_default();
-    (0..=((MAX_Q - MIN_Q) / DELTA_Q) as usize)
+    q_values
-        .map(|index| {
+        .iter()
+        .copied()
+        .map(|q| {
-            let q = MIN_Q + index as f64 * DELTA_Q;
             let powers: Vec<_> = (0..=max_count)
                 .map(|count| (count as f64).powf(q))
                 .collect();
@@ -249,18 +452,18 @@
             StructureRow {
                 q,
                 tau_tilde: tau_sum / count,
+                sd: d2_sum.sqrt() / count,
-                sd: (d2_sum / count).sqrt(),
             }
         })
         .collect()
 }
 
-fn compute_spectrum(structure: &[StructureRow]) -> Vec<SpectrumRow> {
+fn compute_spectrum(structure: &[StructureRow], q_step: f64) -> Vec<SpectrumRow> {
     let alphas: Vec<_> = structure
         .windows(3)
         .map(|rows| {
             let row = rows[1];
-            let alpha = (rows[2].tau_tilde - rows[0].tau_tilde) / (2.0 * DELTA_Q);
+            let alpha = (rows[2].tau_tilde - rows[0].tau_tilde) / (2.0 * q_step);
             SpectrumRow {
                 alpha,
                 f: row.q * alpha - row.tau_tilde,
@@ -270,7 +473,7 @@
     let mut rows = Vec::new();
     let mut started = false;
     for pair in alphas.windows(2) {
-        let decreasing = pair[0].alpha > pair[1].alpha;
+        let decreasing = pair[0].alpha - pair[1].alpha > GRID_EPSILON;
         if !started && !decreasing {
             continue;
         }
@@ -289,6 +492,9 @@
     structure: &[StructureRow],
     total_addresses: usize,
 ) -> Vec<DimensionRow> {
+    if prefix_lengths.is_empty() || structure.is_empty() {
+        return Vec::new();
+    }
     let mut rows = vec![DimensionRow {
         q: 1.0,
         dim: info_dimension(counts, prefix_lengths, total_addresses),
@@ -350,38 +556,32 @@
         assert!((left - right).abs() <= 1e-12, "{left} != {right}");
     }
 
+    fn reference_compute(
+        addresses: impl IntoIterator<Item = Ipv4Addr>,
+        config: MaadConfig,
+    ) -> MaadResult {
-    fn reference_compute(addresses: impl IntoIterator<Item = Ipv4Addr>) -> MaadResult {
         let addresses: BTreeSet<_> = addresses.into_iter().map(u32::from).collect();
         if addresses.len() < MIN_MAAD_ADDRESSES {
             return empty_result(addresses.len());
         }
         let counts = reference_prefix_counts(&addresses);
-        let min_prefix_length = (1..=32)
-            .find(|&length| counts[length].values().any(|&count| count == 1))
-            .map_or(33, |length| length as u8);
-        let max_prefix_length = (1..=32)
-            .find(|&length| {
-                let capacity = 2_f64.powi(32 - length as i32);
-                counts[length]
-                    .values()
+        let prepared = reference_prepare_valid_moments(&counts, &config);
+        if prepared.is_empty() {
-                    .any(|&count| count as f64 / capacity >= 1.0 - SPILLOVER_THRESHOLD)
-            })
-            .map_or(33, |length| length as u8);
-        if min_prefix_length > max_prefix_length {
             return empty_result(addresses.len());
         }
+        let (prefix_lengths, prepared): (Vec<_>, Vec<_>) = prepared.into_iter().unzip();
+        let q_values = reference_q_values(&config);
+        let structure = reference_structure(&prepared, &q_values);
-        let prefix_lengths: Vec<_> = (min_prefix_length..=max_prefix_length).collect();
+        let spectrum = compute_spectrum(&structure, config.q_step);
-        let prepared = reference_prepare_moments(&counts, &prefix_lengths);
-        let structure = reference_structure(&prepared);
-        let spectrum = compute_spectrum(&structure);
         let dimensions =
             reference_dimensions(&counts, &prefix_lengths, &structure, addresses.len());
         MaadResult {
-            schema_version: 1,
+            schema_version: SCHEMA_VERSION,
             metadata: MaadMetadata {
                 input: "-",
-                min_prefix_length: Some(min_prefix_length),
+                min_prefix_length: prefix_lengths.first().copied(),
+                max_prefix_length: prefix_lengths.last().copied(),
-                max_prefix_length: Some(max_prefix_length),
+                prefix_lengths,
                 total_addrs: addresses.len(),
             },
             structure,
@@ -402,46 +602,95 @@
         counts
     }
 
+    fn reference_prepare_valid_moments(
-    fn reference_prepare_moments(
         counts: &[BTreeMap<u32, usize>],
+        config: &MaadConfig,
+    ) -> Vec<(u8, PreparedMoment)> {
+        let mut prepared = Vec::new();
-        prefix_lengths: &[u8],
-    ) -> Vec<PreparedMoment> {
-        prefix_lengths
-            .iter()
-            .map(|&prefix_length| {
-                let Some(children) = counts.get(usize::from(prefix_length) + 1) else {
-                    return PreparedMoment {
-                        parent_counts: Vec::new(),
-                        child_counts: Vec::new(),
-                    };
-                };
+        let mut path_allowed = BTreeMap::from([(0, true)]);
+
+        for prefix_length in 0..=config.max_prefix_length {
+            let parents = &counts[usize::from(prefix_length)];
+            let children = &counts[usize::from(prefix_length) + 1];
+
+            if prefix_length >= config.min_prefix_length {
                 let mut parent_counts = Vec::new();
                 let mut child_counts = Vec::new();
+                for (&prefix, &count) in parents {
-                for (&prefix, &count) in &counts[usize::from(prefix_length)] {
+                    if !path_allowed[&prefix]
+                        || !reference_valid_parent(count, prefix_length, config.full_threshold)
+                    {
-                    if count <= 1 {
                         continue;
                     }
+                    let child_counts_for_parent: Vec<_> = [prefix << 1, (prefix << 1) | 1]
-                    let children: Vec<_> = [prefix << 1, (prefix << 1) | 1]
                         .into_iter()
                         .filter_map(|child| children.get(&child).copied())
                         .collect();
-                    if !children.is_empty() {
+                    if !child_counts_for_parent.is_empty() {
                         parent_counts.push(count);
+                        child_counts.push(child_counts_for_parent);
-                        child_counts.push(children);
                     }
                 }
+                if !parent_counts.is_empty() {
+                    prepared.push((
+                        prefix_length,
+                        PreparedMoment {
+                            parent_counts,
+                            child_counts,
+                        },
+                    ));
+                }
+            }
+
+            if prefix_length < config.max_prefix_length {
+                path_allowed = children
+                    .keys()
+                    .map(|&child| {
+                        let parent = child >> 1;
+                        let parent_count = parents[&parent];
+                        let is_branch = children.contains_key(&(parent << 1))
+                            && children.contains_key(&((parent << 1) | 1));
+                        let allowed = path_allowed[&parent]
+                            && (!is_branch
+                                || reference_valid_parent(
+                                    parent_count,
+                                    prefix_length,
+                                    config.full_threshold,
+                                ));
+                        (child, allowed)
+                    })
+                    .collect();
+            }
+        }
+
+        prepared
+    }
+
+    fn reference_valid_parent(count: usize, prefix_length: u8, full_threshold: f64) -> bool {
+        count > 1 && (count as f64).log2() / f64::from(32 - prefix_length) < 1.0 - full_threshold
+    }
+
+    fn reference_q_values(config: &MaadConfig) -> Vec<f64> {
+        let count = ((config.q_max - config.q_min) / config.q_step).round() as usize;
+        (0..=count)
+            .map(|index| {
+                let q = config.q_min + index as f64 * config.q_step;
+                if q.abs() <= GRID_EPSILON {
+                    0.0
+                } else if (q - 2.0).abs() <= GRID_EPSILON {
-                PreparedMoment {
-                    parent_counts,
-                    child_counts,
+                    2.0
+                } else {
+                    q
                 }
             })
             .collect()
     }
 
-    fn reference_structure(prepared: &[PreparedMoment]) -> Vec<StructureRow> {
-        (0..=((MAX_Q - MIN_Q) / DELTA_Q) as usize)
-            .map(|index| {
+    fn reference_structure(prepared: &[PreparedMoment], q_values: &[f64]) -> Vec<StructureRow> {
+        q_values
-                let q = MIN_Q + index as f64 * DELTA_Q;
+            .iter()
+            .copied()
+            .map(|q| {
                 let (tau_sum, d2_sum) = prepared
                     .iter()
                     .map(|moment| {
@@ -479,7 +728,7 @@
                 StructureRow {
                     q,
                     tau_tilde: tau_sum / count,
-                    sd: (d2_sum / count).sqrt(),
+                    sd: d2_sum.sqrt() / count,
                 }
             })
             .collect()
@@ -535,8 +784,9 @@
     }
 
     fn assert_matches_reference(addresses: Vec<Ipv4Addr>) {
-        let result = compute(addresses.clone());
+        let config = MaadConfig::default();
+        let result = compute_with_config(addresses.clone(), config).unwrap();
+        let reference = reference_compute(addresses, config);
-        let reference = reference_compute(addresses);
         assert_eq!(result.metadata, reference.metadata);
         assert_eq!(result.structure.len(), reference.structure.len());
         assert_eq!(result.spectrum.len(), reference.spectrum.len());
@@ -611,31 +861,40 @@
         let result = compute(addresses);
 
         assert_eq!(result.metadata.total_addrs, 513);
+        assert_eq!(result.metadata.prefix_lengths, (8..=22).collect::<Vec<_>>());
+        assert_eq!(result.metadata.min_prefix_length, Some(8));
-        assert_eq!(result.metadata.min_prefix_length, Some(1));
+        assert_eq!(result.metadata.max_prefix_length, Some(22));
-        assert_eq!(result.metadata.max_prefix_length, Some(23));
         assert_eq!(result.structure.len(), 33);
         close(result.structure[0].q, -0.5);
+        close(result.structure[0].tau_tilde, 0.0);
-        close(result.structure[0].tau_tilde, -0.06521739130434782);
         close(result.structure[16].q, 1.5);
-        close(result.structure[32].tau_tilde, 0.10869565217391304);
-        assert_eq!(result.spectrum.len(), 1);
-        close(result.spectrum[0].alpha, 0.043478260869565216);
+        close(result.structure[32].tau_tilde, 0.0);
+        assert!(result.spectrum.is_empty());
-        close(result.spectrum[0].f, 0.043478260869565216);
         assert_eq!(result.dimensions.len(), 3);
         close(result.dimensions[0].q, 1.0);
         close(result.dimensions[0].dim, 0.0);
         close(result.dimensions[1].q, 0.0);
-        close(result.dimensions[1].dim, 0.043478260869565216);
+        close(result.dimensions[1].dim, 0.0);
         close(result.dimensions[2].q, 2.0);
+        close(result.dimensions[2].dim, 0.0);
+    }
+
+    #[test]
+    fn linear_structure_curve_has_no_spectrum_rows() {
+        let addresses = (0..1024).map(|index| Ipv4Addr::from(index << 22));
+
+        let result = compute(addresses);
+
+        assert!(result.spectrum.is_empty());
-        close(result.dimensions[2].dim, 0.043478260869565216);
     }
 
     #[test]
     fn sparse_two_address_set_is_safe_at_the_32_bit_boundary() {
         let result = compute([Ipv4Addr::new(192, 0, 2, 1), Ipv4Addr::new(192, 0, 2, 2)]);
 
+        assert_eq!(result.metadata.prefix_lengths, (8..=24).collect::<Vec<_>>());
-        assert_eq!(result.metadata.min_prefix_length, Some(31));
+        assert_eq!(result.metadata.min_prefix_length, Some(8));
-        assert_eq!(result.metadata.max_prefix_length, Some(32));
+        assert_eq!(result.metadata.max_prefix_length, Some(24));
         assert_eq!(result.structure.len(), 33);
         assert!(result.structure.iter().all(|row| row.q.is_finite()));
         assert!(
@@ -653,7 +912,205 @@
 
         assert_eq!(
             String::from_utf8(output).unwrap(),
+            "{\"schemaVersion\":2,\"metadata\":{\"input\":\"-\",\"prefixLengths\":[],\"minPrefixLength\":null,\"maxPrefixLength\":null,\"totalAddrs\":0},\"structure\":[],\"spectrum\":[],\"dimensions\":[]}\n"
+        );
+    }
+
+    #[test]
+    fn default_config_uses_valid_prefixes_in_the_candidate_range() {
+        let addresses = (0..2)
+            .flat_map(|third| (0..=255).map(move |fourth| Ipv4Addr::new(10, 0, third, fourth)))
+            .chain([Ipv4Addr::new(192, 0, 2, 1)]);
+
+        let result = compute(addresses);
+
+        assert_eq!(result.metadata.prefix_lengths, (8..=22).collect::<Vec<_>>());
+        assert_eq!(result.metadata.min_prefix_length, Some(8));
+        assert_eq!(result.metadata.max_prefix_length, Some(22));
+        assert_eq!(result.structure.len(), 33);
+    }
+
+    #[test]
+    fn configurable_q_grid_is_uniform_and_includes_dimension_qs() {
+        let config = MaadConfig {
+            q_min: -1.0,
+            q_max: 2.0,
+            q_step: 0.5,
+            ..MaadConfig::default()
+        };
+        let result = compute_with_config(
+            [Ipv4Addr::new(192, 0, 2, 1), Ipv4Addr::new(192, 0, 2, 2)],
+            config,
+        )
+        .unwrap();
+
+        let qs: Vec<_> = result.structure.iter().map(|row| row.q).collect();
+        assert_eq!(qs, vec![-1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0]);
+        assert_eq!(
+            result
+                .dimensions
+                .iter()
+                .map(|row| row.q)
-            "{\"schemaVersion\":1,\"metadata\":{\"input\":\"-\",\"minPrefixLength\":null,\"maxPrefixLength\":null,\"totalAddrs\":0},\"structure\":[],\"spectrum\":[],\"dimensions\":[]}\n"
+                .collect::<Vec<_>>(),
+            vec![1.0, 0.0, 2.0]
         );
     }
+
+    #[test]
+    fn invalid_configuration_returns_a_typed_error() {
+        let cases = [
+            MaadConfig {
+                q_step: 0.0,
+                ..MaadConfig::default()
+            },
+            MaadConfig {
+                q_min: 1.0,
+                q_max: 0.0,
+                ..MaadConfig::default()
+            },
+            MaadConfig {
+                q_min: -0.25,
+                q_max: 2.0,
+                q_step: 0.5,
+                ..MaadConfig::default()
+            },
+            MaadConfig {
+                min_prefix_length: 25,
+                max_prefix_length: 24,
+                ..MaadConfig::default()
+            },
+            MaadConfig {
+                min_prefix_length: 24,
+                max_prefix_length: 24,
+                ..MaadConfig::default()
+            },
+            MaadConfig {
+                full_threshold: 1.0,
+                ..MaadConfig::default()
+            },
+        ];
+
+        for config in cases {
+            assert!(
+                compute_with_config(
+                    [Ipv4Addr::new(192, 0, 2, 1), Ipv4Addr::new(192, 0, 2, 2)],
+                    config,
+                )
+                .is_err()
+            );
+        }
+    }
+
+    #[test]
+    fn q_grid_rejects_unbounded_output_and_missing_required_values() {
+        let too_many_values = MaadConfig {
+            q_min: 0.0,
+            q_max: 2.0,
+            q_step: 0.001,
+            ..MaadConfig::default()
+        };
+        let rounded_but_absent_q2 = MaadConfig {
+            q_min: -1e13,
+            q_max: 1e13,
+            q_step: 5e12,
+            ..MaadConfig::default()
+        };
+
+        assert!(matches!(
+            compute_with_config([], too_many_values),
+            Err(MaadError::QGridTooLarge { .. })
+        ));
+        assert_eq!(
+            compute_with_config([], rounded_but_absent_q2),
+            Err(MaadError::RequiredQNotOnGrid { q: 2.0 })
+        );
+    }
+
+    #[test]
+    fn one_sided_nearly_full_parents_are_removed_before_children_are_prepared() {
+        let nearly_full = (0..200).map(|last| Ipv4Addr::new(10, 0, 0, last));
+        let valid = [Ipv4Addr::new(10, 0, 1, 0), Ipv4Addr::new(10, 0, 1, 1)];
+        let addresses = nearly_full.chain(valid);
+        let config = MaadConfig {
+            q_min: 0.0,
+            q_max: 2.0,
+            q_step: 1.0,
+            min_prefix_length: 24,
+            max_prefix_length: 25,
+            ..MaadConfig::default()
+        };
+
+        let result = compute_with_config(addresses, config).unwrap();
+
+        assert_eq!(result.metadata.prefix_lengths, vec![24, 25]);
+        close(result.structure[0].tau_tilde, 0.0);
+        close(result.structure[2].tau_tilde, 0.0);
+    }
+
+    #[test]
+    fn sparse_sets_with_no_valid_candidate_level_are_safe() {
+        let config = MaadConfig::default();
+        let result = compute_with_config(
+            [Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(192, 0, 2, 1)],
+            config,
+        )
+        .unwrap();
+
+        assert_eq!(result.metadata.total_addrs, 2);
+        assert!(result.metadata.prefix_lengths.is_empty());
+        assert!(result.metadata.min_prefix_length.is_none());
+        assert!(result.structure.is_empty());
+        assert!(result.dimensions.is_empty());
+    }
+
+    #[test]
+    fn full_branching_ancestors_prune_their_descendants() {
+        let addresses = (0..8)
+            .chain(8..14)
+            .map(|last| Ipv4Addr::new(192, 0, 2, last));
+        let config = MaadConfig {
+            min_prefix_length: 27,
+            max_prefix_length: 29,
+            ..MaadConfig::default()
+        };
+
+        let result = compute_with_config(addresses, config).unwrap();
+
+        assert_eq!(result.metadata.prefix_lengths, vec![27]);
+        assert_eq!(result.metadata.min_prefix_length, Some(27));
+        assert_eq!(result.metadata.max_prefix_length, Some(27));
+    }
+
+    #[test]
+    fn duplicate_addresses_do_not_change_a_non_empty_result() {
+        let unique = [Ipv4Addr::new(192, 0, 2, 1), Ipv4Addr::new(192, 0, 2, 2)];
+        let duplicate = [unique[0], unique[0], unique[1], unique[1], unique[1]];
+
+        assert_eq!(compute(unique), compute(duplicate));
+    }
+
+    #[test]
+    fn structure_sd_uses_upstream_root_mean_square_scaling() {
+        let addresses = [
+            Ipv4Addr::new(10, 0, 0, 1),
+            Ipv4Addr::new(10, 0, 0, 2),
+            Ipv4Addr::new(10, 0, 0, 3),
+            Ipv4Addr::new(10, 0, 1, 1),
+            Ipv4Addr::new(10, 0, 1, 2),
+            Ipv4Addr::new(10, 0, 1, 129),
+        ];
+        let config = MaadConfig {
+            q_min: 0.0,
+            q_max: 2.0,
+            q_step: 1.0,
+            min_prefix_length: 23,
+            max_prefix_length: 24,
+            ..MaadConfig::default()
+        };
+
+        let result = compute_with_config(addresses, config).unwrap();
+        let q2 = result.structure.iter().find(|row| row.q == 2.0).unwrap();
+
+        close(q2.sd, (2.0 / 49.0_f64).sqrt() / 2.0);
+    }
 }
```

## tools/netflow-db/src/pipeline.rs

Status: modified; +54; −3

```diff
@@ -466,6 +466,7 @@
     connection: &Connection,
     pipeline: &ResolvedPipeline,
 ) -> Result<(), PipelineError> {
+    let maad_config = serde_json::to_value(crate::maad::MaadConfig::default())?;
     let schema = json!({
         "version": 2,
         "tables": [
@@ -484,9 +485,12 @@
             "input_contract": nfdump::INPUT_CONTRACT,
             "output_contract": nfdump::OUTPUT_CONTRACT
         },
-        // This is a result-contract identifier retained for existing databases;
+        "maad": {
+            "enabled": pipeline.run_maad,
+            "backend": "in-process",
+            "contract_version": 2,
+            "config": maad_config
+        }
-        // the in-process Rust implementation is byte-compatible with that backend.
-        "maad": {"enabled": pipeline.run_maad, "backend": "subprocess", "contract_version": 1}
     });
     let identity = ProductIdentity::create(
         &schema,
@@ -2390,6 +2394,53 @@
     }
 
     #[test]
+    fn pipeline_persists_in_process_maad_identity() {
+        let temporary = tempdir().unwrap();
+        let database = temporary.path().join("pipeline.sqlite");
+        let config = temporary.path().join("pipeline.json");
+        fs::write(
+            &config,
+            serde_json::to_vec(&json!({
+                "database_path": database,
+                "timezone": "UTC",
+                "run_maad": true,
+                "inputs": []
+            }))
+            .unwrap(),
+        )
+        .unwrap();
+
+        run(PipelineRequest::config(&config)).unwrap();
+
+        let connection = Connection::open(database).unwrap();
+        let config_json: Value = connection
+            .query_row(
+                "SELECT config_json FROM pipeline_product WHERE singleton = 1",
+                [],
+                |row| row.get::<_, String>(0),
+            )
+            .unwrap()
+            .parse()
+            .unwrap();
+        assert_eq!(
+            config_json["maad"],
+            json!({
+                "enabled": true,
+                "backend": "in-process",
+                "contract_version": 2,
+                "config": {
+                    "q_min": -0.5,
+                    "q_max": 3.5,
+                    "q_step": 0.125,
+                    "min_prefix_length": 8,
+                    "max_prefix_length": 24,
+                    "full_threshold": 0.05
+                }
+            })
+        );
+    }
+
+    #[test]
     fn config_run_publishes_dense_five_minute_buckets_and_only_complete_rollups() {
         let temporary = tempdir().unwrap();
         let mapping = temporary.path().join("mapping.json");
```

## tools/netflow-db/tests/fixtures/maad-conformance/README.md

Status: added; +15; −0

```diff
@@ -1,0 +1,15 @@
+# MAAD conformance fixtures
+
+Each `golden.json` is raw JSON emitted independently by the Haskell `MAAD`
+oracle from `chris-misa/maad` at commit
+`3ae75363d44e08faacdee186d4ff8906c6ccd06a`, with the sole source adjustment
+`deltaQ = 1.0 / 8.0` (instead of `1.0 / 16.0`). The oracle was invoked with
+`--input - --output - --format json --structure --spectrum --dimensions`,
+feeding the corresponding address list on standard input; therefore committed
+metadata has no machine-local input path. The integration test compares the
+public Rust CLI JSON after normalizing only Rust/Haskell metadata naming.
+
+The cases cover a clustered set, deterministic random addresses, a mixed set,
+one-sided nearly-full-prefix pruning with ancestor propagation, and balanced
+branching. Empty/sparse inputs and the 1024-address uniform-rounding case are
+intentionally excluded.
```

## tools/netflow-db/tests/fixtures/maad-conformance/balanced-branches/golden.json

Status: added; +154; −0

```diff
@@ -1,0 +1,154 @@
+{
+  "dimensions": [
+    { "dim": 8.427644921917286e-2, "q": 1 },
+    { "dim": 4.749146600338848e-2, "q": 0 },
+    { "dim": 5.750094034403652e-2, "q": 2 }
+  ],
+  "metadata": {
+    "didAutoStop": null,
+    "input": "-",
+    "maxPrefixLength": 24,
+    "minPrefixLength": 8,
+    "prefix_counts": [
+      { "count": 4, "pl": 8 },
+      { "count": 4, "pl": 9 },
+      { "count": 4, "pl": 10 },
+      { "count": 4, "pl": 11 },
+      { "count": 4, "pl": 12 },
+      { "count": 4, "pl": 13 },
+      { "count": 4, "pl": 14 },
+      { "count": 4, "pl": 15 },
+      { "count": 7, "pl": 16 },
+      { "count": 7, "pl": 17 },
+      { "count": 7, "pl": 18 },
+      { "count": 7, "pl": 19 },
+      { "count": 7, "pl": 20 },
+      { "count": 7, "pl": 21 },
+      { "count": 7, "pl": 22 },
+      { "count": 7, "pl": 23 },
+      { "count": 7, "pl": 24 }
+    ],
+    "totalAddrs": 128
+  },
+  "schemaVersion": 1,
+  "spectrum": [],
+  "structure": [
+    {
+      "q": -0.5,
+      "sd": 1.645326920575005e-2,
+      "tauTilde": -5.6102421505423605e-2
+    },
+    {
+      "q": -0.375,
+      "sd": 1.4073365182973578e-2,
+      "tauTilde": -5.530793206460912e-2
+    },
+    {
+      "q": -0.25,
+      "sd": 1.1670721368496185e-2,
+      "tauTilde": -5.359821794829558e-2
+    },
+    {
+      "q": -0.125,
+      "sd": 9.371376422988997e-3,
+      "tauTilde": -5.0977259878265964e-2
+    },
+    { "q": 0, "sd": 7.277524401549904e-3, "tauTilde": -4.749146600338848e-2 },
+    {
+      "q": 0.125,
+      "sd": 5.456750730825126e-3,
+      "tauTilde": -4.322103480029435e-2
+    },
+    {
+      "q": 0.25,
+      "sd": 3.939700756730238e-3,
+      "tauTilde": -3.826793491517653e-2
+    },
+    {
+      "q": 0.375,
+      "sd": 2.724901000914254e-3,
+      "tauTilde": -3.274338791462162e-2
+    },
+    {
+      "q": 0.5,
+      "sd": 1.7876273535263562e-3,
+      "tauTilde": -2.6757173790453014e-2
+    },
+    {
+      "q": 0.625,
+      "sd": 1.0895807754644883e-3,
+      "tauTilde": -2.0409997245778744e-2
+    },
+    {
+      "q": 0.75,
+      "sd": 5.871183283726079e-4,
+      "tauTilde": -1.3789104600192588e-2
+    },
+    {
+      "q": 0.875,
+      "sd": 2.3705047206668285e-4,
+      "tauTilde": -6.966647375723202e-3
+    },
+    { "q": 1, "sd": 0, "tauTilde": 0 },
+    {
+      "q": 1.125,
+      "sd": 1.581222823749028e-4,
+      "tauTilde": 7.066753568461241e-3
+    },
+    {
+      "q": 1.25,
+      "sd": 2.657434654985441e-4,
+      "tauTilde": 1.4200791334906737e-2
+    },
+    {
+      "q": 1.375,
+      "sd": 3.455953172019204e-4,
+      "tauTilde": 2.137841260045105e-2
+    },
+    { "q": 1.5, "sd": 4.1459115585856213e-4, "tauTilde": 2.858303993044291e-2 },
+    {
+      "q": 1.625,
+      "sd": 4.838000151727363e-4,
+      "tauTilde": 3.5803488444396486e-2
+    },
+    { "q": 1.75, "sd": 5.590655492340367e-4, "tauTilde": 4.303254187899084e-2 },
+    {
+      "q": 1.875,
+      "sd": 6.423829221573035e-4,
+      "tauTilde": 5.026582537623958e-2
+    },
+    { "q": 2, "sd": 7.334906438556834e-4, "tauTilde": 5.750094034403652e-2 },
+    {
+      "q": 2.125,
+      "sd": 8.311060556725141e-4,
+      "tauTilde": 6.473681778901229e-2
+    },
+    { "q": 2.25, "sd": 9.336443733579606e-4, "tauTilde": 7.197324648447735e-2 },
+    {
+      "q": 2.375,
+      "sd": 1.039549375793105e-3,
+      "tauTilde": 7.921053674991818e-2
+    },
+    { "q": 2.5, "sd": 1.1474105788588907e-3, "tauTilde": 8.644928676721365e-2 },
+    {
+      "q": 2.625,
+      "sd": 1.2559860182956041e-3,
+      "tauTilde": 9.369022472124044e-2
+    },
+    { "q": 2.75, "sd": 1.3641917380118928e-3, "tauTilde": 0.10093410586658325 },
+    { "q": 2.875, "sd": 1.471083858410273e-3, "tauTilde": 0.10818164857690803 },
+    { "q": 3, "sd": 1.5758421038031495e-3, "tauTilde": 0.11543349746868865 },
+    {
+      "q": 3.125,
+      "sd": 1.6777566918015276e-3,
+      "tauTilde": 0.12269020487187088
+    },
+    { "q": 3.25, "sd": 1.7762181312777838e-3, "tauTilde": 0.129952224365334 },
+    {
+      "q": 3.375,
+      "sd": 1.8707089817726467e-3,
+      "tauTilde": 0.13721991193638372
+    },
+    { "q": 3.5, "sd": 1.9607967625899243e-3, "tauTilde": 0.14449353168539514 }
+  ]
+}
```

## tools/netflow-db/tests/fixtures/maad-conformance/balanced-branches/input.txt

Status: added; +128; −0

```diff
@@ -1,0 +1,128 @@
+10.0.0.0
+10.0.0.1
+10.0.0.10
+10.0.0.11
+10.0.0.12
+10.0.0.13
+10.0.0.14
+10.0.0.15
+10.0.0.16
+10.0.0.17
+10.0.0.18
+10.0.0.19
+10.0.0.2
+10.0.0.20
+10.0.0.21
+10.0.0.22
+10.0.0.23
+10.0.0.24
+10.0.0.25
+10.0.0.26
+10.0.0.27
+10.0.0.28
+10.0.0.29
+10.0.0.3
+10.0.0.30
+10.0.0.31
+10.0.0.4
+10.0.0.5
+10.0.0.6
+10.0.0.7
+10.0.0.8
+10.0.0.9
+10.1.0.0
+10.1.0.1
+10.1.0.10
+10.1.0.11
+10.1.0.12
+10.1.0.13
+10.1.0.14
+10.1.0.15
+10.1.0.16
+10.1.0.17
+10.1.0.18
+10.1.0.19
+10.1.0.2
+10.1.0.20
+10.1.0.21
+10.1.0.22
+10.1.0.23
+10.1.0.24
+10.1.0.25
+10.1.0.26
+10.1.0.27
+10.1.0.28
+10.1.0.29
+10.1.0.3
+10.1.0.30
+10.1.0.31
+10.1.0.4
+10.1.0.5
+10.1.0.6
+10.1.0.7
+10.1.0.8
+10.1.0.9
+11.0.0.0
+11.0.0.1
+11.0.0.10
+11.0.0.11
+11.0.0.12
+11.0.0.13
+11.0.0.14
+11.0.0.15
+11.0.0.16
+11.0.0.17
+11.0.0.18
+11.0.0.19
+11.0.0.2
+11.0.0.20
+11.0.0.21
+11.0.0.22
+11.0.0.23
+11.0.0.3
+11.0.0.4
+11.0.0.5
+11.0.0.6
+11.0.0.7
+11.0.0.8
+11.0.0.9
+11.1.0.0
+11.1.0.1
+11.1.0.10
+11.1.0.11
+11.1.0.12
+11.1.0.13
+11.1.0.14
+11.1.0.15
+11.1.0.2
+11.1.0.3
+11.1.0.4
+11.1.0.5
+11.1.0.6
+11.1.0.7
+11.1.0.8
+11.1.0.9
+12.0.0.0
+12.0.0.1
+12.0.0.10
+12.0.0.11
+12.0.0.2
+12.0.0.3
+12.0.0.4
+12.0.0.5
+12.0.0.6
+12.0.0.7
+12.0.0.8
+12.0.0.9
+12.1.0.0
+12.1.0.1
+12.1.0.2
+12.1.0.3
+12.1.0.4
+12.1.0.5
+12.1.0.6
+12.1.0.7
+13.0.0.0
+13.0.0.1
+13.0.0.2
+13.0.0.3
```

## tools/netflow-db/tests/fixtures/maad-conformance/clustered/golden.json

Status: added; +101; −0

```diff
@@ -1,0 +1,101 @@
+{
+  "dimensions": [
+    { "dim": 1.4155452840928675e-2, "q": 1 },
+    { "dim": 5.8823529411764705e-2, "q": 0 },
+    { "dim": 3.272902050143441e-2, "q": 2 }
+  ],
+  "metadata": {
+    "didAutoStop": null,
+    "input": "-",
+    "maxPrefixLength": 24,
+    "minPrefixLength": 8,
+    "prefix_counts": [
+      { "count": 1, "pl": 8 },
+      { "count": 1, "pl": 9 },
+      { "count": 1, "pl": 10 },
+      { "count": 1, "pl": 11 },
+      { "count": 1, "pl": 12 },
+      { "count": 1, "pl": 13 },
+      { "count": 1, "pl": 14 },
+      { "count": 1, "pl": 15 },
+      { "count": 1, "pl": 16 },
+      { "count": 1, "pl": 17 },
+      { "count": 1, "pl": 18 },
+      { "count": 1, "pl": 19 },
+      { "count": 1, "pl": 20 },
+      { "count": 1, "pl": 21 },
+      { "count": 1, "pl": 22 },
+      { "count": 1, "pl": 23 },
+      { "count": 2, "pl": 24 }
+    ],
+    "totalAddrs": 40
+  },
+  "schemaVersion": 1,
+  "spectrum": [
+    { "alpha": 8.782855672089379e-2, "f": 5.7574350636567864e-2 },
+    { "alpha": 8.283184162010643e-2, "f": 5.850775287618767e-2 },
+    { "alpha": 7.776047616984488e-2, "f": 5.8823529411764705e-2 },
+    { "alpha": 7.26891107195832e-2, "f": 5.8507752876187646e-2 },
+    { "alpha": 6.769239561879586e-2, "f": 5.757435063656787e-2 },
+    { "alpha": 6.284066497983444e-2, "f": 5.60637644989787e-2 },
+    { "alpha": 5.819613825679505e-2, "f": 5.4039154615307824e-2 },
+    { "alpha": 5.381005388309941e-2, "f": 5.1580764244279154e-2 },
+    { "alpha": 4.972097267400212e-2, "f": 4.8779301521637036e-2 },
+    { "alpha": 4.595428806454177e-2, "f": 4.572923945531554e-2 },
+    { "alpha": 4.252280436775485e-2, "f": 4.252280436775485e-2 },
+    { "alpha": 3.942812647764734e-2, "f": 3.924517859425603e-2 },
+    { "alpha": 3.666255596322004e-2, "f": 3.597116333461321e-2 },
+    { "alpha": 3.4211200790289366e-2, "f": 3.2763298402745646e-2 },
+    { "alpha": 3.205405898412938e-2, "f": 2.9671256659209888e-2 },
+    { "alpha": 3.016790799103028e-2, "f": 2.673223305548963e-2 },
+    { "alpha": 2.8527902924729343e-2, "f": 2.3972021303534603e-2 },
+    { "alpha": 2.7108846746770665e-2, "f": 2.1406494489078085e-2 },
+    { "alpha": 2.588613884765166e-2, "f": 1.9043257193868908e-2 },
+    { "alpha": 2.4836435482051306e-2, "f": 1.6883297526329194e-2 },
+    { "alpha": 2.3938068837862703e-2, "f": 1.4922525513243842e-2 },
+    { "alpha": 2.3171274949846915e-2, "f": 1.3153132923390917e-2 },
+    { "alpha": 2.2518277827165672e-2, "f": 1.1564746458505214e-2 },
+    { "alpha": 2.1963270975115023e-2, "f": 1.0145371770390016e-2 },
+    { "alpha": 2.1492330034806523e-2, "f": 8.882141742530214e-3 },
+    { "alpha": 2.1093282853986095e-2, "f": 7.761891157221472e-3 },
+    { "alpha": 2.0755556657216512e-2, "f": 6.771583404965288e-3 },
+    { "alpha": 2.0470016412860365e-2, "f": 5.898615077895963e-3 },
+    { "alpha": 2.0228804050283045e-2, "f": 5.131022493520557e-3 },
+    { "alpha": 2.0025184775111782e-2, "f": 4.457611391138816e-3 }
+  ],
+  "structure": [
+    { "q": -0.5, "sd": 0, "tauTilde": -0.10270156165675515 },
+    { "q": -0.375, "sd": 0, "tauTilde": -9.081887225892442e-2 },
+    { "q": -0.25, "sd": 0, "tauTilde": -7.953148981679131e-2 },
+    { "q": -0.125, "sd": 0, "tauTilde": -6.886173307870097e-2 },
+    { "q": 0, "sd": 0, "tauTilde": -5.8823529411764705e-2 },
+    { "q": 0.125, "sd": 0, "tauTilde": -4.942161403623975e-2 },
+    { "q": 0.25, "sd": 0, "tauTilde": -4.0651251731868905e-2 },
+    { "q": 0.375, "sd": 0, "tauTilde": -3.2498515131540784e-2 },
+    { "q": 0.5, "sd": 0, "tauTilde": -2.4941085486910295e-2 },
+    { "q": 0.625, "sd": 0, "tauTilde": -1.794948056734202e-2 },
+    { "q": 0.75, "sd": 0, "tauTilde": -1.1488572016135443e-2 },
+    { "q": 0.875, "sd": 0, "tauTilde": -5.51923739884149e-3 },
+    { "q": 1, "sd": 0, "tauTilde": 0 },
+    { "q": 1.125, "sd": 0, "tauTilde": 5.111463693097222e-3 },
+    { "q": 1.25, "sd": 0, "tauTilde": 9.857031619411835e-3 },
+    { "q": 1.375, "sd": 0, "tauTilde": 1.4277102683902232e-2 },
+    { "q": 1.5, "sd": 0, "tauTilde": 1.8409831816984176e-2 },
+    { "q": 1.625, "sd": 0, "tauTilde": 2.2290617429934576e-2 },
+    { "q": 1.75, "sd": 0, "tauTilde": 2.5951808814741746e-2 },
+    { "q": 1.875, "sd": 0, "tauTilde": 2.9422593161116912e-2 },
+    { "q": 2, "sd": 0, "tauTilde": 3.272902050143441e-2 },
+    { "q": 2.125, "sd": 0, "tauTilde": 3.589412787302983e-2 },
+    { "q": 2.25, "sd": 0, "tauTilde": 3.893812937194724e-2 },
+    { "q": 2.375, "sd": 0, "tauTilde": 4.18786450824955e-2 },
+    { "q": 2.5, "sd": 0, "tauTilde": 4.473094810940897e-2 },
+    { "q": 2.625, "sd": 0, "tauTilde": 4.750821453928692e-2 },
+    { "q": 2.75, "sd": 0, "tauTilde": 5.022176585318772e-2 },
+    { "q": 2.875, "sd": 0, "tauTilde": 5.288129704798855e-2 },
+    { "q": 3, "sd": 0, "tauTilde": 5.549508656668425e-2 },
+    { "q": 3.125, "sd": 0, "tauTilde": 5.807018621229268e-2 },
+    { "q": 3.25, "sd": 0, "tauTilde": 6.061259066989934e-2 },
+    { "q": 3.375, "sd": 0, "tauTilde": 6.312738722486344e-2 },
+    { "q": 3.5, "sd": 0, "tauTilde": 6.561888686367728e-2 }
+  ]
+}
```

## tools/netflow-db/tests/fixtures/maad-conformance/clustered/input.txt

Status: added; +40; −0

```diff
@@ -1,0 +1,40 @@
+10.0.0.0
+10.0.0.1
+10.0.0.2
+10.0.0.3
+10.0.0.4
+10.0.0.5
+10.0.0.6
+10.0.0.7
+10.0.0.8
+10.0.0.9
+10.0.0.10
+10.0.0.11
+10.0.0.12
+10.0.0.13
+10.0.0.14
+10.0.0.15
+10.0.0.16
+10.0.0.17
+10.0.0.18
+10.0.0.19
+10.0.0.20
+10.0.0.21
+10.0.0.22
+10.0.0.23
+10.0.0.24
+10.0.0.25
+10.0.0.26
+10.0.0.27
+10.0.0.28
+10.0.0.29
+10.0.0.30
+10.0.0.31
+10.0.1.0
+10.0.1.1
+10.0.1.2
+10.0.1.3
+10.0.1.4
+10.0.1.5
+10.0.1.6
+10.0.1.7
```

## tools/netflow-db/tests/fixtures/maad-conformance/mixed/golden.json

Status: added; +172; −0

```diff
@@ -1,0 +1,172 @@
+{
+  "dimensions": [
+    { "dim": 0.15848541574905534, "q": 1 },
+    { "dim": 0.48630818058102654, "q": 0 },
+    { "dim": 9.563138967007195e-2, "q": 2 }
+  ],
+  "metadata": {
+    "didAutoStop": null,
+    "input": "-",
+    "maxPrefixLength": 24,
+    "minPrefixLength": 8,
+    "prefix_counts": [
+      { "count": 5, "pl": 8 },
+      { "count": 9, "pl": 9 },
+      { "count": 17, "pl": 10 },
+      { "count": 25, "pl": 11 },
+      { "count": 28, "pl": 12 },
+      { "count": 20, "pl": 13 },
+      { "count": 12, "pl": 14 },
+      { "count": 10, "pl": 15 },
+      { "count": 7, "pl": 16 },
+      { "count": 5, "pl": 17 },
+      { "count": 4, "pl": 18 },
+      { "count": 2, "pl": 19 },
+      { "count": 1, "pl": 20 },
+      { "count": 1, "pl": 21 },
+      { "count": 1, "pl": 22 },
+      { "count": 1, "pl": 23 },
+      { "count": 2, "pl": 24 }
+    ],
+    "totalAddrs": 128
+  },
+  "schemaVersion": 1,
+  "spectrum": [
+    { "alpha": 0.7151040096912356, "f": 0.4824890366161838 },
+    { "alpha": 0.6998274338318646, "f": 0.48516537815620797 },
+    { "alpha": 0.6772974801593303, "f": 0.48630818058102654 },
+    { "alpha": 0.6455726128772776, "f": 0.48401601395501814 },
+    { "alpha": 0.6040581066665642, "f": 0.4759295540283482 },
+    { "alpha": 0.5537099320218729, "f": 0.45994613518691974 },
+    { "alpha": 0.4966281828776834, "f": 0.4348016357950806 },
+    { "alpha": 0.43540034557717006, "f": 0.4002730809450279 },
+    { "alpha": 0.37259283272497956, "f": 0.357082082505681 },
+    { "alpha": 0.3105498318477856, "f": 0.30673075964486407 },
+    { "alpha": 0.2514168402023277, "f": 0.2514168402023277 },
+    { "alpha": 0.19717329055224397, "f": 0.1939653985987442 },
+    { "alpha": 0.1495019677492229, "f": 0.13758413704846761 },
+    { "alpha": 0.10951898013906955, "f": 8.535855248138464e-2 },
+    { "alpha": 7.755435301208724e-2, "f": 3.965846184530247e-2 },
+    { "alpha": 5.3151073096857016e-2, "f": 1.7518603195348509e-3 },
+    { "alpha": 3.528589755176581e-2, "f": -2.8210515231452474e-2 },
+    { "alpha": 2.2682214892117247e-2, "f": -5.0910954928079406e-2 },
+    { "alpha": 1.4077917224721459e-2, "f": -6.747555522062904e-2 },
+    { "alpha": 8.384495618307064e-3, "f": -7.91425339680771e-2 },
+    { "alpha": 4.743841571771212e-3, "f": -8.705387003816349e-2 },
+    { "alpha": 2.5196640498434952e-3, "f": -9.216134543154411e-2 },
+    { "alpha": 1.2594329703956575e-3, "f": -9.520884716112045e-2 },
+    { "alpha": 6.500881751049237e-4, "f": -9.67539243328709e-2 },
+    { "alpha": 4.787254471624869e-4, "f": -9.720345665118899e-2 }
+  ],
+  "structure": [
+    { "q": -0.5, "sd": 3.1750082682512504e-2, "tauTilde": -0.8426837994684427 },
+    {
+      "q": -0.375,
+      "sd": 2.9980992285519675e-2,
+      "tauTilde": -0.7514198098079999
+    },
+    {
+      "q": -0.25,
+      "sd": 2.8367124785529413e-2,
+      "tauTilde": -0.6612650390389927
+    },
+    {
+      "q": -0.125,
+      "sd": 2.6660944071071255e-2,
+      "tauTilde": -0.572643807385191
+    },
+    { "q": 0, "sd": 2.451805340828242e-2, "tauTilde": -0.48630818058102654 },
+    {
+      "q": 0.125,
+      "sd": 2.1713020013486028e-2,
+      "tauTilde": -0.40331943734535847
+    },
+    { "q": 0.25, "sd": 1.832313114421922e-2, "tauTilde": -0.32491502736170713 },
+    {
+      "q": 0.375,
+      "sd": 1.4686874429319392e-2,
+      "tauTilde": -0.2523049106787174
+    },
+    { "q": 0.5, "sd": 1.1179905703881741e-2, "tauTilde": -0.1864875443562389 },
+    {
+      "q": 0.625,
+      "sd": 8.017576091067168e-3,
+      "tauTilde": -0.12814786495929656
+    },
+    {
+      "q": 0.75,
+      "sd": 5.198594409935857e-3,
+      "tauTilde": -7.763745796194639e-2
+    },
+    {
+      "q": 0.875,
+      "sd": 2.576935719152974e-3,
+      "tauTilde": -3.499965677805167e-2
+    },
+    { "q": 1, "sd": 3.729791574141121e-18, "tauTilde": 9.42184265827917e-18 },
+    { "q": 1.125, "sd": 2.58451208287189e-3, "tauTilde": 2.785455327253026e-2 },
+    { "q": 1.25, "sd": 5.116550994346554e-3, "tauTilde": 4.9293322638061e-2 },
+    {
+      "q": 1.375,
+      "sd": 7.483069153304027e-3,
+      "tauTilde": 6.523004520983598e-2
+    },
+    { "q": 1.5, "sd": 9.586221403953814e-3, "tauTilde": 7.667306767282839e-2 },
+    {
+      "q": 1.625,
+      "sd": 1.1378375822747885e-2,
+      "tauTilde": 8.461863346285779e-2
+    },
+    { "q": 1.75, "sd": 1.285998184210417e-2, "tauTilde": 8.996083594704264e-2 },
+    {
+      "q": 1.875,
+      "sd": 1.405941446360083e-2,
+      "tauTilde": 9.344010785079924e-2
+    },
+    { "q": 2, "sd": 1.5013688311364334e-2, "tauTilde": 9.563138967007195e-2 },
+    {
+      "q": 2.125,
+      "sd": 1.575761960109601e-2,
+      "tauTilde": 9.695958715697961e-2
+    },
+    {
+      "q": 2.25,
+      "sd": 1.6320317129274422e-2,
+      "tauTilde": 9.772751357464872e-2
+    },
+    {
+      "q": 2.375,
+      "sd": 1.6725397355708053e-2,
+      "tauTilde": 9.814554754992241e-2
+    },
+    { "q": 2.5, "sd": 1.6992262847763635e-2, "tauTilde": 9.83574295871096e-2 },
+    {
+      "q": 2.625,
+      "sd": 1.7137270096911208e-2,
+      "tauTilde": 9.846040579252133e-2
+    },
+    {
+      "q": 2.75,
+      "sd": 1.7174531016053783e-2,
+      "tauTilde": 9.851995163088582e-2
+    },
+    {
+      "q": 2.875,
+      "sd": 1.711643429582498e-2,
+      "tauTilde": 9.858008715431195e-2
+    },
+    { "q": 3, "sd": 1.697401285087209e-2, "tauTilde": 9.867040853813017e-2 },
+    {
+      "q": 3.125,
+      "sd": 1.6757231439643777e-2,
+      "tauTilde": 9.88108001685707e-2
+    },
+    { "q": 3.25, "sd": 1.647521942872769e-2, "tauTilde": 9.901456264568395e-2 },
+    {
+      "q": 3.375,
+      "sd": 1.6136450539991503e-2,
+      "tauTilde": 9.929048214384534e-2
+    },
+    { "q": 3.5, "sd": 1.574886805827989e-2, "tauTilde": 9.96442019065783e-2 }
+  ]
+}
```

## tools/netflow-db/tests/fixtures/maad-conformance/mixed/input.txt

Status: added; +128; −0

```diff
@@ -1,0 +1,128 @@
+10.0.0.0
+10.0.0.1
+10.0.0.10
+10.0.0.11
+10.0.0.12
+10.0.0.13
+10.0.0.14
+10.0.0.15
+10.0.0.16
+10.0.0.17
+10.0.0.18
+10.0.0.19
+10.0.0.2
+10.0.0.20
+10.0.0.21
+10.0.0.22
+10.0.0.23
+10.0.0.24
+10.0.0.25
+10.0.0.26
+10.0.0.27
+10.0.0.28
+10.0.0.29
+10.0.0.3
+10.0.0.30
+10.0.0.31
+10.0.0.4
+10.0.0.5
+10.0.0.6
+10.0.0.7
+10.0.0.8
+10.0.0.9
+10.0.1.0
+10.0.1.1
+10.0.1.2
+10.0.1.3
+10.0.1.4
+10.0.1.5
+10.0.1.6
+10.0.1.7
+11.110.122.164
+11.112.132.20
+11.14.102.60
+11.164.255.200
+11.165.60.44
+11.168.28.32
+11.178.33.84
+11.184.159.128
+11.189.95.48
+11.195.171.124
+11.206.114.224
+11.208.141.100
+11.242.164.216
+11.245.13.120
+11.248.42.180
+11.28.92.4
+11.37.239.40
+11.37.51.208
+11.38.88.112
+11.42.224.220
+11.63.153.140
+11.86.46.136
+12.10.173.61
+12.109.61.217
+12.130.208.25
+12.143.92.17
+12.15.88.161
+12.160.185.49
+12.187.164.205
+12.188.205.101
+12.190.158.21
+12.202.62.249
+12.219.166.81
+12.222.149.137
+12.222.180.133
+12.252.204.105
+12.38.158.141
+12.44.125.193
+12.49.118.69
+12.57.42.29
+12.60.217.173
+12.70.110.245
+12.72.207.213
+12.82.147.73
+13.102.58.82
+13.110.138.102
+13.13.106.154
+13.131.116.178
+13.141.170.110
+13.164.178.62
+13.173.112.54
+13.173.192.250
+13.175.54.206
+13.221.10.6
+13.226.127.18
+13.232.203.34
+13.232.228.58
+13.250.87.214
+13.252.155.202
+13.254.29.130
+13.26.214.158
+13.45.45.106
+13.50.15.118
+13.60.238.14
+13.66.89.166
+13.9.63.226
+14.131.135.123
+14.135.98.87
+14.142.17.15
+14.142.8.103
+14.144.8.191
+14.160.214.131
+14.161.128.71
+14.169.232.207
+14.176.6.139
+14.185.160.99
+14.194.12.107
+14.206.16.171
+14.220.12.223
+14.236.133.179
+14.237.55.147
+14.24.137.155
+14.246.114.119
+14.246.28.163
+14.31.249.115
+14.65.160.135
+14.71.116.239
+14.77.32.255
```

## tools/netflow-db/tests/fixtures/maad-conformance/random/golden.json

Status: added; +122; −0

```diff
@@ -1,0 +1,122 @@
+{
+  "dimensions": [
+    { "dim": 0.5091568135164136, "q": 1 },
+    { "dim": 0.8115457541681342, "q": 0 },
+    { "dim": 0.6982893075100184, "q": 2 }
+  ],
+  "metadata": {
+    "didAutoStop": null,
+    "input": "-",
+    "maxPrefixLength": 17,
+    "minPrefixLength": 8,
+    "prefix_counts": [
+      { "count": 4, "pl": 8 },
+      { "count": 8, "pl": 9 },
+      { "count": 16, "pl": 10 },
+      { "count": 27, "pl": 11 },
+      { "count": 29, "pl": 12 },
+      { "count": 20, "pl": 13 },
+      { "count": 13, "pl": 14 },
+      { "count": 6, "pl": 15 },
+      { "count": 2, "pl": 16 },
+      { "count": 1, "pl": 17 }
+    ],
+    "totalAddrs": 96
+  },
+  "schemaVersion": 1,
+  "spectrum": [
+    { "alpha": 0.9007621617148542, "f": 0.8078687779356823 },
+    { "alpha": 0.8860542567850467, "f": 0.8106218076253104 },
+    { "alpha": 0.8712160883010069, "f": 0.8115457541681342 },
+    { "alpha": 0.8562881136433309, "f": 0.8106105818536058 },
+    { "alpha": 0.8413107959599657, "f": 0.8078014247472929 },
+    { "alpha": 0.826324502430104, "f": 0.803118557069487 },
+    { "alpha": 0.8113693741090875, "f": 0.7965772872043193 },
+    { "alpha": 0.7964851594536251, "f": 0.7882077497894419 },
+    { "alpha": 0.7817110099051509, "f": 0.7780545677152324 },
+    { "alpha": 0.767085241145477, "f": 0.7661763586569309 },
+    { "alpha": 0.7526450674822465, "f": 0.7526450674822465 },
+    { "alpha": 0.7384263192164844, "f": 0.7375451149026219 },
+    { "alpha": 0.7244631539410838, "f": 0.7209723626222337 },
+    { "alpha": 0.7107877727763189, "f": 0.7030329048662444 },
+    { "alpha": 0.6974301518526542, "f": 0.6838417047807805 },
+    { "alpha": 0.6844177981605781, "f": 0.6635211013465818 },
+    { "alpha": 0.6717755374062504, "f": 0.6421992179225928 },
+    { "alpha": 0.659525339886692, "f": 0.6200083072716274 },
+    { "alpha": 0.6476861887270897, "f": 0.597083069944161 },
+    { "alpha": 0.6362739931706796, "f": 0.5735589832900013 },
+    { "alpha": 0.625301548024709, "f": 0.549570677252907 },
+    { "alpha": 0.614778538881759, "f": 0.5252503906403079 },
+    { "alpha": 0.6047115913877095, "f": 0.5007265379461456 },
+    { "alpha": 0.5951043616465954, "f": 0.4761224122715155 },
+    { "alpha": 0.5859576638647361, "f": 0.4515550446932475 },
+    { "alpha": 0.5772696305610046, "f": 0.4271342348459066 },
+    { "alpha": 0.5690359001176484, "f": 0.4029617617779171 },
+    { "alpha": 0.5612498261167156, "f": 0.37913077856834265 },
+    { "alpha": 0.5539027027920147, "f": 0.35572538896984085 },
+    { "alpha": 0.5469840010072682, "f": 0.3328203996551331 }
+  ],
+  "structure": [
+    { "q": -0.5, "sd": 4.262429053244891e-2, "tauTilde": -1.2618841781943309 },
+    {
+      "q": -0.375,
+      "sd": 3.948718532252456e-2,
+      "tauTilde": -1.1465691301521548
+    },
+    { "q": -0.25, "sd": 3.627794891293679e-2, "tauTilde": -1.033059318364396 },
+    {
+      "q": -0.125,
+      "sd": 3.2991251280084774e-2,
+      "tauTilde": -0.9213785897234412
+    },
+    { "q": 0, "sd": 2.9623771521080443e-2, "tauTilde": -0.8115457541681342 },
+    {
+      "q": 0.125,
+      "sd": 2.6174219608782467e-2,
+      "tauTilde": -0.7035745676481895
+    },
+    { "q": 0.25, "sd": 2.264328410098567e-2, "tauTilde": -0.5974737257573015 },
+    {
+      "q": 0.375,
+      "sd": 1.903352004185307e-2,
+      "tauTilde": -0.49324686865819806
+    },
+    { "q": 0.5, "sd": 1.5349190884591421e-2, "tauTilde": -0.3908926001497755 },
+    {
+      "q": 0.625,
+      "sd": 1.1596076968570646e-2,
+      "tauTilde": -0.2904045251309262
+    },
+    { "q": 0.75, "sd": 7.781261395379442e-3, "tauTilde": -0.19177131028636923 },
+    {
+      "q": 0.875,
+      "sd": 3.9129024278057225e-3,
+      "tauTilde": -9.497677265463847e-2
+    },
+    { "q": 1, "sd": 1.377331113139258e-17, "tauTilde": 1.6017132519074587e-17 },
+    {
+      "q": 1.125,
+      "sd": 3.947837333661058e-3,
+      "tauTilde": 9.318449421592315e-2
+    },
+    { "q": 1.25, "sd": 7.920619671509196e-3, "tauTilde": 0.18460657980412112 },
+    { "q": 1.375, "sd": 1.1908192692166784e-2, "tauTilde": 0.2743002827011941 },
+    { "q": 1.5, "sd": 1.5900440304771484e-2, "tauTilde": 0.36230352299820084 },
+    { "q": 1.625, "sd": 1.988746966289035e-2, "tauTilde": 0.44865782066435766 },
+    { "q": 1.75, "sd": 2.3859775751064495e-2, "tauTilde": 0.5334079725383454 },
+    { "q": 1.875, "sd": 2.7808383348594756e-2, "tauTilde": 0.6166017050159203 },
+    { "q": 2, "sd": 3.172496480854753e-2, "tauTilde": 0.6982893075100184 },
+    { "q": 2.125, "sd": 3.560193275307995e-2, "tauTilde": 0.7785232521976927 },
+    { "q": 2.25, "sd": 3.943250746001074e-2, "tauTilde": 0.8573578058026883 },
+    { "q": 2.375, "sd": 4.3210759376381566e-2, "tauTilde": 0.93484863920387 },
+    { "q": 2.5, "sd": 4.6931627819061e-2, "tauTilde": 1.011052440523128 },
+    { "q": 2.625, "sd": 5.059091748983781e-2, "tauTilde": 1.0860265370507973 },
+    { "q": 2.75, "sd": 5.418527492669052e-2, "tauTilde": 1.159828530934777 },
+    { "q": 2.875, "sd": 5.771214742173305e-2, "tauTilde": 1.2325159530169814 },
+    { "q": 3, "sd": 6.116972725064629e-2, "tauTilde": 1.304145938575028 },
+    { "q": 3.125, "sd": 6.455688427161593e-2, "tauTilde": 1.3747749280463935 },
+    { "q": 3.25, "sd": 6.787309005950386e-2, "tauTilde": 1.444458395104207 },
+    { "q": 3.375, "sd": 7.11183367412341e-2, "tauTilde": 1.5132506037443971 },
+    { "q": 3.5, "sd": 7.429305359238074e-2, "tauTilde": 1.581204395356024 }
+  ]
+}
```

## tools/netflow-db/tests/fixtures/maad-conformance/random/input.txt

Status: added; +96; −0

```diff
@@ -1,0 +1,96 @@
+12.104.119.148
+12.118.143.24
+12.125.96.244
+12.13.1.112
+12.132.201.188
+12.137.202.108
+12.147.51.164
+12.147.96.228
+12.150.223.28
+12.151.7.204
+12.164.240.88
+12.181.68.124
+12.184.110.96
+12.20.58.176
+12.205.105.104
+12.208.133.32
+12.218.136.200
+12.228.23.184
+12.249.209.192
+12.251.246.16
+12.255.154.8
+12.72.34.68
+12.84.154.84
+12.92.149.44
+13.122.88.13
+13.125.178.137
+13.13.255.89
+13.131.144.125
+13.147.185.85
+13.15.129.133
+13.159.206.57
+13.179.255.197
+13.212.244.201
+13.219.250.193
+13.221.237.145
+13.243.245.1
+13.33.96.113
+13.35.236.237
+13.37.139.169
+13.46.120.165
+13.6.17.205
+13.61.47.225
+13.66.109.93
+13.79.234.53
+13.86.171.21
+13.93.45.25
+13.94.99.81
+13.97.67.157
+14.0.58.166
+14.113.158.222
+14.124.85.58
+14.140.30.218
+14.147.89.194
+14.152.230.78
+14.156.160.242
+14.160.73.230
+14.165.154.126
+14.169.241.170
+14.170.115.62
+14.182.239.14
+14.187.134.146
+14.207.139.82
+14.234.92.34
+14.236.48.118
+14.31.56.22
+14.39.184.122
+14.48.94.106
+14.55.130.174
+14.65.64.10
+14.66.39.98
+14.76.218.70
+14.77.15.182
+15.108.65.163
+15.111.119.183
+15.113.26.31
+15.127.234.179
+15.143.149.135
+15.144.245.199
+15.154.166.15
+15.161.66.219
+15.185.111.203
+15.201.182.243
+15.207.199.151
+15.212.210.47
+15.213.253.227
+15.222.14.155
+15.234.25.235
+15.33.160.187
+15.38.206.63
+15.4.189.167
+15.48.117.255
+15.66.23.195
+15.67.39.119
+15.8.14.79
+15.8.213.171
+15.99.200.211
```

## tools/netflow-db/tests/fixtures/maad-conformance/threshold-ancestor/golden.json

Status: added; +101; −0

```diff
@@ -1,0 +1,101 @@
+{
+  "dimensions": [
+    { "dim": 1.5712950457112795e-3, "q": 1 },
+    { "dim": 5.8823529411764705e-2, "q": 0 },
+    { "dim": 1.6803719801791572e-3, "q": 2 }
+  ],
+  "metadata": {
+    "didAutoStop": null,
+    "input": "-",
+    "maxPrefixLength": 24,
+    "minPrefixLength": 8,
+    "prefix_counts": [
+      { "count": 1, "pl": 8 },
+      { "count": 1, "pl": 9 },
+      { "count": 1, "pl": 10 },
+      { "count": 1, "pl": 11 },
+      { "count": 1, "pl": 12 },
+      { "count": 1, "pl": 13 },
+      { "count": 1, "pl": 14 },
+      { "count": 1, "pl": 15 },
+      { "count": 1, "pl": 16 },
+      { "count": 1, "pl": 17 },
+      { "count": 1, "pl": 18 },
+      { "count": 1, "pl": 19 },
+      { "count": 1, "pl": 20 },
+      { "count": 1, "pl": 21 },
+      { "count": 1, "pl": 22 },
+      { "count": 1, "pl": 23 },
+      { "count": 1, "pl": 24 }
+    ],
+    "totalAddrs": 202
+  },
+  "schemaVersion": 1,
+  "spectrum": [
+    { "alpha": 0.29575811766988047, "f": 4.729353127965119e-2 },
+    { "alpha": 0.24963812514142641, "f": 5.5617946668383815e-2 },
+    { "alpha": 0.1962519639920254, "f": 5.8823529411764705e-2 },
+    { "alpha": 0.1428658028426244, "f": 5.5617946668383815e-2 },
+    { "alpha": 9.674581031417037e-2, "f": 4.72935312796512e-2 },
+    { "alpha": 6.176340250763442e-2, "f": 3.673454467487608e-2 },
+    { "alpha": 3.783246075394665e-2, "f": 2.6582458451173324e-2 },
+    { "alpha": 2.2629839052876962e-2, "f": 1.825880295407461e-2 },
+    { "alpha": 1.3432655096974881e-2, "f": 1.2083259633711919e-2 },
+    { "alpha": 8.035073243923035e-3, "f": 7.787678860215442e-3 },
+    { "alpha": 4.924326155648914e-3, "f": 4.924326155648914e-3 },
+    { "alpha": 3.15037934751495e-3, "f": 3.0700849988248777e-3 },
+    { "alpha": 2.144877956836071e-3, "f": 1.8935026091663517e-3 },
+    { "alpha": 1.5769066034729629e-3, "f": 1.157935323436865e-3 },
+    { "alpha": 1.2567060502305287e-3, "f": 7.032375875988149e-4 },
+    { "alpha": 1.07638807244668e-3, "f": 4.2464284882976367e-4 },
+    { "alpha": 9.749067248147128e-4, "f": 2.551682625670992e-4 },
+    { "alpha": 9.17813897282036e-4, "f": 1.5268660730404795e-4 },
+    { "alpha": 8.857001179951567e-4, "f": 9.102825581115627e-5 },
+    { "alpha": 8.67638643226345e-4, "f": 5.4092637257424506e-5 },
+    { "alpha": 8.574811168497861e-4, "f": 3.205087192627537e-5 },
+    { "alpha": 8.517688629747933e-4, "f": 1.894129075412929e-5 },
+    { "alpha": 8.485565452756288e-4, "f": 1.1167506459630171e-5 },
+    { "alpha": 8.467501005782387e-4, "f": 6.5701188879645905e-6 },
+    { "alpha": 8.457342539441703e-4, "f": 3.857816472466798e-6 },
+    { "alpha": 8.451629988312018e-4, "f": 2.261163023750245e-6 },
+    { "alpha": 8.448417576611186e-4, "f": 1.3231414015537832e-6 },
+    { "alpha": 8.44661110218697e-4, "f": 7.730714021939727e-7 },
+    { "alpha": 8.445595246152297e-4, "f": 4.5104586302024963e-7 },
+    { "alpha": 8.445023988066496e-4, "f": 2.628167874003072e-7 }
+  ],
+  "structure": [
+    { "q": -0.5, "sd": 0, "tauTilde": -0.20391819206622544 },
+    { "q": -0.375, "sd": 0, "tauTilde": -0.16076224172853223 },
+    { "q": -0.25, "sd": 0, "tauTilde": -0.12123306069712131 },
+    { "q": -0.125, "sd": 0, "tauTilde": -8.682271231106212e-2 },
+    { "q": 0, "sd": 0, "tauTilde": -5.8823529411764705e-2 },
+    { "q": 0.125, "sd": 0, "tauTilde": -3.7759721313055765e-2 },
+    { "q": 0.25, "sd": 0, "tauTilde": -2.3107078701108605e-2 },
+    { "q": 0.375, "sd": 0, "tauTilde": -1.3573268734513172e-2 },
+    { "q": 0.5, "sd": 0, "tauTilde": -7.666228074199999e-3 },
+    { "q": 0.625, "sd": 0, "tauTilde": -4.115153546026508e-3 },
+    { "q": 0.75, "sd": 0, "tauTilde": -2.0087683109807587e-3 },
+    { "q": 0.875, "sd": 0, "tauTilde": -7.569897717827872e-4 },
+    { "q": 1, "sd": 0, "tauTilde": 0 },
+    { "q": 1.125, "sd": 0, "tauTilde": 4.740917671294412e-4 },
+    { "q": 1.25, "sd": 0, "tauTilde": 7.875948368787375e-4 },
+    { "q": 1.375, "sd": 0, "tauTilde": 1.010311256338459e-3 },
+    { "q": 1.5, "sd": 0, "tauTilde": 1.1818214877469782e-3 },
+    { "q": 1.625, "sd": 0, "tauTilde": 1.3244877688960912e-3 },
+    { "q": 1.75, "sd": 0, "tauTilde": 1.4509185058586482e-3 },
+    { "q": 1.875, "sd": 0, "tauTilde": 1.5682144500997694e-3 },
+    { "q": 2, "sd": 0, "tauTilde": 1.6803719801791572e-3 },
+    { "q": 2.125, "sd": 0, "tauTilde": 1.7896394795985586e-3 },
+    { "q": 2.25, "sd": 0, "tauTilde": 1.8972816409857434e-3 },
+    { "q": 2.375, "sd": 0, "tauTilde": 2.004009758811005e-3 },
+    { "q": 2.5, "sd": 0, "tauTilde": 2.1102238567294418e-3 },
+    { "q": 2.625, "sd": 0, "tauTilde": 2.2161488951299123e-3 },
+    { "q": 2.75, "sd": 0, "tauTilde": 2.3219113818740015e-3 },
+    { "q": 2.875, "sd": 0, "tauTilde": 2.427582458615955e-3 },
+    { "q": 3, "sd": 0, "tauTilde": 2.533202131581802e-3 },
+    { "q": 3.125, "sd": 0, "tauTilde": 2.6387928980312345e-3 },
+    { "q": 3.25, "sd": 0, "tauTilde": 2.744367409136476e-3 },
+    { "q": 3.375, "sd": 0, "tauTilde": 2.849932779185042e-3 },
+    { "q": 3.5, "sd": 0, "tauTilde": 2.9554930088381386e-3 }
+  ]
+}
```

## tools/netflow-db/tests/fixtures/maad-conformance/threshold-ancestor/input.txt

Status: added; +202; −0

```diff
@@ -1,0 +1,202 @@
+10.0.0.0
+10.0.0.1
+10.0.0.10
+10.0.0.100
+10.0.0.101
+10.0.0.102
+10.0.0.103
+10.0.0.104
+10.0.0.105
+10.0.0.106
+10.0.0.107
+10.0.0.108
+10.0.0.109
+10.0.0.11
+10.0.0.110
+10.0.0.111
+10.0.0.112
+10.0.0.113
+10.0.0.114
+10.0.0.115
+10.0.0.116
+10.0.0.117
+10.0.0.118
+10.0.0.119
+10.0.0.12
+10.0.0.120
+10.0.0.121
+10.0.0.122
+10.0.0.123
+10.0.0.124
+10.0.0.125
+10.0.0.126
+10.0.0.127
+10.0.0.128
+10.0.0.129
+10.0.0.13
+10.0.0.130
+10.0.0.131
+10.0.0.132
+10.0.0.133
+10.0.0.134
+10.0.0.135
+10.0.0.136
+10.0.0.137
+10.0.0.138
+10.0.0.139
+10.0.0.14
+10.0.0.140
+10.0.0.141
+10.0.0.142
+10.0.0.143
+10.0.0.144
+10.0.0.145
+10.0.0.146
+10.0.0.147
+10.0.0.148
+10.0.0.149
+10.0.0.15
+10.0.0.150
+10.0.0.151
+10.0.0.152
+10.0.0.153
+10.0.0.154
+10.0.0.155
+10.0.0.156
+10.0.0.157
+10.0.0.158
+10.0.0.159
+10.0.0.16
+10.0.0.160
+10.0.0.161
+10.0.0.162
+10.0.0.163
+10.0.0.164
+10.0.0.165
+10.0.0.166
+10.0.0.167
+10.0.0.168
+10.0.0.169
+10.0.0.17
+10.0.0.170
+10.0.0.171
+10.0.0.172
+10.0.0.173
+10.0.0.174
+10.0.0.175
+10.0.0.176
+10.0.0.177
+10.0.0.178
+10.0.0.179
+10.0.0.18
+10.0.0.180
+10.0.0.181
+10.0.0.182
+10.0.0.183
+10.0.0.184
+10.0.0.185
+10.0.0.186
+10.0.0.187
+10.0.0.188
+10.0.0.189
+10.0.0.19
+10.0.0.190
+10.0.0.191
+10.0.0.192
+10.0.0.193
+10.0.0.194
+10.0.0.195
+10.0.0.196
+10.0.0.197
+10.0.0.198
+10.0.0.199
+10.0.0.2
+10.0.0.20
+10.0.0.21
+10.0.0.22
+10.0.0.23
+10.0.0.24
+10.0.0.25
+10.0.0.26
+10.0.0.27
+10.0.0.28
+10.0.0.29
+10.0.0.3
+10.0.0.30
+10.0.0.31
+10.0.0.32
+10.0.0.33
+10.0.0.34
+10.0.0.35
+10.0.0.36
+10.0.0.37
+10.0.0.38
+10.0.0.39
+10.0.0.4
+10.0.0.40
+10.0.0.41
+10.0.0.42
+10.0.0.43
+10.0.0.44
+10.0.0.45
+10.0.0.46
+10.0.0.47
+10.0.0.48
+10.0.0.49
+10.0.0.5
+10.0.0.50
+10.0.0.51
+10.0.0.52
+10.0.0.53
+10.0.0.54
+10.0.0.55
+10.0.0.56
+10.0.0.57
+10.0.0.58
+10.0.0.59
+10.0.0.6
+10.0.0.60
+10.0.0.61
+10.0.0.62
+10.0.0.63
+10.0.0.64
+10.0.0.65
+10.0.0.66
+10.0.0.67
+10.0.0.68
+10.0.0.69
+10.0.0.7
+10.0.0.70
+10.0.0.71
+10.0.0.72
+10.0.0.73
+10.0.0.74
+10.0.0.75
+10.0.0.76
+10.0.0.77
+10.0.0.78
+10.0.0.79
+10.0.0.8
+10.0.0.80
+10.0.0.81
+10.0.0.82
+10.0.0.83
+10.0.0.84
+10.0.0.85
+10.0.0.86
+10.0.0.87
+10.0.0.88
+10.0.0.89
+10.0.0.9
+10.0.0.90
+10.0.0.91
+10.0.0.92
+10.0.0.93
+10.0.0.94
+10.0.0.95
+10.0.0.96
+10.0.0.97
+10.0.0.98
+10.0.0.99
+10.0.1.0
+10.0.1.1
```

## tools/netflow-db/tests/maad_conformance.rs

Status: added; +143; −0

```diff
@@ -1,0 +1,143 @@
+use serde_json::Value;
+use std::{fs, path::Path, process::Command};
+
+const ABSOLUTE_TOLERANCE: f64 = 1e-10;
+const RELATIVE_TOLERANCE: f64 = 1e-10;
+const FIXTURE_NAMES: &[&str] = &[
+    "clustered",
+    "random",
+    "mixed",
+    "threshold-ancestor",
+    "balanced-branches",
+];
+
+#[test]
+fn maad_fixtures_match_haskell_goldens() {
+    for name in FIXTURE_NAMES {
+        assert_fixture_matches_golden(name);
+    }
+}
+
+fn assert_fixture_matches_golden(name: &str) {
+    let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
+        .join("tests/fixtures/maad-conformance")
+        .join(name)
+        .join("input.txt");
+    let golden = fixture.with_file_name("golden.json");
+
+    let output = Command::new(env!("CARGO_BIN_EXE_netflow-db"))
+        .args(["maad"])
+        .arg(&fixture)
+        .output()
+        .unwrap();
+    assert!(
+        output.status.success(),
+        "stdout={}\nstderr={}",
+        String::from_utf8_lossy(&output.stdout),
+        String::from_utf8_lossy(&output.stderr)
+    );
+
+    let actual: Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|error| {
+        panic!(
+            "{name}: netflow-db emitted invalid JSON: {error}; stdout={}",
+            String::from_utf8_lossy(&output.stdout)
+        )
+    });
+    let expected: Value =
+        serde_json::from_str(&fs::read_to_string(&golden).unwrap_or_else(|error| {
+            panic!("{name}: unable to read {}: {error}", golden.display())
+        }))
+        .unwrap_or_else(|error| panic!("{name}: invalid Haskell golden JSON: {error}"));
+
+    assert_metadata_matches(name, &actual, &expected);
+    assert_numeric_rows(
+        name,
+        "structure",
+        &actual,
+        &expected,
+        &["q", "tauTilde", "sd"],
+    );
+    assert_numeric_rows(name, "spectrum", &actual, &expected, &["alpha", "f"]);
+    assert_numeric_rows(name, "dimensions", &actual, &expected, &["q", "dim"]);
+}
+
+fn assert_metadata_matches(name: &str, actual: &Value, expected: &Value) {
+    let actual_metadata = &actual["metadata"];
+    let expected_metadata = &expected["metadata"];
+    assert_eq!(
+        prefix_lengths(actual_metadata),
+        prefix_lengths(expected_metadata),
+        "{name}: retained prefix levels differ"
+    );
+    assert_eq!(
+        numeric_metadata(actual_metadata, "totalAddrs"),
+        numeric_metadata(expected_metadata, "totalAddrs"),
+        "{name}: total address counts differ"
+    );
+}
+
+fn prefix_lengths(metadata: &Value) -> Vec<u64> {
+    if let Some(lengths) = metadata.get("prefixLengths") {
+        return lengths
+            .as_array()
+            .expect("prefixLengths must be an array")
+            .iter()
+            .map(|length| length.as_u64().expect("prefix length must be an integer"))
+            .collect();
+    }
+    metadata["prefix_counts"]
+        .as_array()
+        .expect("prefix_counts must be an array")
+        .iter()
+        .map(|prefix| {
+            prefix["pl"]
+                .as_u64()
+                .expect("prefix level must be an integer")
+        })
+        .collect()
+}
+
+fn numeric_metadata(metadata: &Value, key: &str) -> u64 {
+    metadata[key]
+        .as_u64()
+        .unwrap_or_else(|| panic!("metadata.{key} must be an integer"))
+}
+
+fn assert_numeric_rows(
+    name: &str,
+    section: &str,
+    actual: &Value,
+    expected: &Value,
+    fields: &[&str],
+) {
+    let actual_rows = actual[section]
+        .as_array()
+        .unwrap_or_else(|| panic!("{name}: {section} must be an array"));
+    let expected_rows = expected[section]
+        .as_array()
+        .unwrap_or_else(|| panic!("{name}: golden {section} must be an array"));
+    assert_eq!(
+        actual_rows.len(),
+        expected_rows.len(),
+        "{name}: {section} row count differs"
+    );
+
+    for (row_index, (actual_row, expected_row)) in actual_rows.iter().zip(expected_rows).enumerate()
+    {
+        for field in fields {
+            let actual_number = actual_row[field]
+                .as_f64()
+                .unwrap_or_else(|| panic!("{name}: {section}[{row_index}].{field} is not numeric"));
+            let expected_number = expected_row[field].as_f64().unwrap_or_else(|| {
+                panic!("{name}: golden {section}[{row_index}].{field} is not numeric")
+            });
+            let difference = (actual_number - expected_number).abs();
+            let tolerance = ABSOLUTE_TOLERANCE
+                + RELATIVE_TOLERANCE * actual_number.abs().max(expected_number.abs());
+            assert!(
+                difference <= tolerance,
+                "{name}: {section}[{row_index}].{field} differs: actual={actual_number:?}, expected={expected_number:?}, difference={difference:?}, tolerance={tolerance:?}"
+            );
+        }
+    }
+}
```
