Skip to content

v3.0.0: full ground-up rewrite, ported from c-RCP - #172

Merged
SoundMatt merged 44 commits into
mainfrom
rewrite/v3-from-c-rcp
Aug 22, 2026
Merged

v3.0.0: full ground-up rewrite, ported from c-RCP#172
SoundMatt merged 44 commits into
mainfrom
rewrite/v3-from-c-rcp

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

v3.0.0: full ground-up rewrite, ported from c-RCP

Final cutover for the cpp-RCP v3.0.0 ground-up rewrite (issue #129, ROADMAP.md Phase 17: "TC18 RCP — General Availability, first release where cpp-RCP is the OPEN Alliance TC18 Remote Control Protocol"). This merges the long-lived rewrite/v3-from-c-rcp branch, where the entire codebase was rebuilt against c-RCP's current RC5-conformant, atomicity-audited, ASIL-D-hardened implementation as the content source of truth, re-expressed in idiomatic modern C++.

What this covers (all phases of the original rewrite plan, now complete)

  • Phases 1-4 (prior sessions): core wire/protocol layer, safety layer, per-endpoint modules, server/dispatch, transport — all re-derived from c-RCP's current implementation.
  • Phase 5 (prior sessions): remaining safety-mechanism hardening (admin/shmem deadlock fixes, mutex-across-callback bug class).
  • Phase 6 (this session, 13 batches, PRs rewrite(phase6): batch 1 — ACF/AVTP/WIREERR catalog re-derivation #155-166): full .fusa-reqs.json requirement catalog re-derivation from c-RCP's current catalog. 412 → 1133 entries, 174 genuine gaps/id-collisions honestly documented in .fusa-reqs-pending.json rather than force-fit, zero tag-coverage gaps (confirmed against the real pinned cpfusa binary).
  • Phase 7 (this session, 3 batches, PRs rewrite(phase7): batch 1 — port LifecycleStateMachine/E2ESafePoint TLA+ specs, add TLC CI job #167-169): formal verification (ported 2 missing TLA+ specs, added TLC CI automation for all 5 — none had ever run in CI before), real MC/DC coverage (LLVM, ratchet-gated at a freshly-measured ~67%), release-pipeline hardening (version-sources-agree gate, turned two silently-softened safety gates into real ones, .fusa-dispositions.json, CHANGELOG.md).
  • Phase 8 (this session, 2 batches, PRs rewrite(phase8): batch B — content-parity audit (tsn/powerstate/faultinject/ratelimit/mdns/authz/relay/bridges) #170-171): remaining modules (record/observe/config/cli/tsn/powerstate/faultinject/ratelimit/mdns/authz/relay.hpp/live bridges/dyndata/redundancy/sim) audited for content parity against c-RCP; found and fixed a real latent bug in config.hpp's manifest parser (a malformed entry was silently dropped instead of rejected).

Verification (this cutover, in addition to every individual batch's own independent re-verification before merge)

  • Fresh full rebuild: 0 errors, 0 warnings.
  • ctest: 58/58 suites passing (100%).
  • cpfusa check: 0 errors (1 pre-existing, out-of-scope warning already present on main before this rewrite — a project-ASIL-vs-hazard-ASIL declaration mismatch, unrelated to this rewrite's content).
  • cpfusa trace: 1133/1133 requirements traced and tested, 0 tag-coverage gaps.
  • All 5 TLA+ specs (WatchdogSafeState, RxSequenceGuard, CrcSafeStateLatch, LifecycleStateMachine, E2ESafePoint): TLC reports zero errors/violations/deadlocks.
  • Latest CI run on this branch's HEAD: all 24 checks green, including the MC/DC ratchet gate and formal verification.

After merge

Tag v3.0.0 and update RELAY's docs/RCP-ARCHITECTURE.md.

SoundMatt and others added 30 commits August 21, 2026 10:24
…hase 16's organic conclusion

The incremental path through v2.23.0 never reached TC18 RC5 conformance
-- this codebase was built and 'conformance-fixed' against the pre-RC5
baseline the whole time (confirmed via a real defect: spi.hpp is missing
RC5's spi_nr_cs 4-bit re-encoding and new deassert_cs_pause bit, ticket
NXP_100, not just a citation gap). It also carries no MC/DC coverage, no
requirement-atomicity audit, no fixed-capacity architecture, and a
replay-detection mechanism implemented but never wired into mock.hpp's
dispatch.

Rather than patch all of that in place, v3.0.0 is reached by rebuilding
this codebase from c-RCP's current (RC5-conformant, atomicity-audited,
ASIL-D-hardened) content, module by module, on branch
rewrite/v3-from-c-rcp. Adds Phase 17 documenting the phase breakdown,
supersedes the six parity-gap issues (#122-127) filed earlier the same
day once their scope was found too narrow.

Tracked at #129.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
The v3.0.0 rewrite (Phase 17, issue #129) deliberately lives on this
branch rather than main for however many sessions it takes, but
ci.yml/dco.yml were still scoped to push/pull_request against main only
-- meaning every phase PR against this branch got zero CI signal. Add
this branch alongside main in both workflows' triggers so 'CI green
before merge' (the discipline every phase PR is held to) actually means
something here, not just on the eventual cutover PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
…ference (#130)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. First module pair: the wire-protocol core
everything else in Phase 1 depends on.

Real content deltas found and fixed against c-RCP's src/acf.c+include/rcp/
acf.h and src/avtp.c+include/rcp/avtp.h:

- ACF_GBB Message Info geometry was wrong (regression from a prior
  v2.22.0 pass): message_timestamp was spliced *between* the header's two
  quadlets. c-RCP's rcp_acf_encode_gbb() and REQ-ACF-044 are unanimous
  the real layout is contiguous -- 8-byte header + 8-byte timestamp +
  payload. Every ACF_GBB frame this library emitted since v2.22.0 was
  unreadable by a conformant peer. Reverted to contiguous.
- Added missing ACF protocol-logic surface (additive, no renames):
  pad_len(), peek_msg_type(), header_is_request(),
  request_header_constraints_valid(), evt_row2_is_plain()/
  evt_requests_acknowledge() (Table 33), compound_wait_evt_valid()/
  compound_wait_match() (all 8 TC18 §13.5.1 comparison modes),
  reg_write_len() (TC18 §13.7.1.2, RC5-corrected -2 formula),
  peek_gbb_request_type(), and response builders
  build_error_response()/build_acknowledge_response()/
  build_acknowledge_rejected_response().
- avtp.hpp: NtscfHeader gained sv/version; TscfHeader gained
  sv/version/mr/tu/reserved0/reserved1 (round-tripped, decode-populated,
  defaults reproduce prior encode behavior exactly). Added
  extend_timestamp() (48-bit gPTP reconstruction), TscfFallback +
  should_drop_tscf(), tscf_reserved_all_zero() -- TC18 §13.3 rules,
  previously entirely absent.

test_acf.cpp: 24 -> 63 test cases (267 assertions).
test_avtp.cpp: 15 -> 34 test cases (86 assertions).

The GBB geometry fix broke 3 test_e2e.cpp assertions that hardcoded the
old (wrong) spliced layout as their expected value -- corrected those
3 assertions plus their surrounding comments to the contiguous layout;
did not touch e2e.hpp's own production logic (that's Phase 2 scope).

Deliberately NOT ported from c-RCP, with rationale:
- c-RCP's stricter decode_acf_abb/_gbb (rejects a payload not exactly
  acf_msg_length*4 bytes, trims trailing pad) -- every real payload
  builder in this tree hands non-quadlet-aligned payloads to encode_*
  without pre-padding, and test_e2e.cpp asserts decode does NOT trim pad
  today. Left a TODO(phase1-followup) with the full rationale; revisit
  once a later phase makes every payload builder pre-pad via pad_len().
- c-RCP's always-recompute/always-auto-pad encode contract -- kept
  cpp-RCP's existing 0-means-auto-fill/nonzero-means-override contract,
  since e2e.hpp's apply_acf_length_adjustment() (CRC trailer accounting)
  is load-bearing on it and e2e.hpp is out of Phase 1 scope.
- c-RCP's transport vtable + in-process loopback transport -- cpp-RCP's
  own idiom (l2.hpp/udp.hpp's concrete per-transport classes, no shared
  interface, documented in l2.hpp's own header comment) is deliberately
  different; porting a shared vtable would contradict it.

Verified: full tree rebuilds clean (217/217 targets), 53/53 ctest suites
pass (100%), zero call-site fixes needed outside the 5 files touched.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ty, ported from c-RCP (#131)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Second batch of Phase 1, alongside
request/sequencer/scheduler (separate PR).

- fragment.hpp (NEW): cpp-RCP has never had multi-AVTPDU fragmentation --
  this codebase made an explicit no-go call at ROADMAP.md milestone 52
  (v2.8.0). Ported rcp::fragment::plan_count()/plan() (encode) and
  Reassembler (decode) from c-RCP's fragment.h/fragment.c (REQ-FRAG-*,
  18 requirements). Fixed-capacity: Reassembler embeds a
  std::array<uint8_t, 4096> instead of c-RCP's realloc()-grown buffer --
  no heap allocation at all, stronger than c-RCP's own C version. Ported
  c-RCP issues #614/#616's oversized-reassembly lesson as a design
  principle: this module stays ACF-agnostic (matching c-RCP -- the real
  fix there lived in mock.c's dispatch layer, not fragment.c) and always
  reports the true, complete reassembled length, so a future integrator
  (Phase 3/4 wiring, out of scope here) can check against an ACF frame
  ceiling before re-encoding, exactly the check c-RCP's dispatch layer
  had to learn to add. 30 test cases, 132 assertions.

- respqueue.hpp (NEW): the per-response/ack-stream TX queue TC18
  Sec12.7.9 describes -- also entirely absent before. Ported from c-RCP's
  respqueue.h/respqueue.c (REQ-RMAP-059/061/062/063/064/065/085):
  octet-budgeted FIFO, per-message max_avtpdu_size_octets ceiling, and
  TC18 Sec12.9.4/Sec12.9.5's evict-lowest-sequence_num overflow policy
  (not FIFO-oldest). kMaxEntries=64 from c-RCP's
  RCP_RESPQUEUE_MAX_ENTRIES, backing a fixed std::array of entry slots.
  27 test cases, 405 assertions.

- loan.hpp: found BufferPool::pool_ was an unbounded
  std::vector<unique_ptr<vector<uint8_t>>> with no capacity ceiling --
  a real gap flagged in an earlier parity audit this session. Converted
  to a std::array<vector<uint8_t>*, 64> matching c-RCP's own
  RCP_LOAN_POOL_MAX_ENTRIES bound exactly (c-RCP bounds entry count
  only, not per-buffer size -- matched that, not a stricter policy).
  Once the free list is full, release now frees the buffer outright
  instead of growing storage further, mirroring c-RCP's pool_append()
  degradation. Also removed a redundant heap re-wrap the old code did
  on every reuse. Test count 6 -> 9 (new: pool sanity, destructor-frees-
  free-list, and a fixed-capacity overflow test that mutation-proves the
  bound).

Not cleanly ported, documented in-file rather than left as a follow-up
TODO (structural, not a gap to close later): c-RCP's
RCP_FRAGMENT_REASM_ERR_ALLOC and its allocation-failure-injection test
cases across all three modules have no C++ analog -- the fixed-capacity,
no-heap design structurally eliminates the allocation-failure class,
and cpp-RCP has no allocation-failure-injection seam yet (that's Phase
2's allocation-seam work).

Verified: full tree rebuilds clean (221/221 targets), 55/55 ctest
suites pass (100%). No endpoint file (can.hpp/spi.hpp/uart.hpp/
regmap.hpp's kOptFragmentation) touched -- wiring fragmentation into
real dispatch is Phase 3/4 scope, not this pass.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…nformant reference (#132)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Third and final Phase 1 module: the
conditional-request layer (compound, compound-wait, triggered, chained,
timed, cancellation) plus the shared sequencer/scheduler state c-RCP
keeps as 3 separate modules -- kept in cpp-RCP's existing single-module
unification (request.hpp already did this better than c-RCP's own
current split).

Real content deltas found and fixed against c-RCP's request.c/
request_sequencer.c/scheduler.c:

- Fixed cpp-RCP issue #58 (cs-bit polarity inverted in
  compound_wait_check_of()/should_execute_chained()) as part of this
  port, confirmed against c-RCP's rcp_chained_advance(): was
  'cs || !predecessor_errored' (cs=true never aborted anything
  regardless of outcome), now '!cs || !predecessor_errored', matching
  RCP_CHAINED_CS_CONTINUE_ON_ERROR(0)/ABORT_ON_ERROR(1).
- compound_wait_check_of()/CompoundWaitCheck had no basis in c-RCP --
  traced through rcp_compound_encode_request()/_decode_request(), cs is
  hardcoded to 0 and never surfaced for compound/compound-wait. Removed;
  the real comparison-mode selector is the ACF header's evt[2:0]
  (acf::compound_wait_evt_valid/compound_wait_match, from the acf/avtp
  port), now exposed via CompoundRequest's evt_op field.
- cancel_single() collapsed two distinct c-RCP outcomes
  (NOT_FOUND vs NOT_CANCELLABLE) into one. Added
  RequestErrc::request_not_cancellable, split the cases.
- SequencerTable::try_advance's '+1 wrapping' model was wrong --
  replaced with CompoundStep + advance_guard()/start_condition_met()/
  apply_next_state()/tick()/wait_tick(), matching c-RCP's explicit
  next_state field and the guard/start-condition distinction
  (rcp_compound_advance_guard() vs rcp_compound_start_condition_met()).

Also newly ported (behavior c-RCP has that cpp-RCP never had):
structured wire sub-fields + encode/decode for every request kind,
reserved-octet and evt/hs/cs wire validation, TriggeredRuntime,
wraparound-safe timed admission mapped onto acf::WireErrorCode, and
frame_timing_consistent.

Fixed-capacity: RequestLedger::records_ (unconditional-push_back
std::vector) -> BoundedVector<RequestRecord, 64>. c-RCP has no ledger
to port a bound from (its request store is a pure-function library) --
this is this port's own decision. Chose 64, not 256 (the full
transaction_num range): at 256/256 every possible id is already a
collision, making the capacity check unreachable. Mutation-tested
(disabled the full() guard, confirmed the new overflow test failed,
restored).

Not ported, by design (noted in-header, not a follow-up TODO): c-RCP's
positional (has_predecessor/chain_position) chain model -- cpp-RCP's
existing predecessor/successor graph in RequestLedger is strictly more
capable and stays as the one chained-cascade model.

Left as // TODO(phase1-followup): REQ-SEQ-013/014's per-sequencer owner
+ fail-closed access control (needs regmap.hpp storage, out of this
pass's request.hpp-only scope); rcp_compound_peek_request_type()/
rcp_timed_encode_request_tscf() convenience wrappers (composable today
from existing acf/avtp primitives).

12 TEST_CASEs (REQ-SEQ-only) -> 135 TEST_CASEs / 610 assertions,
covering REQ-CMP/TRIG/CHAIN/TIMED/CANCEL/SEQ/SCHED.

Verified: full tree rebuilds clean (221/221 targets, incl. the just-
merged fragment/respqueue), 55/55 ctest suites pass (100%) --
independently re-verified from a fresh build, not just the porting
agent's own report. No call sites outside request.hpp/test_request.cpp
required changes (e2e.hpp/watchdog.hpp/authz.hpp all kept their exact
prior signatures against the new API).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…m, ported from c-RCP (#133)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. First batch of Phase 2, alongside e2e/
lifecycle (separate PR).

watchdog.hpp: investigation established cpp-RCP's StreamWatchdog/Manager
is not a stale transliteration target -- it's a deliberate, already-
shipped v2.10.0 design (ROADMAP.md milestone 54) with its own
REQ-WDG-001..008 catalog that doesn't correspond 1:1 to c-RCP's
REQ-WDG-001..012. c-RCP's own watchdog has since evolved to a
background-thread rcp_watchdog_keeper_t (a later c-RCP redesign, not
what this codebase's design was ever meant to mirror) -- porting that
would contradict this codebase's own stated 'no clock/thread of its
own' convention and break sim.hpp/mdns.hpp's existing dependents.
Treated as a content audit instead, mapping each of c-RCP's 12
REQ-WDG-* against what transfers vs. doesn't. One genuine gap found and
fixed: the c-RCP-17 fixed-capacity conversion (RCP_WATCHDOG_MAX_STREAMS
/RCP_WATCHDOG_MAX_CALLBACKS, both 16) had never been ported -- Manager
used unbounded std::map/std::vector, unlike every other Phase 17
module. Converted to fixed std::array-backed storage with swap-removal
(matching loan.hpp's free-list technique); register_stream/subscribe
now return std::error_code (new stream_capacity_exceeded/
callback_capacity_exceeded) instead of silently discarding overflow.

alloc.hpp (NEW): cpp-RCP had zero allocation-failure-injection
capability -- confirmed zero try/catch(std::bad_alloc) anywhere in the
tree. c-RCP's own mechanism (a global rcp_alloc_set_hooks() function-
pointer table) is deliberately NOT ported literally -- it's exactly the
pattern that needed a later access-control retrofit in c-RCP itself
(issue #600/PR #615, rcp_alloc_lock_hooks()) after any caller could
silently redirect the allocator backing safety code. Built a small,
instance-owned rcp::alloc::FaultInjector instead (mutex-guarded,
arm(count)/should_fail()), matching faultinject.hpp's existing idiom.
Phase 1's fixed-capacity modules (request/fragment/respqueue/loan) were
checked first and found to not need it -- their capacity-exceeded paths
are std::array-backed and never touch the heap, so they're already
directly testable and can't be meaningfully fault-injected. Wired
FaultInjector into loan.hpp's BufferPool::loan() cache-miss branch
instead (its one real heap allocation), as a genuine, non-artificial
demonstration it composes with a real caller -- optional constructor
parameter, defaulted nullptr, fully backward-compatible.

REQ-ALLOC-* (11 in c-RCP) breakdown, documented in alloc.hpp's header:
none transfer as written -- 001/002/010/011 describe the global hook
table this design deliberately lacks; 003-006 describe C's four
separate allocator entry points, no C++ equivalent without a global
operator new override; 007-009 (lock/unlock/query) exist only to
retrofit access control onto a table that doesn't exist here. The one
concept that transfers -- deterministic, portable allocation-failure
testability -- is what FaultInjector::should_fail() provides.

Call sites touched: sim.hpp's Simulator::register_stream widened from
void to std::error_code to propagate the new capacity-exceeded result
(existing callers discard it as a statement, no break).

test_watchdog: 12 -> 15 test cases, 137 assertions.
test_alloc (new): 13 test cases, 98 assertions.

Verified: full tree rebuilds clean (223/223 targets), 56/56 ctest
suites pass (100%) -- independently re-verified from a fresh build, not
just the porting agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…rstated H-004 claim (#134)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Second and final batch of Phase 2 -- the
safety-critical core: CRC32 safe points, safety-request handling,
replay/staleness detection, and the RC-Server lifecycle state machine.

Real content deltas found and fixed against c-RCP's e2e.h/e2e.c and
lifecycle.h/lifecycle.c:

- CRC coverage was missing 3 leading bytes (c-RCP issue #465):
  avtp_subtype/header_octet1/tu -- TC18 Figures 20/21's own 'header
  CRC' region -- were absent entirely from coverage_buffer()/
  compute_crc()/verify_crc(). A conformant peer's CRC32 would never
  have matched this library's prior output.
- wrap()/unwrap() didn't exist at all -- only append_crc() (appends at
  buffer end), which produces the wrong wire order
  [header][real][pad][CRC] whenever a payload carries trailing pad
  bytes; TC18 requires [header][real][CRC][pad] (c-RCP issue #420).
- RxSequenceGuard (REQ-E2E-028/029, the mechanism a prior audit this
  session flagged as implemented-but-unwired) was substantively wrong,
  not just incomplete: compared sequence numbers as a non-wrapping
  uint32_t (would spuriously reject every request once an 8-bit AVTPDU
  sequence_num wraps) and never consulted rx_seq_safestate_enable at
  all. Replaced with c-RCP's RFC 1982 forward-window comparison.
- endpoint_in_configured_safe_state() was missing REQ-SEQ-012's
  fail-closed rule -- a disabled sequencer (state==0) could satisfy a
  safe-state check if rx_safe_sequencer_state was also misconfigured
  to 0.
- Missing entirely, now ported: fragment_carries_crc()/
  compute_fragmented_crc() (REQ-E2E-010/038), crc_error_should_enter_
  safe_state() (REQ-E2E-045), StreamFaultTracker (REQ-E2E-021),
  StreamStatus (REQ-E2E-046), length_with_crc()/
  data_length_for_protected_members() (REQ-E2E-004/037),
  overflow_should_enter_safe_state() (REQ-E2E-030).

lifecycle.hpp had only a bare 3-state machine with no real
access-control layer -- c-RCP's whole issue #198 (writer authorization,
unicast enforcement, idle-gating) was absent. Added, additive:
EndpointPlausibility/RequestStreamPlausibility/PlausibilitySnapshot +
check_hw_cfg()/check_rcp_cfg() (real TC18 Sec12.3.1.2 plausibility
content, not just an opaque callback); WriterCtx;
ServerLifecycle::transition() (full writer-authorization + idle-gating
state machine, including the RCP_CONFIGURED->HW_CONFIGURED demotion
and both authorized resets advance() never modeled); Disposition/
should_accept() (per-state request filtering); field_writable()/
field_write_error() family (register-locking-by-state). advance()
kept unchanged (mock.hpp and existing tests depend on its differing
same-state semantics, a documented pre-existing design choice);
transition() coexists as the richer, spec-complete entry point.

HARA.md/FORMAL_VERIFICATION.md correction: HARA.md's H-004 section
described RxSequenceGuard::check in present-tense mitigating language
with SG-004 listed as a clean ASIL-B mitigation and no caveat. Added a
'STATUS CORRECTED' block: the mechanism is implemented and unit-tested
but never instantiated outside its own test -- not in mock::Server's
dispatch or any transport Server -- citing c-RCP's #601/#606
resolution as the parallel precedent, stating H-004 is 'effectively
Open' pending that wiring (Phase 4/server-dispatch scope, not this
pass -- mock.hpp was NOT touched, RxSequenceGuard is referenced only
in e2e.hpp itself). FORMAL_VERIFICATION.md got the same caveat, plus a
second, independently-found correction: tla/RxSequenceGuard.tla itself
still models the old non-wrapping comparison, not the RFC 1982
algorithm now in e2e.hpp -- flagged as a real, un-re-derived
divergence and tracked as a follow-up (re-deriving the TLA+ spec is
its own formal-modeling task).

Fixed-capacity: StreamFaultTracker uses a std::array<Slot, 16>, no
heap allocation -- bound taken directly from c-RCP's
RCP_E2E_STREAM_FAULT_TRACKER_MAX_STREAMS. lifecycle.c confirmed to
have no alloc.h/heap dependency; no fixed-capacity work needed there.

Left as // TODO(phase2-followup): apply_acf_length_adjustment() doesn't
fail-safe on 9-bit field overflow the way c-RCP's
adapt_acf_msg_length() does (low real exposure, but a fallible return
type would touch every existing call site) -- named gap, not silently
dropped. RxSequenceGuard.tla's staleness (above) is a second, separate
tracked follow-up.

test_e2e.cpp: 32 -> 72 TEST_CASEs. test_lifecycle.cpp: 16 -> 47
TEST_CASEs.

Verified: full tree rebuilds clean (223/223 targets, incl. the merged
watchdog/alloc-seam batch), 56/56 ctest suites pass (100%) --
independently re-verified from a fresh build, not just the porting
agent's own report. No call sites outside target files required
changes (regmap.hpp/mock.hpp depend on lifecycle.hpp, watchdog.hpp
depends on e2e.hpp, all compile and pass unchanged since every new
piece is additive).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…7 missing i2c entry (#135)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. First of Phase 3's Wave 1 batches to land,
alongside can/lin and adc/gpio (separate PRs).

ROADMAP.md fix: Phase 17's endpoint-module list omitted i2c, a real 11th
endpoint type both c-RCP and cpp-RCP have. Added.

Both target modules were pre-rewrite (v2.20.0-era) content this rewrite's
Phase 1/2 never touched -- not incremental drift, a wholesale mismatch
against c-RCP's current design, confirmed against ep_i2c.h/.c and
ep_iseled.h/.c:

- i2c.hpp: the 'pilot' I2cMode/i2c_mode_of (a 2-way decode of
  AcfMessageInfo::hs) and a local compound_wait_matches_bits helper were
  both invented and wrong. c-RCP's i2c_mode is a persistent 5-way preset
  (STANDARD..ULTRA_FAST) stored in a Table 49 register block, never
  carried on hs; I2C has no endpoint-specific compound-wait logic in
  c-RCP at all -- acf.hpp's own compound_wait_match() (ported in Phase 1)
  is the correct generic primitive. Removed both. Ported: I2cFunctionalCfg
  + lifecycle-gated setters, the Table 49 register block, I2cDir-
  parameterized bidirectional transfer request/response codec (c-RCP
  REQ-I2C-010/018/023), i2c_mode_valid.
- iseled.hpp: the old IseledRequest/IseledResponse structs (12-bit
  Instruction/Address/Data ACF-payload fields) modeled the wrong layer --
  c-RCP's ACF codec treats the payload as opaque raw bytes (same as I2C/
  LIN); the 12-bit-field decode belongs to ISELED's physical symbol
  layer, not the ACF wire codec. Replaced wholesale with c-RCP's real
  content: native 4-bit/5-bit even-parity bit framing, an independent
  CRC-8 integrity layer, requires_isp_n (issue #270 polarity fix),
  IseledFunctionalCfg + Table 58 register block, and the write-direction
  vs. read-direction request split (issue #471).

fragment.hpp usage: c-RCP's ep_iseled.c genuinely #includes rcp/fragment.h
and calls its plan_count()/plan() directly -- ported identically
(response_fragment_count()/encode_response_fragmented()). I2C has no
fragmentation in c-RCP; none added.

Fixed-capacity: EpFuncBlock in both modules is a fixed std::array (11/14
octets), matching c-RCP's fixed-size register block.

test_i2c.cpp: 14 -> 44 TEST_CASEs (203 assertions).
test_iseled.cpp: 24 -> 75 TEST_CASEs (337 assertions).

Call sites touched: mock.hpp's dispatch_iseled()/set_iseled_response()
and 3 test_mock.cpp ISELED test cases depended on the removed Iseled
Request/Response types -- updated dispatch_iseled to pass the raw ACF
payload straight to IseledEndpoint::handle_request (mirroring
dispatch_i2c's existing pattern), set_iseled_response to take
std::vector<uint8_t>, removed the stale field_out_of_range wire-error
mapping. i2c.hpp needed no mock.hpp changes (I2cEndpoint::handle_request's
signature was unchanged).

Not touched here, explicitly tracked for later phases: cpp-RCP's
.fusa-reqs.json still has only 7 stale REQ-ISELED-*/REQ-I2C-* entries
(Phase 6 scope); ISELED response fragmentation's mock-dispatch wiring
is Phase 4 scope (only ep_iseled.c-side encode was ported here).

Verified: full tree rebuilds clean (223/223 targets), 56/56 ctest
suites pass (100%) -- independently re-verified from a fresh build, not
just the porting agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ia fragment.hpp (#136)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Second of Phase 3's Wave 1 batches,
alongside iseled/i2c and adc/gpio (separate PRs).

can.hpp: the pre-rewrite header was validation-only -- its own v2.24.0
ROADMAP.md note said 'no ACF-level wire encode/decode ... anywhere in
this codebase'. Ported the real wire codec from ep_can.c's
rcp_ep_can_encode_frame_request/response, _decode_frame_request/
response. FrameFormat widened from an invented 3-value
{Classical,Fd,Xl} to c-RCP's real 6-value Table 57 set (Cbff/Ceff/Fbff/
Feff/XlClassicalPl/XlNewPl), each with its own 11-/29-bit id width
derived from format -- fixes a real defect where the old
CanIdentifier::extended flag was settable independently of FrameFormat,
permitting contradictory states. CanBitTimingPhase::sync_jump_width
corrected uint16_t->uint8_t; default prescaler corrected 1->0 to match
c-RCP's zero-init contract. Ported CanFunctionalConfig (three bit-
timing sets, delay compensation, xl_filters[4]) and the Table 56
EP_func register block, porting forward c-RCP issue #470's enable&clr
bit-4 fix (was bit 1) directly rather than re-introducing the bug.

CAN XL fragmentation (c-RCP issues #610/#611/#612/#613) wired via
Phase 1's fragment.hpp: frame_request_fragment_count()/
encode_frame_request_fragmented() and the response-side
count/encode/decode/reassemble quartet, all built on
fragment::plan_count()/plan()/Segment and fragment::Reassembler.
Combined-payload and fragment-plan buffers are fixed std::array (2058
octets / 256 segments, c-RCP issue #521's bounds) -- no heap
allocation. Tests round-trip a worst-case 2048-octet CAN XL write
request and read response end to end through Reassembler, and prove
the c-RCP #614/#616 oversized-reassembly lesson holds (kErrTooLarge,
not silent truncation, both immediately and partway through a
multi-fragment sequence).

lin.hpp: ported response_matches() (thin wrapper over the already-
ported acf::compound_wait_match), LinTrigger/trigger_fires(),
LinFunctionalConfig + Table 55 register block, and the real
encode_command_request/decode_command_request/encode_response/
decode_response wire codec -- none of this existed in the pre-rewrite
header.

Fixed-capacity: kXlMaxEncodedLen/kMaxFragmentSegments/kMaxXlFilters all
std::array-backed, ported from c-RCP's own bound constants.
CanDataFrame::data/LinEndpoint's byte buffers deliberately stayed
std::vector<uint8_t> -- mock.hpp assigns from a
const std::vector<uint8_t>&, which wouldn't compile against a fixed
array.

test_can.cpp: 20 -> 43 TEST_CASEs (267 assertions).
test_lin.cpp: 12 -> 28 TEST_CASEs (103 assertions).

Call sites touched: none. mock.hpp confirmed unchanged and its own
test suite passes against the new headers unmodified.

Left as // TODO(phase3-followup) in both headers: once regmap.hpp's
functional-config split is re-derived from c-RCP (Phase 17 item 4),
recompose the five local ep_enable/ep_clear_req_storage/
ep_req_crc_enable/ep_response_ts_enable/ep_suppress_response flags onto
that shared struct instead of carrying local duplicates. can.hpp also
documents (not a TODO, a deferral matching c-RCP's own disposition)
that Table 56's acceptance-filter register region has a genuine
unresolved address-collision defect in the primary source, so this
module's register-block model deliberately stops at kEpFuncLen.

Verified: full tree rebuilds clean (223/223 targets), 56/56 ctest
suites pass (100%) -- independently re-verified from a fresh build, not
just the porting agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ng regression (#137)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Third of Phase 3's Wave 1 batches,
alongside iseled/i2c and can/lin (separate PRs).

adc.hpp (55 requirements) was substantially wrong, not just incomplete:

- Critical bug: the pre-rewrite pilot-era AdcEndpoint::request_reading
  averaged adc_combine_avg_values interval-means down to *one* output
  value. c-RCP's own file header records this exact mistake was already
  made and fixed once in c-RCP's history -- adc_combine_avg_values is a
  packing count, not a second reduction; a response carries N values,
  never one (Sec13.7.9.2/.3). Fixed: average_interval/
  collect_response_values now port rcp_ep_adc_average_interval()/
  rcp_ep_adc_collect_response_values() 1:1; AdcEndpoint redesigned
  around execute_measurement_cycle()/response_ready()/
  collect_response().
- Field-naming bug: AdcAveragingConfig::adc_avg_intervals_per_request
  was actually being used for a layer-1 samples-per-interval count --
  c-RCP's adc_samples_per_avg_interval's role, not layer 2's meaning.
- Missing entirely, now ported: wall-clock inter-sample spacing
  validation (REQ-ADC-033), cadence classification (REQ-ADC-037/053),
  Table 53 threshold/measurement-finished triggers (REQ-ADC-031/
  048-052), the full EP_func register block + apply_reconfig
  (REQ-ADC-035/036/038-040, closing the evt[2:0]=111b gap the old
  header stubbed as config_write_not_supported), lifecycle-authorized
  functional-config setters, real ACF-level read-request/response/
  reconfig wire codec.

gpio.hpp (46 requirements) core was already correct; extended:

- Bug found and fixed: apply_gpio_write's Subtract semantics computed
  current-operand; c-RCP's rcp_ep_gpio_apply_write() (REQ-GPIO-011)
  defines the row normatively as request minus current. Fixed locally
  in gpio.hpp, not in the shared endpoint.hpp helper, to avoid silently
  changing pwm.hpp's PWM_OUT semantics (out of this phase's scope, not
  yet re-verified).
- Missing entirely, now ported: Table 43 wire trigger-signal numbering
  (REQ-GPIO-034), per-pin debounce filtering (REQ-GPIO-035/044),
  response-timing classification (REQ-GPIO-036), reserved evt[2:0]=
  100b wire rejection (REQ-GPIO-012/045/046), the full EP_func register
  block + apply_reconfig (REQ-GPIO-013/038-042, with c-RCP's own
  resolved gpio_debounce_IO31 editorial-defect offset at 0x0028),
  functional-config setters, real ACF-level wire codec.
- rcp_ep_gpio_toggle_pin_direction() (no longer a real TC18 mechanism
  per c-RCP's own comment) deliberately not ported -- low-value,
  non-wire caller convenience.

Fixed-capacity: AdcEndpoint's pending-averaged-value accumulator is a
std::array<AdcAvgValue, 255>-backed BoundedVector (255 = Table 54's own
8-bit adc_combine_avg_values register width). GPIO's existing
std::array<uint8_t, kMaxPins> pattern extended to
GpioFunctionalConfig::pins/debounce.

test_adc.cpp: 18 -> 62 TEST_CASEs. test_gpio.cpp: 20 -> 62 TEST_CASEs.

Call sites touched: mock.hpp's dispatch_adc() rewritten against the new
AdcEndpoint API and to route evt[2:0]=111b through the now-real
adc::apply_reconfig instead of rejecting it; wire_error_code_for()
updated (removed config_write_not_supported, added reconfig_short/
reconfig_out_of_range mappings). tests/test_mock.cpp: two ADC
config-write tests updated for the now-real reconfig behavior.

Left as // TODO(phase3-followup): rcp_ep_gpio_toggle_pin_direction()
not ported (noted in gpio.hpp's header, no stub added). Full
dispatch-level wiring (response-shape composition, RxSequenceGuard,
fragmentation) for both endpoints' new EP_func/reconfig paths remains
mock.hpp's own Phase 4 scope -- this pass's minimal dispatch_adc
wiring is enough to exercise apply_reconfig and keep the tree green,
not a full Phase 4 implementation.

Verified: full tree rebuilds clean (223/223 targets), 56/56 ctest
suites pass (100%) -- independently re-verified from a fresh build, not
just the porting agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…n regression (#138)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. First of Phase 3 Wave 2's three batches,
alongside spi/uart and pwm/wakeup (separate PRs).

c-RCP's largest single endpoint source file (ep_mdio.c, 1195 lines) --
mdio.hpp needed the largest single content expansion of any Phase 3
batch (309 -> 1354 lines).

- A prior session's own 'addressing-model fix' (issue #72) was itself
  wrong and has been reverted: it discarded the real Clause-22/Clause-45
  (MMD) addressing (5-bit prtad, plus regad directly or devad+16-bit
  regad) for an invented opaque mdio_address field, on the theory that
  Clause 22/45 had 'no basis' in TC18. c-RCP's ep_mdio.h/.c
  (rcp_ep_mdio_addr_t, rcp_ep_mdio_addr_valid(),
  rcp_ep_mdio_burst_next_regad()) keeps this addressing as real,
  load-bearing content -- restored as MdioAddr/addr_valid()/
  burst_next_regad() (REQ-MDIO-001/002).
- No ACF-level wire codec existed at all -- ported the complete
  read/write request/response pair for both the MMD family and the
  entirely separate MMS family (REQ-MDIO-012-019/025-028).
- MMS (Memory Map Selector) addressing family was entirely missing --
  added MdioMmsAddr, mms_addr_valid()/mms_uses_32bit_words()/
  mms_burst_next_addr()/mms_mode_for_word_count(), plus 32-bit word
  packing (REQ-MDIO-022/024).
- REQ-MDIO-024 (the external-spec ambiguity) reflected explicitly:
  TC18 itself never specifies mdio_address's bit layout for MMS mode
  (spec-defects item 55, still open); c-RCP closed it with a
  documented, externally-sourced assumption (OPEN Alliance 10BASE-T1x
  MAC-PHY Serial Interface Spec V1.1's 4-bit MMS selector + 16-bit
  ADDR) -- carried forward as MdioMmsAddr{mms, addr}, kept
  characterized as PARTIAL not IMPLEMENTED, matching c-RCP's own
  .fusa-reqs.json status.
- Table 59 functional-config register block was entirely missing --
  added, with no base_clk row (unlike every other endpoint type,
  since Table 59 genuinely defines none) (REQ-MDIO-009/010/020/023).
- Invented TriggerRegistry/MdioSignal::TransferComplete removed --
  c-RCP's own file header states MDIO has no trigger-signal table at
  all, mirroring can.hpp's CanEndpoint (no TriggerRegistry either).

Fixed-capacity: EpFuncBlock is std::array<uint8_t, 6> (narrower than
sibling endpoints since MDIO has no base_clk row).

test_mdio.cpp: 12 -> 121 TEST_CASEs (420 assertions).

Call sites touched: none. MdioRequest/MdioResponse/MdioEndpoint's
transact/handle_request/last_request kept fully source-compatible, so
mock.hpp's dispatch_mdio() and its own MDIO tests compile and pass
unmodified.

Verified: full tree rebuilds clean (223/223 targets), 56/56 ctest
suites pass (100%) -- independently re-verified from a fresh build, not
just the porting agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… operand order (#140)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Third of Phase 3 Wave 2's three batches,
alongside mdio and spi/uart (separate PRs).

## pwm.hpp

- Fixed the same operand-order bug class found and fixed in gpio.hpp
  during PR #137: PWM_OUT's Subtract conditional-request handling used
  current - operand instead of c-RCP's normative request - current.
  This is deliberately fixed per-endpoint (not centralized into
  endpoint.hpp), matching the same caution documented in #137 that this
  bug needed independent re-verification at every endpoint using
  Subtract semantics, not an assumption that one fix covers all of
  them -- confirmed here to be a real, independent instance of the same
  class, not a duplicate report.
- Ported against c-RCP's current RC5-conformant ep_pwm.h/.c.

## wakeup.hpp

- Prior cpp-RCP wakeup.hpp content was a wholesale invention with no
  real relationship to c-RCP's actual ep_wakeup.h/.c -- re-derived from
  scratch against c-RCP's current implementation.
- REQ-WAKEUP-020/026's regmap.c-scoped behavior intentionally NOT
  ported here (out of scope for the endpoint module itself; deferred to
  Phase 4's regmap port, noted in-file).

## Call sites

None touched -- mock.hpp and powerstate.hpp (and their tests) confirmed
compatible with the new pwm.hpp/wakeup.hpp APIs without modification.

## Verification

Full tree rebuilds clean (223/223 targets), 56/56 ctest suites pass
(100%) -- independently re-verified from a fresh build in the worktree,
not just the porting agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…pause fix (#139)

* rewrite(phase3): port spi and uart from c-RCP, RC5 nr_cs/deassert_cs_pause fix

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Second of Phase 3 Wave 2's three batches,
alongside mdio and pwm/wakeup (separate PRs).

## spi.hpp

- The RC5 wire-format defect that originally motivated this whole rewrite
  (see #129's own opening rationale) is now actually fixed: spi_nr_cs
  narrowed from an 8-bit count to a 4-bit (count-1) field, plus a new
  deassert_cs_pause bit (RC5 ticket NXP_100). Confirmed against c-RCP's
  current ep_spi.h/.c bit layout, not just cited from memory.
- Compound-wait handling for SPI transactions ported (SPI has its own
  distinct compound-wait semantics from the generic request.hpp path,
  per c-RCP's ep_spi.c).
- Fixed-capacity conversions applied throughout (no dynamic allocation
  on the transfer path).

## uart.hpp

- nr_bits bug fixed: prior cpp-RCP state miscomputed the bit-width field
  against c-RCP's actual ep_uart.c encoding.
- Ported against c-RCP's current RC5-conformant ep_uart.h/.c.

## Call sites

mock.hpp: one enum-rename fix (spi::SpiErrc::channel_out_of_range ->
bad_channel) to match the renamed error enumerator; no other call sites
needed changes.

test_spi.cpp / test_uart.cpp expanded to match the new content (compound
wait, nr_cs/deassert_cs_pause, nr_bits coverage).

## Verification

Full tree rebuilds clean (223/223 targets), 56/56 ctest suites pass
(100%) -- independently re-verified from a fresh build in the worktree,
not just the porting agent's own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* fix(spi): avoid cpfusa cyber CYBER002 false-positive on bare 'RC4' token

The bare substring 'RC4' in a comment describing TC18 spec revision
history was matched by cpfusa's weak-symmetric-cipher heuristic
(CYBER002/CWE-327, 'Weak symmetric cipher (DES/3DES/RC4) detected') as
if it were the RC4 stream cipher, failing PR #139's cpfusa cyber
--strict gate (the only [ERROR]-level finding in that run; the other
568 findings in the log are pre-existing [WARNING]-level noise that
doesn't gate). c-RCP's own ep_spi.h uses the same bare 'RC4' token in
its equivalent comment and isn't subject to this particular gate.

Reworded to 'the RC 4 revision' (space inserted) -- same meaning, no
contiguous 'RC4' substring for the scanner to match. No functional or
documentation-content change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…eneric/functional split (#142)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch A of a two-batch regmap port
(batch B — HW pin map, named-signal index, request-stream/response-queue
config, EP-ID map, optional-subsystem sections — is a separate later PR).

c-RCP's regmap.h/.c (3549+2662 = 6211 lines) is regmap's own content
source of truth; cpp-RCP's prior regmap.hpp (534 lines) was a stub with
real content gaps, matching ROADMAP.md's own flagged '~7x more complete
in c-RCP' finding that originally scoped this phase.

## Ported (c-RCP file:line -> cpp-RCP)

- EP0/root-client model (regmap.h:358-371, regmap.c:12-15) ->
  regmap::is_ep0(), kEp0.
- svr_implemented_options (REQ-RMAP-030, regmap.h:384-413): the 5 real
  bits (kOptCompoundWait/Trigger/Chained/TimeSync/EnhCancel); kept
  legacy kOptConditionalRequests/kOptSafetyRequests/kOptFragmentation
  (still referenced by request.hpp/e2e.hpp) with a comment explaining
  they have no TC18 basis per REQ-RMAP-004..008's retirement.
- General register map (regmap.h:416-1141, regmap.c:41-288): new
  GeneralMap struct (~40 fields, REQ-RMAP-023/026-039), render(),
  encode_read_response()/decode_read_response()/decode_write_request()
  (Table 20 wire codec), GeneralMapErrc.
- Root-client/writer_ctx (regmap.h:1152-1211, regmap.c:1684-1741):
  EpClient, writer_ctx() deriving lifecycle::WriterCtx.
- SvrEpCfg (TC18 Sec13.7.1.2, regmap.h:1211-1303): svr_discovery_timeout,
  svr_ep_status.
- EndpointGenericConfig (regmap.h:1310-1608): real fields
  (ep_type/ep_used/ep_delay_time/ep_req_storage_size/ep_description/
  ep_tx_buffer_size/ep_rx_buffer_size), ep_generic_cfg:: namespace with
  boundary conversions (issue #311 batches 2-4), render(),
  apply_reconfig() (per-field, matching c-RCP's documented rationale),
  EpGenericCfgReconfigErrc.

## Deliberate deferrals (marked TODO(phase4-batch-b) in-file)

- writer_ctx()'s via_valid_stream_association always returns false
  (fail-closed) until batch B's EP-ID/byte_bus_id association table
  lands (needs a request_stream_index field EpIdMappingEntry doesn't
  have yet).
- HW pin map, request-stream-cfg, response-queue-cfg, EP-ID map,
  optional-subsystem config, ep_functional_cfg_t: untouched, batch B.

## Cross-cutting note

c-RCP's ep_generic_cfg_t has no per-role CRC fields (that's
ep_functional_cfg_t's, batch B); cpp-RCP's pre-existing
EndpointGenericConfig already had three (ep_req/ack/response_crc_enable)
depended on by e2e.hpp/test_e2e.cpp -- kept, marked in-file as
pre-existing/non-c-RCP content rather than silently dropped or
relocated (relocation needs ep_functional_cfg_t, batch B's scope).

## Call sites

mock.hpp: RegisterMap::endpoint_count -> general.svr_ep_count,
magic -> general.magic (2 lines). test_mock.cpp: matching 4-line fix.
No other files touched.

## Verification

test_regmap.cpp: 558 -> 1105 lines, 28 -> 57 TEST_CASEs (327
assertions). Full tree rebuilds clean (0 errors, 0 warnings), 56/56
ctest suites pass (100%) -- independently re-verified from a fresh
build, not just the porting agent's own report. Heavy regmap
dependents (test_e2e, test_watchdog, test_request, test_discovery,
test_mock, test_gpio, test_adc) spot-checked with unchanged assertion
counts.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…pp (#141)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Runs independently of the regmap batches
(no shared files) -- part of the {server, discovery} parallel wave the
Phase 4 scoping pass called out.

c-RCP's server.h/.c (673+1045 = 1718 lines) had no cpp-RCP home at all:
endpoint.hpp is narrower (evt[2:0] write-semantics/trigger-signal
tables, confirmed by reading both files in full), and request.hpp
already deliberately owns the request.c/request_sequencer.c/scheduler.c
merge from Phase 1 (RequestLedger, SequencerTable, TriggeredRuntime,
select_next_due) -- neither had server.c's own ep_enable pre-load queue
or its fixed-capacity conditional-request admission/scheduling store.
New include/rcp/server.hpp (1180 lines) mirrors c-RCP's own module
boundary, built on rcp::request's primitives rather than duplicating
them.

## Ported (c-RCP server.h/.c -> rcp::server::Endpoint)

All 21 public functions, REQ-SRV-001..042, REQ-PWRMODE-028,
REQ-CANCEL-012, REQ-ACF-021/032, REQ-TIMED-012:
- submit/set_enable/drain_one/queue_len -- ep_enable pre-load FIFO,
  including the config-write (evt[2:0]=111b) fast path, the
  CompoundWait carve-out (REQ-SRV-015), and the REQ-SRV-016
  acknowledge-on-queue fix.
- admit/admit_with_ack -- request_type-aware routing (AdmitOutcome::
  ExecuteNow/Queued/Pending/Cancellation/Rejected/Suspended),
  REQ-ACF-021's response-frame rejection, REQ-TIMED-012's TSCF gate via
  admit_under_tscf_gate.
- select_due/complete -- tick evaluation (TickContext) and
  repeat-count/re-arm rule.
- notify_trigger, GptpTriggerState/gptp_trigger_evaluate (Table 37).
- chain_predecessor_done, pending() (chain_group/chain_position).
- cancel_all/cancel_single/cancel_non_safestate/watchdog_purge/
  cancel_chain_from, CancelLifecycle/CancelResult/cancel_attempt/
  cancel_chain_should_cascade.

## Deliberate design decisions (documented in-file)

- select_due() inlines its own rank+FIFO comparator rather than calling
  request::select_next_due() (which takes a std::vector&) -- avoids a
  per-tick heap allocation on this safety-relevant path.
- kMaxQueuedFrames = 32: c-RCP's ep_enable queue is genuinely unbounded
  (realloc-doubling, no cap); since this rewrite requires fixed-capacity
  storage on admission-path structures, a full queue reproduces c-RCP's
  own documented 'still returns false (queued), nothing actually
  stored' allocation-failure contract exactly -- no caller-visible
  behavior changed, only a probabilistic failure became deterministic.
- cancel_chain_from/cancel_chain_should_cascade reintroduce c-RCP's
  positional chain model (chain_group/chain_position) rather than
  RequestLedger's graph-based cascade -- this store isn't a
  RequestLedger and has no graph; matches c-RCP's own layering (caller
  assigns position after seeing a whole frame's member layout).

No content bugs found versus c-RCP; the logic mapped cleanly onto
rcp::request's existing primitives.

## Deferred

mock.c-level integration (dispatch_frame, apply_cancellation,
response-shape wiring, stream-wide overflow broadcast) -- explicitly
scoped to the later mock.hpp batch, which lands last in Phase 4.
Integration points this needs are documented in server.hpp's header
comment (admit_with_ack per inbound frame, select_due/complete per
tick, notify_trigger + gptp_trigger_evaluate on gPTP lock changes,
chain_predecessor_done, set_admission_suspended around SleepCMD drain,
watchdog_purge/cancel_* from watchdog/cancellation handling).

## Verification

New tests/test_server.cpp: 47 TEST_CASEs, 260 assertions. Full tree
rebuilds clean (0 errors, 0 warnings), 57/57 ctest suites pass (100%)
-- independently re-verified from a fresh build, not just the porting
agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…P-ID map, optional subsystems (#143)

* rewrite(phase4): port regmap batch A from c-RCP — general map, EP0, generic/functional split

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch A of a two-batch regmap port
(batch B — HW pin map, named-signal index, request-stream/response-queue
config, EP-ID map, optional-subsystem sections — is a separate later PR).

c-RCP's regmap.h/.c (3549+2662 = 6211 lines) is regmap's own content
source of truth; cpp-RCP's prior regmap.hpp (534 lines) was a stub with
real content gaps, matching ROADMAP.md's own flagged '~7x more complete
in c-RCP' finding that originally scoped this phase.

## Ported (c-RCP file:line -> cpp-RCP)

- EP0/root-client model (regmap.h:358-371, regmap.c:12-15) ->
  regmap::is_ep0(), kEp0.
- svr_implemented_options (REQ-RMAP-030, regmap.h:384-413): the 5 real
  bits (kOptCompoundWait/Trigger/Chained/TimeSync/EnhCancel); kept
  legacy kOptConditionalRequests/kOptSafetyRequests/kOptFragmentation
  (still referenced by request.hpp/e2e.hpp) with a comment explaining
  they have no TC18 basis per REQ-RMAP-004..008's retirement.
- General register map (regmap.h:416-1141, regmap.c:41-288): new
  GeneralMap struct (~40 fields, REQ-RMAP-023/026-039), render(),
  encode_read_response()/decode_read_response()/decode_write_request()
  (Table 20 wire codec), GeneralMapErrc.
- Root-client/writer_ctx (regmap.h:1152-1211, regmap.c:1684-1741):
  EpClient, writer_ctx() deriving lifecycle::WriterCtx.
- SvrEpCfg (TC18 Sec13.7.1.2, regmap.h:1211-1303): svr_discovery_timeout,
  svr_ep_status.
- EndpointGenericConfig (regmap.h:1310-1608): real fields
  (ep_type/ep_used/ep_delay_time/ep_req_storage_size/ep_description/
  ep_tx_buffer_size/ep_rx_buffer_size), ep_generic_cfg:: namespace with
  boundary conversions (issue #311 batches 2-4), render(),
  apply_reconfig() (per-field, matching c-RCP's documented rationale),
  EpGenericCfgReconfigErrc.

## Deliberate deferrals (marked TODO(phase4-batch-b) in-file)

- writer_ctx()'s via_valid_stream_association always returns false
  (fail-closed) until batch B's EP-ID/byte_bus_id association table
  lands (needs a request_stream_index field EpIdMappingEntry doesn't
  have yet).
- HW pin map, request-stream-cfg, response-queue-cfg, EP-ID map,
  optional-subsystem config, ep_functional_cfg_t: untouched, batch B.

## Cross-cutting note

c-RCP's ep_generic_cfg_t has no per-role CRC fields (that's
ep_functional_cfg_t's, batch B); cpp-RCP's pre-existing
EndpointGenericConfig already had three (ep_req/ack/response_crc_enable)
depended on by e2e.hpp/test_e2e.cpp -- kept, marked in-file as
pre-existing/non-c-RCP content rather than silently dropped or
relocated (relocation needs ep_functional_cfg_t, batch B's scope).

## Call sites

mock.hpp: RegisterMap::endpoint_count -> general.svr_ep_count,
magic -> general.magic (2 lines). test_mock.cpp: matching 4-line fix.
No other files touched.

## Verification

test_regmap.cpp: 558 -> 1105 lines, 28 -> 57 TEST_CASEs (327
assertions). Full tree rebuilds clean (0 errors, 0 warnings), 56/56
ctest suites pass (100%) -- independently re-verified from a fresh
build, not just the porting agent's own report. Heavy regmap
dependents (test_e2e, test_watchdog, test_request, test_discovery,
test_mock, test_gpio, test_adc) spot-checked with unchanged assertion
counts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* rewrite(phase4): port regmap batch B from c-RCP — HW pins, streams, EP-ID map, optional subsystems

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch B of the two-batch regmap
port; batch A (general map, EP0, generic/functional config split,
merged as #141) is the prerequisite this builds on.

## Ported (c-RCP regmap.h/.c -> cpp-RCP regmap.hpp)

- EpFunctionalCfg (regmap.h:1614-1627) -- content-modeling only, see
  reconciliation below.
- HW pin mapping: HwPinMapEntry (regmap.h:1685-1697, replaces the old
  pin_id/function placeholder -- zero consumers existed for it
  anywhere in the tree), hw_pin::k* bit constants (Table 22),
  hw_pin_map::render/apply_reconfig, HwPinMapReconfigErrc
  (REQ-RMAP-040/041).
- NamedSignal enum + named_signal_string()/named_signal_ep_signal_nr()
  (regmap.h:1798-1906).
- RequestStreamConfig extended (not replaced -- rx_wd_timeout_interval
  etc. kept verbatim since e2e.hpp/watchdog.hpp/sim.hpp and their
  tests depend on those exact names) with rx_secure_channel_index/
  rx_ack_stream_index/rx_resp_stream_index(default 1)/
  rx_stream_max_request_size; request_stream_cfg::
  wd_timeout_ms_to_ticks/_ticks_to_ms, render/apply_reconfig (TC18
  0.5.1_RC5 issue #458 4-bit reconciliation, AND-not-OR coupling),
  resolve_index.
- ResponseQueueConfig replaced (old shape had zero real consumers;
  comments in respqueue.hpp/deadline.hpp already referenced fields
  that never existed) with c-RCP's real per-queue row +
  response_queue_stream_id(), render/apply_reconfig.
- EpIdMappingEntry extended with request_stream_index/crc_required
  appended last (preserves existing 2-element brace-inits in
  mock.hpp/tests) + ep_id_map::{is_ascending, effective_count,
  row_init_default, render, apply_reconfig, has_single_client_per_ep,
  shared_bus_homogeneous, ep_type_has_fixed_ep_id,
  byte_bus_ids_for_stream, is_valid_association}.
- OptionalSubsystemCfg + optional_subsystem_cfg::apply_reconfig
  (REQ-RMAP-039), wired into RegisterMap as 4 new fields.

## Batch-A TODOs closed

- writer_ctx()'s via_valid_stream_association now calls the real
  ep_id_map::is_valid_association() instead of the fail-closed stub.
- EndpointGenericConfig's 3 pre-existing CRC fields: confirmed as a
  deliberate, distinct mechanism (not c-RCP's ep_req_crc_enable) --
  every concrete endpoint type from this project's own Phase 3
  (gpio.hpp, adc.hpp, etc.) already independently ported c-RCP's real
  functional-cfg fields inline into its own struct, so EpFunctionalCfg
  is content-modeling only, matching SvrEpCfg's existing disposition.

## Deliberately deferred (documented in-file)

c-RCP's EP0 address-routed dispatcher (regmap.h:3072-3549, ~500 lines
combining every sub-table with lifecycle authorization + sequencer
ownership) and the sequencer-table wire codec -- neither is named in
batch B's scope; only their content/mechanism pieces were ported.

No bugs found versus c-RCP during the port (render/apply_reconfig
logic, including the AND-not-OR bit coupling and saturation rules,
reproduced faithfully); one self-contradictory assertion in this
batch's own new test was caught and fixed before it reached the build.

## Verification

regmap.hpp: 1328 -> 2654 lines. test_regmap.cpp: 1105 -> 1904 lines,
59 -> 120 TEST_CASEs (332 -> 2433 assertions). Full tree rebuilds
clean (0 errors, 0 warnings), 56/56 ctest suites pass (100%) --
independently re-verified from a fresh build, not just the porting
agent's own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…idation gaps (#144)

* rewrite(phase4): port regmap batch A from c-RCP — general map, EP0, generic/functional split

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch A of a two-batch regmap port
(batch B — HW pin map, named-signal index, request-stream/response-queue
config, EP-ID map, optional-subsystem sections — is a separate later PR).

c-RCP's regmap.h/.c (3549+2662 = 6211 lines) is regmap's own content
source of truth; cpp-RCP's prior regmap.hpp (534 lines) was a stub with
real content gaps, matching ROADMAP.md's own flagged '~7x more complete
in c-RCP' finding that originally scoped this phase.

## Ported (c-RCP file:line -> cpp-RCP)

- EP0/root-client model (regmap.h:358-371, regmap.c:12-15) ->
  regmap::is_ep0(), kEp0.
- svr_implemented_options (REQ-RMAP-030, regmap.h:384-413): the 5 real
  bits (kOptCompoundWait/Trigger/Chained/TimeSync/EnhCancel); kept
  legacy kOptConditionalRequests/kOptSafetyRequests/kOptFragmentation
  (still referenced by request.hpp/e2e.hpp) with a comment explaining
  they have no TC18 basis per REQ-RMAP-004..008's retirement.
- General register map (regmap.h:416-1141, regmap.c:41-288): new
  GeneralMap struct (~40 fields, REQ-RMAP-023/026-039), render(),
  encode_read_response()/decode_read_response()/decode_write_request()
  (Table 20 wire codec), GeneralMapErrc.
- Root-client/writer_ctx (regmap.h:1152-1211, regmap.c:1684-1741):
  EpClient, writer_ctx() deriving lifecycle::WriterCtx.
- SvrEpCfg (TC18 Sec13.7.1.2, regmap.h:1211-1303): svr_discovery_timeout,
  svr_ep_status.
- EndpointGenericConfig (regmap.h:1310-1608): real fields
  (ep_type/ep_used/ep_delay_time/ep_req_storage_size/ep_description/
  ep_tx_buffer_size/ep_rx_buffer_size), ep_generic_cfg:: namespace with
  boundary conversions (issue #311 batches 2-4), render(),
  apply_reconfig() (per-field, matching c-RCP's documented rationale),
  EpGenericCfgReconfigErrc.

## Deliberate deferrals (marked TODO(phase4-batch-b) in-file)

- writer_ctx()'s via_valid_stream_association always returns false
  (fail-closed) until batch B's EP-ID/byte_bus_id association table
  lands (needs a request_stream_index field EpIdMappingEntry doesn't
  have yet).
- HW pin map, request-stream-cfg, response-queue-cfg, EP-ID map,
  optional-subsystem config, ep_functional_cfg_t: untouched, batch B.

## Cross-cutting note

c-RCP's ep_generic_cfg_t has no per-role CRC fields (that's
ep_functional_cfg_t's, batch B); cpp-RCP's pre-existing
EndpointGenericConfig already had three (ep_req/ack/response_crc_enable)
depended on by e2e.hpp/test_e2e.cpp -- kept, marked in-file as
pre-existing/non-c-RCP content rather than silently dropped or
relocated (relocation needs ep_functional_cfg_t, batch B's scope).

## Call sites

mock.hpp: RegisterMap::endpoint_count -> general.svr_ep_count,
magic -> general.magic (2 lines). test_mock.cpp: matching 4-line fix.
No other files touched.

## Verification

test_regmap.cpp: 558 -> 1105 lines, 28 -> 57 TEST_CASEs (327
assertions). Full tree rebuilds clean (0 errors, 0 warnings), 56/56
ctest suites pass (100%) -- independently re-verified from a fresh
build, not just the porting agent's own report. Heavy regmap
dependents (test_e2e, test_watchdog, test_request, test_discovery,
test_mock, test_gpio, test_adc) spot-checked with unchanged assertion
counts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* rewrite(phase4): port regmap batch B from c-RCP — HW pins, streams, EP-ID map, optional subsystems

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch B of the two-batch regmap
port; batch A (general map, EP0, generic/functional config split,
merged as #141) is the prerequisite this builds on.

## Ported (c-RCP regmap.h/.c -> cpp-RCP regmap.hpp)

- EpFunctionalCfg (regmap.h:1614-1627) -- content-modeling only, see
  reconciliation below.
- HW pin mapping: HwPinMapEntry (regmap.h:1685-1697, replaces the old
  pin_id/function placeholder -- zero consumers existed for it
  anywhere in the tree), hw_pin::k* bit constants (Table 22),
  hw_pin_map::render/apply_reconfig, HwPinMapReconfigErrc
  (REQ-RMAP-040/041).
- NamedSignal enum + named_signal_string()/named_signal_ep_signal_nr()
  (regmap.h:1798-1906).
- RequestStreamConfig extended (not replaced -- rx_wd_timeout_interval
  etc. kept verbatim since e2e.hpp/watchdog.hpp/sim.hpp and their
  tests depend on those exact names) with rx_secure_channel_index/
  rx_ack_stream_index/rx_resp_stream_index(default 1)/
  rx_stream_max_request_size; request_stream_cfg::
  wd_timeout_ms_to_ticks/_ticks_to_ms, render/apply_reconfig (TC18
  0.5.1_RC5 issue #458 4-bit reconciliation, AND-not-OR coupling),
  resolve_index.
- ResponseQueueConfig replaced (old shape had zero real consumers;
  comments in respqueue.hpp/deadline.hpp already referenced fields
  that never existed) with c-RCP's real per-queue row +
  response_queue_stream_id(), render/apply_reconfig.
- EpIdMappingEntry extended with request_stream_index/crc_required
  appended last (preserves existing 2-element brace-inits in
  mock.hpp/tests) + ep_id_map::{is_ascending, effective_count,
  row_init_default, render, apply_reconfig, has_single_client_per_ep,
  shared_bus_homogeneous, ep_type_has_fixed_ep_id,
  byte_bus_ids_for_stream, is_valid_association}.
- OptionalSubsystemCfg + optional_subsystem_cfg::apply_reconfig
  (REQ-RMAP-039), wired into RegisterMap as 4 new fields.

## Batch-A TODOs closed

- writer_ctx()'s via_valid_stream_association now calls the real
  ep_id_map::is_valid_association() instead of the fail-closed stub.
- EndpointGenericConfig's 3 pre-existing CRC fields: confirmed as a
  deliberate, distinct mechanism (not c-RCP's ep_req_crc_enable) --
  every concrete endpoint type from this project's own Phase 3
  (gpio.hpp, adc.hpp, etc.) already independently ported c-RCP's real
  functional-cfg fields inline into its own struct, so EpFunctionalCfg
  is content-modeling only, matching SvrEpCfg's existing disposition.

## Deliberately deferred (documented in-file)

c-RCP's EP0 address-routed dispatcher (regmap.h:3072-3549, ~500 lines
combining every sub-table with lifecycle authorization + sequencer
ownership) and the sequencer-table wire codec -- neither is named in
batch B's scope; only their content/mechanism pieces were ported.

No bugs found versus c-RCP during the port (render/apply_reconfig
logic, including the AND-not-OR bit coupling and saturation rules,
reproduced faithfully); one self-contradictory assertion in this
batch's own new test was caught and fixed before it reached the build.

## Verification

regmap.hpp: 1328 -> 2654 lines. test_regmap.cpp: 1105 -> 1904 lines,
59 -> 120 TEST_CASEs (332 -> 2433 assertions). Full tree rebuilds
clean (0 errors, 0 warnings), 56/56 ctest suites pass (100%) --
independently re-verified from a fresh build, not just the porting
agent's own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* rewrite(phase4): port discovery from c-RCP, fix claim-release and validation gaps

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Closes the gap on discovery.hpp, which the
Phase 4 scoping pass found already about 1/3 of the way there (core
request/response and the DiscoveryClaim state machine already worked).

## Kept as-is (already correct)

kDiscoveryByteBusId/kDiscoveryRegisterAddress/kDiscoveryDefaultReadSize,
make_discovery_request/encode_discovery_request, should_answer_discovery,
and the core DiscoveryClaim claim/timeout state machine (ClaimOutcome,
on_discovery_request, may_configure, has_active_claim/current_holder) --
all already matched c-RCP's semantics.

## Ported (c-RCP src/discovery.c/include/rcp/discovery.h)

- should_drop_discovery() (c-RCP:56-59, REQ-DISC-001): generalized the
  NTSCF-only check to any non-NTSCF subtype, not just TSCF.
- Shared detail::decode_common_frame() (c-RCP:68-102) for
  request/response/fragment decode -- see bug fix below.
- DiscoveryResult, make_discovery_response, encode_discovery_response,
  decode_discovery_response (c-RCP:174-245, REQ-DISC-009..014), 14-octet
  general slice with the documented svr_version-is-32-bit-not-16 layout,
  pinned by an octet-address test.
- Fragmented response path (c-RCP:250-378, REQ-DISC-025..028):
  discovery_response_fragment_count, encode/decode fragment,
  decode_discovery_reassembled_response, built on rcp::fragment
  following uart.hpp's existing wiring idiom (std::vector-returning,
  not c-RCP's fixed 255-entry stack array -- this codebase already
  heap-allocates on every other wire-encode path, so that particular
  ASIL-D optimization doesn't apply here).
- DiscoveryCache (c-RCP:440-503, REQ-DISC-023/030): thin client-side
  convenience, std::vector-backed by design (not a protocol state
  machine, no fixed-capacity requirement).
- release() on DiscoveryClaim (REQ-DISC-022, was missing entirely).
- Expanded DiscoveryErrc from 1 to 5 values (short_frame,
  tscf_headed_request_dropped, bad_msg_type, wrong_bus, wrong_op),
  numbered to mirror c-RCP's rcp_discovery_errc_t 1:1.

## Bugs found and fixed

1. decode_discovery_request never validated byte_bus_id/op at all
   (REQ-DISC-006/007 untested and unenforced) -- fixed via the shared
   decode_common_frame.
2. DiscoveryClaim::on_configuration_request() previously *released* the
   claim on the first config write (holder_.reset()), contradicting
   c-RCP's rcp_discovery_claim_note_config_write() (c-RCP
   src/discovery.c:423-430), which *extends* the deadline and keeps the
   claim held so a claimant can issue multiple writes over time. Fixed
   to refresh claimed_at_, matching c-RCP's own
   test_claim_config_write_refreshes_deadline_for_claimant, now ported.

## regmap.hpp interplay

Confirmed c-RCP's discovery.c never calls
rcp_regmap_ep_id_map_is_valid_association() -- that's lifecycle.hpp's
concern (via_valid_stream_association), not discovery's. Documented
in-file rather than wiring it in.

## Deferred

Wiring any of this into mock.hpp's dispatch -- out of scope, part of
the later mock.hpp batch that closes out Phase 4.

## Verification

discovery.hpp: 270 -> 818 lines. test_discovery.cpp: 230 -> 812 lines,
~20 -> 49 TEST_CASEs (686 assertions). Full tree rebuilds clean (0
errors, 0 warnings), 57/57 ctest suites pass (100%) -- independently
re-verified from a fresh build against the fully-merged regmap base
(both batches), not just the porting agent's own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ld, add test_adapt.cpp (#145)

* rewrite(phase4): port regmap batch A from c-RCP — general map, EP0, generic/functional split

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch A of a two-batch regmap port
(batch B — HW pin map, named-signal index, request-stream/response-queue
config, EP-ID map, optional-subsystem sections — is a separate later PR).

c-RCP's regmap.h/.c (3549+2662 = 6211 lines) is regmap's own content
source of truth; cpp-RCP's prior regmap.hpp (534 lines) was a stub with
real content gaps, matching ROADMAP.md's own flagged '~7x more complete
in c-RCP' finding that originally scoped this phase.

## Ported (c-RCP file:line -> cpp-RCP)

- EP0/root-client model (regmap.h:358-371, regmap.c:12-15) ->
  regmap::is_ep0(), kEp0.
- svr_implemented_options (REQ-RMAP-030, regmap.h:384-413): the 5 real
  bits (kOptCompoundWait/Trigger/Chained/TimeSync/EnhCancel); kept
  legacy kOptConditionalRequests/kOptSafetyRequests/kOptFragmentation
  (still referenced by request.hpp/e2e.hpp) with a comment explaining
  they have no TC18 basis per REQ-RMAP-004..008's retirement.
- General register map (regmap.h:416-1141, regmap.c:41-288): new
  GeneralMap struct (~40 fields, REQ-RMAP-023/026-039), render(),
  encode_read_response()/decode_read_response()/decode_write_request()
  (Table 20 wire codec), GeneralMapErrc.
- Root-client/writer_ctx (regmap.h:1152-1211, regmap.c:1684-1741):
  EpClient, writer_ctx() deriving lifecycle::WriterCtx.
- SvrEpCfg (TC18 Sec13.7.1.2, regmap.h:1211-1303): svr_discovery_timeout,
  svr_ep_status.
- EndpointGenericConfig (regmap.h:1310-1608): real fields
  (ep_type/ep_used/ep_delay_time/ep_req_storage_size/ep_description/
  ep_tx_buffer_size/ep_rx_buffer_size), ep_generic_cfg:: namespace with
  boundary conversions (issue #311 batches 2-4), render(),
  apply_reconfig() (per-field, matching c-RCP's documented rationale),
  EpGenericCfgReconfigErrc.

## Deliberate deferrals (marked TODO(phase4-batch-b) in-file)

- writer_ctx()'s via_valid_stream_association always returns false
  (fail-closed) until batch B's EP-ID/byte_bus_id association table
  lands (needs a request_stream_index field EpIdMappingEntry doesn't
  have yet).
- HW pin map, request-stream-cfg, response-queue-cfg, EP-ID map,
  optional-subsystem config, ep_functional_cfg_t: untouched, batch B.

## Cross-cutting note

c-RCP's ep_generic_cfg_t has no per-role CRC fields (that's
ep_functional_cfg_t's, batch B); cpp-RCP's pre-existing
EndpointGenericConfig already had three (ep_req/ack/response_crc_enable)
depended on by e2e.hpp/test_e2e.cpp -- kept, marked in-file as
pre-existing/non-c-RCP content rather than silently dropped or
relocated (relocation needs ep_functional_cfg_t, batch B's scope).

## Call sites

mock.hpp: RegisterMap::endpoint_count -> general.svr_ep_count,
magic -> general.magic (2 lines). test_mock.cpp: matching 4-line fix.
No other files touched.

## Verification

test_regmap.cpp: 558 -> 1105 lines, 28 -> 57 TEST_CASEs (327
assertions). Full tree rebuilds clean (0 errors, 0 warnings), 56/56
ctest suites pass (100%) -- independently re-verified from a fresh
build, not just the porting agent's own report. Heavy regmap
dependents (test_e2e, test_watchdog, test_request, test_discovery,
test_mock, test_gpio, test_adc) spot-checked with unchanged assertion
counts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* rewrite(phase4): port regmap batch B from c-RCP — HW pins, streams, EP-ID map, optional subsystems

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch B of the two-batch regmap
port; batch A (general map, EP0, generic/functional config split,
merged as #141) is the prerequisite this builds on.

## Ported (c-RCP regmap.h/.c -> cpp-RCP regmap.hpp)

- EpFunctionalCfg (regmap.h:1614-1627) -- content-modeling only, see
  reconciliation below.
- HW pin mapping: HwPinMapEntry (regmap.h:1685-1697, replaces the old
  pin_id/function placeholder -- zero consumers existed for it
  anywhere in the tree), hw_pin::k* bit constants (Table 22),
  hw_pin_map::render/apply_reconfig, HwPinMapReconfigErrc
  (REQ-RMAP-040/041).
- NamedSignal enum + named_signal_string()/named_signal_ep_signal_nr()
  (regmap.h:1798-1906).
- RequestStreamConfig extended (not replaced -- rx_wd_timeout_interval
  etc. kept verbatim since e2e.hpp/watchdog.hpp/sim.hpp and their
  tests depend on those exact names) with rx_secure_channel_index/
  rx_ack_stream_index/rx_resp_stream_index(default 1)/
  rx_stream_max_request_size; request_stream_cfg::
  wd_timeout_ms_to_ticks/_ticks_to_ms, render/apply_reconfig (TC18
  0.5.1_RC5 issue #458 4-bit reconciliation, AND-not-OR coupling),
  resolve_index.
- ResponseQueueConfig replaced (old shape had zero real consumers;
  comments in respqueue.hpp/deadline.hpp already referenced fields
  that never existed) with c-RCP's real per-queue row +
  response_queue_stream_id(), render/apply_reconfig.
- EpIdMappingEntry extended with request_stream_index/crc_required
  appended last (preserves existing 2-element brace-inits in
  mock.hpp/tests) + ep_id_map::{is_ascending, effective_count,
  row_init_default, render, apply_reconfig, has_single_client_per_ep,
  shared_bus_homogeneous, ep_type_has_fixed_ep_id,
  byte_bus_ids_for_stream, is_valid_association}.
- OptionalSubsystemCfg + optional_subsystem_cfg::apply_reconfig
  (REQ-RMAP-039), wired into RegisterMap as 4 new fields.

## Batch-A TODOs closed

- writer_ctx()'s via_valid_stream_association now calls the real
  ep_id_map::is_valid_association() instead of the fail-closed stub.
- EndpointGenericConfig's 3 pre-existing CRC fields: confirmed as a
  deliberate, distinct mechanism (not c-RCP's ep_req_crc_enable) --
  every concrete endpoint type from this project's own Phase 3
  (gpio.hpp, adc.hpp, etc.) already independently ported c-RCP's real
  functional-cfg fields inline into its own struct, so EpFunctionalCfg
  is content-modeling only, matching SvrEpCfg's existing disposition.

## Deliberately deferred (documented in-file)

c-RCP's EP0 address-routed dispatcher (regmap.h:3072-3549, ~500 lines
combining every sub-table with lifecycle authorization + sequencer
ownership) and the sequencer-table wire codec -- neither is named in
batch B's scope; only their content/mechanism pieces were ported.

No bugs found versus c-RCP during the port (render/apply_reconfig
logic, including the AND-not-OR bit coupling and saturation rules,
reproduced faithfully); one self-contradictory assertion in this
batch's own new test was caught and fixed before it reached the build.

## Verification

regmap.hpp: 1328 -> 2654 lines. test_regmap.cpp: 1105 -> 1904 lines,
59 -> 120 TEST_CASEs (332 -> 2433 assertions). Full tree rebuilds
clean (0 errors, 0 warnings), 56/56 ctest suites pass (100%) --
independently re-verified from a fresh build, not just the porting
agent's own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* rewrite(phase4): port discovery from c-RCP, fix claim-release and validation gaps

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Closes the gap on discovery.hpp, which the
Phase 4 scoping pass found already about 1/3 of the way there (core
request/response and the DiscoveryClaim state machine already worked).

## Kept as-is (already correct)

kDiscoveryByteBusId/kDiscoveryRegisterAddress/kDiscoveryDefaultReadSize,
make_discovery_request/encode_discovery_request, should_answer_discovery,
and the core DiscoveryClaim claim/timeout state machine (ClaimOutcome,
on_discovery_request, may_configure, has_active_claim/current_holder) --
all already matched c-RCP's semantics.

## Ported (c-RCP src/discovery.c/include/rcp/discovery.h)

- should_drop_discovery() (c-RCP:56-59, REQ-DISC-001): generalized the
  NTSCF-only check to any non-NTSCF subtype, not just TSCF.
- Shared detail::decode_common_frame() (c-RCP:68-102) for
  request/response/fragment decode -- see bug fix below.
- DiscoveryResult, make_discovery_response, encode_discovery_response,
  decode_discovery_response (c-RCP:174-245, REQ-DISC-009..014), 14-octet
  general slice with the documented svr_version-is-32-bit-not-16 layout,
  pinned by an octet-address test.
- Fragmented response path (c-RCP:250-378, REQ-DISC-025..028):
  discovery_response_fragment_count, encode/decode fragment,
  decode_discovery_reassembled_response, built on rcp::fragment
  following uart.hpp's existing wiring idiom (std::vector-returning,
  not c-RCP's fixed 255-entry stack array -- this codebase already
  heap-allocates on every other wire-encode path, so that particular
  ASIL-D optimization doesn't apply here).
- DiscoveryCache (c-RCP:440-503, REQ-DISC-023/030): thin client-side
  convenience, std::vector-backed by design (not a protocol state
  machine, no fixed-capacity requirement).
- release() on DiscoveryClaim (REQ-DISC-022, was missing entirely).
- Expanded DiscoveryErrc from 1 to 5 values (short_frame,
  tscf_headed_request_dropped, bad_msg_type, wrong_bus, wrong_op),
  numbered to mirror c-RCP's rcp_discovery_errc_t 1:1.

## Bugs found and fixed

1. decode_discovery_request never validated byte_bus_id/op at all
   (REQ-DISC-006/007 untested and unenforced) -- fixed via the shared
   decode_common_frame.
2. DiscoveryClaim::on_configuration_request() previously *released* the
   claim on the first config write (holder_.reset()), contradicting
   c-RCP's rcp_discovery_claim_note_config_write() (c-RCP
   src/discovery.c:423-430), which *extends* the deadline and keeps the
   claim held so a claimant can issue multiple writes over time. Fixed
   to refresh claimed_at_, matching c-RCP's own
   test_claim_config_write_refreshes_deadline_for_claimant, now ported.

## regmap.hpp interplay

Confirmed c-RCP's discovery.c never calls
rcp_regmap_ep_id_map_is_valid_association() -- that's lifecycle.hpp's
concern (via_valid_stream_association), not discovery's. Documented
in-file rather than wiring it in.

## Deferred

Wiring any of this into mock.hpp's dispatch -- out of scope, part of
the later mock.hpp batch that closes out Phase 4.

## Verification

discovery.hpp: 270 -> 818 lines. test_discovery.cpp: 230 -> 812 lines,
~20 -> 49 TEST_CASEs (686 assertions). Full tree rebuilds clean (0
errors, 0 warnings), 57/57 ctest suites pass (100%) -- independently
re-verified from a fresh build against the fully-merged regmap base
(both batches), not just the porting agent's own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* rewrite(phase4): fix adapt.hpp's missing read_size_or_segment_num field, add test_adapt.cpp

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Closes out adapt.hpp, the last piece
before mock.hpp finishes Phase 4.

## The scoping claim did NOT fully hold up

A prior scoping pass characterized adapt.hpp's simpler generic
RequestFn/op+evt_op design as behaviorally equivalent to c-RCP's richer
per-op field-table adapt.c/.h, with the only real gap being missing
test coverage. That was a sizing estimate, not a line-by-line
verification -- this batch actually did that comparison and found a
genuine, load-bearing bug underneath the otherwise-correct shape.

## Bug found and fixed (real, not cosmetic)

message_to_request()/response_to_message() never threaded the ACF
header's own 12-bit read_size_or_segment_num field through at all,
silently leaving it 0 for every relay::Message. This field is what
uart.hpp/spi.hpp/adc.hpp/i2c.hpp (and mock.hpp's dispatch_uart())
directly depend on -- confirmed against their own doc comments and
dispatch code. For I2C it is the ONLY thing selecting read vs. write
direction (0 = write, nonzero = read): before this fix, an I2C read
could never be requested through Adapt() at all, and a UART read would
always drain 0 bytes regardless of what was actually buffered.

c-RCP's own src/adapt.c threads the identical wire field through this
same layer via 5 separate per-op table rows (rcp.uart.read_size /
rcp.spi.read_size / rcp.adc.read_size / rcp.i2c.read_size /
rcp.iseled.read_size, src/adapt.c ~246-359). Fixed here with one
generic read_size_from_meta() (rcp.read_size, decimal, 0-4095
matching the ACF header's own field width) wired into both directions
-- one key suffices since it's the same wire field regardless of
endpoint type, consistent with how rcp.evt_op already covers
GPIO/PWM_OUT/SPI generically. REQ-RELAY-005's .fusa-reqs.json text
updated to mention read_size_or_segment_num.

Purely additive (new optional meta key, old default-0 behavior
preserved when absent) -- no call site needed updating. Checked
redundancy.hpp, faultinject.hpp, cli.hpp (both its direct-flag
send_one() path, unaffected, and its NDJSON send_stream() path, which
already forwards meta generically and gains read_size support for
free) plus their existing tests.

## Flagged, explicitly out of scope

CAN's frame_format/arbitration_id and MDIO's clause/prtad/devad/regad
have no path through Adapt() today -- a pre-existing, already-
documented gap (no CAN wire codec exists yet in this layer; MDIO's
address-packing helpers are namespace detail, not public), not an
adapt.hpp regression. Inventing a convention here without an
established downstream codec would be unfounded.

## Verification

New tests/test_adapt.cpp: 508 lines, 29 TEST_CASEs, 67 assertions --
meta parsing (including read_size's new boundary/garbage cases), full
message_to_request/response_to_message field mapping, opaque-payload
passthrough, RequestFn error propagation, and end-to-end regression
tests against mock::Server proving the fix (a UART read via Adapt()
now correctly drains exactly rcp.read_size bytes, GPIO-write/SPI-
channel evt_op equivalence). adapt.hpp: 228 -> 304 lines.

Full tree rebuilds clean (0 errors, 0 warnings), 58/58 ctest suites
pass (100%) -- independently re-verified from a fresh build against
the merged discovery+regmap+server base, not just the porting agent's
own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…146)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch A of a four-batch mock.hpp
port -- the foundation the remaining three batches (B: response
classifier + regmap-batch-B/discovery wiring; C: E2E RxSequenceGuard +
StreamFaultTracker + RxWatchdog; D: fragmentation/respqueue +
AVTPDU frame-level dispatch) build on.

A fresh Phase 4 scoping pass found mock.hpp had NO admission, queueing,
or scheduling logic at all: server::Endpoint (the just-merged
server.hpp) was never referenced anywhere in mock.hpp; dispatch() was a
flat byte_bus_id switch, and every dispatch_gpio/dispatch_spi/etc.
handler ran synchronously and unconditionally behind only a
lifecycle-state gate. Not a case of replacing ad hoc admission -- this
adds admission from scratch, matching c-RCP's own mock.c design where
every endpoint slot embeds rcp_server_endpoint_t queue.

## Ported (c-RCP src/mock.c -> mock::Server)

- src/mock.c:35-233 (slot embedding rcp_server_endpoint_t queue) -> ten
  new server::Endpoint members, one per operational endpoint type.
- src/mock.c:1277-1300 (admission_reject_response_shape) ->
  mock::admission_reject_response_shape().
- src/mock.c:1361-1423 (finish_admission) + :1549-1551 (admit_with_ack
  call site) -> mock::Server::admit_and_classify(), built on
  acf::make_response/acf::encode_error_payload and a new
  mock::make_acknowledge_rejected_response() (the Ack+err combo
  acf::make_response() alone can't express).
- src/mock.c:1180-1245 (apply_cancellation) ->
  mock::Server::apply_cancellation() (ClearAll/ClearNonSafestate only;
  ClearSingle deferred, see below).
- src/mock.c:2856-2870,3587-3628,3670-3717,3785-3802 (drain_endpoint,
  tick, notify_trigger, notify_gptp_lock_state, pending_count,
  watchdog_purge) -> thin Server passthroughs onto server::Endpoint.
- REQ-SRV-015/016, REQ-PWRMODE-028, REQ-ACF-021, REQ-TIMED-012,
  REQ-CANCEL-012, REQ-SRV-018 citations preserved.

## dispatch_* changes

All ten (dispatch_gpio...dispatch_mdio) now call admit_and_classify()
immediately after the existing operational_requests_allowed()
lifecycle gate; the handler body runs only on
AdmitOutcome::ExecuteNow. Since every mock request is encoded ACF_ABB,
only ExecuteNow/Queued/Suspended/Rejected are reachable through
dispatch() -- Pending/Cancellation are implemented for classifier
completeness and exercised directly via the new admission() accessor.

## New Server API

admission(byte_bus_id), pending_count(), watchdog_purge(), drain_one(),
notify_trigger(), notify_gptp_lock_state(), tick(). tick()/drain_one()
deliberately don't decode+execute the returned frame -- this mock has
no generic byte-level handler (each type has its own decoded-struct
dispatch path) -- documented as the caller's job.

## Behavioral contract change (verified backward-compatible)

dispatch() can now return with no wire response at all (out_resp.rsp
== false) for admission outcomes TC18 sends nothing for -- signaled
via new mock::DispatchErrc{queued,pending,cancelled,suspended}. Since
every server::Endpoint defaults to enabled/not-suspended, all 8
external dependents (sim.hpp, cli.hpp, test_capi/test_relay/
test_redundancy/test_faultinject/bench_mock/command_latency_test) get
ExecuteNow unconditionally and needed zero changes -- confirmed by
full ctest passing before test_mock.cpp was touched.

## Deferred (TODO(phase4-batch-d) in-file)

apply_cancellation()'s ClearSingle case is a documented no-op --
REQ-CANCEL-012's chain-cascade needs a decoded target transaction_num
from a real GBB frame, unreachable until batch D's dispatch_frame().

## Verification

All 49 pre-existing test_mock.cpp cases pass unmodified (no
admission-aware setup was actually needed -- verified empirically, not
assumed). 11 new cases added: queued (no ack)/queued (with ack),
config-write bypassing a disabled queue (REQ-SRV-015),
admission_suspended (REQ-PWRMODE-028), REQ-ACF-021 response-as-request
rejection (both ack shapes), drain_one() round-trip, pending_count()/
watchdog_purge() against a directly-admitted Triggered request,
notify_trigger()/notify_gptp_lock_state() edge detection, two tick()
cases (TSCF gate due / fail-closed while unlocked).

mock.hpp: 917 -> 1387 lines. test_mock.cpp: 1006 -> 1285 lines, 49 ->
60 TEST_CASEs (503 assertions). Full tree rebuilds clean (0 errors, 0
warnings), 58/58 ctest suites pass (100%) -- independently re-verified
from a fresh build against the merged regmap+server+discovery+adapt
base, not just the porting agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…egmap/discovery wiring (#147)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch B of a four-batch mock.hpp
port, built on batch A's server::Endpoint admission wiring.

## Ported (c-RCP src/mock.c -> mock::Server)

- Table 24 response suppression (src/mock.c:1716-1760, REQ-RMAP-048/049):
  new private static Server::suppress_response_per_stream_cfg(),
  resolving stream_id via regmap::request_stream_cfg::resolve_index()
  and zeroing out_resp/out_resp_payload when the resolved stream's
  rx_ack_stream_index (Acknowledge-classified responses) or
  rx_resp_stream_index (everything else) is 0.
- Storage: Server::set_request_stream_cfg()/request_stream_cfg() and
  set_ep_id_map()/ep_id_map(), mirroring c-RCP's
  rcp_mock_server_set_request_stream_cfg()/_set_ep_id_map()
  (src/mock.c:444-460/565-580), bounds-checked against
  regmap::request_stream_cfg::kMaxEntries/regmap::ep_id_map::kMaxEntries,
  syncing svr_request_stream_cfg_capacity/svr_ep_bytebus_id_map_capacity
  (REQ-RMAP-034/037).
- Discovery wiring (mock.h:501-529, src/mock.c:663-687, REQ-RMAP-066):
  discovery::DiscoveryClaim member, discovery_claim() getter,
  set_discovery_timeout_us() keeping it and
  regs_.svr_ep_cfg.svr_discovery_timeout in sync. Confirmed by direct
  source read that c-RCP's own mock.c never wires the claim into
  dispatch, so nothing beyond storage was added.

## dispatch_* changes

Each of the ten operational dispatch_*() functions (gpio/spi/i2c/adc/
pwm_in/lin/can/uart/iseled/mdio) renamed to dispatch_*_inner()
(unchanged batch-A body), with a new thin dispatch_*() wrapper calling
_inner then suppress_response_per_stream_cfg() -- mirroring c-RCP's
dispatch_plain()/dispatch_plain_inner() split, generalized per
endpoint type. dispatch() gained a trailing, defaulted
avtp::StreamId stream_id parameter threaded to all ten; the default
resolves to "no stream configured," so no pre-existing call site's
behavior changes. dispatch_ep0() left untouched (not one of the ten
operational endpoints).

## Bug found and fixed

make_initial_register_map()'s ten default ep_id_mapping rows all left
request_stream_index at its struct default (0) -- TC18's own
end-of-table sentinel -- which would make
regmap::ep_id_map::effective_count() read the whole populated table as
zero rows. Fixed to 1 (matching row_init_default()'s own power-on
convention); also fixed svr_ep_bytebus_id_map_capacity to sync to 10.
Neither was exercised by existing tests, so this is inert today but
fixes future batch-C consumers (broadcast_safe_state, sequencer access
control).

## Small addition to already-merged discovery.hpp

Added DiscoveryClaim::set_timeout() -- batch B's
set_discovery_timeout_us() needed a way to re-derive the claim's
window from a new register value without resetting holder_/
claimed_at_ (c-RCP's own documented contract), and no such mutator
existed.

## Verification

Existing 60 test cases required no changes (default request_streams
table is empty, so suppression is a no-op unless a test opts in). 11
new TEST_CASEs added: ep_id_mapping bug-fix assertions, bounds-check
tests, 5 Table-24 suppression tests ported from c-RCP's
tests/test_mock.c:1061-1204, one sweep across all ten endpoint types,
3 discovery-claim tests ported from tests/test_mock.c:1917-1980
(adapted to DiscoveryClaim's steady_clock-based API).

include/rcp/mock.hpp: 1387 -> 1759 lines. tests/test_mock.cpp: 1285 ->
1561 lines, 60 -> 71 TEST_CASEs (583 assertions). include/rcp/
discovery.hpp: 818 -> 837 lines. Full tree rebuilds clean (0 errors, 0
warnings), 58/58 ctest suites pass (100%) -- independently re-verified
from a fresh build, not just the porting agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…Tracker, RxWatchdog (#148)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 4 batch C of a four-batch mock.hpp
port, built on batch A (admission) and batch B (response suppression +
regmap/discovery wiring).

This closes the exact gap flagged during Phase 2 (PR #134, e2e.hpp's
own header comment): RxSequenceGuard, StreamFaultTracker, and
RxWatchdog were all fully implemented in e2e.hpp but never called from
any dispatch path anywhere in cpp-RCP -- e2e.hpp's own comment
explicitly deferred this wiring to "Phase 4 (server/dispatch) scope,
matching c-RCP's own mock.c wiring (frame_seq_gate_admits(),
dispatch_frame_e2e())." That deferred work lands here.

## Ported (c-RCP src/mock.c -> Server)

- rcp_mock_server_dispatch_e2e() (src/mock.c:1892-2038, REQ-E2E-021/
  031/041, REQ-WDG-010) -> new Server::dispatch_e2e().
- frame_seq_gate_admits() (src/mock.c:3038-3063, REQ-E2E-028/029) ->
  private Server::seq_gate_admits(), folded into single-member
  dispatch per this batch's scope (multi-member AVTPDU frame splitting
  is batch D's job; this batch's guard logic is built at the
  single-member level so batch D can reuse the same guard instance).
- rcp_mock_server_check_watchdog() (src/mock.c:991-1021, REQ-E2E-046)
  -> public Server::check_watchdog_overflow().
- rcp_mock_server_stream_status_rx_blocked() (src/mock.c:981-989) ->
  Server::stream_rx_blocked().
- Watchdog-kick call sites (REQ-WDG-010) -> unconditional
  rx_watchdog_kick() at the top of dispatch_e2e().

## Shape of the new E2E dispatch surface

One public Server::dispatch_e2e() entry point, not ten
dispatch_<type>_e2e() siblings. ep_req_crc_enable (TC18 Sec12.7.1) is a
per-endpoint, not per-endpoint-type, toggle, and c-RCP's own
rcp_mock_server_dispatch_e2e() is likewise a single function
parameterized by byte_bus_id, not duplicated per handler -- mirrors
dispatch()'s own established single-entry-point convention. All 10
operational endpoint types get E2E support uniformly via
regs_.generic_configs[].ep_req_crc_enable (the real register). EP0
gets none -- it has no generic_configs[] row, matching c-RCP's own
EP0 exclusion from admission entirely.

## New Server state

e2e::StreamFaultTracker stream_fault_tracker_;
std::array<e2e::RxSequenceGuard/RxWatchdog/StreamStatus, kMaxEntries=64>
keyed by resolved request_streams[] index (mirrors c-RCP's
srv->seq_tracker[]/srv->stream_status[]) -- all owned directly, matching
mock.hpp's existing convention over c-RCP's pointer/setter style.

## Deliberate scope boundaries (documented in-file, not TODO-stubbed --
none block correctness)

- REQ-E2E-045/029's full stream-wide "broadcast safe state to every
  bound endpoint" actuation is not implemented (no SequencerTable-
  driving actuator exists in this file yet) -- only the observable
  StreamStatus latch is. Pre-existing gap class shared with batch A/B's
  own admission-reject overflow-safestate consequence, not new here.
- check_watchdog_overflow()'s purge targets server::Endpoint::
  watchdog_purge() (the pending/conditional store) rather than
  rcp::watchdog::Manager (RequestLedger-keyed, architecturally
  mismatched to this mock's admission storage).
- No TSCF variant of dispatch_e2e (c-RCP's dispatch_e2e_tscf, REQ-
  TIMED-012/013) -- out of scope since plain dispatch() has no TSCF
  timing gate either yet.

No content bugs found versus c-RCP. One test-fixture-only issue found
while writing coverage: GPIO's apply_gpio_write() masks writes by pin
direction (default all-input, TC18 Sec13.7.4.3) -- tests needed a
configure_gpio_all_outputs() fixture helper before asserting OR-write
effects; not a library bug.

## Verification

New Server::dispatch_e2e() + 6 new TEST_CASEs covering replay
detection, stream-fault short-circuit, and watchdog kick/overflow,
integration-tested via mock.hpp's dispatch (not re-testing e2e.hpp's
own already-unit-tested internals).

include/rcp/mock.hpp: 1759 -> 2162 lines. tests/test_mock.cpp: 1561 ->
1832 lines, 71 -> 77 TEST_CASEs (632 assertions). include/rcp/e2e.hpp:
header comment updated to record the closed deferral (primitives
unchanged). Full tree rebuilds clean (0 errors, 0 warnings, verified
with -Wall -Wextra), 58/58 ctest suites pass (100%) -- independently
re-verified from a fresh build, not just the porting agent's own
report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…for E2E fragmented dispatch (#149)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. First of two sub-batches (D1/D2) making up
Phase 4's fourth and final batch. Built on batches A (admission), B
(response suppression + regmap/discovery), and C (single-member E2E
dispatch with RxSequenceGuard/StreamFaultTracker/RxWatchdog).

fragment.hpp and respqueue.hpp were both fully ported in Phase 1
(fixed-capacity, zero heap allocation, with the c-RCP issue #614/#616
oversized-reassembly error-handling lesson baked in from day one) but
neither was ever wired into any dispatch path. This closes that gap.

## Ported (c-RCP src/mock.c:2175-2504, rcp_mock_server_dispatch_e2e_fragment)

REQ-E2E-038/039/046, REQ-ISELED-025, REQ-FRAG-*. Confirmed by direct
source read that c-RCP's own fragment-dispatch function has no
sequence-gate call and no TSCF variant reachable from this rewrite's
scope -- mirroring batch C's own established "NTSCF-only, no per-frame
seq gate" simplification for dispatch_e2e(), neither
dispatch_e2e_fragment_tscf nor a sequence-number gate were ported here;
both are legitimately deferred to D2's future frame-level
dispatch_frame_e2e().

## New public surface on mock::Server

- dispatch_e2e_fragment(client, stream_id, fragment, out_resp,
  out_resp_payload) -- feeds fragments into a per-request-stream
  fragment::Reassembler (keyed by
  regmap::request_stream_cfg::resolve_index(), same indexing as
  seq_trackers_/rx_watchdogs_/stream_status_). On completion validates
  e2e::compute_fragmented_crc() (over the first fragment's header +
  reassembled payload, deliberately ignoring unwrap()'s own wrong
  single-frame verdict, matching c-RCP's own comment), then delegates
  into the existing single-member decode_and_dispatch()/dispatch()
  path -- no duplicated CRC-unwrap/admission logic.
- maybe_fragment_response(...) -- new (c-RCP's own equivalent,
  rcp_mock_server_dispatch_multi_response(), is a per-endpoint
  registered-handler pattern this rewrite's hardcoded
  ten-dispatch_*()-method architecture has no analog for). Slices an
  oversized genuine response via fragment::plan() and pushes each
  fragment onto a respqueue::RespQueue keyed by rx_resp_stream_index
  (1-based, resolved against regs_.response_streams -- multiple
  request streams can share one response queue).
- Accessors: fragment_reassembler(stream_id), resp_queue_for_stream(
  stream_id) -- nullable pointers mirroring c-RCP's own
  rcp_mock_server_fragment_reassembler() precedent.
- Three new DispatchErrc values: fragment_pending, fragment_rejected
  (both no-wire-response, mirroring c-RCP's identical dispositions),
  response_fragmented (real multi-frame response now on the RespQueue).

## Oversized-reassembly error path (c-RCP issue #614/#616) -- verified
real, not silent

After CRC validation, the reassembled total is checked against the ACF
wire ceiling before re-encoding; on overflow, set_error_response()
produces a genuine RequestRejected ErrorResponse. Verified by test with
a payload under the Reassembler's own capacity but over the ACF wire
ceiling, asserting response_kind_of(resp) == ErrorResponse -- distinct
from a separate test proving the Reassembler's own internal
kErrTooLarge bound produces fragment_rejected with no wire response,
matching c-RCP's own RCP_MOCK_DISPATCH_REJECTED precedent. Symmetric
coverage on the response side: an unconfigured response-stream also
produces a genuine RequestRejected ErrorResponse rather than dropping
silently.

## Bug found and fixed

acf::decode_acf_abb()/decode_acf_gbb() are deliberately lenient (a
documented Phase-1 TODO) and don't trim wire pad octets off the
decoded payload -- left as-is this would have corrupted both the
reassembled payload and the fragmented-CRC comparison whenever pad !=
0. Added a local trim_wire_pad() helper applied to both intermediate-
and final-fragment decodes, scoped to this batch rather than touching
the shared decoder (out of D1's fragmentation-only scope).

## Build-clean fix

std::array<fragment::Reassembler, N>/std::array<respqueue::RespQueue, N>
member declarations with a trailing {} triggered a "converting from
initializer list would use explicit constructor" warning (both types
have explicit sole constructors) -- fixed by dropping the {}.

## Deferred (doc-only TODO update)

apply_cancellation()'s ClearSingle TODO relabeled from
TODO(phase4-batch-d) to TODO(phase4-batch-d2) now that D is split;
D1's own dispatch_e2e_fragment() still routes everything through
admit_and_classify(), so AdmitOutcome::Cancellation remains as
unreachable as it was after batch A -- no behavior touched.

## Verification

13 new TEST_CASEs: plain-command-mode passthrough, unresolvable-stream
fallback, never-fragmented fallback, genuine 2-fragment reassembly,
out-of-order rejection + recovery, fragmented-CRC mismatch + stream-
fault latch, both oversized paths, three response-fragmentation
scenarios (successful multi-entry split with byte-exact reassembly,
unconfigured-queue rejection, max_avtpdu_size-driven tighter
splitting), accessor nullptr behavior.

include/rcp/mock.hpp: 2162 -> 2723 lines. tests/test_mock.cpp: 1832 ->
2349 lines, 77 -> 89 TEST_CASEs (764 assertions). Full tree rebuilds
clean (0 errors, 0 warnings), 58/58 ctest suites pass (100%) --
independently re-verified from a fresh build, not just the porting
agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ses out Phase 4 (#150)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Second of two sub-batches (D1/D2) making
up Phase 4's fourth and FINAL batch. This completes mock.hpp -- the
last module of Phase 4 -- and closes out the server/dispatch layer in
full. Built on batches A (admission), B (response suppression +
regmap/discovery), C (single-member E2E dispatch), and D1 (E2E
fragmented dispatch).

## Ported (c-RCP src/mock.c/src/scheduler.c/src/request.c, REQ-MOCK-019/
020/029, REQ-E2E-033, REQ-CANCEL-012)

- split_frame_members() -- ported from rcp_sched_split_frame_members()
  (scheduler.c:63-106), a new namespace-scope helper (request.hpp's own
  header explicitly notes this splitting logic lives nowhere else in
  the rewrite -- c-RCP keeps it mock.c-local too).
- FrameMemberResult + Server::dispatch_frame()/dispatch_frame_e2e() --
  ported from rcp_mock_server_dispatch_frame()/_dispatch_frame_e2e()
  (mock.c:3068-3382). Each returns per-member results (mirrors c-RCP's
  own result-array shape -- c-RCP does not aggregate members' responses
  into one outgoing wire frame either, confirmed by source read).
- Chain-group/position bookkeeping (REQ-CANCEL-012, ported from
  rcp_chained_advance/is_chained_member/last_pending_index,
  mock.c:2950-3231, request.c:673-694) -- via a new shared
  dispatch_frame_impl() template, using request::should_execute_chained()
  for the cs-polarity predicate and a new last_pending_index() helper.
- apply_cancellation()'s ClearSingle TODO (open since batch A) filled
  in: decodes request::decode_clear_single() from the raw frame,
  captures the target's chain_group/chain_position before removing it,
  cascades via cancel_chain_from().

## Bug found and fixed (beyond the literal ask)

admit_and_classify() always re-encoded ABB from req/payload, silently
stripping any genuine GBB conditional/cancellation opcode's semantics
-- meaning ClearSingle/Chained frames were never actually reachable
before this batch, regardless of how they'd been decoded. Refactored
to take the raw frame directly (mechanical edit across its 10 call
sites, zero behavior change for them) and added
decode_and_dispatch()'s own conditional-opcode-aware routing branch, so
dispatch_frame()/dispatch_frame_e2e() -- and every existing
dispatch_e2e()/dispatch_e2e_fragment() caller -- can now genuinely
admit Chained/Compound/Triggered/Timed/Cancellation GBB members. Also
extracted dispatch_e2e_core()/dispatch_e2e_fragment_core() with an
apply_seq_gate flag so the frame-level sequence gate runs exactly once
per frame (matching the pre-existing doc comment's own stated intent),
not once per member.

## Delta from c-RCP (documented in-file)

dispatch_frame_e2e() routes every member through
dispatch_e2e_fragment_core() rather than plain dispatch_e2e_core() --
c-RCP has no frame-level fragment-aware dispatch at all; this is a
safe superset since the fragment-core function falls back identically
for non-fragmented members.

## Test-writing findings (documented, not library bugs)

split_frame_members() requires each member's declared
acf_msg_length*4 to exactly match its real encoded byte count, but
acf::encode_acf_abb()/e2e::wrap_framed() never auto-pad payloads to
quadlet boundaries (documented as caller-owned in acf.hpp) -- never
mattered for single-member dispatch before; all new multi-member test
frames use quadlet-aligned payloads with an explanatory comment.
Confirmed DispatchErrc::unknown_bus is unreachable in this codebase
(acf.hpp's decode is deliberately lenient vs c-RCP's strict decode) --
documented as defensive-only with a test pinning the actual lenient
behavior.

## Final Phase-4 sweep

Grepped the whole file for TODO(phase4-batch-*) markers: only
apply_cancellation()'s ClearSingle stub existed, now resolved. No
dangling phase-4 markers remain anywhere in mock.hpp.

## Verification

New split_frame_members()/dispatch_frame()/dispatch_frame_e2e() +
ClearSingle cascade, tested end-to-end via multi-member AVTPDU frames.
include/rcp/mock.hpp: 2723 -> 3413 lines. tests/test_mock.cpp: 2349 ->
2775 lines, test_mock: 101 TEST_CASEs (863 assertions). Full tree
rebuilds clean (0 errors, 0 warnings), 58/58 ctest suites pass (100%)
-- independently re-verified from a fresh build, not just the porting
agent's own report.

## Phase 4 complete

mock.hpp grew from its pre-Phase-4 baseline of 917 lines to 3413 lines
across four batches: A added server::Endpoint admission wiring, B
added Table 24 response suppression plus regmap/discovery storage, C
wired RxSequenceGuard/StreamFaultTracker/RxWatchdog into single-member
E2E dispatch (closing a gap flagged since PR #134), D1 wired
fragment.hpp/respqueue.hpp for E2E fragmented dispatch (closing a gap
open since Phase 1), and this D2 batch added AVTPDU frame-level
multi-member splitting/routing/chain-cascade cancellation. Combined
with regmap (batches A/B), the new server.hpp, discovery, and adapt --
all merged earlier in Phase 4 -- the server/dispatch layer is now
complete with no phase-4-scoped work left deferred.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…l dispatch (#151)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 5 (transport layer), Wave 1.

## The gap

udp::Server::Handler was shaped to match mock::Server::dispatch()'s
single-already-isolated-message contract, so Server::serve() decoded
each datagram into individual ACF messages and called Handler once per
message. A caller wiring that Handler straight to mock::Server::
dispatch() -- the obvious, natural thing to do -- never reached
mock.hpp's Phase 4 batch D2 frame-level entry points (dispatch_frame()/
dispatch_frame_e2e()), silently losing Table 24 response suppression,
conditional/cancellation-opcode routing, and E2E CRC verification/
fragment reassembly for every request that goes over UDP.

Note: c-RCP's current src/udp.c/include/rcp/udp.h have been
re-architected into a pure raw-AVTPDU transport (send/recv only) with
no dispatch wiring at all -- that concern is deliberately deferred to
a future layer per udp.h's own comment. This gap is purely internal to
cpp-RCP's own design (where udp::Server bakes in decode+dispatch+
encode), not a delta versus a current c-RCP dispatch implementation.

## Fix

Widened Server::Handler to a frame-level contract:
  using Handler = std::function<size_t(size_t client, avtp::StreamId
      stream_id, uint8_t sequence_num, const std::vector<uint8_t>&
      acf_frame, std::vector<FrameResponse>& out_responses)>;
acf_frame is the raw ACF-region bytes -- exactly the shape mock::
Server::dispatch_frame()/dispatch_frame_e2e() accept. New FrameResponse
type mirrors the response half of mock::FrameMemberResult without
udp.hpp needing to #include mock.hpp. serve() now hands raw bytes
straight through and treats FrameResponse::info.rsp == false as "no
wire message for this member" (Table 24 suppression/queued/pending/
etc.), skipping it from the reply MultiFrame; if every member was
suppressed, no reply datagram is sent at all (previously a reply was
always sent).

Chose widening Handler's own signature over keeping the old
single-message shape as a parallel "compatibility path" -- two wiring
surfaces would keep inviting the exact mistake this gap describes; one
correct surface is safer. Both the real POSIX Server and the Windows
stub Server were updated identically.

## Call sites

tests/test_udp.cpp: every existing handler lambda ported to the new
signature. include/rcp/tls.hpp needed no change (SecureServer::Handler
is just `using Handler = udp::Server::Handler;`, follows automatically
-- confirmed via rcp_tls ctest pass).

## Other findings

No other behavioral deltas found versus c-RCP's current udp.c/udp.h --
its Annex J wrap/unwrap and control port already match cpp-RCP's
existing encode_annexj_datagram/decode_annexj_datagram/
kAnnexJControlPort, left untouched. Documented (via a new test) that
regmap::RequestStreamConfig::rx_resp_stream_index defaults to 1, not 0
(intentional "answer discovery before configuration" power-on
default) -- the suppression test sets it explicitly to 0 to actually
exercise suppression.

## Verification

4 new end-to-end tests wiring a real mock::Server through UDP:
multi-member dispatch_frame, Table 24 suppression (client now
genuinely times out instead of getting a default response), a
Triggered conditional-opcode request (routed to Pending admission, not
mistaken for Standard), and an E2E-CRC-protected request via
dispatch_frame_e2e.

include/rcp/udp.hpp: 747 -> 846 lines. tests/test_udp.cpp: 599 -> 923
lines, test_udp: 24 TEST_CASEs (151 assertions). Full tree rebuilds
clean (0 errors, 0 warnings), 58/58 ctest suites pass (100%) --
independently re-verified from a fresh build, not just the porting
agent's own report.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…el dispatch (#152)

* rewrite(phase5): l2.hpp — add FrameHandler wired to Phase 4 frame-level dispatch

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 5 (transport layer), Wave 1 (parallel
with udp.hpp's equivalent fix in #151).

## The gap

Server::serve() decoded every inbound frame straight into a MultiFrame
(parsed acf::AcfEntry list) and called Handler once per already-decoded
member -- the same shape as mock::Server::dispatch(), which never
receives a stream_id at all. A caller wiring Handler to
mock::Server::dispatch() can never reach Table 24 suppression (only
reachable via the ten dispatch_<type>() wrappers' stream_id parameter),
never reaches peek_conditional_request_type()/decode_and_dispatch()
so conditional/cancellation-opcode requests get misread as ordinary
Standard requests and executed immediately instead of queued, and
never reaches dispatch_frame()/dispatch_frame_e2e() so E2E/fragment
dispatch is unreachable too.

## Fix

Added a namespace-scope FrameMemberResult (deliberately a parallel
type to mock::Server::FrameMemberResult, not a shared one -- matching
this file's own documented "parallel concrete types, no shared
interface" convention) and a FrameHandler type receiving one whole
frame's raw, unparsed ACF bytes plus its stream_id/sequence_num -- the
exact shape mock::Server::dispatch_frame()/dispatch_frame_e2e()
expect. Two new pure, socket-free helpers, decode_avtp_frame_header()/
decode_l2_frame_header(), split the AVTP envelope off without parsing
the ACF payload, so serve() can hand raw bytes to a FrameHandler
instead of re-encoding an already-parsed MultiFrame. set_frame_handler()
takes priority over the pre-existing Handler when both are set; a
member whose response.rsp == false (Table 24 suppression, a Queued/
Pending outcome, or a frame-level sequence-gate rejection) is dropped
from the outgoing frame, and if every member was dropped, nothing is
sent at all.

The old Handler path is untouched byte-for-byte for backward
compatibility with tests/l2_veth_roundtrip.cpp (a real-frames-over-
real-veth-interfaces CI gate) -- this was a deliberate additive choice
rather than replacing Handler's signature (the sibling udp.hpp fix in
#151 widened Handler directly instead; both are valid shapes for the
same underlying gap, no reconciliation needed since they're
independent files).

## Bug found and fixed (c-RCP delta)

rcp_l2_mac_is_unicast() (c-RCP src/l2.c:79-82, include/rcp/l2.h:121-137,
REQ-L2-011) had no cpp-RCP equivalent anywhere -- added
is_unicast_mac(). Confirmed via repo-wide search that
regmap::writer_ctx()'s via_unicast parameter has no production caller
yet (lifecycle.hpp's REQ-LIFECYCLE-027 unicast gate is data-model-only
today) -- a real content gap worth closing now, but wiring it into a
live dispatch path is a separate, broader gap, documented as
out-of-scope for this batch.

## Verification

Verified in a Docker/Ubuntu 24.04 container with CAP_NET_RAW/
CAP_NET_ADMIN and a real veth pair, replicating ci.yml's exact
l2-veth job flags: clean build (0 errors, 0 warnings), full ctest
58/58, and l2_veth_roundtrip actually EXECUTED (not just compiled)
against real AF_PACKET sockets, both before and after the change. A
scratch program exercising the new FrameHandler path over the same
real veth pair (multi-member dispatch plus a suppressed member
producing no wire response) also passed. macOS build clean, 0/0
warnings, 58/58 ctest (l2_veth_roundtrip is Linux-only, gated
separately -- confirmed absent from the macOS ctest registration as
expected, re-confirmed by the "l2 veth round trip" CI job itself on
this PR).

include/rcp/l2.hpp: 808 -> 1069 lines. tests/test_l2.cpp: 244 -> 596
lines, 11 -> 21 TEST_CASEs (7 new for FrameHandler/Table-24/
conditional-opcode/multi-member/E2E wiring, 3 new for is_unicast_mac).
.fusa-reqs.json: +2 entries (REQ-L2-009, REQ-L2-010).
tests/l2_veth_roundtrip.cpp: unchanged by design (still uses Handler,
exercising raw-socket round-trip fidelity, not dispatch semantics). No
production call site wires l2::Server anywhere else in the tree
(repo-wide grep confirmed), so no other call sites needed updating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* fix(l2): cast AvtpFrameHeader::sequence_num (uint16_t) to uint8_t for FrameHandler calls in tests

Fixes an MSVC /W4 /WX narrowing-conversion error (C2220/C4244) at two
new test call sites (test_l2.cpp:525,572) that call FrameHandler's
uint8_t sequence_num parameter directly with hdr.sequence_num
(uint16_t) -- not caught by clang/gcc's warning sets on the other CI
platforms. Test-only, no production code affected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…te-level buffer (#153)

* rewrite(phase5): shmem.hpp — rebuild Channel around a real bounded byte-level buffer

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 5 (transport layer), Wave 2.

A dedicated investigation (this session) found cpp-RCP's shmem.hpp was
a decoded-object pass-through with no byte encoding at all and no
capacity bound -- Channel::request() called a std::function handler
directly and synchronously in the same process. c-RCP's
rcp_shmem_avtp_pair_new() is a real byte-level bounded ring-buffer
transport, real memcpy framing, RCP_ERR_BUSY backpressure when full --
but is ALSO in-process only under the hood (plain heap, non-pshared
mutex/condvar). Neither codebase has ever had genuine cross-process
shared memory; "shmem" means "a fast paired in-process queue" in both
trees. Decision: rebuild Channel around a real byte-level bounded
buffer (closing the two real gaps -- no capacity bound, codec never
exercised) while keeping the existing decoded-object Handler signature
at the boundary (matching udp::Server's own internal-codec/external-
decoded-callback shape), so admin.hpp/config.hpp need zero changes.

## Ported (c-RCP src/shmem.c 266 lines / include/rcp/shmem.h 99 lines)

- shmem.c:68-71 (RCP_ERR_BUSY on full queue) -> Channel's new
  detail::FrameSlots<kMaxQueueCapacity> bounded pool + ErrBusy return,
  REQ-SHMEM-006.
- shmem.c:64-67/109-113 (closed-side send/recv RCP_ERR_CLOSED) ->
  Channel::request()'s existing closed_ check, now cited REQ-SHMEM-005/010.
- The codec-never-exercised gap: Channel::request() now genuinely
  calls acf::encode_acf_abb/encode_acf_gbb and decode_acf_abb/
  decode_acf_gbb (selection mirrors udp.hpp's encode_frame/decode_frame)
  on both the request and response legs before/after the Handler runs.
- shmem.h:91-93's runtime queue_capacity parameter (clamped to min 1,
  shmem.c:204) -> Channel(stream_key, queue_capacity =
  kDefaultQueueCapacity), clamped into [1, kMaxQueueCapacity].

## New shape

Channel keeps its old Handler signature and synchronous, no-thread-
of-its-own dispatch model exactly. Internally, request() now: encodes
-> acquires a fixed-capacity slot (ErrBusy if none free) -> decodes
back out of that slot -> calls Handler with decoded objects (handler
copied out from under a separate mutex and invoked UNLOCKED, so
concurrent callers' handlers genuinely run in parallel) -> encodes/
decodes the response -> releases the slot via RAII. Registry's public
surface is byte-for-byte unchanged -- confirmed by reading all of
admin.hpp/config.hpp/test_admin.cpp/test_config.cpp in full: they only
use Registry::add_channel/deregister/lookup/channels/close and
Channel::stream_key()/new_channel(), never Channel::request()'s
internals. No changes needed to any of the four.

## Bug found and fixed mid-task (real concurrency bug)

The original code held the handler mutex ACROSS the handler call
itself, which would have serialized every concurrent request and
deadlocked under the new capacity tests -- fixed by copying the
handler out under the lock, then invoking it unlocked.

## Architecture note (documented in the header)

Unlike c-RCP's genuinely two-party, strict-FIFO ring (two independent
threads holding *out_a/*out_b), Channel::request() is both producer
and consumer in one synchronous call, so the bounded buffer is a
per-caller indexed slot pool, not a literal head/tail FIFO -- same
capacity bound and BUSY-without-enqueuing contract, no independent-
reader ordering to preserve since there's only ever one reader.

## REQ-SHMEM mapping / c-RCP citation drift found (not propagated)

.fusa-reqs.json was split 2026-08-18 (issue #533) into REQ-SHMEM-001..
010 (not 001..009). Found three req-id mis-citations in c-RCP's own
src/shmem.c inline comments (shmem_side_send citing 007 instead of
006; shmem_side_recv wrongly also citing 006; shmem_side_destroy
citing 007 instead of 008) -- c-RCP's own test_shmem.c citations are
correct, only the .c file's comments drifted; not propagated here.
REQ-SHMEM-004 (recv timeout) and REQ-SHMEM-007 (recv buffer-too-small)
have no analog in the new cpp shape, documented as such rather than
force-fit.

## Verification

include/rcp/shmem.hpp: 173 -> 458 lines. tests/test_shmem.cpp: 170 ->
526 lines, test_shmem: 20 TEST_CASEs (68 assertions), re-run 5x
back-to-back with no flakiness in the new thread/condition-variable-
based backpressure tests. Full tree rebuilds clean (0 errors, 0
warnings), 58/58 ctest suites pass (100%) -- independently re-verified
from a fresh build, not just the porting agent's own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* fix(shmem): correct REQ-SHMEM-004/007 test coverage — numbering mix-up, not a real gap

The prior commit's header comment claimed REQ-SHMEM-004/007 had 'no
analog' in the new architecture, based on c-RCP's own shmem.h numbering
(where those ids mean recv() timeout / recv() destination-buffer-too-
small). That was a numbering mix-up, not an actual gap: THIS project's
.fusa-reqs.json assigns REQ-SHMEM-004 to 'Registry::lookup finds a
channel registered via add_channel' and REQ-SHMEM-007 to 'the
caller-supplied client id is forwarded to the handler unchanged' --
both ordinary, already-implemented, directly testable behaviors.

- REQ-SHMEM-004: the existing 'Registry::lookup finds a channel
  registered via add_channel' test already covered this exact
  behavior -- it was just missing the requirement tag ([registry]
  only). Added the tag.
- REQ-SHMEM-007: no test existed for client-id forwarding at all.
  Added one, asserting the handler sees exactly the client id passed
  to request() across two different values (not cached/defaulted from
  a prior call).

Fixes the cpfusa trace (100%% coverage) gate failure on PR #153
(tested=410/total=412 -- these were the two untested entries).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…, port deadlock fix (#154)

* rewrite(phase5): shmem.hpp — rebuild Channel around a real bounded byte-level buffer

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 5 (transport layer), Wave 2.

A dedicated investigation (this session) found cpp-RCP's shmem.hpp was
a decoded-object pass-through with no byte encoding at all and no
capacity bound -- Channel::request() called a std::function handler
directly and synchronously in the same process. c-RCP's
rcp_shmem_avtp_pair_new() is a real byte-level bounded ring-buffer
transport, real memcpy framing, RCP_ERR_BUSY backpressure when full --
but is ALSO in-process only under the hood (plain heap, non-pshared
mutex/condvar). Neither codebase has ever had genuine cross-process
shared memory; "shmem" means "a fast paired in-process queue" in both
trees. Decision: rebuild Channel around a real byte-level bounded
buffer (closing the two real gaps -- no capacity bound, codec never
exercised) while keeping the existing decoded-object Handler signature
at the boundary (matching udp::Server's own internal-codec/external-
decoded-callback shape), so admin.hpp/config.hpp need zero changes.

## Ported (c-RCP src/shmem.c 266 lines / include/rcp/shmem.h 99 lines)

- shmem.c:68-71 (RCP_ERR_BUSY on full queue) -> Channel's new
  detail::FrameSlots<kMaxQueueCapacity> bounded pool + ErrBusy return,
  REQ-SHMEM-006.
- shmem.c:64-67/109-113 (closed-side send/recv RCP_ERR_CLOSED) ->
  Channel::request()'s existing closed_ check, now cited REQ-SHMEM-005/010.
- The codec-never-exercised gap: Channel::request() now genuinely
  calls acf::encode_acf_abb/encode_acf_gbb and decode_acf_abb/
  decode_acf_gbb (selection mirrors udp.hpp's encode_frame/decode_frame)
  on both the request and response legs before/after the Handler runs.
- shmem.h:91-93's runtime queue_capacity parameter (clamped to min 1,
  shmem.c:204) -> Channel(stream_key, queue_capacity =
  kDefaultQueueCapacity), clamped into [1, kMaxQueueCapacity].

## New shape

Channel keeps its old Handler signature and synchronous, no-thread-
of-its-own dispatch model exactly. Internally, request() now: encodes
-> acquires a fixed-capacity slot (ErrBusy if none free) -> decodes
back out of that slot -> calls Handler with decoded objects (handler
copied out from under a separate mutex and invoked UNLOCKED, so
concurrent callers' handlers genuinely run in parallel) -> encodes/
decodes the response -> releases the slot via RAII. Registry's public
surface is byte-for-byte unchanged -- confirmed by reading all of
admin.hpp/config.hpp/test_admin.cpp/test_config.cpp in full: they only
use Registry::add_channel/deregister/lookup/channels/close and
Channel::stream_key()/new_channel(), never Channel::request()'s
internals. No changes needed to any of the four.

## Bug found and fixed mid-task (real concurrency bug)

The original code held the handler mutex ACROSS the handler call
itself, which would have serialized every concurrent request and
deadlocked under the new capacity tests -- fixed by copying the
handler out under the lock, then invoking it unlocked.

## Architecture note (documented in the header)

Unlike c-RCP's genuinely two-party, strict-FIFO ring (two independent
threads holding *out_a/*out_b), Channel::request() is both producer
and consumer in one synchronous call, so the bounded buffer is a
per-caller indexed slot pool, not a literal head/tail FIFO -- same
capacity bound and BUSY-without-enqueuing contract, no independent-
reader ordering to preserve since there's only ever one reader.

## REQ-SHMEM mapping / c-RCP citation drift found (not propagated)

.fusa-reqs.json was split 2026-08-18 (issue #533) into REQ-SHMEM-001..
010 (not 001..009). Found three req-id mis-citations in c-RCP's own
src/shmem.c inline comments (shmem_side_send citing 007 instead of
006; shmem_side_recv wrongly also citing 006; shmem_side_destroy
citing 007 instead of 008) -- c-RCP's own test_shmem.c citations are
correct, only the .c file's comments drifted; not propagated here.
REQ-SHMEM-004 (recv timeout) and REQ-SHMEM-007 (recv buffer-too-small)
have no analog in the new cpp shape, documented as such rather than
force-fit.

## Verification

include/rcp/shmem.hpp: 173 -> 458 lines. tests/test_shmem.cpp: 170 ->
526 lines, test_shmem: 20 TEST_CASEs (68 assertions), re-run 5x
back-to-back with no flakiness in the new thread/condition-variable-
based backpressure tests. Full tree rebuilds clean (0 errors, 0
warnings), 58/58 ctest suites pass (100%) -- independently re-verified
from a fresh build, not just the porting agent's own report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* rewrite(phase5): admin.hpp — fixed-capacity subscriber/counter bounds, port deadlock fix

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 5 (transport layer), final module.

## Ported (c-RCP src/admin.c 247 lines / include/rcp/admin.h 177 lines)

- admin.c:109-110 (RCP_ADMIN_MAX_SUBSCRIBERS=16, RCP_ADMIN_MAX_COUNTERS=256)
  -> std::array<..., kMaxSubscribers>/std::array<..., kMaxCounters> bounds
  on Server's subscriber list and counter table, replacing cpp-RCP's
  unbounded std::vector/std::map. Matches this rewrite's fixed-capacity,
  no-dynamic-allocation-on-safety-relevant-paths architecture (already
  applied to server.hpp's admission queue and shmem.hpp's FrameSlots).
- admin.c:133-147 (rcp_admin_server_subscribe, capacity check before
  add) -> Server::subscribe() returns AdminErrc::subscriber_capacity_exceeded
  once kMaxSubscribers is reached, matching c-RCP's fail-closed "cb not
  added" behavior (admin.h:145) rather than growing unbounded.
- admin.c:173-198 (rcp_admin_server_record_counter, capacity check on a
  genuinely new counter name+labels pair) -> counter_capacity_exceeded
  once kMaxCounters distinct counters exist (admin.h:158), existing
  counters still update past the bound.
- Ported c-RCP's two boundary tests (subscribe-at-capacity,
  record_counter-at-capacity) into Catch2 TEST_CASEs.

## Bug found and fixed (real concurrency bug, same class as shmem.hpp/#153)

emit() (ported from rcp_admin_server_emit(), admin.c:149-171) held the
subscriber-list mutex across every subscriber callback invocation --
a subscriber callback that itself calls back into Server (a realistic
pattern for a metrics/event sink) would deadlock. Fixed by copying the
subscriber list out under the lock, then invoking callbacks unlocked --
same fix shape as shmem.hpp's Channel::request() handler-invocation fix
in #153.

## Deliberate non-port, documented in-file

c-RCP's admin.c is caller-driven: rcp_registry_t was retired project-
wide (per c-RCP's own history) and every admin_server_t call site now
passes state in directly rather than looking it up from a live registry.
cpp-RCP's admin.hpp instead holds a reference to shmem::Registry and
looks up endpoint/channel state through it live. NOT migrated to
c-RCP's caller-driven pattern: c-RCP's rationale for going caller-driven
was retiring rcp_registry_t entirely, which doesn't apply here --
shmem::Registry is live, current, and still the single source of truth
for channel state in this rewrite (see shmem.hpp, #153). Reasoning
recorded in admin.hpp's own header comment, not just here.

## Verification

include/rcp/admin.hpp: 128 -> 338 lines. tests/test_admin.cpp: 123 ->
200 lines. Full tree rebuilds clean (0 errors, 0 warnings) from a
from-scratch rebuild against the fully-merged rewrite/v3-from-c-rcp
base (picked up #151/#152/#153 via merge-forward first), 58/58 ctest
suites pass (100%) -- independently re-verified, not just the porting
agent's own report.

## Phase 5 status

udp (#151), l2 (#152), shmem (#153) already merged; this closes out
admin, the fourth and final "port from c-RCP" module in Phase 5's
scope. tls/capi remain as re-verify-only per ROADMAP.md Phase 17 —
confirmed clean against the new core, no content changes needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 1 of 13.

## Background

cpp-RCP's .fusa-reqs.json (412 entries) is missing hundreds of
requirement entries the code was already ported to implement in
Phases 1-5, and cpp-RCP's code already references 1015 distinct REQ-*
ids via //fusa:req/[REQ-XXX] tags against only 412 defined entries --
603 orphaned citations left behind by porting agents as unresolved
"ported from c-RCP REQ-XXX, see file.c:NNN" provenance notes. Phase 6
walks these existing citations prefix-by-prefix and turns each into a
real catalog entry (or a documented gap), rather than re-deriving
requirements from scratch.

## Prefix-scheme decision

Kept REQ-ACF-*/REQ-AVTP-*/REQ-WIREERR-* as their own prefixes reusing
c-RCP's exact id numbers 1:1, rather than folding into cpp-RCP's
existing REQ-WIRE-*/REQ-ACF-*/REQ-ERR-* buckets -- the code already
had orphaned citations using c-RCP's exact numbering (REQ-ACF-044,
REQ-AVTP-014/021/022/023), signaling that was the intended scheme all
along. Existing REQ-WIRE-*/REQ-ACF-*/REQ-ERR-* entries are untouched;
duplicate-coverage mappings recorded below for anyone reconciling the
two schemes later.

## New catalog entries (36)

- REQ-ACF-{001,004,012,013,014,017,019,021,023,024,025-031,032,033,
  038,044,047-053} (28 entries)
- REQ-AVTP-{013,014,015,021,022,023,031} (7 entries)
- REQ-WIREERR-001 (WireErrorCode enum matches TC18 Table 27/30 numbering)

Each has a real //fusa:req tag in acf.hpp/avtp.hpp and //fusa:test +
Catch2 bracket tag coverage in test_acf.cpp/test_avtp.cpp. 3 new tests
added where coverage was genuinely missing: acf_category()/
avtp_category() message-uniqueness (REQ-ACF-001, REQ-AVTP-015),
encoder-forces-correct-type-despite-wrong-caller-header (REQ-ACF-004/
038), WireErrorCode numeric-value pinning (REQ-WIREERR-001). Also
fixed a mislabeled inline comment in avtp.hpp that cited REQ-AVTP-022
for behavior that's actually REQ-AVTP-031's (tscf_reserved_all_zero).

## Genuine gaps found (9, filed to .fusa-reqs-pending.json, not the catalog)

- REQ-ACF-006/040: encode_acf_abb/_gbb honor a caller-supplied nonzero
  acf_msg_length instead of always recomputing -- a documented,
  deliberate divergence needed by e2e.hpp's CRC-trailer accounting,
  flagged here for an explicit accept/reject call rather than silently
  carried forward.
- REQ-ACF-009/041: decoders never validate declared acf_msg_length
  against buffer size -- matches the code's own pre-existing
  TODO(phase1-followup) comment.
- REQ-ACF-011: encode_acf_gbb doesn't zero message_timestamp when
  mtv=false.
- REQ-ACF-018: no read_size_or_segment_num kind-selector; existing doc
  comment contradicts TC18's op-based rule.
- REQ-AVTP-012: no combined stream_id+byte_bus_id address-equality
  construct anywhere.
- REQ-WIREERR-002: no WireErrorCode-to-string function.
- REQ-WIREERR-005: SequencerNotKnown enumerator has zero consumers
  (owner-tracking not yet built).

## Judgment calls, not filed as gaps (documented here, not in the pending file)

- REQ-ACF-037: cpp's op is a 2-state bool, not c-RCP's 3-state enum --
  the OP_NONE fallback case is structurally inapplicable, not missing.
- REQ-AVTP-016/027 (transport retain/release refcounting): cpp-RCP
  uses RAII via l2.hpp/udp.hpp/shmem.hpp instead of a manual-refcount
  transport abstraction -- an architecture divergence, and neither has
  a TC18 citation in c-RCP's own catalog, so they don't fit the
  pending file's TC18-scope contract.

## Existing-duplicate mappings (no action; c-RCP id -> cpp-RCP id already covering it)

ACF: 002/022/034/035/036/037->WIRE-010, 005/039->WIRE-013, 007/008->
WIRE-014, 010->WIRE-004, 015/016/046->WIRE-006, 020->WIRE-004,
042/043/045->WIRE-005.
AVTP: 001/003->WIRE-001, 002/004->WIRE-002, 005/006->WIRE-013,
007/008->WIRE-003, 010/011/025->WIRE-007.

## Out of scope (implemented, but in other files -- noted only, not this batch's concern)

AVTP-009/017-020/024/028/029/030/032-034 (l2.hpp, udp.hpp, shmem.hpp,
lifecycle.hpp, mock.hpp, server.hpp); WIREERR-003/004/006/007 (e2e.hpp,
lifecycle.hpp, request.hpp, pwm.hpp) -- confirmed genuinely implemented
there, will get their own tags when those prefixes' batches run.

## Verification

.fusa-reqs.json: 412 -> 448 entries (+36, 3672 -> 4092 lines).
.fusa-reqs-pending.json: 3 -> 12 entries (+9, 35 -> 123 lines).
include/rcp/acf.hpp: +28 lines. include/rcp/avtp.hpp: +14 lines net.
tests/test_acf.cpp: +84 net. tests/test_avtp.cpp: +22 net.

Independently re-verified (not just the porting agent's own report):
confirmed programmatically that zero of the 412 pre-existing catalog
entries were modified or deleted (semantic id-keyed diff against the
pre-batch base, not just a raw line diff -- the raw diff shows churn
in unrelated CLI/RELAY/Discovery entries that's pure JSON
re-serialization noise, not content change), and that zero orphaned
REQ-ACF-*/REQ-AVTP-*/REQ-WIREERR-* tags remain anywhere in acf.hpp/
avtp.hpp/test_acf.cpp/test_avtp.cpp. Full tree rebuilt from scratch:
0 errors, 0 warnings, 58/58 ctest suites pass (100%).

## CI gate fix found while watching this PR's checks

`cpfusa trace`'s sec-tested gate failed: it was a 17% floor (64/376 at
the time it was written) against `secTestedRequirements/totalRequirements`,
but its own comment already stated the real intent was "a regression
floor against the exact, CYBERSECURITY.md-enumerated subset" losing
test coverage -- not a claim about what fraction of the codebase is
security-relevant. A percentage-of-total is the wrong shape for that
intent once the total catalog is deliberately growing (exactly what
Phase 6 does): this batch's 36 new, correctly non-cybersecurity
REQ-ACF-*/REQ-AVTP-*/REQ-WIREERR-* entries dropped the ratio from
72/412=17.5% to 72/448=16.1%, failing a floor that was never meant to
track total-catalog size. Converted ci.yml's "Enforce security-test
coverage" step to an absolute-count floor (>= 72, the current
CYBERSECURITY.md-enumerated total) instead -- this still catches the
real regression the step exists for (an enumerated id losing its test
tag, or being dropped from severity:cybersecurity) without being
diluted by legitimate, intentional catalog growth elsewhere. Without
this fix, every subsequent Phase 6 batch would hit the same false
failure as the denominator keeps growing toward c-RCP's 1282-entry
scale.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ation (#156)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 2 of 13.

## Scope

fragment.hpp's REQ-FRAG-* prefix (18 c-RCP entries, all already
orphan-cited in cpp-RCP's own code), the 7-entry REQ-RMAP-* slice that
respqueue.hpp's code already cites (059/061-065/085 -- a real subset of
c-RCP's combined register-map+response-queue module, specific to
response-queue behavior; the rest of REQ-RMAP-*/REQ-REGMAP-* is
deliberately deferred to batch 11's regmap.hpp rename), and loan.hpp's
REQ-LOAN-* prefix (9 c-RCP entries).

## New catalog entries (26)

- REQ-FRAG-{001-015,017,018} (17) -- fragment.hpp's plan_count()/
  plan()/Reassembler, all verified against real code. REQ-FRAG-010
  needed one new test (destruction-safety, RAII) that didn't exist yet.
- REQ-LOAN-{008,009} (2) -- BufferPool destructor free-list release,
  new_buffer_pool(). LOAN-001-007 needed no action: already had
  correct catalog entries and code tags from prior work (LOAN-007's
  tag lives in rcp.hpp/test_rcp.cpp, not loan.hpp -- confirmed
  found-and-covered, not a gap).
- REQ-RMAP-{059,061,062,063,064,065,085} (7) -- respqueue.hpp's
  RespQueue class. REQ-RMAP-065 copied honestly as c-RCP has it:
  scope="tc18-gap", status="partial" -- c-RCP's own text states the
  heartbeat-composition primitives are all present and proven
  composable, but driving them against a real clock/transport is left
  to the integrator; cpp-RCP is at the identical boundary.

## Judgment call, not filed as a gap (documented in-file, not the pending file)

REQ-FRAG-016 (rcp_fragment_reasm_result_t's RCP_FRAGMENT_REASM_ERR_ALLOC,
a distinct allocation-failure return value): no C++ analog -- Reassembler
uses a fixed std::array member with no heap growth and therefore no
allocation-failure path to distinguish. c-RCP's own REQ-FRAG-016 also
carries no TC18 citation, matching the same "architecture divergence,
no spec basis to force a gap entry" precedent as batch 1's
REQ-ACF-037/REQ-AVTP-016/027. The orphaned tag lines were removed from
fragment.hpp/test_fragment.cpp and replaced with an explanatory
comment (prose mentions of the id remain for context, not real
//fusa:req/test tags -- confirmed these don't register as references
to cpfusa's trace tool).

## No gaps filed this batch

Every c-RCP requirement in scope was either genuinely implemented,
already covered by a pre-existing entry, or the one documented
judgment call above -- .fusa-reqs-pending.json is unchanged.

## Verification

Independently re-verified (not just the porting agent's own report):
semantic id-keyed diff confirms zero pre-existing catalog entries
modified/deleted, exactly the 26 new ids added. Zero real orphaned
FRAG/LOAN/RMAP tags remain in the six files in scope (the one apparent
match, REQ-FRAG-016, is plain prose in a regular comment, not a
//fusa:req/test tag -- confirmed by direct grep). Confirmed no new
entry carries "severity": "cybersecurity" (would have diluted the
sec-tested absolute-count floor fixed in batch 1/#155) -- sec count
holds exactly at 72/474, matching the floor. Full tree rebuilt from
scratch: 0 errors, 0 warnings, 58/58 ctest suites pass (100%).

.fusa-reqs.json: 448 -> 474 entries (+26, 4092 -> 4378 lines).
include/rcp/fragment.hpp, tests/test_fragment.cpp: net changes for the
REQ-FRAG-016 tag removal/explanatory-comment swap and the new
REQ-FRAG-010 test. respqueue.hpp/loan.hpp and their test files:
unchanged (already correctly tagged and tested).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ivation (#157)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 3 of 13
-- the largest single-concern batch so far.

## Scope

request.hpp's conditional-request cluster: Compound (REQ-CMP-*, 29
c-RCP entries), Triggered (REQ-TRIG-*, 13), Chained (REQ-CHAIN-*, 12),
Timed (REQ-TIMED-*, 13), Cancellation (REQ-CANCEL-*, 15), and the
scheduler (REQ-SCHED-*, 8) -- 90 c-RCP entries total, all already
orphan-cited across request.hpp/test_request.cpp. c-RCP splits this
across three files (request.c/request_sequencer.c/scheduler.c);
cpp-RCP's rewrite deliberately keeps its own prior unification into
one file (documented cpp-RCP design choice, not re-split), while
keeping c-RCP's id-prefix scheme per RELAY docs/RCP-ARCHITECTURE.md's
canonical choice #7.

## New catalog entries (70)

- REQ-CMP-{001-003,010-029} (23)
- REQ-TRIG-{001,004-013} (11)
- REQ-CHAIN-{002,004-007,010-012} (8)
- REQ-TIMED-{002-011} (10)
- REQ-CANCEL-{002-015} (14)
- REQ-SCHED-{002,003,007,008} (4)

Each verified against real request.hpp code/test_request.cpp tests,
not rubber-stamped from the pre-existing orphan-tag manifest. Two new
tests added (REQ-CHAIN-005, REQ-TIMED-004) that had no dedicated test
before.

## Genuine gaps found (7 new, filed to .fusa-reqs-pending.json, total now 19)

- REQ-CMP-008/009, REQ-TRIG-003, REQ-CHAIN-003: encode_compound_
  request()/encode_triggered_request()/encode_chained_member() never
  validate their type/payload-size before encoding -- they forward
  straight to acf::encode_acf_gbb(), which by design never rejects --
  and their plain std::vector<uint8_t> return type has no channel to
  signal rejection at all, unlike encode_timed_request()'s own
  std::optional<...> (confirmed: request.hpp:569/745/970 return bare
  std::vector, request.hpp:1077 returns std::optional -- the exact
  asymmetry this finding describes). No test exercised these paths.
  Removed the false orphan tags claiming coverage, documented the
  finding in-file (request.hpp:242-249).
- REQ-CHAIN-008: RequestLedger::submit() never rejects a Chained
  request with no chained_predecessor; no CHAIN_ERROR-equivalent
  exists (c-RCP's rcp_chained_advance() returns
  RCP_CHAINED_MEMBER_CHAIN_ERROR for exactly this case, TC18
  §11.2.2.6).
- REQ-TIMED-012/013: no dispatch/admission entry point anywhere in
  cpp-RCP threads a real TSCF avtp_timestamp into timed_admit()/
  timed_due() -- the primitives exist, the loop doesn't. c-RCP's own
  entries describe a real dispatch-layer TSCF-threading feature
  (rcp_mock_server_dispatch_tscf() and its E2E-aware siblings);
  cpp-RCP's mock.hpp has no equivalent entry point yet. Matches this
  file's own documented "no scheduler thread" scope -- a real,
  TC18-cited gap, not force-fit as implemented.

## Duplicate-coverage mappings (no new entry, no gap)

CMP-004, TRIG-002, CHAIN-001, TIMED-001, CANCEL-001 -> pre-existing
REQ-SEQ-009 ("RequestErrc is a distinct error category with
non-empty, distinct messages") -- cpp-RCP unified all per-kind
strerror requirements into one shared error category. SCHED-001 ->
pre-existing REQ-SEQ-002 (category_of() already documents this
classification). Note: cpp-RCP's REQ-SEQ-* is its own, pre-existing,
differently-scoped prefix about this same file -- unrelated to
c-RCP's request_sequencer.h REQ-SEQ-*, left untouched, out of scope.

## Judgment calls, already documented in-file (no new action)

CMP-005/006/007 (peek_request_type) and SCHED-004/005/006
(frame-splitting) -- both already explicitly documented in request.hpp's
own header as deliberate non-ports, composable via other already-covered
functions. CHAIN-009 -- covered by RequestLedger's graph-based
cascade_cancel()/propagate_chain_completion(), already documented as
strictly more capable than c-RCP's positional model.

## REQ-SCHED-* prefix decision

Code's own orphan tags already used REQ-SCHED-* verbatim, matching
c-RCP -- kept as-is, no rename needed.

## Stale pre-existing gap entries found, not touched (out of this batch's scope)

.fusa-reqs-pending.json's pre-existing REQ-SEQ-013/014/015 (filed
2026-08-07, describing missing Compound exec_delay/repetitions,
Triggered trigger fields, Timed presentation_time) appear stale --
current request.hpp code genuinely implements all three (CompoundStep,
TriggeredStep, TimedRequest + timed_admit()/timed_due()). Left as-is
since fixing them means touching the REQ-SEQ-* prefix; flagging here
for whoever picks up REQ-SEQ-* in a later batch.

## Verification

Independently re-verified (not just the porting agent's own report):
semantic id-keyed diff confirms zero pre-existing catalog entries
modified/deleted, exactly the 70 new ids added matching the report's
own breakdown per prefix exactly. Confirmed no new entry carries
"severity": "cybersecurity" -- sec-tested count holds exactly at
72/544, matching the absolute-count floor fixed in batch 1. Spot-
verified the encode_*_request asymmetry claim directly against
request.hpp's real function signatures, and REQ-CHAIN-008/REQ-TIMED-
012/013's c-RCP source citations directly against c-RCP's own
.fusa-reqs.json entries -- both check out. Full tree rebuilt from
scratch: 0 errors, 0 warnings, 58/58 ctest suites pass (100%),
test_request alone: 137 TEST_CASEs, 615 assertions.

.fusa-reqs.json: 474 -> 544 entries (+70, 4378 -> 5203 lines).
.fusa-reqs-pending.json: 12 -> 19 entries (+7, 123 -> 192 lines).
include/rcp/request.hpp: 1508 -> 1537 lines. tests/test_request.cpp:
1685 -> 1717 lines.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
SoundMatt and others added 14 commits August 22, 2026 04:43
…atalog change (#158)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 5 of 13.

## Scope

watchdog.hpp's REQ-WDG-* prefix. c-RCP has REQ-WDG-001..012 (12
entries); cpp-RCP's catalog already had REQ-WDG-001..008 (8 entries,
prior work); watchdog.hpp/test_watchdog.cpp reference all 12 ids, so
009-012 looked orphaned going in. Investigation found the premise only
half held.

## No new catalog entries this batch

REQ-WDG-009 (synchronous initial verdict), REQ-WDG-011 (default_config
poll interval), REQ-WDG-012 (destroy() null-safety): never tagged
anywhere in cpp-RCP -- they only appear in watchdog.hpp's own
header-comment prose explaining why they don't transfer, and carry no
c-RCP TC18 citation. They're c-RCP-specific API-shape artifacts
(background-thread poll config, cached-state staleness, manual
destroy()) structurally moot in cpp-RCP's RAII/live-compute design.
Confirmed this reasoning is sound; left as documented non-transfers,
no catalog entry, no pending-file gap (nothing is actually missing).

## One genuine finding: REQ-WDG-010 is implemented and tested, but has zero formal tags anywhere

Filed to .fusa-reqs-pending.json as a traceability-tag gap, NOT a
behavioral gap. The behavior itself is real and correctly wired:
mock.hpp's dispatch_e2e_core() calls rx_watchdog_kick() unconditionally
at both its call sites (mock.hpp:1147, mock.hpp:1343), forwarding to
watchdog::Manager::on_request_received(); test_mock.cpp has a
dedicated end-to-end test ("dispatch_e2e kicks the per-stream
RxWatchdog (REQ-WDG-010) on every call...", test_mock.cpp:1739+);
sim.hpp's Simulator independently wires the same hook. The blocker is
purely mechanical: cpp-FuSa's trace tool only counts a requirement as
traced/tested when a literal `// fusa:req`/`// fusa:test REQ-WDG-010`
comment exists somewhere -- mock.hpp's existing prose mentions and
test_mock.cpp's `[REQ-WDG-010]` Catch2 tag string don't match that
regex, so this id has zero formal annotations anywhere despite being
genuinely covered. Adding it to .fusa-reqs.json today (with no real
tag anywhere) would fail CI's hard `cpfusa trace --req-coverage 100`
/tested-coverage gates. Fixing this needs two tags added to
include/rcp/mock.hpp/tests/test_mock.cpp -- both out of this batch's
file-scope (batch 5 is restricted to watchdog.hpp/test_watchdog.cpp).
Filed here so batch 13 (MOCK/PWRMODE, the batch that touches mock.hpp)
can add those two tags and migrate this entry from pending into the
real catalog in the same change.

## REQ-WDG-001..008 re-verified, untouched

Existing tags checked against real code/tests and found accurate --
no placement fix needed, no catalog change.

## Verification

.fusa-reqs.json: unchanged, byte-identical (544 entries, sec-tested
count still exactly 72). .fusa-reqs-pending.json: 19 -> 20 entries
(+1, REQ-WDG-010). include/rcp/watchdog.hpp: expanded its existing
REQ-WDG-010 header-comment bullet with the mock.hpp citation and the
pending-migration note (+21/-3 net). tests/test_watchdog.cpp:
unchanged. Full tree rebuilt from scratch: 0 errors, 0 warnings, 58/58
ctest suites pass (100%).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 6 of 13
-- both safety-mechanism files (E2E CRC/sequence guard, LIFECYCLE state
machine/access control).

## Scope

e2e.hpp's REQ-E2E-* (c-RCP has 47 entries) and lifecycle.hpp's
REQ-LIFECYCLE-* (c-RCP has 39 entries), 86 c-RCP entries total.

## Major finding: a pre-existing id-numbering collision, unique to this batch

Unlike every prior batch, cpp-RCP's pre-existing REQ-E2E-001..014 and
REQ-LIFECYCLE-001..007 entries were numbered independently
(sequential-by-file-order) before this rewrite's later convention of
reusing c-RCP's exact ids took hold -- they collide head-on with
c-RCP's own ids 001-014/001-007, describing entirely different
requirements (verified directly: cpp's REQ-E2E-004 is "verify_crc
detects a mismatched or corrupted CRC"; c-RCP's REQ-E2E-004 is
"rcp_e2e_length_with_crc() applies the length-accounting
pre-adjustment" -- unrelated content, same id). Not renamed here (out
of scope, no established rename plan for this specific collision
unlike REQ-REGMAP-*/REQ-RMAP-*'s already-decided rename). Six c-RCP
ids that are genuinely implemented but whose natural id is already
taken (REQ-E2E-004-009) are filed to .fusa-reqs-pending.json as
id-collision gaps -- a new gap category, distinct from a missing-tag
or missing-behavior gap -- for a future renumbering-cleanup batch.

## New catalog entries (39)

- E2E (13): 021, 028, 029, 030, 032, 035, 036, 037, 038, 040, 042,
  045, 046. None carry severity:cybersecurity -- c-RCP's own catalog
  has zero severity fields anywhere (confirmed via grep of the whole
  file); cpp-RCP's existing 14 cybersecurity-marked E2E entries are
  this rewrite's own earlier independent judgment, not a c-RCP
  citation, so nothing new was copied. sec-tested count unchanged at
  exactly 72/583 -- no CI floor change needed.
- LIFECYCLE (26): 013-022, 024-039 (023 excluded, see below). Two
  entries (022, 034) copied honestly as scope="tc18-gap"/
  status="partial", matching c-RCP's own tracked findings and
  mirrored by cpp-RCP's own pre-existing LifecycleErrc::eps_not_idle
  doc comment.

## Real bug found and fixed: REQ-LIFECYCLE-023 tag was mislabeled

The pre-existing orphan tag for REQ-LIFECYCLE-023 was attached to
field_writable()'s GENERAL per-FieldKind tests (which actually match
c-RCP's REQ-LIFECYCLE-018/019/020), not to c-RCP-023's actual narrow
claim (EP_GEN_CFG/QUEUE_CFG mapped to HwGeneric's lock rule, per
Figure 17). Retagged the four field_writable() tests to their correct
ids (018/019/020, plus their real secondary associations
026/027/030/035/036/037). REQ-LIFECYCLE-023's actual claim confirmed
NOT implemented anywhere -- regmap.hpp's own header comment explicitly
defers the "EP0 address-routed dispatcher" carrying this decision --
filed to .fusa-reqs-pending.json as a genuine behavioral gap, not a
tag issue. Independently spot-verified: confirmed cpp's REQ-E2E-004
content mismatch and the REQ-LIFECYCLE-023 tag removal directly
against the diff.

## Other gaps filed (5 more, total 11 new pending entries this batch)

REQ-E2E-033/039/041/047: genuinely implemented+tested in mock.hpp/
test_mock.cpp (out-of-scope files) with zero formal tags -- same
REQ-WDG-010 pattern from batch 5, verified by reading the actual
dispatch code. Filed for a future mock.hpp-scoped batch (13) to tag
and migrate.

## New test added

LifecycleErrc category/message distinctness (REQ-LIFECYCLE-021) --
was implemented but had zero test coverage.

## Duplicate/consolidation mappings (no action, largest area of the batch)

E2E: c-RCP 001-020, 022-027, 031, 034, 043, 044 map onto existing/new
cpp entries (043/044 subsumed into new StreamFaultTracker entry 021;
026/035 and 030/036 are literal duplicate pairs c-RCP itself
acknowledges, both kept separately per c-RCP's own practice).
LIFECYCLE: c-RCP 001-012 map onto cpp's existing 001-007 (e.g. 010+011
both subsumed by cpp-006's single unconditional-deconfigure
statement).

## Out-of-scope finding, flagged only

Pre-existing orphan tags REQ-RMAP-049/REQ-RMAP-055 in lifecycle.hpp's
own manifest, untouched -- for whoever picks up the RMAP prefix
(batch 11).

## Verification

Independently re-verified (not just the porting agent's own report):
semantic id-keyed diff confirms zero pre-existing catalog entries
modified/deleted, exactly the 39 new ids added matching the report's
per-prefix breakdown exactly. Confirmed no new entry carries
severity:cybersecurity -- sec-tested count holds exactly at 72/583.
Directly spot-checked the id-collision claim (cpp vs c-RCP REQ-E2E-004
content) and the REQ-LIFECYCLE-023 mislabel-and-retag against the real
diff -- both check out. Confirmed e2e.hpp/lifecycle.hpp diffs are
PURE tag-manifest additions/corrections with zero behavioral code
changes -- appropriate caution for safety-mechanism files. Full tree
rebuilt from scratch: 0 errors, 0 warnings, 58/58 ctest suites pass
(100%).

.fusa-reqs.json: 544 -> 583 entries (+39, 5203 -> 5663 lines).
.fusa-reqs-pending.json: 20 -> 31 entries (+11, 202 -> 310 lines).
include/rcp/e2e.hpp: +6 lines (tags only). tests/test_e2e.cpp: +8
lines net. include/rcp/lifecycle.hpp: +17 lines (tags only).
tests/test_lifecycle.cpp: +52 lines net (retags + 1 new test).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…160)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 7 of 13.

## Scope

can.hpp's REQ-CANEP-* (c-RCP: 42 entries), lin.hpp's REQ-LINEP-* (34),
iseled.hpp's REQ-ISELED-* (42) -- 118 c-RCP entries total. Base bus/
frame-layer REQ-CAN-*/REQ-LIN-* prefixes explicitly out of scope.

## Major finding: can.hpp/lin.hpp/iseled.hpp were already far ahead of the catalog

An earlier, untracked rewrite pass had already brought these three
files close to c-RCP's real design -- most of the 118 requirements
were already genuinely implemented and tested; the gap was almost
entirely in the catalog/tags, not the code. iseled.hpp/test_iseled.cpp
already carried complete file-level tags for essentially the whole
c-RCP id range -- zero changes needed to either file. can.hpp/lin.hpp
only had tags for their old, pre-rewrite low-numbered ids and needed
33/33 new tags respectively.

## Id-collision pattern (same class as batch 6/#159)

cpp's pre-existing low ids (CANEP 001-009, LINEP 006, ISELED 001-007)
were numbered independently before this rewrite's "reuse c-RCP's exact
id" convention took hold, and collide with c-RCP's own same-numbered
but content-unrelated requirements (verified directly: cpp's
REQ-CANEP-001 is "CAN's ep_type id is 0x0B"; c-RCP's is
"rcp_ep_can_frame_format_valid() accepts exactly the six FrameFormat
values Table 57 defines" -- unrelated content, same id). Filed as 17
id-collision gaps to .fusa-reqs-pending.json (CANEP-001..009 x9,
LINEP-006 x1, ISELED-001..007 x7) rather than corrupting catalog id
uniqueness, for a future renumbering-cleanup batch.

## New catalog entries (98)

- CANEP: 33 (010-028, 030-042; 029 included as an honest
  status:not-implemented spec-gap entry, matching c-RCP's own tracked
  defect -- not force-fit as implemented)
- LINEP: 31 (007-039, excluding collision 006 and retired 023/026)
- ISELED: 34 (008-027, 029-042, excluding collisions 001-007 and
  retired 028)

## Retired, no action (3)

REQ-LINEP-023/026, REQ-ISELED-028 -- c-RCP itself retired these as
near-duplicates; correctly not ported.

## Real test-coverage gaps found and closed (new TEST_CASEs added)

can.hpp: CanErrc message distinctness; CanFunctionalConfig zero-init;
full reject-taxonomy completions for decode_frame_request/
decode_frame_response/decode_frame_response_fragment/
decode_reassembled_frame_response (bad_msg_type/bad_frame_format/
bad_arbitration_id were previously never exercised, only wrong_bus/
short_frame/wrong_op/bad_evt were); a dedicated regression test for
the ep_clear_req_storage wire-bit-4 fix (a wrong-but-self-consistent
bit that round-trip tests alone can't catch); explicit byte-level
right-alignment assertion; remote-frame-absence documentation test.
lin.hpp: LinErrc message distinctness; decode_command_request
bad_evt/bad_msg_type paths that were named in an existing test's title
but never actually exercised; LinReconfigErrc message non-emptiness.
Two initially-incorrect test constructions were caught and fixed
before finalizing (a structurally-unreachable bad_msg_type-via-ACF_GBB
path, a byte-index mislabel in the right-alignment test).

## Verification

Independently re-verified (not just the porting agent's own report,
which itself ran the real cpfusa v0.18.0 trace tool and reported
Total 681/Annotated 681/Tested 681, 100% both metrics, secTested
holding at 72): semantic id-keyed diff confirms zero pre-existing
catalog entries modified/deleted, exactly 98 new ids matching the
report's per-prefix breakdown exactly (CANEP 33, LINEP 31, ISELED 34),
zero duplicate ids anywhere in the catalog. Confirmed no new entry
carries severity:cybersecurity -- sec-tested count holds exactly at
72/681. Directly spot-checked the CANEP-001 id-collision claim against
both catalogs' actual content -- confirmed real. Confirmed
can.hpp/lin.hpp diffs are pure additions (zero deleted lines across
all six touched files) -- tags and new tests only, no existing
behavior touched. Full tree rebuilt from scratch: 0 errors, 0
warnings, 58/58 ctest suites pass (100%).

.fusa-reqs.json: 583 -> 681 entries (+98, 5663 -> 6806 lines).
.fusa-reqs-pending.json: 31 -> 48 entries (+17, 310 -> 517 lines).
include/rcp/can.hpp: +33 lines (tags only). include/rcp/lin.hpp: +33
lines (tags only). tests/test_can.cpp: +335 lines. tests/test_lin.cpp:
+101 lines. include/rcp/iseled.hpp, tests/test_iseled.cpp: unchanged.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* rewrite(phase6): batch 8 — I2C/ADC/GPIO catalog re-derivation

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 8 of 13.

## Scope

i2c.hpp's REQ-I2C-* (c-RCP: 26 entries), adc.hpp's REQ-ADC-* (55),
gpio.hpp's REQ-GPIO-* (46) -- 127 c-RCP entries total.

## Major finding: same pattern as batch 7 -- code was already far ahead of the catalog

All three modules' code was already re-implemented against c-RCP's real
design in an earlier, untracked pass, with top-of-file manifests
already covering nearly the full c-RCP id range. The gap was almost
entirely in the catalog and the 9 pre-existing low-numbered ids per
module, not missing implementation. GPIO needed zero code/test edits
at all; ADC's module was fully redesigned (compute_average/
AdcEndpoint::request_reading/handle_request/encode_adc_value no longer
exist), making its 9 old-id collisions the cleanest case of the three
-- single-occupant stale content with zero remaining tags pointing to
them.

## New catalog entries (101)

- I2C: 18 (REQ-I2C-008-018, 020-026)
- ADC: 46 (REQ-ADC-010-055)
- GPIO: 37 (REQ-GPIO-010-046)

## Id-collision gaps filed (21, same class as batches 6/7)

- I2C: 7 (001-007) -- old entries describe now-removed pre-rewrite
  functions (i2c_mode_of 2-way decode, compound_wait_matches_bits) or
  content that migrated to other ids; real c-RCP content genuinely
  implemented+tested, often via legitimate dual-tagging with the old
  entry's own still-valid test.
- ADC: 9 (001-009) -- cleanest collision case: verified directly,
  cpp's REQ-ADC-008 ("AdcEndpoint::handle_request applies Table 33 Row
  2...", carries severity:cybersecurity, left untouched) describes a
  function (AdcEndpoint::handle_request) confirmed to no longer exist
  anywhere in the codebase; c-RCP's real REQ-ADC-008 ("collect_
  response_values reports a short count...") is unrelated content,
  same numeral.
- GPIO: 5 (001, 006-009) -- old entries still valid for their own
  content, real c-RCP content at the same numerals implemented+tested
  elsewhere via dual-tags.

## Genuine behavioral gaps filed (4, GPIO only)

REQ-GPIO-002/003/004/005 -- c-RCP's pin_index_valid()/pin_mask()/
pin_get()/write_semantics_valid() have no standalone cpp equivalents;
bounds/mask/get logic is inlined ad hoc at call sites rather than
exposed as independently-tested primitives (confirmed via zero
matching bracket tags anywhere).

## Retired, no action

REQ-I2C-019 only -- c-RCP itself retired it, superseded by 001/021/022.
No retired ids in ADC or GPIO's ranges.

## Real bug fixed: mislabeled tag

tests/test_i2c.cpp's config_write_not_supported TEST_CASE was
mistagged [REQ-I2C-012] (that id's real meaning is "decode_transfer_
request rejects a malformed frame" -- unrelated); retargeted to
[REQ-I2C-007], restoring a legitimate dual-tag with the old catalog
entry it actually matches.

## New test added

ADC's collect_response_values leading-count truncation (REQ-ADC-007)
had no test exercising the avg_count > value_count case -- added.
Plus two manifest-line/assertion fixes (REQ-ADC-035/041 tags added to
the header manifest; an out-of-range-code assertion strengthened in
the existing AdcErrc message test).

## Verification

Independently re-verified (the porting agent itself also ran the real
cpfusa trace tool: 782/782 annotated, 780/782 tested by design -- the
2 untested, REQ-ADC-032/034, mirror c-RCP's own test_ep_adc.c
convention where "testedness" lives in other modules' suites, secTested
holding at 72): semantic id-keyed diff confirms zero pre-existing
catalog entries modified/deleted, exactly 101 new ids matching the
report's per-prefix breakdown exactly (I2C 18, ADC 46, GPIO 37), zero
duplicate ids anywhere. Confirmed no new entry carries
severity:cybersecurity -- sec-tested count holds exactly at 72/782.
Directly spot-checked the REQ-ADC-008 id-collision claim against both
catalogs' actual content -- confirmed real. Full tree rebuilt from
scratch: 0 errors, 0 warnings, 58/58 ctest suites pass (100%).

.fusa-reqs.json: 681 -> 782 entries (+101, 6806 -> 8003 lines).
.fusa-reqs-pending.json: 48 -> 73 entries (+25, 517 -> 783 lines).
include/rcp/i2c.hpp, include/rcp/gpio.hpp: unchanged.
tests/test_i2c.cpp: net 0 lines (1 line content-changed).
tests/test_adc.cpp: +22 lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* fixup: close the tested-coverage gap CI found (REQ-ADC-032/034)

Real CI (not just the porting agent's local check) caught a genuine
gap this batch's own local verification missed: REQ-ADC-032/034 were
traced (had //fusa:req tags) but had zero //fusa:test tags or bracket
tags anywhere in the tree -- cpfusa's tested-coverage gate requires
tested == total exactly, with no tolerance for "req-only by design"
entries, unlike the porting agent's own local read of the situation.

REQ-ADC-034 ("ADC sampling occurs only while a request executes, and
a compound wait compares the last acquired average without sampling"):
the catalog entry's own text already identifies the real mechanism as
acf.hpp's compound_wait_match() -- a shared, endpoint-agnostic
comparator, not ADC-specific code. Dual-tagged the existing
"compound_wait_match exact-match mode" TEST_CASE in test_acf.cpp with
[REQ-ADC-034] (bracket tag) and added the matching //fusa:test
REQ-ADC-034 manifest line, following this rewrite's established
cross-file dual-tagging convention for shared mechanisms.

REQ-ADC-032 ("An ADC endpoint serves exactly one channel, is limited
to 16-bit resolution, and requires a selected analog input pin"): no
existing test pinned the 16-bit wire-format constant directly (every
use of kAdcValueLen was incidental, inside unrelated codec tests).
Added a real, dedicated TEST_CASE asserting kAdcValueLen ==
sizeof(uint16_t) == 2, plus the matching manifest line.

## Verification

Full tree rebuilt from scratch: 0 errors, 0 warnings, 58/58 ctest
suites pass. Independently re-derived cpfusa's own req/test coverage
logic in Python against the real .fusa-reqs.json + all include/rcp/
*.hpp + tests/*.cpp files: 782/782 requirements now have both a
//fusa:req reference and a //fusa:test-or-bracket reference, zero
gaps -- confirming the real CI gate will pass on the next run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 9 of 13.

## Scope

mdio.hpp's REQ-MDIO-* (c-RCP: 28 entries), pwm.hpp's REQ-PWM-* (75,
the largest single per-endpoint prefix in the whole catalog) -- 103
c-RCP entries total.

## Major finding: same pattern as batches 7/8

Both modules' code and tests were already ported ahead of the catalog
in an earlier untracked pass -- mdio.hpp/test_mdio.cpp needed zero
edits at all (top-of-file manifests already covered the full 28-id
c-RCP range). The gap was almost entirely catalog-side, plus 14
pre-existing low-numbered id collisions.

## New catalog entries (87)

- MDIO: 21 (REQ-MDIO-008-028)
- PWM: 66 (REQ-PWM-010-026, 060-075 minus already-covered/collision ids)

## Id-collision gaps filed (14)

- MDIO: 7 (001-007) -- old entries describe unrelated pre-rewrite
  content (ep_type id, payload width validation); real c-RCP content
  (addr_valid, burst_next_regad, word_encode/decode, pack_len/words,
  word_count_of) genuinely implemented+tested under the same bracket
  numerals -- a legitimate dual-tag overlap, not a gap.
- PWM: 7 (001, 004-009) -- verified content collisions directly, e.g.
  cpp's REQ-PWM-008 carries severity:cybersecurity and describes
  handle_request Row-2 classification, while c-RCP's real REQ-PWM-008
  (RESERVED4 no-op) is unrelated and genuinely tested elsewhere.
  REQ-PWM-001 is an honest architecture divergence, not force-fit as a
  collision or a gap: cpp has no validate-and-reject
  write_semantics_valid(); endpoint::write_semantics_of() masks to 3
  bits instead, making every input constructively valid -- same safety
  property, different shape, cited to test_endpoint.cpp's
  REQ-ENDPOINT-002 tests.

## Already-covered, no action (2)

REQ-PWM-002 (exact 1:1 match), REQ-PWM-003 (cpp's own entry bundles
Or/And/Xor/Add/Subtract under one id; OR is a literal subset).

## Real tested-coverage gaps found and closed (2, following batch 8's lesson)

Grepped the whole include/+tests/ tree for every id before adding it
(per the explicit instruction added to this batch's dispatch after
batch 8's CI-caught miss): found REQ-PWM-041 (PWM_IN strerror
uniqueness -- cpp shares one PwmErrc/pwm_category() across PWM_OUT/
PWM_IN, unlike c-RCP's two separate functions, so no dedicated test
existed) and REQ-PWM-061 (decode_read_request rejects a non-ACF_ABB
frame -- no test covered bad_msg_type for PWM_OUT's read-request
decode at all). Strengthened the existing REQ-PWM-024 message test
into an exhaustive non-empty/distinct-message-per-code loop (dual-
tagged [REQ-PWM-024][REQ-PWM-041]), and added a new TEST_CASE for
REQ-PWM-061 mirroring test_mdio.cpp's existing pattern for the same
check.

## Verification

Independently re-verified (the porting agent's own final step re-ran
a whole-catalog tag-coverage script covering all 869 entries, not
just this batch's 103 -- confirming no regression to batches 1-8):
semantic id-keyed diff confirms zero pre-existing entries modified/
deleted, exactly 87 new ids matching the report's per-prefix breakdown
exactly (MDIO 21, PWM 66), zero duplicate ids. Independently re-ran my
own from-scratch Python tag-coverage check against the merged worktree
(not trusting the agent's script output alone): confirmed 869/869
entries have both a real //fusa:req and a real //fusa:test-or-bracket
tag somewhere in the tree, zero gaps. Confirmed no new entry carries
severity:cybersecurity -- sec-tested count holds exactly at 72/869.
Full tree rebuilt from scratch: 0 errors, 0 warnings, 58/58 ctest
suites pass (100%).

.fusa-reqs.json: 782 -> 869 entries (+87, 8003 -> 9026 lines).
.fusa-reqs-pending.json: 73 -> 87 entries (+14, 783 -> 961 lines).
tests/test_pwm.cpp: +42 lines. include/rcp/mdio.hpp,
include/rcp/pwm.hpp, tests/test_mdio.cpp: unchanged.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 10 of 13.

## Scope

spi.hpp's REQ-SPI-* (c-RCP: 42 entries), uart.hpp's REQ-UART-* (49),
wakeup.hpp's REQ-WAKEUP-* (36) -- 127 c-RCP entries total.

## Major finding: mixed -- spi/wakeup already ahead, uart needed real work

spi.hpp/test_spi.cpp and wakeup.hpp/test_wakeup.cpp were already fully
ported ahead of the catalog in an earlier untracked pass -- zero code
edits needed in either. uart.hpp needed genuine work: its req-manifest
only covered 9 of 49 ids despite most of the underlying code/tests
already existing, plus two genuine tested-coverage gaps this batch's
mandatory whole-tree grep caught before they reached CI.

## New catalog entries (106)

- SPI: 36 (REQ-SPI-006-030, 033-036, 038-044)
- UART: 40 (REQ-UART-010-049, including 048 at c-RCP's own honest
  status:partial -- not force-fit as fully implemented)
- WAKEUP: 30 (REQ-WAKEUP-006-025, 027-036) -- REQ-WAKEUP-020's real
  implementation lives in regmap.hpp/test_regmap.cpp (not wakeup.hpp),
  already fully tagged/tested there from an earlier pass; added as a
  cross-module mapping with no code touch needed.

## Id-collision gaps filed (19)

SPI 001-005, UART 001-009, WAKEUP 001-005 -- real behavior implemented
and tested under a shared numeral, but the id is already taken by
unrelated pre-rewrite catalog content, matching the established
pattern from batches 6-9.

## Other gaps filed (2)

- REQ-SPI-037 (SPI error-state EP-enable-reset behavior): matches
  c-RCP's own not-implemented status -- a genuine TC18 §13.7.3.3 spec
  silence, not force-fit as done.
- REQ-WAKEUP-026 (regmap.hpp's ep_id_map::apply_reconfig() never
  enforces WakeUp's fixed-EP_Nr invariant at write time): the
  read-only diagnostic half (REQ-WAKEUP-020) exists and is tested; the
  write-time-denial half does not. Out of this batch's scope since
  regmap.hpp isn't one of the three in-scope modules -- flagged for
  whoever picks up REQ-RMAP-*/regmap.hpp (batch 11).

## Real tested-coverage gaps found and closed (3, per batch 8's now-mandatory whole-tree check)

- REQ-UART-032 (ep_status register round-trip): added a new
  apply_reconfig-side test, dual-tagged with the existing render-side
  test.
- REQ-UART-035 (compound-wait bounded by RX FIFO): added a new
  UART-flavored test exercising acf::compound_wait_match() directly,
  mirroring SPI's own analogous test.
- REQ-UART-048 (partial status, separate-units fields): dual-tagged
  onto the existing Table 51 register-round-trip test.

## Verification

Independently re-verified (the porting agent's own final step re-ran
a whole-catalog tag-coverage script covering all 975 entries):
semantic id-keyed diff confirms zero pre-existing entries modified/
deleted, exactly 106 new ids matching the report's per-prefix
breakdown exactly (SPI 36, UART 40, WAKEUP 30), zero duplicate ids.
Independently re-ran my own from-scratch Python tag-coverage check
against the merged worktree (not trusting the agent's script alone):
confirmed 975/975 entries have both a real //fusa:req and a real
//fusa:test-or-bracket tag somewhere in the tree, zero gaps. Confirmed
no new entry carries severity:cybersecurity -- sec-tested count holds
exactly at 72/975. Full tree rebuilt from scratch: 0 errors, 0
warnings, 58/58 ctest suites pass (100%).

.fusa-reqs.json: 869 -> 975 entries (+106, 9026 -> 10288 lines).
.fusa-reqs-pending.json: 87 -> 108 entries (+21, 961 -> 1226 lines).
include/rcp/uart.hpp: +58 lines. tests/test_uart.cpp: +103 lines net.
include/rcp/spi.hpp, tests/test_spi.cpp, include/rcp/wakeup.hpp,
tests/test_wakeup.cpp, include/rcp/regmap.hpp, tests/test_regmap.cpp:
unchanged.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… catalog (#164)

* rewrite(phase6): batch 11 — REQ-REGMAP-*→REQ-RMAP-* rename + RMAP/SRV catalog

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 11 of 13
-- the largest single batch so far, combining a real id-field rename with
two full-prefix derivations.

## Part A — REQ-REGMAP-* → REQ-RMAP-* rename

Pure id-field rename per ROADMAP.md Phase 17's already-established
canonical naming (RELAY's docs/RCP-ARCHITECTURE.md cites REQ-RMAP-* as
canonical): all 15 REQ-REGMAP-001..015 catalog entries renamed to
REQ-RMAP-001..015 (id field only, no other field changed), plus all 15
code tags in include/rcp/regmap.hpp and 36 test tags in
tests/test_regmap.cpp. severity:cybersecurity preserved on all 15
(these are part of the enumerated 72-id cybersecurity subset per
ci.yml's own comment) -- sec-tested count holds at exactly 72. Zero
REQ-REGMAP-* remnants anywhere in .fusa-reqs.json, .fusa-reqs-pending.json,
or the include/+tests/ tree (one pre-existing entry's own prose text
still literally says "REQ-REGMAP-005" referring to another entry by
name -- left untouched, it's descriptive text, not an id). Stale
REQ-REGMAP-* mentions remain in CYBERSECURITY.md/TARA-ANALYSIS.md/
ROADMAP.md/ci.yml's comment -- outside this batch's scope, left for a
docs pass; the CI gate itself reads the severity field dynamically, not
a hardcoded id list, so it's unaffected.

## Part B — REQ-RMAP-* full derivation (87 c-RCP entries)

- 59 new catalog entries (016-045, 047-058, 060, 066-067, 070-071,
  073-079, 082-084, 086-087), including 055 (already fully tagged in
  lifecycle.hpp/test_lifecycle.cpp from an earlier batch -- cross-module
  mapping, no code touch) and two status:partial entries (023, 067)
  honestly preserved from c-RCP.
- 15 id-collision/gap entries filed to .fusa-reqs-pending.json:
  001/002/003/009/010/011/012/013/014/015 -- the rename claimed these
  numerals first for cpp's own pre-existing content; c-RCP's real
  content at the same numbers (is_ep0(), writer_ctx() grant/withhold,
  hw_pin bit-layout distinctness, named_signal_string()) is separately
  implemented+tested, filed as collisions rather than corrupting id
  uniqueness -- matching the established pattern from batches 6-10.
  068/069/072/080: real gaps matching regmap.hpp's own explicit
  "EP0 address-routed dispatcher NOT ported in this batch" scope note.
  081: genuine TC18 spec-defect gap (EP_RESP_ON_ERROR, a Table 31 prose
  reference with no defined bit/field) -- matches c-RCP's own
  not-implemented disposition verbatim; cpp additionally has a
  defensive test proving no bit is invented for it.
- 6 retired ids (004-008, 046) -- c-RCP itself retired these
  (superseded by 030/039); no entry or gap filed, since there's no real
  c-RCP content for cpp to collide with or gap against.
- 15 renamed + 7 pre-existing respqueue-specific (untouched) + 59 new +
  15 pending + 6 retired = all 87 c-RCP ids accounted for.

## Part C — REQ-SRV-* full derivation (42 c-RCP entries, all new)

include/rcp/server.hpp already carried all 42 fusa:req tags from an
earlier untracked pass -- zero code edits needed there (0 line delta).
tests/test_server.cpp only had 24 of 42 tested; added 14 new TEST_CASEs
plus several dual-tags to close the remaining 18 (019, 020, 022,
024-034, 037-039, 042), including genuinely new coverage for per-kind
select_due() gating (027-032), CompoundWait's independent per-slot
evt/payload evaluation and wait_tick() application (020, 033, 042),
no-sequencer-advance-for-Triggered/Timed/Chained plus always-removed
(034, 037), and chain-timer-not-restarted (038). All 42 SRV catalog
entries added.

No new entry (RMAP or SRV) carries severity:cybersecurity -- confirmed
neither c-RCP's real 87 RMAP nor 42 SRV entries mark any as
security-relevant; sec-tested count unchanged at exactly 72.

## Verification

Independently re-verified (not just the porting agent's own report,
which itself ran a whole-catalog tag-coverage script twice, catching
and fixing one gap where REQ-RMAP-082 had a test tag but no fusa:req
tag before finalizing): semantic id-keyed diff confirms zero
pre-existing entries modified, exactly 116 new/renamed ids (15 renamed
+ 59 new RMAP + 42 new SRV) matching the report's breakdown exactly,
zero duplicate ids anywhere (catalog or pending). Independently
re-derived the whole-tree tag-coverage check from scratch in Python
against the merged worktree (not trusting the agent's script alone):
confirmed all 1076 entries have both a real fusa:req and a real
fusa:test-or-bracket tag somewhere in include/+tests/, zero gaps.
Confirmed severity:cybersecurity was preserved on exactly the 15
renamed entries and granted to zero genuinely-new entries -- sec-tested
count holds at exactly 72/1076. Directly spot-checked the REQ-RMAP-009
writer_ctx tag placement, the REQ-RMAP-081 spec-defect gap text, and
the new REQ-SRV-027/028/033 select_due()-gating tests against the real
diff -- all confirmed accurate. Full tree rebuilt from scratch: 0
errors, 0 warnings, 58/58 ctest suites pass (100%).

.fusa-reqs.json: 975 -> 1076 entries (+116, well over +101 in raw
additions once the 15 rename removals are netted out; 10288 -> 11498
lines). .fusa-reqs-pending.json: 108 -> 123 entries (+15, 1226 -> 1420
lines). include/rcp/regmap.hpp: +11 lines net (rename tags only).
tests/test_regmap.cpp: +23 lines net (rename tags + dual-tag
additions). include/rcp/server.hpp: unchanged (0 line delta).
tests/test_server.cpp: +339 lines net (14 new TEST_CASEs + dual-tags).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* fixup: batch 11 -- add missing //fusa:test manifest tags cpfusa's real gate needs

Real CI failure on PR #164's cpfusa-trace job: tested=1051 total=1076
(gate requires exact equality). Root cause, diagnosed against the real
pinned cpfusa v0.18.0 binary (built locally, not guessed at): cpfusa's
trace tool scans for a specific `// fusa:test REQ-ID` comment-line form
(src/trace/trace.cpp's test_tag_re) -- it does NOT recognize Catch2
`[REQ-ID]` bracket tags as evidence of testedness at all. My own
verification script for this batch (and the porting agent's) checked
for either form, which is why both reported false 100% clean.

Same lesson as the batch-8 CI incident: verify against the real gate
tool, not an approximation of it.

24 REQ-RMAP-* ids + REQ-SRV-017 had real, correct bracket-tagged
TEST_CASEs (or, for REQ-SRV-017, real testable content in an adjacent
module) but no matching //fusa:test manifest line anywhere in the tree:

- 22 ids (018-023, 026-029, 031-033, 035-036, 038-039, 043, 073-075,
  082) already had real TEST_CASEs with correct bracket tags -- pure
  missing-manifest-line fix, no test content changed.
- REQ-RMAP-034/037 had zero bracket-tag occurrences anywhere: found the
  existing "GeneralMap default-constructs zeroed" TEST_CASE already
  REQUIREs their exact fields (svr_request_stream_cfg_capacity/
  svr_response_stream_cfg_capacity for 034; svr_ep_bytebus_id_map_ptr/
  _capacity for 037) as part of its zero-init sweep -- matching the
  exact same depth of coverage already accepted for their sibling
  Table-20 pointer/capacity fields (026-029/031-033/035-036/038, all
  covered by this same test). Added the two missing bracket tags to
  that TEST_CASE's existing tag list; no new assertions needed, the
  content was already there.
- REQ-SRV-017 (status:partial, cross-module by its own catalog text --
  server.hpp's part is content-modeling/admission only, not response-
  queue transmission): dual-tagged onto test_respqueue.cpp's existing
  "should_flush_by_time is independent of queue state" TEST_CASE (REQ-
  RMAP-065), which already asserts the exact claim (an empty queue
  still fires the Flush_time trigger) before any push() -- the real,
  and only, test target for this id's actual behavior.

Re-verified against the real pinned cpfusa binary (built locally from
the same v0.18.0 tag CI uses, not approximated): tested=1076
total=1076, cpfusa trace --gaps returns zero requirement ids. Dangling-
tag warning count unchanged at 26 (all pre-existing, out-of-scope
DISC/SHMEM ids -- batch 12's territory, not touched here). Full tree
rebuilt from scratch: 0 errors, 0 warnings, 58/58 ctest suites pass
(100%).

tests/test_regmap.cpp: +30 lines (manifest tags + 2 bracket tags on one
existing TEST_CASE). tests/test_respqueue.cpp: +7 lines (manifest tag +
dual-tag + explanatory comment on one existing TEST_CASE).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 12 of 13.

## Scope

discovery.hpp's REQ-DISC-* (c-RCP: 30 entries), adapt.hpp's REQ-RELAY-*
(c-RCP: 17 entries) -- 47 c-RCP entries total.

## Major finding: DISC already far ahead of the catalog, RELAY needed real work

discovery.hpp/test_discovery.cpp already carried real fusa:req/bracket-tag
citations for nearly the full 001-030 range from an earlier untracked
pass (only 5 missing //fusa:req manifest lines for 025-028/030) --
almost entirely a catalog-side derivation. adapt.hpp/test_adapt.cpp only
covered cpp's own pre-rewrite REQ-RELAY-001-005 numbering; the real
c-RCP 006-017 range needed genuine investigation against c-RCP's
per-op field-table design vs cpp's simpler generic-RequestFn shape.

## Id-collision pattern (same class as batches 6-11)

Both prefixes' pre-existing low-numbered ids were assigned before this
rewrite's "reuse c-RCP's exact id" convention took hold and collide
with unrelated, real c-RCP content at the same numerals (verified
directly, e.g. cpp's REQ-DISC-001 "Discovery request framing" vs
c-RCP's real REQ-DISC-001 "should_drop flags non-NTSCF subtype" --
unrelated). Filed as 14 id-collision gaps (DISC 001-009 x9, RELAY
001-005 x5) to .fusa-reqs-pending.json -- in every DISC case the real
c-RCP behavior turned out to already be correctly implemented+tested
under the matching numeral, only the catalog entry was stale/collided.

## New catalog entries (29)

- DISC: 21 (010-030) -- genuinely implemented+tested already; 5 needed
  their missing //fusa:req manifest tag added (025-028, 030).
- RELAY: 8 (006, 008, 009, 010, 012, 014, 016, 017) -- genuinely
  implemented+tested, with text adapted to document where cpp's
  generic-RequestFn design diverges from c-RCP's per-op field table
  (no failure path on 006, no bound-kind check on 008/009, no
  transport/kind params on 012). 014/016/017 actually live in
  relay.hpp/rcp.hpp rather than adapt.hpp -- tagged in adapt.hpp per
  this repo's existing convention (001-005 already do the same), with
  test tags placed in test_relay.cpp where the real tests live. Added
  2 small missing tests (Channel::is_closed(), to_string(Protocol)
  uniqueness) to genuinely earn 002's and 016's claims.

## Architecture-divergence gaps filed (4, RELAY only, deliberate not oversights)

REQ-RELAY-007 (no per-op opcode enum -- explicitly documented in
test_adapt.cpp's own header comment as intentionally unported),
REQ-RELAY-011 (no adapt-specific error category/strerror -- std::errc
reused instead), REQ-RELAY-013 (no RCP-namespaced SpecVersion alias --
single-namespace architecture has nothing to duplicate, c-RCP's two-
package split doesn't apply here), REQ-RELAY-015 (no manual caller
retain/refcount -- superseded by std::unique_ptr RAII, also explicitly
documented as unported in test_adapt.cpp).

No new entry (DISC or RELAY) carries severity:cybersecurity -- confirmed
via ci.yml/CYBERSECURITY.md that only cpp's existing (untouched)
REQ-DISC-001..009 are in the enumerated 72-id cybersecurity subset;
REQ-RELAY-* never appears there. sec-tested count unchanged at exactly
72.

## Verification

Independently re-verified (the porting agent's own final step re-ran
the real pinned cpfusa v0.18.0 binary, not an approximating script --
the batch-11 lesson applied from the start this time): semantic
id-keyed diff confirms zero pre-existing entries modified/deleted,
exactly 29 new ids matching the report's breakdown exactly (DISC 21,
RELAY 8), zero duplicate ids. Independently re-ran the real cpfusa
binary myself against the merged worktree: testedRequirements ==
totalRequirements == 1105, cpfusa trace --gaps returns zero ids.
Confirmed sec-tested count holds at exactly 72/1105. Directly
spot-checked the REQ-DISC-001 collision claim and the REQ-RELAY-013
architecture-divergence gap text against the real diff -- both
accurate. Full tree rebuilt from scratch: 0 errors, 0 warnings, 58/58
ctest suites pass (100%).

.fusa-reqs.json: 1076 -> 1105 entries (+29, 11498 -> 11836 lines).
.fusa-reqs-pending.json: 123 -> 141 entries (+18, 1420 -> 1600 lines).
include/rcp/adapt.hpp: +19 lines (tags only). include/rcp/discovery.hpp:
+5 lines (tags only). tests/test_adapt.cpp: +5 lines (tags only).
tests/test_relay.cpp: +36 lines (tags + 2 new tests).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
… 6 complete) (#166)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 13 of
13 -- the FINAL batch, covering the two largest files in the codebase
(mock.hpp/test_mock.cpp) plus genuinely new PWRMODE porting work.

## Scope

mock.hpp's REQ-MOCK-* (c-RCP: 33 entries), powerstate.hpp's REQ-PWRMODE-*
(c-RCP: 28 entries, split across c-RCP's power.c+powerstate.c, folded
into cpp-RCP's single powerstate.hpp per this rewrite's earlier design
choice) -- 61 c-RCP entries total. Plus mandatory pickup of 5
mock.hpp-scoped deferred gaps from batches 5 and 6.

## Part C — deferred-gap pickup (mandatory, done first)

REQ-WDG-010 (batch 5), REQ-E2E-033/039/041/047 (batch 6): all confirmed
still genuinely implemented+tested in mock.hpp/test_mock.cpp exactly as
found originally -- promoted from .fusa-reqs-pending.json straight into
.fusa-reqs.json with real fusa:req/fusa:test manifest tags added (they'd
only ever had prose/bracket-tag mentions, never a real manifest tag).

## REQ-MOCK-* (33 c-RCP entries)

3 new catalog entries (027 pending_count, 028 REQUEST_NOT_FOUND, 029
CHAIN_ERROR/ABORTED -- free ids with real pre-existing tests, now
manifest-tagged). All 26 of cpp's pre-existing REQ-MOCK-001..026 are
confirmed id-collisions (same pattern as every batch since 6 -- cpp's
own entries describe unrelated Table-33 evt-classification content
numbered before this rewrite's id-reuse convention). Filed to pending
with per-id verification: 15 collision ids are separately implemented
+tested elsewhere in cpp (cross-referenced to REQ-LIFECYCLE-*/REQ-SRV-*/
REQ-CANCEL-*), 1 not-applicable (C++ RAII supersedes a manual-refcount
requirement), 10 genuine gaps (mostly cpp's fixed-10-endpoint
architecture lacking c-RCP's dynamic add/remove-endpoint API). Free ids
030-033 (stream-scoped lookup family): genuine architectural gaps, filed
honestly rather than force-fit.

## REQ-PWRMODE-* (28 c-RCP entries, genuinely new porting work)

Unlike every other batch this phase, powerstate.hpp had ZERO existing
REQ-PWRMODE-* citations to walk -- this required reading c-RCP's
power.c+powerstate.c source directly against cpp's powerstate.hpp.
20 new catalog entries (19 investigated + 028, which had code tags but
no catalog entry). Implemented 3 small, safe fixes with new tests:
cold_start_lifecycle_target() (closes 003+014), and a backward-
compatible network_available parameter on begin_wake_from_sleep()
(closes 016). REQ-PWRMODE-004 was initially assessed as a small fix but
reclassified to a genuine architectural gap on closer inspection: the
literal transition-table fix would contradict an already-tested,
protected REQ-PWR-002 assertion -- filed to pending rather than forcing
a design decision that isn't this batch's to make. 8 total architectural
gaps filed (004, 012, 017, 019, 021, 022, 026, 027), most notably 026
(mode commits before the sleep/standby response is confirmed
transmitted -- a real safety-relevant ordering gap, not cosmetic).

No new entry (MOCK or PWRMODE) carries severity:cybersecurity -- verified
against CYBERSECURITY.md/ci.yml and c-RCP's own severity fields. sec-
tested count unchanged at exactly 72.

## Verification

Independently re-verified (the porting agent's own final step re-ran
the real pinned cpfusa v0.18.0 binary against the WHOLE catalog, not
just this batch, to catch any regression across all 12 prior batches):
semantic id-keyed diff confirms zero pre-existing entries modified/
deleted, exactly 28 new ids matching the report's breakdown exactly
(MOCK 3, PWRMODE 20, promoted-deferred 5), zero duplicate ids in either
the catalog or the pending file. Independently re-ran the real cpfusa
binary myself against the merged worktree: testedRequirements ==
totalRequirements == 1133, cpfusa trace --gaps returns zero ids.
Confirmed sec-tested count holds at exactly 72/1133. Directly
spot-checked the REQ-PWRMODE-004 reclassification and REQ-WDG-010's
promotion against the real diff -- both accurate, and the 004
reclassification shows genuinely careful judgment (catching a real
conflict rather than force-fitting a fix). Full tree rebuilt from
scratch: 0 errors, 0 warnings, 58/58 ctest suites pass (100%).

## Phase 6 complete

All 13 batches merged. Final catalog: .fusa-reqs.json 1105 -> 1133
entries (+28, all 100% traced and tested, zero regressions across all
13 batches, independently re-verified via a fresh whole-tree cpfusa
trace). Final pending/gap catalog: .fusa-reqs-pending.json 141 -> 174
entries (+38 added, -5 promoted = net +33), honestly documenting every
id-collision and architectural gap found across the whole phase. The
whole-catalog traceability gate (testedRequirements == totalRequirements)
is clean. A detailed Phase 6 completion summary will be posted to
issue #129 separately.

include/rcp/mock.hpp: +8 lines (tags only). include/rcp/powerstate.hpp:
+73 lines (2 new functions + tags). include/rcp/wakeup.hpp: +1 line
(tag only). tests/test_mock.cpp: +8 lines (tags only).
tests/test_powerstate.cpp: +76 lines (2 new test blocks).
tests/test_wakeup.cpp: +1 line (tag only).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
…A+ specs, add TLC CI job (#167)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 7 (formal verification + MC/DC +
release-pipeline hardening), batch 1 of several: TLA+ formal verification
only.

## Scope

cpp-RCP had 3 TLA+ specs (WatchdogSafeState, RxSequenceGuard,
CrcSafeStateLatch) but was missing c-RCP's LifecycleStateMachine and
E2ESafePoint specs -- and, a real pre-existing gap discovered during
scoping, none of the 3 existing specs were ever actually run in CI.

## Part A -- ported specs (both pass TLC clean)

tla/LifecycleStateMachine.tla (+229) + .cfg (+10): models
rcp::lifecycle::ServerLifecycle::transition()/check_hw_cfg()/
check_rcp_cfg(). TLC: 12 distinct states, depth 5, no error.

tla/E2ESafePoint.tla (+215) + .cfg (+17): models rcp::e2e::RxWatchdog/
apply_watchdog_overflow()/endpoint_in_configured_safe_state()/
may_execute_now(). TLC: 256 distinct states, depth 7, no error.

Both cross-referenced directly against cpp-RCP's include/rcp/lifecycle.hpp
and include/rcp/e2e.hpp against c-RCP's src/lifecycle.c/src/e2e.c --
confirmed field-for-field ports from Phase 2's content-parity pass, so
both are faithful direct ports, not mechanical transliterations. No
implementation bugs found; no C++ source touched.

One known abstraction gap documented (not fixed, correctly out of this
batch's scope): cpp's field_writable() computes FunctionalWStar locking
as a pure function of current ServerState, and transition() supports an
additional RcpConfigured->HwConfigured partial demotion that neither
this spec nor c-RCP's original models. c-RCP's own spec already
abstracts this away (sticky field_lock variable, omits the demotion
transition); this port preserves that same abstraction and flags it in
a "Known abstraction gap" comment block for future re-derivation.

Independently spot-checked against RxSequenceGuard.tla's own
FORMAL_VERIFICATION.md: a separate, genuinely pre-existing gap there
(the spec still models pre-Phase-2 non-wraparound sequence comparison,
not the real RFC 1982 forward-window rule evaluate() implements today)
is confirmed untouched by this batch (byte-identical diff against base)
-- correctly out of scope, a distinct re-derivation task.

## Part B -- TLC CI automation (new gap closed for all 5 specs, not just 2)

New `formal-verification` job in .github/workflows/ci.yml (+44 lines),
following c-RCP's own ci.yml job as a structural reference but written
in cpp-RCP's own job-naming/style conventions: downloads tla2tools.jar
once, then runs TLC against all 5 specs in tla/ (the pre-existing 3 plus
the 2 new ports), each as its own step so a single spec's failure is
individually attributable in the Actions UI. No `needs:` -- TLC checking
is independent of the C++ build, matching c-RCP's own pattern.

## Verification

Independently re-verified: rebuilt tla2tools.jar locally and re-ran TLC
against all 5 specs myself (not trusting the agent's own run) -- all 5
report zero errors/violations/deadlocks (WatchdogSafeState 104 states,
RxSequenceGuard 32, CrcSafeStateLatch 4, LifecycleStateMachine 12,
E2ESafePoint 256). Validated the new ci.yml job's YAML parses cleanly
and has the expected 7-step structure. Directly spot-checked the "Known
abstraction gap" comment's honesty against real lifecycle.hpp behavior,
and confirmed the RxSequenceGuard pre-existing-gap claim via a
byte-identical diff of FORMAL_VERIFICATION.md against the pre-batch
base. Full tree rebuilt from scratch: 0 errors, 0 warnings, 58/58 ctest
suites pass (100%) -- unaffected, since no C++ source was touched.

tla/LifecycleStateMachine.tla: new, +229. tla/LifecycleStateMachine.cfg:
new, +10. tla/E2ESafePoint.tla: new, +215. tla/E2ESafePoint.cfg: new,
+17. .github/workflows/ci.yml: +44 (765 -> 809 lines).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
#168)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 7 (formal verification + MC/DC +
release-pipeline hardening), batch 2: real MC/DC (Modified Condition/
Decision Coverage) evidence in CI. cpp-RCP had zero MC/DC infrastructure
before this batch (verified: no mcdc/MCDC/coverage-mcdc references
anywhere in .github/workflows/ci.yml).

## What this adds

Ported c-RCP's existing `mcdc:` CI job (LLVM 18, real
llvm-cov/llvm-profdata condition/decision instrumentation,
ratchet-gated -- not a 100% gate), adapted for cpp-RCP's header-only
C++ architecture:

- CMAKE_C_FLAGS -> CMAKE_CXX_FLAGS, CXX: clang++-18 (not CC:).
- -sources scoped to include/rcp/*.hpp (cpp-RCP has no src/ dir --
  verified; all real implementation logic lives in headers, not the
  test files), matching c-RCP's own intent of measuring implementation
  code, not test code.
- `cpfusa coverage --mcdc-file` confirmed to have the same class of bug
  as c-RCP's `cfusa` equivalent: it parses `covered_true_count`/
  `covered_false_count` object keys that real `llvm-cov export
  -format=text` output never produces (verified directly against both
  cpp-FuSa's source and real export output -- the schema is a
  positional array, no such keys anywhere). Followed c-RCP's precedent:
  the job reads llvm-cov export's own totals.mcdc block directly.

## Floor: 60%, set from a real freshly-measured baseline

Not copied from c-RCP's own 93% (that number reflects c-RCP's C
codebase's own current state, meaningless for cpp-RCP's). Measured
locally end-to-end (instrumented build -> 58/58 tests passing under
instrumentation -> profile merge -> llvm-cov export) on Homebrew LLVM
18.1.8 (matching the CI job's clang-18 major version): 313/466 = 67.17%.
Cross-checked on LLVM 22.1.8: 426/629 = 67.73% -- close agreement
despite a 4-major-version gap, evidence the number reflects the test
suite, not toolchain noise. Floor set with ~7-point margin below the
clang-18-matched measurement, wider than c-RCP's own 2.3-point margin
because this measurement also crosses macOS/arm64 (local) vs Ubuntu
22.04/x86_64 (CI) and Homebrew-built vs apt.llvm.org-built clang-18 --
either could plausibly shift condition-pair counts on a header-only
codebase (many decisions live in platform-conditional code, e.g.
rcp/l2.hpp's raw-socket paths) more than a same-OS compiler bump would.
Provenance written into the gate step's own comment and AUDIT_PACK.md
§3/§5.

## Verification

Independently re-verified (not just the porting agent's own
measurement): reproduced the entire instrumented pipeline myself from
scratch on the same Homebrew LLVM 18.1.8 toolchain -- 58/58 tests pass
under instrumentation, real llvm-cov export produces a non-empty
totals.mcdc block: 315/468 = 67.31%, matching the report's 313/466 =
67.17% almost exactly (trivial build-to-build variance, well within the
~7-point floor margin either way). Directly confirmed the cpfusa
--mcdc-file schema mismatch by inspecting both cpp-FuSa's parser source
and a real llvm-cov export's actual JSON structure side by side.
Validated the new/modified ci.yml job's YAML parses cleanly. Full
non-instrumented tree rebuilt from scratch separately: 0 errors, 0
warnings, 58/58 ctest suites pass (100%) -- confirming this batch
doesn't affect the normal build path at all.

.github/workflows/ci.yml: +192 lines (809 -> 1001). AUDIT_PACK.md: +18
lines (140 -> 158, real-MC/DC provenance paragraph in §3 + new row in
the §5 CI Gate Summary table).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
…CHANGELOG (#169)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 7 (formal verification + MC/DC +
release-pipeline hardening), batch 3 -- closes the remaining Phase 7
scope: release-pipeline hardening, .fusa-dispositions.json, CHANGELOG.md.

## version-sources-agree (new CI job)

Ported from c-RCP's own equivalent, adapted for cpp-RCP's real version.hpp
syntax (a constexpr std::string_view, not a #define macro like c-RCP's).
While building this, found and fixed a real bug: .fusa.json had no real
project.version field at all -- cpfusa's config loader was silently
defaulting to "0.1.0" for every artifact it has ever generated for this
repo. Added "version": "2.26.0" to .fusa.json's project object.

## cpfusa cyber (renamed from its stale "--strict" name)

The job's name claimed --strict but its run: line never passed it --
--strict is a real flag, but cpp-RCP does not cleanly pass under it today
(838 WARNING-severity findings, overwhelmingly the expected shape of a
wire-protocol library's byte-packing/masking idioms and test-suite
loopback addresses, not defects -- but also not several hundred call
sites to responsibly mass-review or mass-waive in one batch). Renamed
the job to match its real behavior rather than either rushing the
findings or quietly keeping a misleading name. Also confirmed
.fusa-dispositions.json structurally cannot fix this either way:
cyber's --strict check never consults dispositions, unlike
check/lint/analyze's gated_exit_code().

## cpfusa analyze (real gate now, || true removed)

Root-caused rather than just removing the escape hatch: the job's own
build step left Catch2's vendored FetchContent source (including
Catch2's own SelfTest suite) inside build/, which cppcheck then
misreported as first-party ERROR findings on Catch2's own BDD macros --
that's what was actually keeping the job red, not a cpp-RCP defect.
Separately, compile_commands.json was left inside build/ while analyze
looks for it at the project root, so clang-tidy had never actually run
in this job at all. Fixed by copying compile_commands.json to the root
then deleting build/ before invoking analyze, so the scan only sees
first-party sources. With both fixed: 0 errors, real hard gate.

## .fusa-dispositions.json (new, legitimately empty)

Neither cpfusa lint (0 errors/138 warnings) nor the now-correctly-scoped
cpfusa analyze (0 errors/269 warnings/161 info) has any ERROR-severity
finding needing a waiver -- {"dispositions": []} is the honest state,
not a placeholder. Confirmed the mechanism itself (rule-ID-only
matching, a blanket per-rule waiver, not per-fingerprint) is reserved
for genuinely rule-wide architectural situations like c-RCP's own
alloc.c precedent -- no such situation exists in cpp-RCP's current
ERROR-level findings.

## release.yml: two new steps

IEC 62443 gap report (SL-2, artifact-only, mirrors the existing
ISO26262/IEC61508/DO-178C siblings) via cpfusa iec62443 --sl SL-2.

Shipped-artifact version verification: adapted from c-RCP's step, not
copy-pasted -- cpp-FuSa v0.18.0's release/qualify commands hardcode
their own module name in sbom.json/provenance.json/etc. regardless of
target project (confirmed by running cpfusa release and inspecting
output), so c-RCP's literal field-path doesn't apply. Uses report.html's
real "cpp-RCP v{version}" field (sourced from .fusa.json) plus a direct
version.hpp/.fusa.json re-comparison, since version-sources-agree
doesn't run on tag pushes.

## CHANGELOG.md (new)

Keep a Changelog format (no prior convention existed). Single
[Unreleased] -> v3.0.0 section grouped by Phase 1-7, one line per real
merged PR pulled from rewrite/v3-from-c-rcp's actual git history -- no
fabricated pre-rewrite history.

## Verification

Independently re-verified: both workflow YAML files parse cleanly.
Reproduced the version-sources-agree grep logic myself against the real
files (2.26.0/2.26.0/2.26.0, match) and against a deliberately-mutated
copy (correctly detects mismatch). Reproduced cpfusa cyber's real
exit codes myself: 838 warnings, exit 0 without --strict, exit 1 with
--strict -- matching the report exactly. Reproduced the fixed cpfusa
analyze pipeline myself from a clean build (compile_commands.json
copied to root, build/ deleted before scanning): 0 errors, 269
warnings, 161 info -- matching the report's 0/269/160 within a single
info-finding's worth of trivial build-path variance. Confirmed
.fusa-dispositions.json's empty state against both tools' real current
output. Spot-checked CHANGELOG.md's entries against real git history --
verbatim PR titles, not fabricated. Full tree rebuilt from scratch: 0
errors, 0 warnings, 58/58 ctest suites pass (100%).

.fusa.json: +1 line. .github/workflows/ci.yml: +86/-2 (1001 -> 1083 lines
... wait, actual diff: +84 net). .github/workflows/release.yml: +59
lines (248 -> 307). .fusa-dispositions.json: new, 3 lines. CHANGELOG.md:
new, 113 lines.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
…inject/ratelimit/mdns/authz/relay/bridges) (#170)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 8 (remaining modules), batch B of 2:
tsn, powerstate, faultinject, ratelimit, mdns, authz, relay/relay.hpp,
restbridge/grpcbridge -- all already fully implemented+tested from an
earlier untracked pass; this batch is a content-parity audit against
c-RCP, not a rewrite.

## Findings, per module

- tsn.hpp: already equivalent within a documented architectural
  divergence (c-RCP wraps a full AVTP transport + raw-frame priority
  classifier; cpp-RCP exposes a primitive apply_priority() called with
  an already-known RequestCategory) -- pre-documented in-header, no
  action. Catalog observation only (c-RCP's REQ-TSN-007/008 have no
  cpp-RCP analog, by design) -- left untouched, Phase 6 is closed.
- powerstate.hpp: no new findings beyond the 8 already-filed pending
  gaps from Phase 6 batch 13 (REQ-PWRMODE-004/012/017/019/021/022/
  026/027) -- confirmed NOT re-litigated. Confirmed wakeup.hpp (not
  powerstate.hpp) carries the actual SleepCMD/WakeUp wire codec
  equivalent to c-RCP's ep_wakeup.h -- a layering difference, not a
  missing capability. No changes.
- faultinject.hpp: capability-equivalent (Drop/Slow/Error/Timeout,
  count-limited rules). Notably its pick_rule() already fixes a real
  use-after-free/data-race an earlier revision had, documented in-file
  as cpp-RCP-D6 -- arguably more correct than c-RCP's own mechanism.
  No action.
- ratelimit.hpp: capability-equivalent (token bucket, safety-tag
  exemption, per-domain keying). Bootstrap/refill semantics match
  exactly. No action.
- mdns.hpp: capability-equivalent (Discoverer/Announcer, StaticDiscoverer,
  make_instance_name). One cosmetic-only divergence (hex case, service
  type string) -- not a wire-interop requirement since mDNS records are
  self-contained. No action.
- authz.hpp: genuine capability-granularity gap found, documented
  in-file rather than ported -- c-RCP keys policy entries on an opaque
  request_type byte (letting a policy distinguish read vs write within
  one request kind); cpp-RCP's RequestCategory taxonomy is coarser and
  cannot express that distinction. Pre-existing since v2.11.0, already
  justified at length in the header's own comment, and changing it
  would break the already-tested PolicyEntry/permit() contract -- out
  of this batch's authority. Documented as a known granularity gap for
  a possible future targeted fix, not force-fit here.
- relay/relay.hpp: confirmed structurally distinct from adapt.hpp
  (relay.hpp is the protocol-agnostic Channel<T>/Context/Node
  abstraction; adapt.hpp is the RCP-specific wiring on top, already
  covered by Phase 6 batch 12). c-RCP's own relay.h header states
  outright it is "a pure-C port of the same subset cpp-RCP's
  include/relay/relay.hpp exposes... not a full RELAY binding" --
  cpp-RCP's version is the reference here, confirmed full parity
  (superset, even -- templated Channel<T> vs one concrete type). No
  action.
- restbridge.hpp/grpcbridge.hpp: both compile-time interface stubs on
  both sides (no backend linked, every call returns not-supported).
  c-RCP's own header comments confirm they mirror cpp-RCP's stub
  behavior. Config shapes and defaults match exactly. No action.

## Verification

Independently re-verified: confirmed the c-RCP relay.h "pure-C port of
cpp-RCP's own relay.hpp" claim, the restbridge.h "mirrors cpp-RCP's own
stub" claim, and the faultinject.hpp cpp-RCP-D6 use-after-free-fix
comment all directly against source -- all accurate. Full tree rebuilt
from scratch: 0 errors, 0 warnings, 58/58 ctest suites pass (100%).

include/rcp/authz.hpp: +17 lines (comment only, zero behavior change).
No other files touched.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
…g/cli + dyndata/redundancy/sim re-verify) (#171)

* rewrite(phase8): batch A — content-parity audit (record/observe/config/cli + dyndata/redundancy/sim re-verify)

Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17),
branch rewrite/v3-from-c-rcp. Phase 8 (remaining modules), batch A of 2:
record, observe, config, cli -- all already implemented+tested from an
earlier untracked pass; audited against c-RCP for genuine capability
gaps. Plus dyndata/redundancy/sim (no c-RCP equivalent) re-verified for
integration/staleness against the now much more mature Phase 5-7 core.

## record.hpp vs c-RCP recorder.c/recorder.h

No functional gaps -- both sides deliberately diverge to different
capture granularities (c-RCP: raw ACF/AVTPDU frames; cpp-RCP: RC-Client
request/response pairs), already documented on both sides. Closed 5
real test gaps instead: write_binary()/read_binary() failure paths,
mutation-immunity of stored entries, Playback::run_all()'s actual-sleep
branch (previously only exercised at speed_factor=0.0), PlaybackConfig{}
default.

## observe.hpp: genuine gaps ported

- New standalone observe::record() free function (mirrors c-RCP's
  rcp_observe_record()) -- lets a caller record a span/counters
  directly without an ObservingClient wrapper; ObservingClient::request()
  now delegates to it.
- Span::stream_key added (previously byte_bus_id only, unlike c-RCP's
  full addr).
- Metric/MetricsSink::record_counter gained byte_bus_id (previously
  stream_key only, losing per-endpoint attribution within a stream).
7 new tests added.

## config.hpp: largest gap, includes a real latent bug fix

c-RCP's config.c had a later rework (server/hw_pin_map schema feeding
mock.c) cpp-RCP never picked up. Ported the two pieces reachable
without touching mock.hpp (out of scope): ServerManifest +
apply_to_mock() with c-RCP's exact merge semantics (magic overwritten
only if nonzero, options OR'd not overwritten), HwPinManifestEntry +
capacity-checked application to RegisterMap::hw_pin_map, load_to_mock()
combinator. NOT ported (documented, deliberate): c-RCP's dynamic
endpoints/streams sections -- mock::Server has no dynamic
endpoint-registration API, fixed at compile time.

Real bug fixed: the endpoint-entry parser only routed an object to
parse_endpoint_entry if BOTH stream_key and byte_bus_id keys were
present in the raw scan -- an entry with only one was silently
DROPPED rather than rejected. The same bug class c-RCP's own config.c
independently found and fixed (per its own 2026-08-13/#338 comment).
Fixed by routing on EITHER key, with parse_endpoint_entry itself now
the one place that validates both are genuinely present (throwing
ParseError if not) -- a malformed entry is now loudly rejected, not
silently dropped. 15 new tests added.

## cli.hpp: genuine gaps ported

- capabilities_json()'s features array now reports the 5 REQ-RMAP-030
  option names (time_sync/enhanced_cancel/trigger/chained/
  compound_bundles) -- cpp-RCP has real underlying implementations for
  all 5 but never advertised them.
- transports now includes "tsn" (a real, non-stub wrapper), matching
  c-RCP's own equivalent module.
5 new tests added (previously-untested help/--help/-h dispatch, dangling
--format value, status default text, new capabilities fields).

## dyndata.hpp / redundancy.hpp / sim.hpp (no c-RCP equivalent)

Confirmed clean, no changes: every call site verified to match current
lifecycle.hpp/e2e.hpp/regmap.hpp/watchdog.hpp/adapt.hpp/mock.hpp shapes
exactly; test coverage confirmed real (genuine construction, real
state-transition assertions, meaningful concurrency stress, real
mock::Server/Adapt() integration), not vestigial.

## Catalog flags, correctly left untouched (Phase 6 is closed)

- REQ-REC-006/007, REQ-OBS-009..022, REQ-CFG-007..020, REQ-CLI-005/006:
  various no-cpp-counterpart or cross-language content-mismatch ids.
- REQ-RED-001/002/003: catalog titles still say "RedundantController"
  (the retired class name) though text/code/tests correctly say
  RedundantRequestFn -- a catalog-only staleness artifact.

## Verification

Independently re-verified: confirmed the config.hpp routing fix
directly against source -- the "else if" now reads
`obj.find("stream_key") != npos || obj.find("byte_bus_id") != npos`
(was implicitly AND-gated before), and parse_endpoint_entry itself
throws ParseError if byte_bus_id is missing -- exactly the claimed
fix. Confirmed the REQ-RED-* title-staleness claim directly against
.fusa-reqs.json (titles say "RedundantController", code/tests say
"RedundantRequestFn" 12/13 times respectively). Full tree rebuilt from
scratch: 0 errors, 0 warnings, 58/58 ctest suites pass (100%).

include/rcp/cli.hpp: +28 (619->647). include/rcp/config.hpp: +267
(172->439). include/rcp/observe.hpp: +43 (168->211). include/rcp/
record.hpp, dyndata.hpp, redundancy.hpp, sim.hpp: unchanged.
tests/test_cli.cpp: +37. tests/test_config.cpp: +156.
tests/test_observe.cpp: +144. tests/test_record.cpp: +85.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

* fixup: batch A — fix MSVC /W4 narrowing warning in test_observe.cpp

PR #171's windows-2022/msvc/C++17 job failed: a braced-init-list
{3, 1, 4, 1, 5} deduces std::initializer_list<int>, and binding each
element to a range-for variable of avtp::ByteBusId (uint16_t) is a
narrowing conversion MSVC's /W4 /WX flags as error C4244 (clang/gcc
did not flag this, or flagged it differently, so it wasn't caught
locally on macOS/Linux toolchains). Fixed by making each list element's
type explicit (avtp::ByteBusId{3} etc.), eliminating the narrowing.

Verified: rebuilt test_observe standalone locally, 54/54 assertions
pass. Full tree rebuilt from scratch: 0 errors, 0 warnings, 58/58 ctest
suites pass (100%).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>

---------

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt
SoundMatt merged commit fe3e575 into main Aug 22, 2026
49 checks passed
SoundMatt added a commit that referenced this pull request Aug 22, 2026
Final step of the v3.0.0 cutover (cpp-RCP issue #129, ROADMAP.md Phase
17). CMakeLists.txt, include/rcp/version.hpp, and .fusa.json all still
declared 2.26.0 after merging the rewrite branch (#172) -- bumped all
three together (verified they agree, matching the version-sources-agree
CI gate's own check) so the v3.0.0 git tag doesn't ship a mismatched
version declaration.

Also promotes CHANGELOG.md's [Unreleased] section to a real [3.0.0]
release entry.

Verified: fresh rebuild, 0 errors/0 warnings, 58/58 ctest suites pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
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