Skip to content

chore: release v0.13.2 - #440

Merged
Zious11 merged 5 commits into
mainfrom
release/0.13.2
Jul 25, 2026
Merged

chore: release v0.13.2#440
Zious11 merged 5 commits into
mainfrom
release/0.13.2

Conversation

@Zious11

@Zious11 Zious11 commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Wave gate

Wave-85 gate status: CONVERGED / CLOSED (D-511). All CI checks green on develop tip (0ab6f52ee3be21687437d29923fadc903ca70387).

CI

Pre-release checks run on release/0.13.2 (b33e45f9):

  • cargo test --all-targets: 0 failed across all suites
  • cargo clippy --all-targets -- -D warnings: clean
  • cargo fmt --check: clean
  • cargo build: compiles at wirerust v0.13.2

Notes

Do NOT merge via the GitHub UI "Squash and merge" button — use a true merge commit (--no-ff) per the D-491 precedent to preserve branch history on main. After merge: tag v0.13.2 on main, create the GitHub Release with 4 binary artifacts (release.yml triggers on tag push), then back-merge main into develop as a true-merge commit.

Zious11 added 5 commits July 21, 2026 14:46
chore: back-merge v0.13.1 main into develop (true-merge)
…BC-2.19.029/030) (#437)

# [STORY-180] IEC-104 Timed Control Command Detection: TypeIDs 58–64

**Epic:** E-22 — IEC-104 Passive Analyzer
**Mode:** feature (feature-iec104, wave-85)
**Convergence:** CONVERGED after 4 adversarial passes (BC-5.39.001: 3
consecutive clean — P2/P3/P4)

![Tests](https://img.shields.io/badge/tests-248%2F248-brightgreen)
![Coverage](https://img.shields.io/badge/coverage-N%2FA-lightgrey)
![Mutation](https://img.shields.io/badge/mutation-N%2FA-lightgrey)
![Holdout](https://img.shields.io/badge/holdout-N%2FA%20wave--gate-blue)

Adds two new detection arms to `detect_iec104_threats` in
`src/analyzer/iec104.rs` to close
the evasion gap (IEC104-TIMED-CMD-GAP-001, CONFIRMED / HIGH confidence)
where CP56Time2a
time-tagged control command TypeIDs 58–64 fell silently through the `_`
catch-all arm without
emitting any finding. TypeIDs 58–60 (C_SC_TA_1 / C_DC_TA_1 / C_RC_TA_1 —
timed switching
commands) now emit exactly one T1692.001 "Unauthorized Message: Command
Message" Possible
finding with CASDU and conditional first_ioa evidence, mirroring the
untimed arm 45..=47
(BC-2.19.029). TypeIDs 61–64 (C_SE_TA_1 / C_SE_TB_1 / C_SE_TC_1 /
C_BO_TA_1 — timed
set-point and bitstring write commands) now emit both T1692.001 Possible
and T0836 "Modify
Parameter" Possible findings, mirroring the untimed arm 48..=51
(BC-2.19.030). The
silent-range code comment is narrowed from "52–99" to "{52–57, 65–99}"
per BC-2.19.022 v1.1
(AC-180-007). 27 new unit tests verified the full AC-180-001..008
contract; full iec104 suite
passes 248/248 with 0 regressions.

---

## Architecture Changes

```mermaid
graph TD
    ParseAsdu["parse_asdu()<br/>(pure-core — unchanged)"] -->|returns Option&lt;Asdu&gt;| DetectThreats["detect_iec104_threats()<br/>(effectful-shell)"]
    DetectThreats --> Arm4547["arm 45..=47<br/>T1692.001 only<br/>(BC-2.19.019, existing)"]
    DetectThreats --> Arm4851["arm 48..=51<br/>T1692.001 + T0836<br/>(BC-2.19.019, existing)"]
    DetectThreats --> Arm5860["arm 58..=60<br/>T1692.001 only<br/>(BC-2.19.029, NEW)"]
    DetectThreats --> Arm6164["arm 61..=64<br/>T1692.001 + T0836<br/>(BC-2.19.030, NEW)"]
    DetectThreats --> CatchAll["_ catch-all<br/>{52–57, 65–99} silent<br/>(BC-2.19.022 v1.1, narrowed)"]
    Arm5860 --> Findings["&mut Vec&lt;Finding&gt;"]
    Arm6164 --> Findings
    style Arm5860 fill:#90EE90
    style Arm6164 fill:#90EE90
    style CatchAll fill:#FFE4B5
```

<details>
<summary><strong>Architecture Decision Record</strong></summary>

### ADR: Match-arm slot order (ADR-013 Decision 3) — slotting timed arms
ahead of `_` catch-all

**Context:** TypeIDs 58–64 fell through the `_` catch-all arm in
`detect_iec104_threats`
because no explicit arm existed for the CP56Time2a time-tagged variants
of the control
command TypeIDs already handled by arms 45..=47 and 48..=51. ADR-013
Decision 3 mandates
that new detection arms be slotted in TypeID ascending order.

**Decision:** Add arm `58..=60` (T1692.001 only) and arm `61..=64`
(T1692.001 + T0836)
between the existing `48..=51` arm and the `100/101/103` arm, ahead of
the `_` catch-all.
Emit logic mirrors the untimed twins exactly (same evidence shape: CASDU
+ conditional
first_ioa; same verdict/confidence/category values:
Possible/Medium/Impact). Summary
strings are distinct with "time-tagged" qualifier and timed mnemonics so
analysts can
distinguish timed from untimed findings.

**Rationale:** Parity with untimed arms is the explicit contract of
BC-2.19.029/030.
The post-emission `[TEST]` loop (lines 924–928) covers the new arms
automatically — no
extra wiring. No new crate dependencies; no `unsafe` needed (immutable
borrow only).

**Alternatives Considered:**
1. Generalize existing arms 45..=47 and 48..=51 with a combined range
pattern — rejected
because ADR-013 Decision 3 requires ascending slot order; combining the
ranges would
conflate two distinct behavioral contracts and break the BC-per-arm
traceability model.
2. Handle in a separate function — rejected because ADR-013 Decision 8
requires all
finding emission to occur in `detect_iec104_threats` or the effectful
shell.

**Consequences:**
- TypeIDs 58–64 are now detected and attributed; the evasion gap is
closed.
- The `_` catch-all arm silent range narrows from "52–99" to "{52–57,
65–99}" — existing
  BC-2.19.022 v1.1 regression guard tests enforce this boundary.

</details>

---

## Story Dependencies

```mermaid
graph LR
    S174["STORY-174<br/>✅ MERGED PR #409<br/>(wave-83)"] --> S180["STORY-180<br/>🟡 this PR"]
    S180 --> Unblocked["(no blocked stories)"]
    style S174 fill:#90EE90
    style S180 fill:#FFD700
    style Unblocked fill:#E0E0E0
```

**Dependency status:** STORY-174 (IEC-104 VP-044/045/046/047 formal
hardening) — merged to develop
as PR #409 (wave-83). No downstream stories are blocked on STORY-180 in
the current wave scope.

---

## Spec Traceability

```mermaid
flowchart LR
    BC029["BC-2.19.029 v1.3<br/>Timed Switching Cmds<br/>TypeIDs 58–60"] --> AC001["AC-180-001<br/>T1692.001 only"]
    BC029 --> AC002["AC-180-002<br/>No T0836"]
    BC029 --> AC004["AC-180-004<br/>timed-variant wording"]
    BC029 --> AC005["AC-180-005<br/>cot_test [TEST] suffix"]
    BC030["BC-2.19.030 v1.2<br/>Timed Set-Point Cmds<br/>TypeIDs 61–64"] --> AC003["AC-180-003<br/>T1692.001 + T0836"]
    BC030 --> AC004
    BC030 --> AC005
    BC022["BC-2.19.022 v1.1<br/>Reserved/Invalid TypeID<br/>regression guard"] --> AC006["AC-180-006<br/>52–57, 65–99 silent"]
    BC022 --> AC007["AC-180-007<br/>comment narrowed"]
    BC029 --> AC008["AC-180-008<br/>count-independent"]
    BC030 --> AC008
    AC001 --> T58["test_BC_2_19_029_type_id_58_emits_t1692_001_only<br/>line 7017"]
    AC003 --> T61["test_BC_2_19_030_type_id_61_emits_two_findings<br/>line 7368"]
    AC006 --> T52["test_BC_2_19_022_v1_1_type_id_52_no_finding<br/>line 7806"]
    T58 --> IEC104["src/analyzer/iec104.rs<br/>arm 58..=60"]
    T61 --> IEC104
    T52 --> IEC104
```

---

## Test Evidence

### PG-W74-PRDESC-ROW-VERIFY — Row-Verification Record

Row-verified 4 entries from the per-test table below against
`tests/iec104_analyzer_tests.rs` on branch
`feature/STORY-180-iec104-timed-cmd-detection`:

- Row 1: `test_BC_2_19_029_type_id_58_emits_t1692_001_only` — confirmed
at line **7017** ✓
- Row 2: `test_BC_2_19_029_casdu_first_ioa_evidence` — confirmed at line
**7155** ✓
- Row 3: `test_BC_2_19_030_type_id_61_emits_two_findings` — confirmed at
line **7368** ✓
- Row 4: `test_BC_2_19_022_v1_1_type_id_52_no_finding` — confirmed at
line **7806** ✓

Aggregate count cross-check:
- Claimed: **27** STORY-180 tests — matches actual `cargo test
story_180` output: "27 passed; 0 failed" ✓
- Claimed: **248** total iec104 tests — matches actual `cargo test
--test iec104_analyzer_tests` output: "248 passed; 0 failed" ✓

### Coverage Summary

| Metric | Value | Threshold | Status |
|--------|-------|-----------|--------|
| IEC-104 suite tests | 248/248 pass | 100% | PASS |
| STORY-180 new tests | 27/27 pass | 100% | PASS |
| Coverage % | N/A | >80% | N/A |
| Mutation kill rate | N/A | >90% | N/A (VP-047 fuzz covers new arms) |
| Holdout satisfaction | N/A — wave gate | >0.85 | N/A — evaluated at
wave gate |

### Test Flow

```mermaid
graph LR
    Unit["248 IEC-104 Unit Tests<br/>(27 new STORY-180)"]
    Adversarial["4 Adversarial Passes<br/>BC-5.39.001 SATISFIED"]
    RedGate["Red Gate<br/>21 red / 227 green"]
    Formal["VP-047 fuzz harness<br/>(auto-covers 58–64)"]

    RedGate -->|TDD verified| Unit
    Unit -->|248/248 PASS| Pass1["PASS"]
    Adversarial -->|3/3 clean streak| Pass2["PASS"]
    Formal -->|covers new arms| Pass3["N/A — no new VP"]

    style Pass1 fill:#90EE90
    style Pass2 fill:#90EE90
    style Pass3 fill:#E0E0E0
```

| Metric | Value |
|--------|-------|
| **New tests** | 27 added (story_180 module), 0 modified |
| **Total iec104 suite** | 248 tests PASS (221 prior + 27 new) |
| **Coverage delta** | N/A (line coverage not instrumented in this
story) |
| **Mutation kill rate** | N/A |
| **Regressions** | 0 — all 221 prior tests pass; untimed twins (TypeIDs
45/51) verified by regression guard tests |

<details>
<summary><strong>Detailed Test Results — STORY-180 (27
tests)</strong></summary>

### New Tests (This PR — story_180 module)

| Test Name | AC | Result |
|-----------|-----|--------|
| `test_BC_2_19_029_type_id_58_emits_t1692_001_only` | AC-180-001/002 |
PASS |
| `test_BC_2_19_029_type_id_59_emits_t1692_001_only` | AC-180-001/002 |
PASS |
| `test_BC_2_19_029_type_id_60_emits_t1692_001_only` | AC-180-001/002 |
PASS |
| `test_BC_2_19_029_type_id_58_verdict_confidence_category` | AC-180-001
| PASS |
| `test_BC_2_19_029_casdu_first_ioa_evidence` | AC-180-001 | PASS |
| `test_BC_2_19_029_type_id_59_first_ioa_none_no_first_ioa_evidence` |
AC-180-001 | PASS |
| `test_BC_2_19_029_timed_summary_contains_time_tagged_qualifier` |
AC-180-004 | PASS |
| `test_BC_2_19_029_timed_summary_differs_from_untimed_twin` |
AC-180-004 | PASS |
| `test_BC_2_19_029_type_id_60_cot_test_suffix` | AC-180-005 | PASS |
| `test_BC_2_19_029_type_id_58_count_zero_still_emits` | AC-180-008 |
PASS |
| `test_BC_2_19_030_type_id_61_emits_two_findings` | AC-180-003 | PASS |
| `test_BC_2_19_030_type_id_62_emits_two_findings` | AC-180-003 | PASS |
| `test_BC_2_19_030_type_id_63_emits_two_findings` | AC-180-003 | PASS |
| `test_BC_2_19_030_type_id_64_emits_two_findings` | AC-180-003 | PASS |
|
`test_BC_2_19_030_type_id_61_verdict_confidence_category_both_findings`
| AC-180-003 | PASS |
| `test_BC_2_19_030_type_id_61_casdu_first_ioa_evidence_both_findings` |
AC-180-003 | PASS |
| `test_BC_2_19_030_type_id_62_first_ioa_none_no_first_ioa_evidence` |
AC-180-003 | PASS |
| `test_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonics` |
AC-180-004 | PASS |
| `test_BC_2_19_030_timed_summaries_differ_from_untimed_twin` |
AC-180-004 | PASS |
| `test_BC_2_19_030_type_id_64_cot_test_both_findings_tagged` |
AC-180-005 | PASS |
| `test_BC_2_19_030_type_id_61_count_zero_still_emits_two_findings` |
AC-180-008 | PASS |
| `test_BC_2_19_022_v1_1_type_id_52_no_finding` | AC-180-006 | PASS |
| `test_BC_2_19_022_v1_1_type_id_57_no_finding` | AC-180-006 | PASS |
| `test_BC_2_19_022_v1_1_type_id_65_no_finding` | AC-180-006 | PASS |
| `test_BC_2_19_022_v1_1_type_id_99_no_finding` | AC-180-006 | PASS |
| `test_BC_2_19_019_v1_1_regression_type_id_45_still_one_finding` |
AC-180-006 | PASS |
| `test_BC_2_19_019_v1_1_regression_type_id_51_still_two_findings` |
AC-180-006 | PASS |

Source: `cargo test --test iec104_analyzer_tests story_180` — "27
passed; 0 failed"
(feature branch ccec171 / `tests/iec104_analyzer_tests.rs`)

</details>

---

## Holdout Evaluation

N/A — evaluated at wave gate per factory process (E-22 epic, wave-85).

---

## Adversarial Review

| Pass | Code Tip | Findings | Critical | High | Medium | Low | Status |
|------|----------|----------|----------|------|--------|-----|--------|
| P1 | d64d660 | 3 | 0 | 0 | 3 | 0 | Fixed (a008703) |
| P2 | a008703 | 3 | 0 | 0 | 0 | 3 | Swept (e40955f) — streak 1/3 |
| P3 | e40955f | 1 | 0 | 0 | 0 | 1 | Fixed (0502c64) — streak 2/3 |
| P4 | 0502c64 | 1 | 0 | 0 | 0 | 1 | Fixed (BC label) — streak 3/3 |

**Convergence:** CONVERGED — BC-5.39.001 SATISFIED (3 consecutive clean
passes P2/P3/P4).
Adversary forced to hallucinate after pass P4. No open HIGH or CRITICAL
findings.

<details>
<summary><strong>Medium-Severity Findings &
Resolutions</strong></summary>

### F-180-P1-001 (MEDIUM): dispatch-table doc comment drift
- **Location:** `src/analyzer/iec104.rs` — match-arm inline comments
- **Category:** code-quality / spec-fidelity
- **Problem:** Inline comments enumerated only untimed TypeIDs 45–51 and
omitted the new
  timed-variant detection arms 58–64.
- **Resolution:** Comments updated to enumerate full TypeID coverage in
`a0087033`.
- **Test added:** None (comment-only fix).

### F-180-P1-002 (MEDIUM): CHANGELOG count mismatch
- **Location:** `CHANGELOG.md` — [Unreleased] entry
- **Category:** documentation
- **Problem:** Entry claimed "21 red assertion-shaped tests" but actual
new-test count was 27.
- **Resolution:** Count corrected to 27 in `a0087033`.

### F-180-P1-003 (MEDIUM): stale present-tense RED docstrings (9 sites)
- **Location:** `tests/iec104_analyzer_tests.rs` — 9 doc sites
- **Category:** documentation / PG-W85-003
- **Problem:** 9 sites retained `currently asserts`, `is expected to`,
and similar RED-phase
phrasing — the exact class that `bin/check-green-doc-tense` is designed
to catch.
- **Resolution:** 9 sites reframed to past-tense GREEN-phase prose in
`a0087033`.

</details>

---

## Security Review

**Result: CLEAN — 0 Critical, 0 High, 0 Medium, 0 Low (new findings
introduced by STORY-180).**

```mermaid
graph LR
    Critical["Critical: 0"]
    High["High: 0"]
    Medium["Medium: 0"]
    Low["Low: 0 (new)"]

    style Critical fill:#90EE90
    style High fill:#90EE90
    style Medium fill:#90EE90
    style Low fill:#90EE90
```

<details>
<summary><strong>Security Scan Details</strong></summary>

### Analysis Summary

The diff consists of two plain match arms reading immutable `&Asdu`
fields (`type_id`,
`casdu`, `first_ioa`, `cot_test` — all pre-parsed, typed Rust fields)
and pushing to
`&mut Vec<Finding>`. No raw user input, no string interpolation with
untrusted data,
no network I/O, no file I/O, no `unsafe` code, no external crate
dependencies.

**Input Validation:** `Asdu` struct fields are pre-validated by
`parse_asdu` upstream.
The new arms perform O(1) pattern matching on a `u8` TypeID value.

**Injection Risks:** None. Evidence strings use `format!()` with typed
`u16`/`u32`
integer values (CASDU, first_ioa). No dynamic dispatch, no SQL, no
command execution.

**Authentication / Authorization:** Passive analyzer operating on
already-captured
network traffic. No authentication boundaries crossed.

**Crypto / Secrets:** None applicable.

**Data Exposure:** CASDU and first_ioa are ICS metadata already present
in the network
capture; not PII; not credentials. Same exposure level as existing
untimed arms 45–51.

### SAST
- Critical: 0 | High: 0 | Medium: 0 | Low: 0 (new)

### Pre-existing Finding (not introduced by STORY-180)
- SEC-001 CWE-22 (LOW) — pre-existing, unchanged by this PR.

### ADR-013 Decision 7 Compliance
No ICS parsing libraries (`iec60870-5`, `wireshark`, `lib60870`, `nom`)
introduced.

</details>

---

## Risk Assessment & Deployment

### Blast Radius
- **Systems affected:** `src/analyzer/iec104.rs`
(`detect_iec104_threats` function only),
`tests/iec104_analyzer_tests.rs` (test file only). No Cargo.toml
changes; no new
  crate dependencies; no public API surface changes.
- **User impact:** Additive — previously undetected TypeIDs 58–64 now
produce findings.
  No existing findings are modified or removed. No breaking change.
- **Data impact:** None. In-memory finding emission only; no persistent
storage.
- **Risk Level:** LOW — additive detection arms in a pure read path; no
behavioral
regression risk (221 prior tests pass; PG-W72-BREAKING-HOLDOUT-SWEEP
does NOT trigger
for this story — additive detection, not a BREAKING or
output-format-change story).

### Performance Impact
| Metric | Before | After | Delta | Status |
|--------|--------|-------|-------|--------|
| Match arm eval per ASDU | N arms | N+2 arms | +2 arms (additive) | OK
|
| Memory per finding | same | same | 0 | OK |
| Throughput | unchanged | unchanged | N/A | OK |

Note: The two new match arms add negligible cost — they are O(1) pattern
comparisons in
a single-level `match` over a `u8` TypeID.

<details>
<summary><strong>Rollback Instructions</strong></summary>

**Immediate rollback (< 2 min):**
```bash
git revert ccec171  # demo evidence commit (top of stack)
git revert 0502c64  # P3 close
git revert e40955f  # P2 sweep
git revert a008703  # P1 remediation
git revert d64d660  # CHANGELOG + fmt
git revert 18d0a91  # detection arms implementation
git push origin develop
```

Or revert the squash-merge commit directly after merge.

**Verification after rollback:**
- Run `cargo test --test iec104_analyzer_tests` — should return to 221
tests passing
- Run `cargo test --all-targets` — 0 failures expected

</details>

### Feature Flags
| Flag | Controls | Default |
|------|----------|---------|
| N/A | No feature flags used — ICS detection arms are always-on | N/A |

---

## Traceability

| Requirement | Story AC | Test | Verification | Status |
|-------------|---------|------|-------------|--------|
| BC-2.19.029 PC1 | AC-180-001 |
`test_BC_2_19_029_type_id_58_emits_t1692_001_only` | unit | PASS |
| BC-2.19.029 PC2/inv2 | AC-180-002 |
`test_BC_2_19_029_type_id_58_emits_t1692_001_only` (count=1) | unit |
PASS |
| BC-2.19.029 PC3 | AC-180-001 |
`test_BC_2_19_029_casdu_first_ioa_evidence` | unit | PASS |
| BC-2.19.029 PC4 | AC-180-004 |
`test_BC_2_19_029_timed_summary_contains_time_tagged_qualifier` | unit |
PASS |
| BC-2.19.029 PC6/inv1 | AC-180-005 |
`test_BC_2_19_029_type_id_60_cot_test_suffix` | unit | PASS |
| BC-2.19.030 PC1-PC3 | AC-180-003 |
`test_BC_2_19_030_type_id_61_emits_two_findings` | unit | PASS |
| BC-2.19.030 PC4-PC5 | AC-180-004 |
`test_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonics` |
unit | PASS |
| BC-2.19.030 PC7/inv1 | AC-180-005 |
`test_BC_2_19_030_type_id_64_cot_test_both_findings_tagged` | unit |
PASS |
| BC-2.19.022 v1.1 inv1 | AC-180-006 |
`test_BC_2_19_022_v1_1_type_id_52_no_finding` | unit | PASS |
| BC-2.19.022 v1.1 arch anchor | AC-180-007 | source-level (grep) |
manual verify | PASS |
| BC-2.19.029 inv3 | AC-180-008 |
`test_BC_2_19_029_type_id_58_count_zero_still_emits` | unit | PASS |
| BC-2.19.030 inv3 | AC-180-008 |
`test_BC_2_19_030_type_id_61_count_zero_still_emits_two_findings` | unit
| PASS |

<details>
<summary><strong>Full VSDD Contract Chain</strong></summary>

```
BC-2.19.029 -> AC-180-001 -> test_BC_2_19_029_type_id_58_emits_t1692_001_only -> src/analyzer/iec104.rs arm 58..=60 -> ADV-P4-CONVERGED -> unit-PASS
BC-2.19.029 -> AC-180-002 -> test_BC_2_19_029_type_id_58_emits_t1692_001_only (count=1) -> src/analyzer/iec104.rs arm 58..=60 -> ADV-P4-CONVERGED -> unit-PASS
BC-2.19.030 -> AC-180-003 -> test_BC_2_19_030_type_id_61_emits_two_findings -> src/analyzer/iec104.rs arm 61..=64 -> ADV-P4-CONVERGED -> unit-PASS
BC-2.19.022 v1.1 -> AC-180-006 -> test_BC_2_19_022_v1_1_type_id_52_no_finding -> src/analyzer/iec104.rs catch-all comment narrowed -> ADV-P4-CONVERGED -> unit-PASS
BC-2.19.022 v1.1 -> AC-180-007 -> source-level grep -> src/analyzer/iec104.rs lines 912-914 comment -> ADV-P4-CONVERGED -> manual-PASS
VP-047 -> fuzz_iec104_parser -> auto-covers TypeIDs 58-64 once arms added -> src/analyzer/iec104.rs -> STORY-174 formal hardening
```

</details>

---

## Demo Evidence

Demo evidence committed at `ccec1711` to feature branch
`feature/STORY-180-iec104-timed-cmd-detection`.

Path: `docs/demo-evidence/STORY-180/` (8 artifacts)

| File | AC Coverage |
|------|-------------|
| `AC-001-002-typeid-58-60-timed-switching.md` | AC-180-001 (BC-2.19.029
PC1+PC3), AC-180-002 (BC-2.19.029 inv2) |
| `AC-003-typeid-61-64-timed-setpoint.md` | AC-180-003 (BC-2.19.030
PC1-PC3) |
| `AC-004-timed-summary-wording.md` | AC-180-004 (BC-2.19.029 PC4;
BC-2.19.030 PC4-PC5) |
| `AC-005-cot-test-tagging.md` | AC-180-005 (BC-2.19.017 inv1;
BC-2.19.029 PC6; BC-2.19.030 PC7) |
| `AC-006-silence-regression-guard.md` | AC-180-006 (BC-2.19.022 v1.1
inv1) |
| `AC-007-silent-range-comment.md` | AC-180-007 (BC-2.19.022 v1.1 arch
anchor; source-level) |
| `AC-008-count-independent-emission.md` | AC-180-008 (BC-2.19.029 inv3;
BC-2.19.030 inv3) |
| `evidence-report.md` | Index (full test run transcripts, coverage map,
edge-case table) |

Coverage: **8 ACs covered × ≥1 artifact each** — PG-W70-DEMO-SCRUB gate
PASSED.

---

## AI Pipeline Metadata

<details>
<summary><strong>Pipeline Details</strong></summary>

```yaml
ai-generated: true
pipeline-mode: feature (feature-iec104, wave-85)
factory-version: "1.0.0-rc.23"
pipeline-stages:
  spec-crystallization: completed (BC-2.19.029 v1.3 + BC-2.19.030 v1.2 + BC-2.19.022 v1.1)
  story-decomposition: completed (STORY-180 v1.1, D-505 human story-approval PASSED)
  tdd-implementation: completed (Red Gate: 21 red/227 green; Green: 248/248)
  holdout-evaluation: N/A — evaluated at wave gate
  adversarial-review: completed (CONVERGED — 4 passes, BC-5.39.001 SATISFIED)
  formal-verification: N/A — VP-047 fuzz auto-covers new arms
  convergence: achieved
convergence-metrics:
  clean_streak: "P2/P3/P4 = 3/3"
  last_classification: NITPICK_ONLY
  open_high_critical: 0
adversarial-passes: 4
models-used:
  builder: claude-sonnet-4-6
  adversary: claude-sonnet-4-6 (per-story adversarial, wave-85 pattern)
generated-at: "2026-07-24T00:00:00Z"
wave: 85
story-version: "1.1"
branch-head: "ccec1711"
```

</details>

---

## Pre-Merge Checklist

- [ ] All CI status checks passing
- [x] Coverage delta is positive or neutral (additive story — 27 new
tests, 0 regressions)
- [x] No critical/high security findings unresolved (0 C/0 H from
adversarial; security reviewer TBD)
- [x] Rollback procedure validated (see Risk Assessment section)
- [x] Feature flag: N/A — no feature flags (always-on detection arms)
- [ ] Human review completed (per DF-MERGE-AUTH-CLASSIFIER-001 /
recent-wave pattern: step 8 merge halted for human execution)
- [x] PG-W72-BREAKING-HOLDOUT-SWEEP: does NOT apply — additive detection
story, not a BREAKING or output-format-change story
- [x] CHANGELOG [Unreleased] entry present (AC-158-001 / changelog-gate
CI job)
- [x] Demo evidence: 8 artifacts × 8 ACs, PG-W70-DEMO-SCRUB PASSED
- [x] Adversarial convergence: BC-5.39.001 SATISFIED
(DF-CONVERGENCE-BEFORE-MERGE-001)
- [x] Dependency STORY-174: PR #409 merged to develop
…spatch (STORY-181, SEC-001) (#438)

# [STORY-181] Fix SEC-001 ENIP Unsafe Split-Borrow in on_data: Eliminate
*mut EnipFlowState Raw Pointer in PDU Dispatch Loop (Behavior-Preserving
Refactor)

**Epic:** E-20 — EtherNet/IP ENIP/CIP Analyzer
**Mode:** maintenance (behavior-preserving refactor)
**Convergence:** CONVERGED after 3 adversarial passes (P1 NITPICK_ONLY /
P2 NITPICK_ONLY / P3 CLEAN — BC-5.39.001 satisfied)

![Tests](https://img.shields.io/badge/tests-2667%2F2667-brightgreen)
![ENIP
Suite](https://img.shields.io/badge/enip_suite-184%2F184-brightgreen)
![Clippy](https://img.shields.io/badge/clippy-0_warnings-brightgreen)
![Holdout](https://img.shields.io/badge/holdout-N%2FA--BY--DESIGN-blue)

This PR eliminates SEC-001 from the wirerust tech-debt register: the
`on_data` PDU dispatch
loop in `src/analyzer/enip.rs` previously cast
`self.flows.get_mut(&flow_key)` to a raw
`*mut EnipFlowState` and called `self.process_pdu(unsafe { &mut
*flow_ptr }, ...)`, relying
on a multi-line SAFETY comment to guarantee that `process_pdu` never
accesses `self.flows`.
The fix replaces this with a safe take-remove-reinsert pattern:
`self.flows.remove(&flow_key)`
produces an owned local flow, `process_pdu(&mut self, &mut flow, ...)`
receives a structurally
disjoint reference, and `self.flows.insert(flow_key, flow)` re-inserts
after the loop. The
compiler now enforces the disjointness invariant — no `unsafe` block, no
raw-pointer cast,
no `#[allow(clippy::ptr_as_ptr)]` remain in `on_data`. The PR also fixes
the ROUTE-W74 OBS-1
residual: the `parse_line()` docstring in `bin/validate-citations` now
documents the
regex-mismatch `None` return path (AC-181-004). All 2667 tests pass
unchanged; behavior is
identical to pre-refactor.

---

## Architecture Changes

```mermaid
graph TD
    EnipAnalyzer["EnipAnalyzer\n(src/analyzer/enip.rs)"]
    OnData["on_data()\n[PDU dispatch loop]"]
    ProcessPdu["process_pdu()\n[per-PDU analysis]"]
    FlowsMap["self.flows\nHashMap<FlowKey, EnipFlowState>"]

    EnipAnalyzer --> OnData
    OnData -->|"BEFORE: unsafe *mut raw ptr\n(aliased self.flows)"| ProcessPdu
    OnData -->|"AFTER: safe remove→local→insert\n(compiler-enforced disjoint)"| ProcessPdu
    OnData -->|"remove() / insert()"| FlowsMap
    ProcessPdu -.->|"NEVER accesses\n(now structurally enforced)"| FlowsMap

    style OnData fill:#90EE90
```

<details>
<summary><strong>Architecture Decision Record</strong></summary>

### ADR: SEC-001 Take-Remove-Reinsert Pattern for EnipFlowState
Split-Borrow

**Context:** `on_data` needed simultaneous access to `self` (for
`process_pdu(&mut self, ...)`)
and to an element of `self.flows` (an `&mut EnipFlowState`). The prior
approach used a raw
`*mut` pointer to sidestep the borrow checker, relying on a documented
invariant that
`process_pdu` never accesses `self.flows`. This invariant was
verified-by-inspection, not
structurally enforced.

**Decision:** Replace the raw pointer with a take-remove-reinsert
pattern:
`flows.remove()` → process loop with local owned value →
`flows.insert()`.

**Rationale:** The `remove`/`insert` approach gives the compiler full
visibility into borrow
lifetimes. The flow is absent from `self.flows` during the dispatch
loop, so any future
`process_pdu` change that accessed `self.flows` would be caught at
compile time rather than
remaining a latent soundness risk. The behavioral contract (carry buffer
accumulation,
direction isolation) is preserved because `process_pdu` only mutates
fields other than
`self.flows`.

**Alternatives Considered:**
1. Refactor `process_pdu` to not require `&mut self` — rejected because
it would require
significant signature changes and introduce a separate borrow splitting
complexity.
2. Keep the unsafe block but add more tests — rejected because tests
cannot enforce the
   aliasing invariant; only structural safety can.

**Consequences:**
- Zero unsafe blocks remain in `src/analyzer/enip.rs` (SEC-001 closed).
- The invariant "process_pdu does NOT access self.flows" is now
compiler-enforced rather
than convention-based — future maintainers get a compile error if this
changes.
- EC-002 (empty pdu_queue): remove+insert is a no-op that preserves flow
state correctly.

</details>

---

## Story Dependencies

```mermaid
graph LR
    STORY181["STORY-181\n✅ this PR"]

    style STORY181 fill:#FFD700
```

**depends_on:** `[]` — no blocking predecessors. All prior E-20 stories
(including
STORY-139 ENIP carry buffer, wave 62) are already merged to develop. No
dependency hold.

---

## Spec Traceability

```mermaid
flowchart LR
    BC["BC-2.17.016\nENIP Per-Direction\nCarry Buffer"]
    AC1["AC-181-001\nunsafe eliminated"]
    AC2["AC-181-002\nbehavior identical"]
    AC3["AC-181-003\nno API change"]
    AC4["AC-181-004\nbin docstring"]

    T1["test_carry_buffer_partial_header\n(regression guard)"]
    T2["test_carry_buffer_two_frames_one_segment\n(regression guard)"]
    T3["test_ec_x1_cross_direction_no_splice\n(regression guard)"]

    S1["src/analyzer/enip.rs\non_data() lines 978–1001"]
    S2["bin/validate-citations\nparse_line() docstring"]

    BC --> AC1
    BC --> AC2
    BC --> AC3
    AC2 --> T1
    AC2 --> T2
    AC2 --> T3
    T1 --> S1
    T2 --> S1
    T3 --> S1
    AC4 --> S2
```

---

## Test Evidence

### Coverage Summary

| Metric | Value | Threshold | Status |
|--------|-------|-----------|--------|
| Full cargo test --all-targets | 2667 / 2667 pass | 100% | PASS |
| ENIP integration suite | 184 / 184 pass | 100% | PASS |
| validate-citations tests | 27 / 27 pass | 100% | PASS |
| Clippy -D warnings | 0 warnings | 0 | PASS |
| Cargo fmt | clean | clean | PASS |
| Holdout satisfaction | N/A-BY-DESIGN | N/A | N/A (refactor) |
| Mutation kill rate | not run (refactor; behavior-preserving) |
advisory | N/A |

### BC-2.17.016 Carry-Path Regression Witnesses (AC-181-002 Mandate)

Per AC-181-002, the PR description MUST enumerate at least three
existing tests that exercise
the carry path. The following three carry-path regression tests
confirmed passing at HEAD
`0b5ba318`:

| Test | Module | Result |
|------|--------|--------|
| `test_carry_buffer_partial_header` | `frame_walk` | PASS |
| `test_carry_buffer_two_frames_one_segment` | `frame_walk` | PASS |
| `test_ec_x1_cross_direction_no_splice` | `direction_and_clock` | PASS
|

**Row-verify (PG-W74-PRDESC-ROW-VERIFY):** These test names are drawn
directly from the
evidence-report at
`docs/demo-evidence/STORY-181/AC-181-002-behavior-identical.md` which
confirms each ran and passed. The ENIP test binary confirmed 184/184 at
HEAD commit
`0b5ba318`. Aggregate count cross-check: `cargo test --all-targets`
returned 2667 passing
/ 0 failed / 5 ignored — consistent with the baseline-identical
characterization in the
convergence report.

### Test Flow

```mermaid
graph LR
    Unit["2667 Unit/Integration Tests"]
    ENIP["184 ENIP Suite Tests"]
    Python["27 Python Tests\n(bin/test_validate_citations.py)"]
    Clippy["Clippy -D warnings"]
    Fmt["cargo fmt --check"]

    Unit -->|"0 failed"| Pass1["PASS"]
    ENIP -->|"0 failed"| Pass2["PASS"]
    Python -->|"0 failed"| Pass3["PASS"]
    Clippy -->|"0 warnings"| Pass4["PASS"]
    Fmt -->|"clean"| Pass5["PASS"]

    style Pass1 fill:#90EE90
    style Pass2 fill:#90EE90
    style Pass3 fill:#90EE90
    style Pass4 fill:#90EE90
    style Pass5 fill:#90EE90
```

| Metric | Value |
|--------|-------|
| **New tests** | 0 added (behavior-preserving refactor; all existing
tests serve as regression guard) |
| **Total suite** | 2667 tests PASS |
| **ENIP suite** | 184 tests PASS |
| **Regressions** | 0 |
| **Red Gate** | N/A-BY-DESIGN (log:
`.factory/cycles/wave-085/STORY-181/implementation/red-gate-log.md`) |

<details>
<summary><strong>Detailed Test Results — BC-2.17.016 Carry-Path
Tests</strong></summary>

### Carry-Path Regression Witnesses

| Test | Module | Result | Duration |
|------|--------|--------|----------|
| `test_carry_buffer_partial_header` | `frame_walk` | PASS | < 1ms |
| `test_carry_buffer_two_frames_one_segment` | `frame_walk` | PASS | <
1ms |
| `test_ec_x1_cross_direction_no_splice` | `direction_and_clock` | PASS
| < 1ms |

These three tests cover the BC-2.17.016 postconditions:
- Partial-header carry accumulation (c2s and s2c buffers survive the
dispatch loop refactor)
- Two-frame-per-segment carry drain (multi-PDU dispatch loop iterates
correctly over local flow)
- Cross-direction isolation (no c2s/s2c carry splice across direction
boundary)

### Red Gate Log

This story is a behavior-preserving refactor (SEC-001 split-borrow
elimination). The TDD
Red Gate is N/A-BY-DESIGN: no new behavioral assertions were required
(the refactor changes
no observable behavior). Baseline at worktree base `421bf572`: 2667
passing / 0 failed /
5 ignored (log commit `e7f76508`). This N/A status was explicitly
adjudicated in the
implementation plan and is recorded in the red-gate-log above.

</details>

---

## Holdout Evaluation

| Metric | Value | Notes |
|--------|-------|-------|
| Result | **N/A — evaluated at wave gate** | Behavior-preserving
refactor; no new behavioral surface |

This is a maintenance refactor story (SEC-001 tech-debt closure). No new
user-facing behavior
is introduced. Holdout evaluation applies at the wave gate level, not
per-story for
behavior-preserving refactors.

---

## Adversarial Review

| Pass | Code Tip | Findings | Critical | High | LOW | Status |
|------|----------|----------|----------|------|-----|--------|
| P1 | e957282 | 2 | 0 | 0 | 2 | SWEPT (294168f) |
| P2 | 294168f | 2 | 0 | 0 | 2 | SWEPT (093ff51) |
| P3 | 093ff51 | 0 | 0 | 0 | 0 | CLEAN — CONVERGED |

**Convergence:** CONVERGED 3/3 (BC-5.39.001 satisfied) — clean streak
P1/P2/P3.
All findings were LOW severity; zero HIGH or CRITICAL at any pass.
Report: `.factory/cycles/wave-085/STORY-181/convergence-report.md`

<details>
<summary><strong>Adversarial Finding Dispositions</strong></summary>

### F-181-P1-001 (LOW) — False pdu_queue Invariant Comment
- **Location:** `src/analyzer/enip.rs` (dispatch-phase inline comment)
- **Category:** code-quality (comment precision)
- **Problem:** Inline comment over-stated a PDU-queue guarantee that was
not fully correct.
- **Resolution:** Comment corrected in `294168fa` to accurately reflect
the actual invariant.

### F-181-P1-002 (LOW) — Stale process_pdu flow_key Parameter Docstring
- **Location:** `src/analyzer/enip.rs` `process_pdu` docstring
- **Category:** code-quality (documentation)
- **Problem:** Pre-existing stale `flow_key` parameter docstring;
adjudicated in-scope.
- **Resolution:** Docstring corrected in `294168fa`.

### F-181-P2-001 (LOW) — RULING-137-002 Cross-Ref Missing
- **Location:** `src/analyzer/enip.rs` inline comment
- **Category:** code-quality (traceability)
- **Problem:** Inline comment cited the ruling but omitted the
back-reference to the
  originating architectural decision.
- **Resolution:** Cross-reference added in `093ff519`.

### F-181-P2-002 (LOW) — `"line ~1033"` Reference Off by 6 Lines
- **Location:** `src/analyzer/enip.rs` inline comment
- **Category:** code-quality (precision)
- **Problem:** Line reference was 6 lines off after the refactor shifted
line numbers.
- **Resolution:** Corrected in `093ff519`.

### O-181-P3-001 (theoretical, non-blocking)
- **Category:** theoretical-only
- **Description:** Panic-unwind flow-drop divergence in a
`debug_assert`-only panic path
  compiled out in release. Explicitly non-blocking; no action required.

</details>

---

## Security Review

```mermaid
graph LR
    Critical["Critical: 0"]
    High["High: 0"]
    Medium["Medium: 0"]
    Low["Low: 0"]

    style Critical fill:#90EE90
    style High fill:#90EE90
    style Medium fill:#90EE90
    style Low fill:#90EE90
```

**SEC-001 CLOSED:** The primary security finding from PR #334 review
(MEDIUM, carried forward
to wave-85 as D-493) is resolved by this PR. Zero `unsafe` blocks remain
in `src/analyzer/enip.rs`.
Adversary-confirmed across all 3 passes.

<details>
<summary><strong>Security Scan Details</strong></summary>

### Unsafe Block Elimination

`grep -n "flow_ptr\|ptr_as_ptr\|\*mut EnipFlowState\|unsafe"
src/analyzer/enip.rs` returns
**zero matches** at HEAD `0b5ba318`. The four SEC-001 symbols are absent
from the file:
- `let flow_ptr: *mut EnipFlowState` — removed
- `unsafe { &mut *flow_ptr }` — removed
- `#[allow(clippy::ptr_as_ptr)]` — removed
- The `unsafe` keyword at the former dispatch site — removed

### Dependency Audit

No new crate dependencies introduced. `Cargo.toml` unchanged.

### Formal Verification

| Property | Method | Status |
|----------|--------|--------|
| Carry buffer accumulation preserved | BC-2.17.016 test suite (184
tests) | VERIFIED |
| Borrow disjointness (structural) | Rust compiler type-checker |
VERIFIED at compile time |
| `process_pdu` self.flows isolation | Exhaustive grep (×3 adversarial
passes) | VERIFIED |

</details>

---

## Risk Assessment & Deployment

### Blast Radius

- **Systems affected:** `src/analyzer/enip.rs` `on_data` dispatch loop
only; no public API changes
- **User impact:** None — behavior-preserving refactor; CLI output and
finding emission are identical
- **Data impact:** None — no storage, no schema changes
- **Risk Level:** LOW (behavior-preserving; compiler-enforced
correctness improvement)

### Performance Impact

| Metric | Before | After | Delta | Status |
|--------|--------|-------|-------|--------|
| Latency | HashMap::remove + HashMap::insert per dispatch cycle | Same
operations, same cost | neutral | OK |
| Memory | EnipFlowState stack-local during dispatch | Same; short-lived
local | neutral | OK |
| Throughput | Identical PDU dispatch rate | Identical | 0 | OK |

The `remove` + `insert` operations on a `HashMap` are O(1) amortized —
equivalent to the
prior `get_mut` + pointer deref. No observable performance difference is
expected.

<details>
<summary><strong>Rollback Instructions</strong></summary>

**Immediate rollback (< 2 min):**
```bash
git revert 224311a  # take-remove-reinsert implementation commit
git push origin develop
```

The revert restores the pre-refactor `*mut EnipFlowState` unsafe block.
All tests pass in
either form — this is a behavior-preserving change. No feature flag; no
migration required.

**Verification after rollback:**
- `cargo test --all-targets` returns 2667 passing / 0 failed
- `grep -n "flow_ptr" src/analyzer/enip.rs` shows the raw pointer
restored

</details>

### Feature Flags

None. This is a direct code change with no feature flag. Rollback is via
`git revert`.

---

## Traceability

| Requirement | Story AC | Test | Verification | Status |
|-------------|---------|------|-------------|--------|
| BC-2.17.016 carry postconditions preserved | AC-181-002 |
`test_carry_buffer_partial_header` | ENIP suite 184/184 | PASS |
| BC-2.17.016 carry postconditions preserved | AC-181-002 |
`test_carry_buffer_two_frames_one_segment` | ENIP suite 184/184 | PASS |
| BC-2.17.016 cross-direction isolation | AC-181-002 |
`test_ec_x1_cross_direction_no_splice` | ENIP suite 184/184 | PASS |
| SEC-001 unsafe eliminated | AC-181-001 | grep zero-match | compiler
type-checker | PASS |
| No public API change | AC-181-003 | git diff --stat | Cargo.toml
absent from diff | PASS |
| ROUTE-W74 OBS-1 docstring | AC-181-004 | `python3
bin/test_validate_citations.py` | 27/27 pass | PASS |

<details>
<summary><strong>Full VSDD Contract Chain</strong></summary>

```
BC-2.17.016 → AC-181-001 → grep "unsafe" src/analyzer/enip.rs → zero matches → compiler-enforced
BC-2.17.016 → AC-181-002 → test_carry_buffer_partial_header → enip.rs on_data carry select → ENIP 184/184
BC-2.17.016 → AC-181-002 → test_carry_buffer_two_frames_one_segment → enip.rs on_data carry select → ENIP 184/184
BC-2.17.016 → AC-181-002 → test_ec_x1_cross_direction_no_splice → enip.rs on_data carry select → ENIP 184/184
BC-2.17.016 → AC-181-003 → git diff --stat → Cargo.toml absent → no API change
ROUTE-W74/OBS-1 → AC-181-004 → bin/validate-citations parse_line() → test_validate_citations.py → 27/27
```

</details>

---

## Demo Evidence

Demo evidence at `docs/demo-evidence/STORY-181/` (5 artifacts, scrub
PASSED 2026-07-24):

| File | AC Coverage |
|------|-------------|
| `AC-181-001-unsafe-eliminated.md` | AC-181-001: grep zero-match +
before/after code excerpt |
| `AC-181-002-behavior-identical.md` | AC-181-002: 184/184 ENIP tests +
3 carry-path witnesses + full suite |
| `AC-181-003-no-api-change.md` | AC-181-003: process_pdu signature grep
+ git diff stat |
| `AC-181-004-bin-docstring.md` | AC-181-004: docstring excerpt + 27/27
test_validate_citations.py |
| `evidence-report.md` | Index — coverage map + scrub gate PASSED |

---

## AI Pipeline Metadata

<details>
<summary><strong>Pipeline Details</strong></summary>

```yaml
ai-generated: true
pipeline-mode: maintenance
factory-version: "1.0.0-rc.23"
pipeline-stages:
  spec-crystallization: completed (wave-85 story decomposition 2026-07-23)
  story-decomposition: completed (STORY-181 v1.1, human approval D-505 2026-07-24)
  tdd-implementation: completed (224311a + 1349135 + e957282; Red Gate N/A-BY-DESIGN)
  holdout-evaluation: "N/A — evaluated at wave gate (behavior-preserving refactor)"
  adversarial-review: completed (3 passes, CONVERGED BC-5.39.001)
  formal-verification: skipped (refactor; compiler type-check is structural verification)
  convergence: achieved (P1/P2/P3 clean streak)
convergence-metrics:
  adversarial-passes: 3
  last-classification: CLEAN
  clean-streak: "P1/P2/P3 = 3/3"
  code-tip-at-convergence: "093ff519"
  open-HIGH-CRIT: 0
models-used:
  builder: claude-sonnet-4-6
  adversary: claude-sonnet-4-6 (step-4.5 adversarial)
generated-at: "2026-07-24T22:00:00Z"
wave: 85
story-version: "1.1"
```

</details>

---

## Pre-Merge Checklist

- [ ] All CI status checks passing
- [x] Coverage delta: neutral (2667/0/5 baseline-identical —
behavior-preserving)
- [x] No critical/high security findings unresolved (SEC-001 CLOSED; 0
HIGH/CRIT adversarial)
- [x] Rollback procedure: `git revert 224311a`; all tests pass in both
states
- [x] No feature flag required (direct code change)
- [ ] Human review completed (autonomy level requires human merge
authorization — see DF-MERGE-AUTH-CLASSIFIER-001; no wave-85 wave-level
grant exists)
- [x] Adversarial convergence: CONVERGED 3/3 (BC-5.39.001)
- [x] Demo evidence: 5 artifacts per-AC, scrub PASSED
- [x] AC-181-002 carry-path witnesses enumerated:
test_carry_buffer_partial_header,
test_carry_buffer_two_frames_one_segment,
test_ec_x1_cross_direction_no_splice
- [x] PG-W74-PRDESC-ROW-VERIFY: row-verified 3 carry-path test entries +
aggregate count cross-checked
- [x] CHANGELOG [Unreleased] entry present (AC-158-001)
…detection (gate fix) (#439)

# F-W85G-GATE1-001: Update ITI diverse e2e expectations for
timed-command detection

**Gate Fix:** F-W85G-GATE1-001 (wave-85 Gate-1)
**Mode:** gate-fix (fix-pr-delivery profile — same rigor as story PRs
minus stubs/Red Gate)
**Source:** Wave-85 integration gate — iti_diverse test failing on
develop (expected 31, got 66)
**Severity:** MEDIUM — test-expectation + doc alignment; no behavioral
regression

![Tests](https://img.shields.io/badge/tests-4%2F4_e2e-brightgreen)
![CI-note](https://img.shields.io/badge/CI-fixture_gitignored-yellow)

![Security](https://img.shields.io/badge/security-test%2Fdoc_only-brightgreen)

![Changelog](https://img.shields.io/badge/changelog--gate-not_triggered-brightgreen)

STORY-180 (BC-2.19.029/030, PR #437) added detection of timed control
command TypeIDs
58–64. The `iec104-iti-diverse.pcap` machine-local fixture (CC-BY-4.0)
contains 25 such
ASDUs that are now correctly detected (+35 findings). The e2e test
expectations were
written before this capture was exercised against the new arms. This PR
updates the
expectations (31→66), adds a time-tagged guard assertion (==35), aligns
the fixture index
doc (`tests/fixtures/E2E-PCAPS.md`), and removes a duplicated scrub-gate
line in
`docs/demo-evidence/STORY-180/evidence-report.md` (gate finding
O-W85G-P1-001).

---

## Architecture Changes

```mermaid
graph TD
    TestFile["tests/iec104_e2e_real_pcaps_tests.rs\n(expectations updated)"]
    FixtureDoc["tests/fixtures/E2E-PCAPS.md\n(count tables aligned)"]
    DemoEvidence["docs/demo-evidence/STORY-180/evidence-report.md\n(duplicate line removed)"]
    Analyzer["src/analyzers/iec104/\n(STORY-180 timed-command arms — unchanged)"]

    TestFile -.->|tests against| Analyzer
    FixtureDoc -.->|documents| TestFile
    DemoEvidence -.->|STORY-180 evidence| Analyzer

    style TestFile fill:#90EE90
    style FixtureDoc fill:#90EE90
    style DemoEvidence fill:#90EE90
```

No architecture changes. No `src/`, `Cargo.toml`, or `bin/`
modifications. The
IEC-104 analyzer implementation from STORY-180 is correct and unchanged;
only
test expectations and documentation are updated to match the actual
analyzer output
against the local fixture.

---

## Story Dependencies

```mermaid
graph LR
    S180["STORY-180 (BC-2.19.029/030)\nPR #437 MERGED\nTimed TypeIDs 58-64 detection"]
    S181["STORY-181 (SEC-001)\nPR #438 MERGED"]
    GATE1["F-W85G-GATE1-001\nthis PR"]

    S180 --> GATE1
    S181 --> GATE1

    style GATE1 fill:#FFD700
    style S180 fill:#90EE90
    style S181 fill:#90EE90
```

Both upstream PRs are merged to develop. This gate fix has no downstream
dependents
beyond the wave-85 gate closure.

---

## Spec Traceability

```mermaid
flowchart LR
    BC["BC-2.19.029/030\nTimed control command\nTypeIDs 58-64"]
    AC["STORY-180 ACs\n(delivered PR #437)"]
    TEST["test_e2e_BC_2_19_iec104_iti_diverse\n_T0836_T1692_001_mixed_asdu"]
    FIX["F-W85G-GATE1-001\nExpectation alignment"]

    BC --> AC
    AC --> TEST
    TEST --> FIX
```

The gate fix closes the expectation gap: STORY-180 delivered the
detection logic;
the fixture in the machine-local corpus confirmed it works correctly;
this PR updates
the test to assert the correct counts.

---

## Test Evidence

### Coverage Summary

| Metric | Value | Threshold | Status |
|--------|-------|-----------|--------|
| E2E tests (local run) | 4/4 pass | 100% | PASS |
| Regressions | 0 | 0 | PASS |
| src/ changes | none | n/a | N/A — test/doc only |
| Changelog gate | not triggered | test/doc exemption | PASS |

**CI note:** The `iec104-iti-diverse.pcap` fixture is in
`tests/fixtures/local-samples/`
(gitignored — CC-BY-4.0 corpus, absent from the CI runner). The updated
`test_e2e_BC_2_19_iec104_iti_diverse_T0836_T1692_001_mixed_asdu` test
will be skipped by
CI (fixture not present). All other tests, lint, and format checks run
normally in CI.
Evidence of the fix passing comes from the local worktree run (4/4 e2e
pass, full suite
0 failed on the fix branch HEAD
`f12c302922d306577be6656349e46f5423ff2bd3`).

### Per-Test Assertion Table (PG-W74-PRDESC-ROW-VERIFY)

Row-verification performed against
`tests/iec104_e2e_real_pcaps_tests.rs` (worktree HEAD `f12c302`).
Fewer than 3 rows in the meaningful assertion set → all rows verified.

| Assertion (row) | Old value | New value | Source line | Verified |
|----------------|-----------|-----------|-------------|---------|
| R1: `iec104.all_findings.len()` == N | 31 | **66** | line 392 | PASS —
exact match |
| R2: `t0836_count` == N | 10 | **20** | line 417 | PASS — exact match |
| R3: `t1692_001_count` == N | 21 | **46** | line 422 | PASS — exact
match |
| R4: `time_tagged_count` == N (new) | (absent) | **35** | line 437 |
PASS — new assertion at line 436–441 |

**Aggregate-count cross-check (PG-W74-PRDESC-ROW-VERIFY §2):**
- Claimed total: 66 = T0836×20 + T1692.001×46. Cross-check: 20+46=66 ✓.
- Derivation confirmed in commit comment:
untimed (31) + timed (+35: x=15 switching TypeIDs 58-59 + 2y=20 setpoint
TypeIDs 61+63) = 66.
  T0836 delta = y = 10 (10→20); T1692.001 delta = x+y = 25 (21→46). ✓
- Aggregate counts are sourced from the local worktree run, NOT from CI
(fixture absent
in CI — explicitly disclosed). Row-verify confirmed values match source
file.

### Test Flow

```mermaid
graph LR
    E2E["4 E2E tests\n(local fixture corpus)"]
    Unit["Unit tests\n(unchanged)"]

    E2E -->|4/4 PASS local| Pass1["PASS"]
    Unit -->|no changes| Pass2["PASS (CI)"]

    style Pass1 fill:#90EE90
    style Pass2 fill:#90EE90
```

<details>
<summary><strong>Detailed Test Results</strong></summary>

### Changed Assertions

| Assertion | File | Line | Old | New | Result |
|-----------|------|------|-----|-----|--------|
| `all_findings.len()` | `tests/iec104_e2e_real_pcaps_tests.rs` | 392 |
31 | 66 | PASS |
| `t0836_count` | same | 417 | 10 | 20 | PASS |
| `t1692_001_count` | same | 422 | 21 | 46 | PASS |
| `time_tagged_count` | same | 437 | (new) | 35 | PASS |
| `detail["total_findings"]` | same | 477 | 31 | 66 | PASS |

### New Assertion Added

`time_tagged_count == 35` at lines 431–441 — guards that exactly x+2y=35
findings
carry a "time-tagged" summary string, decomposing the timed contribution
from STORY-180
(TypeIDs 58-59 → T1692.001 only; TypeIDs 61+63 → T1692.001 + T0836
each).

### Doc Changes

| File | Change | Reason |
|------|--------|--------|
| `tests/fixtures/E2E-PCAPS.md` | iti-diverse row: 31→66, T0836 10→20,
T1692.001 21→46 | Align fixture index with actual analyzer output |
| `docs/demo-evidence/STORY-180/evidence-report.md` | Remove 1 duplicate
scrub-gate line | Gate finding O-W85G-P1-001 |

</details>

---

## Demo Evidence

N/A — this gate fix makes no user-observable behavioral changes. The
IEC-104 analyzer
output is unchanged; only test expectations and documentation are
aligned to the actual
analyzer output against the `iec104-iti-diverse.pcap` local fixture.

STORY-180 demo evidence (which covers the timed-command detection
behavior) is available
at `docs/demo-evidence/STORY-180/evidence-report.md` (PR #437).

| AC | Demo Required | Reason |
|----|---------------|--------|
| F-W85G-GATE1-001 test alignment | No | Transparent expectation update;
no behavioral change |
| O-W85G-P1-001 duplicate line removal | No | Doc cleanup, no behavior |

---

## Holdout Evaluation

N/A — gate fix is a test-expectation alignment, not a behavioral change.
Evaluated at wave gate (wave-85). STORY-180 holdout was completed at PR
#437.

---

## Adversarial Review

N/A — per-story adversarial does not apply to fix-pr-delivery profile.
Wave-level adversarial covers this fix as part of wave-85 gate closure.

---

## Security Review

```mermaid
graph LR
    Critical["Critical: 0"]
    High["High: 0"]
    Medium["Medium: 0"]
    Low["Low: 0"]

    style Critical fill:#90EE90
    style High fill:#90EE90
    style Medium fill:#90EE90
    style Low fill:#90EE90
```

Diff scope: `tests/` and `docs/` only. No `src/`, `Cargo.toml`, or
`bin/` changes.
No new dependencies, no new code paths, no security surface affected.
SAST/cargo audit: N/A for test-expectation update.

---

## Risk Assessment & Deployment

### Blast Radius
- **Systems affected:** Test suite only (local fixture corpus; CI runner
not affected)
- **User impact:** None — no behavioral change in the analyzer
- **Data impact:** None
- **Risk Level:** LOW

### Performance Impact

N/A — test/doc only. No runtime code changes.

<details>
<summary><strong>Rollback Instructions</strong></summary>

**Immediate rollback (< 1 min):**
```bash
git revert f12c302
git push origin develop
```

This would revert test expectations back to 31, re-breaking the wave-85
gate.
Rollback is not expected to be needed — this is a corrective alignment,
not a
speculative change.

</details>

### Feature Flags
None — test/doc only.

---

## Traceability

| Gate Finding | Source | Fix | Test | Status |
|-------------|--------|-----|------|--------|
| Gate-1: iti_diverse expected 31 got 66 | Wave-85 integration gate |
Update assertions 31→66 |
`test_e2e_BC_2_19_iec104_iti_diverse_T0836_T1692_001_mixed_asdu` | PASS
(local) |
| O-W85G-P1-001: duplicate scrub-gate line | Wave-85 gate review |
Remove duplicate line |
`docs/demo-evidence/STORY-180/evidence-report.md` | PASS |

<details>
<summary><strong>Full VSDD Contract Chain</strong></summary>

```
BC-2.19.029/030 → STORY-180 → PR #437 (MERGED) → timed TypeID 58-64 detection
  → iec104-iti-diverse.pcap (local fixture) → 66 findings (was silently dropping 35)
  → F-W85G-GATE1-001 → test expectations aligned → wave-85 gate-1 unblocked
```

</details>

---

## AI Pipeline Metadata

<details>
<summary><strong>Pipeline Details</strong></summary>

```yaml
ai-generated: true
pipeline-mode: gate-fix (fix-pr-delivery)
factory-version: "1.0.0-rc.23"
pipeline-stages:
  gate-finding: F-W85G-GATE1-001 (wave-85 Gate-1 integration run)
  fix-implementation: completed in worktree .worktrees/FIX-W85G
  stubs-red-gate: N/A (not applicable for gate fixes)
  holdout-evaluation: N/A (evaluated at wave gate)
  adversarial-review: N/A (wave-level adversarial covers gate fixes)
  changelog-gate: not triggered (tests/+docs/ only)
generated-at: "2026-07-24"
models-used:
  pr-manager: claude-sonnet-4-6
worktree: .worktrees/FIX-W85G
branch: fix/w85-gate-iti-e2e-expectations
head-sha: f12c302
```

</details>

---

## Pre-Merge Checklist

- [ ] All CI checks passing (fmt, clippy, unit tests, action-pin-gate,
changelog-gate)
- [x] No src/ changes — changelog-gate does not trigger (test/doc
exemption verified)
- [x] No critical/high security findings (test/doc diff only)
- [x] Rollback procedure documented
- [x] Row-verify performed for all assertion rows
(PG-W74-PRDESC-ROW-VERIFY)
- [x] Aggregate counts cross-checked (66=20+46, derivation confirmed)
- [ ] Human merge authorization (DF-MERGE-AUTH-CLASSIFIER-001 — no
wave-85 grant exists; MERGE-AUTH-HALT required)
@Zious11
Zious11 merged commit 9601d71 into main Jul 25, 2026
13 checks passed
@Zious11
Zious11 deleted the release/0.13.2 branch July 25, 2026 00:32
Zious11 added a commit that referenced this pull request Jul 25, 2026
Release PR #440 9601d71 merged to main
(human-merged). Tag v0.13.2 (lightweight). GH release 4 assets published
2026-07-25T00:36:46Z. Back-merge PR #441 TRUE-MERGE e8841d7 to develop
(human-authorized gh pr merge --merge; ancestry PASS, no DRIFT-BACKMERGE-
SQUASH recurrence). Ships wave-85: IEC-104 timed-command detection
(TypeIDs 58-64) + SEC-001 ENIP unsafe elimination + gate-fix. Version
0.13.1→0.13.2. CR-004 CHANGELOG trim applied at release cut.

STATE.md: release fields updated (released_version v0.13.2, released_at
2026-07-25, release_tag v0.13.2, release_commit 9601d71..., develop_head
e8841d7..., cargo_version 0.13.2); EXACT RESUME POINT D-512; Phase
Progress v0.13.2 RELEASED row added; Concurrent Cycles wave-085 develop
head updated; CPS D-507 rolled off / D-512 on; D-512 Decisions Log entry;
Session Resume Checkpoint D-512 replaces D-511; SIZE BUDGET banner
corrected to ~315 lines (wc-l).

cycles/wave-085/session-checkpoints.md: D-511 checkpoint archived.
cycles/wave-085/burst-log.md: D-507 CPS archival + D-512 burst entry.

Count-propagation sweep: no numeric BC/VP/story counts changed in this
burst. Version string sweep: all v0.13.1 and 0ab6f52 hits are legitimate
historical audit-trail entries (prior_released_version, frozen gate SHA,
release row). No propagation gaps.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant