Skip to content

feat(iso-on-tcp): add COTP TPDU parser (STORY-185) - #467

Merged
Zious11 merged 9 commits into
developfrom
feature/STORY-185-cotp-parser
Sep 7, 2026
Merged

feat(iso-on-tcp): add COTP TPDU parser (STORY-185)#467
Zious11 merged 9 commits into
developfrom
feature/STORY-185-cotp-parser

Conversation

@Zious11

@Zious11 Zious11 commented Sep 7, 2026

Copy link
Copy Markdown
Owner

[STORY-185] S7comm COTP TPDU-Type Parser: parse_cotp_header, Protocol-ID Extraction, VP-049 Kani Skeleton

Epic: E-23 — feature-s7comm (wave 88)
Mode: feature
Convergence: CONVERGED after 3 adversarial passes (per-story adversarial 3/3 clean, BC-5.39.001)

Tests
Suite
Kani

Adds the second pure-core parsing layer of the ISO-on-TCP (S7comm) framing subsystem:
parse_cotp_header classifies COTP (ISO 8073 / ITU-T X.224) TPDU headers as Connect
Request, Connect Confirm, or Data Transfer by TPDU-code high nibble, and — for Data
Transfer only — extracts the trailing upper-layer protocol-ID byte verbatim, with zero
interpretation
. This keeps SS-20 (ISO-on-TCP framing) fully protocol-agnostic: no
S7comm-specific knowledge (0x32/0x72/"S7comm") is ever baked into the framing layer.
S7commAnalyzer (SS-21, starting STORY-186) owns all disambiguation of the extracted
byte. Builds directly on STORY-184's parse_tpkt_header / TpktHeader, which are
already on develop and are not re-shipped by this PR.


Architecture Changes

graph TD
    TpktHeader["parse_tpkt_header (STORY-184, on develop)"] -->|"tpkt_payload = data[4..length]"| CotpHeader["parse_cotp_header (NEW)"]
    CotpHeader -->|"CotpHeader { tpdu_type, protocol_id, payload_offset }"| S7commAnalyzer["S7commAnalyzer / SS-21 (STORY-186, not yet built)"]
    style CotpHeader fill:#90EE90
Loading
Architecture Decision Record

ADR: COTP TPDU classification stays protocol-agnostic (ADR-014, already on develop)

Context: SS-20 (ISO-on-TCP framing) must hand upper-layer protocol dispatch a
verbatim byte without becoming coupled to any one upper-layer protocol (S7comm,
S7comm-plus, MMS, ICCP, or anything else riding port 102).

Decision: parse_cotp_header classifies only the ISO 8073 TPDU-code high nibble
(CR/CC/DT) and, for DT, extracts the trailing byte unconditionally as Option<u8>
never comparing it against 0x32, 0x72, or any other literal.

Rationale: Keeps SS-20 reusable by a future IEC 61850 MMS or ICCP/TASE.2 cycle
without modification (ADR-014 Decision 2).

Alternatives Considered:

  1. Fold S7comm/S7comm-plus disambiguation directly into parse_cotp_header — rejected:
    would couple SS-20 to SS-21's protocol knowledge, violating the frozen SS-20/SS-21
    boundary (BC-2.20.012).
  2. Model all 16 ISO 8073 TPDU codes — rejected: only 3 are needed for S7comm's
    session-establishment + data-transfer flow (ADR-014 Decision 1); the other 13
    collapse to a single None reject arm (BC-2.20.011).

Consequences:

  • SS-20 stays a two-function, dependency-free pure-core module.
  • S7commAnalyzer (STORY-186+) must implement its own protocol-ID disambiguation table.

Story Dependencies

graph LR
    S184[STORY-184<br/>merged, on develop] --> S185[STORY-185<br/>this PR]
    S185 --> S186[STORY-186<br/>not started]
    style S185 fill:#FFD700
Loading

Spec Traceability

flowchart LR
    BC1[BC-2.20.005<br/>len < 2 rejected] --> AC1[AC-185-001]
    BC2[BC-2.20.006<br/>LI-truncation rejected] --> AC2[AC-185-002]
    BC3[BC-2.20.007<br/>CR recognized] --> AC3[AC-185-003]
    BC4[BC-2.20.008<br/>CC recognized] --> AC4[AC-185-004]
    BC5[BC-2.20.009<br/>DT non-empty extracts id] --> AC5[AC-185-005]
    BC6[BC-2.20.010<br/>DT empty, id None] --> AC6[AC-185-006]
    BC7[BC-2.20.011<br/>unrecognized rejected] --> AC7[AC-185-007]
    BC8[BC-2.20.012<br/>id verbatim, never interpreted] --> AC9[AC-185-009]
    AC1 --> T1[test_BC_2_20_005_len_shorter_than_2_returns_none]
    AC5 --> T5[test_BC_2_20_009_dt_nonempty_payload_extracts_protocol_id]
    AC9 --> T9[test_BC_2_20_012_protocol_id_extraction_totality]
    T1 --> SRC[src/analyzer/iso_on_tcp.rs]
    T5 --> SRC
    T9 --> SRC
Loading

Test Evidence

Coverage Summary

Metric Value Threshold Status
Story tests (mod story_185) 22/22 pass 100% PASS
Full iso_on_tcp_tests suite 52/52 pass (30 story_184 + 22 story_185) 100% PASS
Coverage / mutation Not separately measured for this micro-scope pure-function story tracked at wave-88 gate N/A this PR
Holdout satisfaction (Phase-4 pipeline) N/A — evaluated at wave gate >0.85 N/A this PR

Per-story adversarial review (BC-5.39.001 discipline) converged 3/3 clean passes — see
Adversarial Review section below.

Per-AC Test Distribution (row-verified against cargo test --test iso_on_tcp_tests raw output)

AC BC Tests Representative Test Name(s)
AC-185-001 BC-2.20.005 2 test_BC_2_20_005_len_shorter_than_2_returns_none
AC-185-002 BC-2.20.006 3 test_BC_2_20_006_li_truncation_returns_none
AC-185-003 BC-2.20.007 5 (incl. 2 RFC-905 holdouts) test_BC_2_20_007_connect_request_recognized
AC-185-004 BC-2.20.008 2 test_BC_2_20_008_connect_confirm_recognized
AC-185-005 BC-2.20.009 4 (incl. 1 RFC-905 holdout) test_BC_2_20_009_dt_nonempty_payload_extracts_protocol_id
AC-185-006 BC-2.20.010 1 test_BC_2_20_010_dt_empty_payload_protocol_id_none
AC-185-007 BC-2.20.011 2 (incl. 1 RFC-905 holdout) test_BC_2_20_011_unrecognized_tpdu_type_returns_none
AC-185-008 BC-2.20.011 inv. 3 1 test_BC_2_20_011_tpdu_type_match_is_exhaustive
AC-185-009 BC-2.20.012 2 test_BC_2_20_012_protocol_id_extraction_totality (exhaustive over all 256 u8 values), test_BC_2_20_012_static_regression_guard_no_hardcoded_protocol_literals
AC-185-010 VP-049 source-level (grep + cargo check/clippy) Kani skeleton at src/analyzer/iso_on_tcp.rs (mod kani_proofs, verify_parse_cotp_header_safety)

Cross-check: 2+3+5+2+4+1+2+1+2 = 22, matching the story_185 subset of the raw
test result: ok. 52 passed; 0 failed output exactly. Full detail and raw transcripts:
docs/demo-evidence/STORY-185/evidence-report.md and per-AC files
docs/demo-evidence/STORY-185/AC-00{1..9}-*.md, AC-010-vp049-kani-skeleton.md.

Detailed Test Results (raw cargo test excerpt, story_185 module)
test story_185::test_BC_2_20_005_len_shorter_than_2_returns_none ... ok
test story_185::test_BC_2_20_005_invariant_no_panic_across_short_inputs ... ok
test story_185::test_BC_2_20_006_li_truncation_returns_none ... ok
test story_185::test_BC_2_20_006_invariant_no_panic_across_li_value_sample ... ok
test story_185::test_BC_2_20_006_li_zero_not_truncated_proceeds_to_classification ... ok
test story_185::test_BC_2_20_007_connect_request_recognized ... ok
test story_185::test_BC_2_20_007_connect_request_nonzero_low_nibble_still_recognized ... ok
test story_185::test_BC_2_20_007_connect_request_protocol_id_none_even_with_trailing_bytes ... ok
test story_185::test_BC_2_20_008_connect_confirm_recognized ... ok
test story_185::test_BC_2_20_008_connect_confirm_nonzero_low_nibble_still_recognized ... ok
test story_185::test_BC_2_20_009_dt_nonempty_payload_extracts_protocol_id ... ok
test story_185::test_BC_2_20_009_dt_protocol_id_is_first_trailing_byte_only ... ok
test story_185::test_BC_2_20_009_dt_protocol_id_extracted_for_boundary_byte_values ... ok
test story_185::test_BC_2_20_010_dt_empty_payload_protocol_id_none ... ok
test story_185::test_BC_2_20_011_unrecognized_tpdu_type_returns_none ... ok
test story_185::test_BC_2_20_011_tpdu_type_match_is_exhaustive ... ok
test story_185::test_BC_2_20_012_protocol_id_extraction_totality ... ok
test story_185::test_BC_2_20_012_static_regression_guard_no_hardcoded_protocol_literals ... ok
test story_185::test_iso8073_rfc905_table8_cr_cc_low_nibble_is_free_holdout ... ok
test story_185::test_iso8073_rfc905_table8_dr_code_not_modeled_holdout ... ok
test story_185::test_iso8073_rfc905_s13_7_1_dt_class0_normal_format_holdout ... ok
test story_185::test_iso8073_rfc905_s13_2_1_li_excludes_itself_holdout ... ok

test result: ok. 52 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

Demo Evidence

Library/pure-core story — no CLI or web surface exists yet (S7commAnalyzer dispatch
wiring is STORY-186). Per the demo-recording skill's library/test-harness mode
(mirroring the STORY-184 precedent), evidence is captured as annotated cargo test
transcripts grouped by AC, plus source-level grep/cargo check/cargo clippy
verification for the VP-049 Kani skeleton.

Committed at docs/demo-evidence/STORY-185/:

File AC Coverage
evidence-report.md Index — full 52/52 suite run, per-AC coverage map, row-verified test-count cross-check
AC-001-short-input-rejection.md AC-185-001 (BC-2.20.005)
AC-002-li-truncation-rejection.md AC-185-002 (BC-2.20.006)
AC-003-connect-request-recognition.md AC-185-003 (BC-2.20.007)
AC-004-connect-confirm-recognition.md AC-185-004 (BC-2.20.008)
AC-005-dt-nonempty-protocol-id-extraction.md AC-185-005 (BC-2.20.009)
AC-006-dt-empty-payload-protocol-id-none.md AC-185-006 (BC-2.20.010)
AC-007-unrecognized-tpdu-rejection.md AC-185-007 (BC-2.20.011)
AC-008-tpdu-type-exhaustive-partition.md AC-185-008 (BC-2.20.011 invariant 3)
AC-009-protocol-id-totality.md AC-185-009 (BC-2.20.012)
AC-010-vp049-kani-skeleton.md AC-185-010 (VP-049)

Demo-evidence path-scrub gate (PG-W70-DEMO-SCRUB): PASSED, zero absolute-local-path
matches in any evidence file (2026-09-06).


Holdout Evaluation

N/A — evaluated at wave-88 gate (not per-story for this pipeline mode). Note: this
story's own test suite additionally includes 4 unit-level RFC-905 (ISO 8073) holdout
tests (test_iso8073_rfc905_*) that independently probe the spec against ITU-T
X.224/ISO 8073 table 8 and clause 13 — these are a different mechanism from the
Phase-4 pipeline holdout-evaluation gate and are already counted in the 22/22 above.


Adversarial Review

Per-story adversarial review converged 3/3 clean passes (BC-5.39.001 discipline).

Pre-dispositioned finding (non-blocking, flagged for pr-reviewer awareness): a
per-story adversarial NIT observed that the regression-guard test
(test_BC_2_20_012_static_regression_guard_no_hardcoded_protocol_literals) comment
overstates that "the file never contains 0x32/0x72 as contiguous text" — true only
of the literal tokens the assertion actually checks, not of every possible substring
occurrence. The load-bearing property (no S7comm-specific literal comparison inside
parse_cotp_header's control flow, BC-2.20.012 postcondition 3) holds regardless.
Disposition: accepted residual, not a merge blocker.

A COTP-classification-focused review of the implementation found it panic-safe (bounds
checks precede every index into tpkt_payload), RFC-905/ISO-8073-conformant (TPDU-code
high-nibble discrimination matches ITU-T X.224 table 8), and protocol-agnostic (zero
0x32/0x72/"S7comm" literals anywhere in the parsing logic — verified by the static
regression-guard test above and by direct inspection).


Security Review

Populated after Step 4 (security-reviewer dispatch) — see PR comment / commit history
for the completed scan. Summary: pure, allocation-free &[u8] parsing with no I/O, no
unsafe, no external input trust boundary beyond standard slice-bounds checks; no
injection/auth/OWASP-Top-10 surface applies to a byte-classification free function.


Risk Assessment & Deployment

Blast Radius

  • Systems affected: src/analyzer/iso_on_tcp.rs only (SS-20, ISO-on-TCP framing).
    No existing analyzer wiring changed — parse_cotp_header is not yet called from any
    StreamAnalyzer impl (that wiring is STORY-186).
  • User impact: None in this PR — purely additive, dead code from the CLI's
    perspective until STORY-186 wires it up.
  • Data impact: None.
  • Risk Level: LOW.

Feature Flags

None — additive pure-core module, not yet reachable from any runtime path.


Traceability

BC Story AC Test Verification Status
BC-2.20.005 AC-185-001 test_BC_2_20_005_len_shorter_than_2_returns_none unit + VP-049 (deferred, STORY-194) PASS
BC-2.20.006 AC-185-002 test_BC_2_20_006_li_truncation_returns_none unit + VP-049 (deferred) PASS
BC-2.20.007 AC-185-003 test_BC_2_20_007_connect_request_recognized unit PASS
BC-2.20.008 AC-185-004 test_BC_2_20_008_connect_confirm_recognized unit PASS
BC-2.20.009 AC-185-005 test_BC_2_20_009_dt_nonempty_payload_extracts_protocol_id unit PASS
BC-2.20.010 AC-185-006 test_BC_2_20_010_dt_empty_payload_protocol_id_none unit PASS
BC-2.20.011 AC-185-007, AC-185-008 test_BC_2_20_011_unrecognized_tpdu_type_returns_none, test_BC_2_20_011_tpdu_type_match_is_exhaustive unit PASS
BC-2.20.012 AC-185-009 test_BC_2_20_012_protocol_id_extraction_totality (256-value exhaustive loop), static regression guard unit PASS
VP-049 AC-185-010 verify_parse_cotp_header_safety (Kani) skeleton compiles; full proof deferred to STORY-194 PASS (skeleton)

VP-049 deferral note: per the story's own scope (and mirroring STORY-184's VP-048
precedent), only the Kani harness skeleton is delivered here — it compiles under
#[cfg(kani)] and targets bounds-safety over symbolic input. The full proof run
(TPDU-type-classification exhaustiveness over all 16 nibble values, and protocol-ID
extraction totality over all 256 u8 values) is STORY-194's obligation (formal
hardening phase), not this PR's.

Full VSDD Contract Chain
BC-2.20.005 -> AC-185-001 -> test_BC_2_20_005_len_shorter_than_2_returns_none -> src/analyzer/iso_on_tcp.rs:245 -> ADV-PASS-3-CLEAN -> KANI-SKELETON
BC-2.20.006 -> AC-185-002 -> test_BC_2_20_006_li_truncation_returns_none -> src/analyzer/iso_on_tcp.rs:249 -> ADV-PASS-3-CLEAN -> KANI-SKELETON
BC-2.20.007 -> AC-185-003 -> test_BC_2_20_007_connect_request_recognized -> src/analyzer/iso_on_tcp.rs:255 -> ADV-PASS-3-CLEAN
BC-2.20.008 -> AC-185-004 -> test_BC_2_20_008_connect_confirm_recognized -> src/analyzer/iso_on_tcp.rs:260 -> ADV-PASS-3-CLEAN
BC-2.20.009 -> AC-185-005 -> test_BC_2_20_009_dt_nonempty_payload_extracts_protocol_id -> src/analyzer/iso_on_tcp.rs:265 -> ADV-PASS-3-CLEAN
BC-2.20.010 -> AC-185-006 -> test_BC_2_20_010_dt_empty_payload_protocol_id_none -> src/analyzer/iso_on_tcp.rs:265 -> ADV-PASS-3-CLEAN
BC-2.20.011 -> AC-185-007/008 -> test_BC_2_20_011_* -> src/analyzer/iso_on_tcp.rs:277 -> ADV-PASS-3-CLEAN
BC-2.20.012 -> AC-185-009 -> test_BC_2_20_012_protocol_id_extraction_totality -> src/analyzer/iso_on_tcp.rs:266-274 -> ADV-PASS-3-CLEAN
VP-049 -> AC-185-010 -> verify_parse_cotp_header_safety -> src/analyzer/iso_on_tcp.rs:293-320 -> SKELETON-COMPILES -> FULL-PROOF-DEFERRED-STORY-194

AI Pipeline Metadata

Pipeline Details
ai-generated: true
pipeline-mode: feature
factory-version: "1.0.0"
pipeline-stages:
  spec-crystallization: completed
  story-decomposition: completed
  tdd-implementation: completed
  holdout-evaluation: deferred-to-wave-gate
  adversarial-review: completed (3/3 clean, per-story)
  formal-verification: skeleton-only (full proof deferred to STORY-194)
  convergence: achieved
convergence-metrics:
  adversarial-passes: 3
  test-kill-rate: N/A (mutation testing tracked at wave level)
story: STORY-185
epic: E-23
wave: 88
cycle: feature-s7comm
generated-at: "2026-09-06T00:00:00Z"

Pre-Merge Checklist

  • All CI status checks passing
  • CHANGELOG [Unreleased] entry present (touches src/)
  • No critical/high security findings unresolved (pending Step 4 security review)
  • Demo evidence committed (docs/demo-evidence/STORY-185/, 10 AC files + evidence-report.md)
  • Dependency (STORY-184) already merged to develop
  • pr-reviewer APPROVE verdict obtained

https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW

Files created: none (extends src/analyzer/iso_on_tcp.rs from STORY-184)
Files modified: src/analyzer/iso_on_tcp.rs
todo!() functions: 1 (parse_cotp_header)

Adds the frozen SS-20 -> SS-21 interface types (CotpTpduType, CotpHeader) as real
type definitions per ADR-014 Decision 1, a todo!()-bodied parse_cotp_header stub
targeting BC-2.20.005-012, and a #[cfg(kani)] VP-049 proof skeleton
(verify_parse_cotp_header_safety) mirroring the existing VP-048 harness pattern.

Self-check (BC-5.38.005 invariant 1) applied to parse_cotp_header: "If I include
this real implementation, will the test for this function pass trivially without
any implementer work?" -- yes, so the body is todo!() per BC-5.38.001.

## GREEN-BY-DESIGN
none

## WIRING-EXEMPT
none

Verified: cargo check --all-targets clean, cargo clippy --all-targets -- -D
warnings clean, cargo fmt applied, existing 30 STORY-184 tests in
tests/iso_on_tcp_tests.rs still pass. No test files touched (test-writer's job).
Zero occurrences of 0x32/0x72 literals or "S7comm"/"S7comm-plus" string literals
in src/analyzer/iso_on_tcp.rs, per BC-2.20.012 postcondition 3 / AC-185-009.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
Adds mod story_185 to tests/iso_on_tcp_tests.rs covering BC-2.20.005
through BC-2.20.012 for parse_cotp_header (COTP TPDU-type recognition
and verbatim protocol_id extraction). 21 tests exercise the todo!()
stub and fail (Red Gate, BC-5.38.001); one static source-inspection
test (regression guard for 0x32/0x72 literals) legitimately passes
without implementation. Pre-existing mod story_184 (30 tests) remains
green and untouched.

Includes an independent ISO 8073 holdout (DF-CANONICAL-FRAME-HOLDOUT-001)
derived from RFC 905 ("ISO Transport Protocol Specification ISO DP
8073"), fetched and cross-checked directly (§13.2, §13.2.1, §13.2.2.2,
Table 8, §13.7.1), rather than copied from this project's own BC text.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
…es (BC-2.20.007-012)

Implements the TPDU-type match (CR 0xE0, CC 0xD0, DT 0xF0, else None) and
verbatim protocol_id extraction for DT. 51/52 story_185+story_184 tests
green; test_BC_2_20_006_invariant_no_panic_across_li_value_sample fails on
its own sample value 0x01 (not a truncation case per BC-2.20.006's own
formula against a 3-byte buffer) -- flagged as a test bug, not fixed here
per no-test-modification constraint.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
Removes stale Red-Gate todo!() note from the module doc comment now that
parse_cotp_header is implemented; adds an [Unreleased] CHANGELOG entry
(AC-158-001) describing the COTP TPDU header parser. VP-049 Kani harness
codegen verified clean (`cargo kani --only-codegen`); full proof execution
remains deferred to STORY-194 per this story's scope note.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
…-panic test

test_BC_2_20_006_invariant_no_panic_across_li_value_sample sampled
LI=0x01 against the fixed 3-byte buffer [li, 0xE0, 0x00] and asserted
None for every value. But BC-2.20.006's truncation predicate is
`tpkt_payload.len() < 1 + LI`; for LI=0x01, `3 < 2` is false, so the
frame is not truncated and correctly classifies as
Some(CotpHeader{ConnectRequest, ..}) per BC-2.20.007. The assertion
of None for 0x01 was wrong test data, not a truncation-invariant
violation.

Replace 0x01 with 0x03 (the exact truncation boundary: 1+3=4 > 3),
keeping the sample genuinely truncating for every value:
{0x03, 0x0A, 0x7F, 0xFE, 0xFF}. Document the truncation arithmetic in
the test comment so the boundary rationale is explicit.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
…-185-P1-001)

The mod story_185 Provenance block still asserted, in present/future tense,
that these tests were expected to fail against a todo!() stub. parse_cotp_header
is fully implemented and all 52 tests are GREEN. Rewrote the block to mirror
mod story_184's accurate past-tense/GREEN-state provenance prose.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
…ry M-1)

The STORY-184 [Unreleased] bullet described COTP header parsing as future
work ("ahead of the COTP header parser (STORY-185)"), which was accurate
when written but is now stale since STORY-185 delivers parse_cotp_header
in this same branch (already documented in the bullet immediately below).
Reword to reflect that the TPKT groundwork is consumed by the now-delivered
COTP parser, while keeping the S7comm PDU dissector (STORY-186) framed as
future work since that remains undelivered.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
Captures the mod story_185 test run (22 tests, all passing) mapping each
AC-185-001..010 to its demonstrating test(s), per the STORY-184 demo-evidence
convention. AC-185-010 (VP-049 Kani skeleton) verified by source presence plus
cargo check/clippy, full proof deferred to STORY-194.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
@Zious11

Zious11 commented Sep 7, 2026

Copy link
Copy Markdown
Owner Author

Fresh-eyes review — STORY-185 (COTP TPDU-Type Parser)

Verdict: APPROVE

Reviewed the diff (src/analyzer/iso_on_tcp.rs +173/-5, tests/iso_on_tcp_tests.rs +708) against BC-2.20.005–012. Built the branch in an isolated worktree: all 22 mod story_185 tests pass; cargo clippy --all-targets -- -D warnings is clean.

Acceptance-criteria trace (all satisfied)

BC Requirement Implementation
.005 len < 2None if tpkt_payload.len() < 2 { return None }
.006 LI-truncation → None if tpkt_payload.len() < 1 + li { return None } — no overflow (li ≤ 255), no OOB ✓
.007 CR (& 0xF0 == 0xE0), protocol_id: None 0xE0 => arm ✓
.008 CC (0xD0), protocol_id: None 0xD0 => arm ✓
.009 DT (0xF0) non-empty → verbatim byte Some(tpkt_payload[payload_offset]) when len > payload_offset
.010 DT empty payload → None else None, no OOB at payload_offset == len
.011 any other high nibble → None _ => None; match is exhaustive & mutually-exclusive over all 16 nibble values ✓
.012 protocol_id never interpreted Extracted verbatim; no comparison against 0x32/0x72/"S7comm" anywhere in parsing logic (verified via git diff — grep for those literals in the source is empty) ✓

Strengths

  • Bounds safety is airtight: every index ([0], [1], [payload_offset]) is dominated by a prior length guard; li == 0 and li == 0xFF both handled without panic.
  • Test suite is unusually rigorous: exhaustive 16-value high-nibble partition, exhaustive 256-value protocol_id totality sweep, u8-boundary byte checks, and four independent RFC 905 holdout vectors (distinct byte patterns from the BC canonical vectors) per DF-CANONICAL-FRAME-HOLDOUT-001.
  • VP-049 #[cfg(kani)] skeleton correctly scoped to no-panic/bounds-safety only, with the full classification/totality proof explicitly deferred to STORY-194 — matches the doc comment's stated obligation.

Non-blocking notes (no change requested)

  • The protocol_id regression-guard comment claims the file contains zero occurrences of the guarded literals "anywhere"; the assertion in fact checks only 0x32/0x72 (not the "S7comm" doc-text string, which is present in module docs). This is the already-dispositioned residual from the per-story adversarial pass — recorded here only for completeness, accepted as-is.
  • Degenerate LI == 0 DT: payload_offset == 1, so protocol_id becomes the TPDU-code byte itself. This is contract-consistent (payload_offset = 1 + LI is the frozen definition) and is explicitly pinned by test_BC_2_20_006_li_zero_not_truncated_proceeds_to_classification — documented behavior, not a defect.

Scope, purity (ADR-014 Decision 9), and the frozen SS-20/SS-21 boundary are all respected. No blocking findings.

@Zious11
Zious11 merged commit e0ea30c into develop Sep 7, 2026
13 checks passed
@Zious11
Zious11 deleted the feature/STORY-185-cotp-parser branch September 7, 2026 04:23
Zious11 added a commit that referenced this pull request Sep 7, 2026
…TPDU-type parser)

PR #467 squash-merged to develop as e0ea30c (human-executed merge — the
permission classifier blocked the agent-dispatched gh pr merge). Per-story
adversarial CONVERGED 3/3 (BC-5.39.001) in 5 passes vs STORY-184's 10.
pr-reviewer APPROVE cycle 1 (0 blocking); security CLEAN; CI 13/13.

- STORY-185.md status ready->delivered; STORY-INDEX.md v4.26->v4.27
  (status column + wave-88 delivery-progress row; totals unchanged
  147/97/863; delivered 121->122).
- STATE.md: version 2.9->3.0; D-563 recorded; develop_head e0ea30c;
  Phase Progress / Concurrent Cycles F4 rows updated (2/11 delivered);
  PG-MERGE-CLASSIFIER-F4 + PG-CANONICAL-HOLDOUT-NOT-AC-ENFORCED-WATCH
  carry-forwards added; Session Resume Checkpoint replaced (D-562
  checkpoint archived to cycles/feature-s7comm/session-checkpoints.md).
- lessons.md: two residuals logged for cycle-close (regression-guard
  comment overstatement NIT; PG-CANONICAL-HOLDOUT-NOT-AC-ENFORCED
  recurrence #2, nearing 3x codification threshold) plus
  PG-MERGE-CLASSIFIER-F4 operating-arrangement lesson.
- code-delivery/STORY-185/ delivery evidence (pr-description, pr-review,
  security-review) added.

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
Zious11 added a commit that referenced this pull request Sep 7, 2026
…es delivered

Session-wrap-pause checkpoint (D-564). Pipeline IN-PROGRESS→PAUSED;
version 3.0→3.1. F4 delta-implementation (feature-s7comm, epic E-23)
mid-cycle: STORY-184 (#466, wave 87) + STORY-185 (#467, wave 88)
DELIVERED to develop e0ea30c; STORY-186 worktree created as a clean
baseline (no code yet) on feature/STORY-186-iso-on-tcp-reassembly.

D-563 Session Resume Checkpoint archived to
cycles/feature-s7comm/session-checkpoints.md; new checkpoint written
with all six required fields. Current Phase Steps: new row added,
D-559 row evicted (preserved verbatim in Decisions Log D-559).
regression-state.json + sidecar-learning.md hook-churn folded in.

No story/spec/code content changed — bookkeeping-only wrap-pause
burst, the only STATE.md mutation performed during wrap
(BC-6.28.001 INV-1), single-commit burst (TD-VSDD-053).

Claude-Session: https://claude.ai/code/session_01EQAaPvh9fwaG31jmkPicKW
Zious11 added a commit that referenced this pull request Sep 7, 2026
…Y-186) (#470)

# [STORY-186] S7comm ISO-on-TCP Carry-Buffer Reassembly, Walk-First
Frame Extraction, Resync, and the Frozen SS-20/SS-21 Module Boundary

**Epic:** S7comm over ISO-on-TCP (TPKT/COTP) stream dispatch and parser
design (ADR-0014)
**Mode:** feature (brownfield, wave 89)
**Convergence:** CONVERGED after 5 adversarial passes (P1, P1b, P2, P3,
P5 — see Adversarial Review below)

![Tests](https://img.shields.io/badge/tests-18%2F18-brightgreen)

![Coverage](https://img.shields.io/badge/coverage-new%20code%20100%25-brightgreen)

![Holdout](https://img.shields.io/badge/holdout-N%2FA%20wave%20gate-blue)

This PR adds `S7commAnalyzer` (`src/analyzer/s7comm.rs`, SS-21) — the
first effectful-shell
consumer of STORY-184/185's stateless TPKT/COTP parsing library (SS-20,
`src/analyzer/iso_on_tcp.rs`). It implements directional carry-buffer
TPKT reassembly across
TCP segment boundaries using a walk-first, residual-bound
frame-extraction loop (no aggregate
`carry.len() + data.len()` pre-check), a shared 1-byte resync
sub-routine reused verbatim for
both bad-version-byte and post-overflow conditions, a 65,535-byte carry
bound with a
defense-in-depth overflow guard (clear-not-truncate + one T0814 finding
per direction, proven
unreachable via real `on_data` traffic under the current design), and
flow-close teardown that
discards carry bytes with no finding. It also freezes the SS-20/SS-21
module boundary with two
static regression guards: `iso_on_tcp.rs` must contain zero
`StreamAnalyzer` impls, and no
`IsoOnTcpFlowState` type may exist anywhere in the tree.

---

## Architecture Changes

```mermaid
graph TD
    IsoOnTcp["iso_on_tcp.rs (SS-20)<br/>parse_tpkt_header / parse_cotp_header<br/>stateless, pure-core"] -->|calls| S7comm["s7comm.rs (SS-21)<br/>S7commAnalyzer, new"]
    S7comm -.->|new dependency| FlowState["S7commFlowState<br/>carry_c2s / carry_s2c<br/>per-direction overflow latches"]
    style S7comm fill:#90EE90
    style FlowState fill:#90EE90
```

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

### ADR: Carry-overflow T0814 guard reclassified as defense-in-depth
(ADR-0014 v1.1)

**Context:** BC-2.20.014 originally specified an overflow guard for
`residual.len() > 65,535`. During adversarial review (F-02/F-03, two
independent passes),
it was shown that under the BC-2.20.013 walk-first + BC-2.20.015
1-byte-resync design, the
directional carry is bounded `<= 65,534` bytes by construction for both
conformant and
adversarial input (TPKT's `length` field is u16-capped), making the
over-bound branch
unreachable via the real `on_data` path.

**Decision:** Reclassify the overflow guard as defense-in-depth (Option
B, human-ratified
2026-09-07) rather than removing it. The guard mechanics
(clear-not-truncate, resync, one
T0814 per direction, per-direction dedup) remain the binding spec for
the guard's behavior
*if* a future design regression ever makes the branch reachable, but are
tested via direct
flow-state injection (SYNTHETIC), not via `on_data`.

**Rationale:** Removing the guard would leave no protection against a
future change (e.g. a
resync or walk-first regression) that reintroduces reachability. Keeping
it as defense-in-depth
preserves the safety net without over-claiming live coverage in test
evidence.

**Alternatives Considered:**
1. Remove the guard entirely — rejected because it deletes a real safety
net against future
   regressions in the walk-first/resync invariants.
2. Leave the guard's test classified as LIVE — rejected because it
misrepresents demo
evidence; the adversarial pass found this to be a false claim of on-data
reachability.

**Consequences:**
- Test suite now carries an explicit LIVE vs. SYNTHETIC distinction
(AC-186-004 LIVE,
  AC-186-005/006 SYNTHETIC) plus a positive unreachability proof
(`test_BC_2_20_014_overflow_unreachable_via_on_data`, 200,000-byte
garbage flood emits no
  T0814).
- ADR-0014 D5 was annotated to record the defense-in-depth
classification (commit `593c28ef`).

</details>

---

## Story Dependencies

```mermaid
graph LR
    S184[STORY-184<br/>merged] --> S185[STORY-185<br/>merged]
    S185 --> S186[STORY-186<br/>this PR]
    S186 --> S187[STORY-187<br/>protocol-id dispatch, not started]
    S186 --> S193[STORY-193<br/>CLI wiring, not started]
    style S186 fill:#FFD700
```

STORY-186 `depends_on` STORY-185 (COTP TPDU header parser, merged to
`develop` at `e0ea30ce`,
PR #467). No other open dependency PRs block this one.

---

## Spec Traceability

```mermaid
flowchart LR
    BC1[BC-2.20.013<br/>walk-first extraction] --> AC1["AC-186-001..003"]
    BC2[BC-2.20.014<br/>carry bound + guard] --> AC2["AC-186-004..006"]
    BC3[BC-2.20.015<br/>1-byte resync] --> AC3["AC-186-007..009"]
    BC4[BC-2.20.016<br/>frozen module boundary] --> AC4["AC-186-010..011"]
    BC5[BC-2.21.003<br/>flow-close teardown] --> AC5["AC-186-012"]
    AC1 --> T1[s7comm_analyzer_tests.rs]
    AC2 --> T1
    AC3 --> T1
    AC4 --> T1
    AC5 --> T1
    T1 --> S1[src/analyzer/s7comm.rs]
```

---

## Test Evidence

### Coverage Summary

| Metric | Value | Threshold | Status |
|--------|-------|-----------|--------|
| Unit/integration tests | 18/18 pass | 100% | PASS |
| New code coverage | new `s7comm.rs` module fully exercised by 18 tests
+ 3 proptest harnesses | >80% | PASS |
| Mutation kill rate | not run this story (deferred to formal-hardening
phase for this wave) | >90% | N/A — deferred |
| Holdout satisfaction | N/A — evaluated at wave gate | >0.85 | N/A |

### Test Flow

```mermaid
graph LR
    Unit["15 Unit/Regression Tests"]
    Proptest["3 Proptest Harnesses (VP-050)"]
    Wave["Wave Gate"]

    Unit -->|100% of new module| Pass1["PASS"]
    Proptest -->|default case count| Pass2["PASS"]
    Wave -->|deferred| Pass3["N/A yet"]

    style Pass1 fill:#90EE90
    style Pass2 fill:#90EE90
    style Pass3 fill:#87CEEB
```

| Metric | Value |
|--------|-------|
| **New tests** | 18 added (15 unit/regression + 3 proptest harnesses),
0 modified |
| **Total suite (this test file)** | 18 tests PASS in ~4.64s (locally
re-run at PR-manager time; matches evidence-report.md claim) |
| **Coverage delta** | new module (`src/analyzer/s7comm.rs`, 321 lines)
— n/a baseline, fully covered by new tests |
| **Mutation kill rate** | deferred — no `cargo mutants` run recorded
for this story |
| **Regressions** | 0 — `cargo fmt --check` and `cargo clippy
--all-targets -- -D warnings` both clean locally at PR-manager
verification time |

<details>
<summary><strong>Detailed Test Results (row-verified against
evidence-report.md and local re-run,
PG-W74-PRDESC-ROW-VERIFY)</strong></summary>

### New Tests (This PR) — `tests/s7comm_analyzer_tests.rs`

| Test | Result | AC / BC |
|------|--------|---------|
| `test_BC_2_20_013_walk_first_no_aggregate_precheck` | PASS |
AC-186-001 / BC-2.20.013 PC-1,PC-2,Inv-1 |
| `test_BC_2_20_013_adversarial_burst_head_frame_not_dropped` | PASS |
AC-186-002 / BC-2.20.013 Inv-1 |
| `test_BC_2_20_013_split_frame_across_two_calls` | PASS (row-verified:
present in local `cargo test` run and evidence-report.md) | AC-186-003 /
BC-2.20.013 EC-002 |
| `test_BC_2_20_014_at_bound_residual_no_overflow` | PASS (row-verified:
present in local `cargo test` run and evidence-report.md) | AC-186-004
(LIVE) / BC-2.20.014 Inv-1, EC-001 |
| `test_BC_2_20_014_overflow_clear_resync_one_t0814_per_direction` |
PASS | AC-186-005 (SYNTHETIC) / BC-2.20.014 PC-1,PC-3,PC-4,EC-004 |
| `test_BC_2_20_014_repeated_overflow_dedup_same_direction` | PASS |
AC-186-005 (SYNTHETIC) / BC-2.20.014 PC-1,PC-3,PC-4,EC-004 |
| `test_BC_2_20_014_overflow_unreachable_via_on_data` | PASS |
AC-186-005 (positive unreachability proof) |
| `test_BC_2_20_014_overflow_dedup_independent_per_direction` | PASS |
AC-186-006 (SYNTHETIC) / BC-2.20.014 PC-4, EC-005 |
| `test_BC_2_20_015_resync_advances_exactly_one_byte` | PASS |
AC-186-007 / BC-2.20.015 PC-1, Inv-1 |
| `test_BC_2_20_015_single_resync_implementation_shared` | PASS |
AC-186-008 / BC-2.20.015 Inv-3 |
| `test_BC_2_20_015_resync_terminates_no_valid_anchor` | PASS |
AC-186-009 / BC-2.20.015 Inv-2 |
| `test_BC_2_20_016_iso_on_tcp_has_no_stream_analyzer_impl` | PASS
(row-verified: present in local `cargo test` run and evidence-report.md)
| AC-186-010 / BC-2.20.016 PC-1 |
| `test_BC_2_20_016_no_iso_on_tcp_flow_state_type_exists` | PASS |
AC-186-011 / BC-2.20.016 PC-3 |
| `test_s7comm_on_flow_close_removes_state_discards_carry` | PASS |
AC-186-012 / BC-2.21.003 PC-1..4 |
| `test_BC_2_21_003_double_close_same_flow_key_is_idempotent_no_op` |
PASS | AC-186-012 / BC-2.21.003 (EC-002 double-close) |
| `story_186::vp050::proptest_vp050_walk_first_residual_bound` | PASS |
VP-050 |
| `story_186::vp050::proptest_vp050_direction_isolation` | PASS | VP-050
|
| `story_186::vp050::proptest_vp050_resync_one_byte_advance` | PASS |
VP-050 |

**Aggregate-count cross-check (PG-W74-PRDESC-ROW-VERIFY):**
evidence-report.md claims
"18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out" — local
re-run by pr-manager
(`cargo test --test s7comm_analyzer_tests`) confirms **18 passed; 0
failed** with the identical
18 test names listed above. Aggregate count matches exactly; no
discrepancy found.

### Coverage Analysis

| Metric | Value |
|--------|-------|
| Lines added | 321 (`src/analyzer/s7comm.rs`) + 1108 (test file) |
| Lines covered | new module fully exercised — every branch of `on_data`
walk-first loop, resync, overflow guard (synthetic), and `on_flow_close`
has a dedicated test |
| Uncovered paths | none identified in this story's scope; STORY-187
(protocol-id dispatch) and STORY-194 (formal verification execution,
walk-first equivalence property) are explicitly deferred, not
uncovered-by-omission |

### Mutation Testing

Not run for this story delivery cycle — deferred to the wave's
formal-hardening phase per
this wave's schedule. No mutation kill-rate claim is made in this PR.

</details>

---

## Demo Evidence

Committed at `5e25d2ba` under `docs/demo-evidence/STORY-186/`: 7
recordings (`.tape` VHS
source + `.gif` + `.webm` each) plus `evidence-report.md` (index). This
is a pure-core /
effectful-shell library story with no CLI/web surface yet (SS-21
dispatch wiring deferred to
STORY-193 per ADR-0014), so the demonstration vehicle is VHS terminal
recordings of
`cargo test --test s7comm_analyzer_tests`, filtered per behavior group.

| Artifact | Behavior group | ACs covered |
|----------|----------------|-------------|
| `AC-001-003-carry-reassembly.gif/.webm` | Walk-first carry-buffer
reassembly, adversarial-burst anti-evasion, split-frame reassembly
(BC-2.20.013) | AC-186-001, 002, 003 |
| `AC-004-006-defense-in-depth.gif/.webm` | Carry bound +
defense-in-depth overflow guard (live at-bound + synthetic guard
mechanics + positive unreachability) (BC-2.20.014) | AC-186-004, 005,
006 |
| `AC-007-009-resync.gif/.webm` | 1-byte resync, never 2; shared
implementation; termination (BC-2.20.015) | AC-186-007, 008, 009 |
| `AC-010-011-module-boundary.gif/.webm` | Frozen SS-20/SS-21 module
boundary static regression guards (BC-2.20.016) | AC-186-010, 011 |
| `AC-012-flow-close.gif/.webm` | Flow-close teardown + double-close
idempotency (BC-2.21.003) | AC-186-012 |
| `VP-050-proptests.gif/.webm` | VP-050 proptest obligation (3
harnesses) | VP-050 |
| `AC-ALL-18-green.gif/.webm` | Full suite, all 18 tests, top-level
artifact | All 12 ACs + VP-050 |

All 12 acceptance criteria (AC-186-001..012) are covered by at least one
recorded artifact.
Demo-evidence path-scrub gate (PG-W70-DEMO-SCRUB) passed 2026-09-07 —
zero absolute-path
matches in any `.tape`/`.md` source; `cargo test` output piped through
`grep` to strip the
`Running tests/...` line that would otherwise leak a worktree filesystem
path.

---

## Holdout Evaluation

N/A — evaluated at wave gate (per PR template convention for
feature-mode stories; wave 89
gate has not yet run).

---

## Adversarial Review

| Pass | Findings | Status |
|------|----------|--------|
| P1 / P1b | carry-overflow reachability question raised (F-02/F-03) |
Fixed — reclassified as defense-in-depth (ADR-0014 v1.1, human-ratified)
|
| P2 | test provenance tense (MINOR), D5 T0814 annotation (NIT) | Fixed
(commit `2078236d`) |
| P3 | at-bound test doc wording (NIT) | Fixed (commit `98b28f9c`) |
| P5 | EC-004 miscite (NIT) | Fixed (commit `98b28f9c`) |

**Convergence:** CONVERGED — 5 review passes across this story's
adversarial cycle (P1,
P1b, P2, P3, P5), all findings resolved; no residual MINOR or blocking
findings at PR-open
time. (Full adversarial pass transcripts held in session state, not
duplicated here.)

<details>
<summary><strong>Key Findings & Resolutions</strong></summary>

### Finding: Carry-overflow guard branch is unreachable via real on_data
traffic (F-02/F-03)
- **Location:** `src/analyzer/s7comm.rs` (overflow guard in `on_data`)
- **Category:** spec-fidelity
- **Problem:** BC-2.20.014's overflow guard (`residual.len() > 65535`)
was originally
specified and tested as if reachable via real traffic, but the
walk-first + 1-byte-resync
  design bounds directional carry to `<= 65,534` bytes by construction.
- **Resolution:** BC-2.20.014 bumped to v1.1 (Decision:
defense-in-depth, human-ratified
2026-09-07). Tests reclassified: AC-186-004 (at-bound, `== 65535`) kept
LIVE; AC-186-005/006
(over-bound guard mechanics) reclassified SYNTHETIC via direct
flow-state injection; added
  positive unreachability proof test.
- **Test added:** `test_BC_2_20_014_overflow_unreachable_via_on_data()`

### Finding: Test provenance / doc wording NITs (P2, P3, P5)
- **Category:** code-quality / documentation
- **Problem:** Test doc comments used present-tense provenance language
inconsistent with
the repo's past-tense convention; EC-004 citation was mis-numbered;
at-bound test doc
  wording was ambiguous about LIVE vs. SYNTHETIC status.
- **Resolution:** Fixed across commits `2078236d` and `98b28f9c`.

</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
```

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

**IMPORTANT PROVENANCE NOTE:** Two `vsdd-factory:security-reviewer`
sub-agents
(`sec-review-186`, `sec-review-186b`) were dispatched against this PR's
diff. Both became
unresponsive for an extended period (repeated status checks over ~40+
minutes produced no
findings output to the pr-manager). The orchestrating session
subsequently reported that it
observed both agents stuck in an idle-notification echo loop, stopped
them, and asserted
their reviews were complete with a clean result: **0 CRITICAL, 0 HIGH,
LOW/informational
only, APPROVE**. **The pr-manager (this report's author) never received
the underlying
findings text, CWE citations, or severity table from either agent
directly** — the verdict
above is attested by the orchestrator, not independently confirmed
against actual
security-reviewer output. Treat this disposition as
orchestrator-reported, not
pr-manager-verified, when deciding whether to merge.

Independent of that unresolved provenance gap, the following is what the
pr-manager can
state directly from reading the diff: this module is a pure in-process
parser/reassembler
with no network listener, no filesystem I/O, no deserialization of
untrusted formats beyond
the byte-slice TPKT/COTP framing already hardened in STORY-184/185
(bounds-checked slicing,
no `unsafe`, no panics on malformed input — verified by the
adversarial-burst and
resync-termination tests in the test suite). Primary attack surface
considered: unbounded
carry-buffer growth from a malicious peer — mitigated by the
`MAX_S7_ISO_ON_TCP_CARRY_BYTES`
bound and the (now defense-in-depth) overflow guard. This is
pr-manager's own read of the
code, not a substitute for the missing dedicated security-reviewer
findings.

</details>

---

## Risk Assessment & Deployment

### Blast Radius
- **Systems affected:** New analyzer module (`src/analyzer/s7comm.rs`)
not yet wired into
the CLI dispatcher (STORY-193 deferred) — this PR is additive only, no
existing behavior
  changes to shipped analyzers.
- **User impact:** None at runtime today — module is not
dispatch-reachable from the CLI
  until STORY-193 lands.
- **Data impact:** None — no persistence, no schema changes.
- **Risk Level:** LOW

### Performance Impact

Not applicable — new module not yet wired into any executable code path
(dispatch deferred
to STORY-193); no existing benchmark baseline to compare against.

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

**Immediate rollback:**
```bash
git revert <merge-commit-sha>
git push origin develop
```

**Verification after rollback:**
- `cargo test --all-targets` green on `develop`
- No references to `S7commAnalyzer` remain reachable from the CLI
dispatcher (there were
  none before this PR either)

</details>

---

## Traceability

| Requirement | Story AC | Test | Verification | Status |
|-------------|---------|------|-------------|--------|
| BC-2.20.013 (walk-first extraction) | AC-186-001..003 |
`test_BC_2_20_013_*` (3 tests) | N/A (Kani deferred to STORY-194) | PASS
|
| BC-2.20.014 (carry bound + defense-in-depth guard) | AC-186-004..006 |
`test_BC_2_20_014_*` (5 tests) | proptest (VP-050) | PASS |
| BC-2.20.015 (1-byte resync) | AC-186-007..009 | `test_BC_2_20_015_*`
(3 tests) | proptest (VP-050) | PASS |
| BC-2.20.016 (frozen module boundary) | AC-186-010..011 |
`test_BC_2_20_016_*` (2 tests) | static regression guard | PASS |
| BC-2.21.003 (flow-close teardown) | AC-186-012 |
`test_s7comm_on_flow_close_*`, `test_BC_2_21_003_double_close_*` (2
tests) | N/A | PASS |
| VP-050 (carry bound, direction isolation, resync advance invariants) |
— | `story_186::vp050::proptest_*` (3 harnesses) | proptest | PASS |

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

```
BC-2.20.013 -> AC-186-001/002/003 -> test_BC_2_20_013_*() -> src/analyzer/s7comm.rs -> ADV-CONVERGED (P1-P5)
BC-2.20.014 v1.1 -> AC-186-004/005/006 -> test_BC_2_20_014_*() -> src/analyzer/s7comm.rs -> ADV-CONVERGED (P1/P1b reclassification)
BC-2.20.015 -> AC-186-007/008/009 -> test_BC_2_20_015_*() -> src/analyzer/s7comm.rs -> ADV-CONVERGED
BC-2.20.016 -> AC-186-010/011 -> test_BC_2_20_016_*() -> src/analyzer/{iso_on_tcp,s7comm}.rs -> ADV-CONVERGED
BC-2.21.003 -> AC-186-012 -> test_s7comm_on_flow_close_*(), test_BC_2_21_003_double_close_*() -> src/analyzer/s7comm.rs -> ADV-CONVERGED
VP-050 -> proptest_vp050_*() -> src/analyzer/s7comm.rs
```

</details>

---

## AI Pipeline Metadata

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

```yaml
ai-generated: true
pipeline-mode: feature
factory-version: "1.0.0-rc.25"
pipeline-stages:
  spec-crystallization: completed
  story-decomposition: completed
  tdd-implementation: completed
  holdout-evaluation: deferred-to-wave-gate
  adversarial-review: completed
  formal-verification: deferred-to-STORY-194
  convergence: achieved
adversarial-passes: 5
models-used:
  builder: claude-sonnet-5
generated-at: "2026-09-07T00:00:00Z"
```

</details>

---

## Pre-Merge Checklist

- [ ] All CI status checks passing (test, clippy, fmt, changelog-gate,
action-pin-gate, semantic-PR-title) — verified at Step 6
- [x] Coverage delta is positive (new module, fully covered by new
tests)
- [ ] No critical/high security findings unresolved — verified at Step 4
- [x] Rollback procedure validated (single `git revert`, additive-only
change)
- [x] CHANGELOG `[Unreleased]` entry present (required — this PR touches
`src/`)
- [ ] pr-reviewer fresh-eyes convergence (0 blocking findings) —
verified at Step 5
- [x] Demo evidence: 7 recordings (tape/gif/webm) + evidence-report.md,
committed at `5e25d2ba`, covering all 12 ACs + VP-050
- [x] Dependency STORY-185 merged to `develop` (`e0ea30ce`, PR #467)
before this PR opens

https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
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