Skip to content

ci: validate h2 security hotfix - #461

Closed
jscott3201 wants to merge 304 commits into
mainfrom
codex/sec-rustsec-2026-0258-h2
Closed

ci: validate h2 security hotfix#461
jscott3201 wants to merge 304 commits into
mainfrom
codex/sec-rustsec-2026-0258-h2

Conversation

@jscott3201

Copy link
Copy Markdown
Owner

Temporary validation-only draft PR for security PR #460 at exact head d111f48. This exists solely to run the main-target hosted Cargo Audit, MSRV, deny, and platform matrix. Do not merge. It will be closed after checks complete.

jscott3201 and others added 30 commits July 27, 2026 16:30
…224)

* fix(objects,server): take Event Type from the event algorithm (#210)

EventStateChange::event_type() derived the notification's Event Type from which
event states the transition involved, returning OUT_OF_RANGE when either end was
HIGH_LIMIT or LOW_LIMIT and CHANGE_OF_STATE otherwise. That is two of roughly
twenty BACnetEventType values, selected from EventState — a different axis.
Clauses 13.8.1.1 and 13.9.1.1: "Otherwise, this parameter shall have the value
associated with the event-initiating object's configured event algorithm."

event_type() now takes the algorithm as a required parameter and applies only
the FAULT rule. Each detector carries a pub const ALGORITHM (OUT_OF_RANGE /
CHANGE_OF_STATE / COMMAND_FAILURE), and fire() computes the final value where
both facts are known, carrying it out on TransitionOutcome. A required parameter
rather than an inferred default is the point: a detector added later cannot
silently fall back to a guess.

Breaking: TransitionOutcome gains pub event_type: EventType, and event_type()
gains a parameter.

The unconditional FAULT override is correct and retained. The sentence quoted in
#210 reads as though conditional on Notify Type, but the Event Type definition
has a third paragraph the issue omitted, giving ACK_NOTIFICATION its own
explicit CHANGE_OF_RELIABILITY rules rather than falling through to the
algorithm; Clause 13.2.5.3 states the rule symmetrically for transitions to or
from FAULT.

The wrong value was unreachable only by coincidence — each wired detector's
state vocabulary happened to map onto its algorithm. CommandFailureDetector is
the counterexample and now has an explicit regression test: its OFFNORMAL
transition reports COMMAND_FAILURE, where the heuristic returned CHANGE_OF_STATE
because OFFNORMAL is neither HIGH_LIMIT nor LOW_LIMIT.

Two related defects found while confirming this are filed, not folded in:
Binary Output and Multi-state Output use the wrong algorithm entirely (#222),
and the Event Enrollment path applies no FAULT override though Clause 12.12
forbids CHANGE_OF_RELIABILITY as an Event_Type value (#223).

Mutants verified dead: each detector's ALGORITHM mis-set; the FAULT branch
removed; the FAULT branch narrowed to the to-direction only.

2296 passed / 0 failed, 34 suites.

Closes #210

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(server): pin the detector-to-wire Event Type seam

An adversarial review found the seam this change exists to create had zero test
coverage: three mutations of the propagation path all survived the full
workspace suite with no failures.

  - the production From<(EventStateChange, EventType)> impl discarding the
    passed Event Type
  - lifecycle's periodic queue pushing a hardcoded Event Type
  - the encoding line substituting a constant

The cause was a cfg(test) From<EventStateChange> shim. All seven
build_and_send_event_notification calls passed a bare EventStateChange, so every
one resolved through the shim, which recomputed the value locally from a
hardcoded OUT_OF_RANGE. The production From impl was entered by no test at all.
The only two wire-level event_type assertions were FAULT transitions expecting
CHANGE_OF_RELIABILITY, which any constant satisfies.

This was also a coverage regression invisible in the diff. Before #210,
event_notifications.rs called change.event_type() directly, so those two FAULT
tests exercised the production derivation. The test file changed by zero lines,
yet they had silently moved onto cfg(test) code.

Remove the shim, so no test can bypass the production conversion, and pass
explicit (change, event_type) tuples at all seven sites. Extend
event_enable_set_permits_per_write_send to decode the frame and assert
OUT_OF_RANGE on the wire — a NON-FAULT transition, since CHANGE_OF_RELIABILITY
is what a hardcoded constant would produce anyway. Add
periodic_time_delay_carries_detector_event_type_to_wire, because the periodic
path propagates independently of the per-write path and a single test cannot
cover both.

Also corrects the CHANGELOG entry introducing TransitionOutcome, which still
described it as a two-field pair. Both entries sit in the same unreleased block,
so there is no release in which that shape was ever true.

Mutants now dead: production From discard (4 tests), periodic hardcode (1),
encoding constant (2).

2297 passed / 0 failed, 34 suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…222) (#227)

Clauses 12.7 and 12.19 require these object types to apply the COMMAND_FAILURE event
algorithm; both were wired to a ChangeOfStateDetector. On Binary Output the doc comment
above the field already read "COMMAND_FAILURE event detector" -- the intent was recorded and
never implemented.

They had no working intrinsic reporting at all, not merely the wrong algorithm. Only two
routes in the crate ever populate a detector's alarm parameters -- the shared
HIGH_LIMIT/LOW_LIMIT write arms used by the analog types, and MultiStateInputObject --
and neither type could reach either, so no Present_Value could raise an event.
COMMAND_FAILURE compares two live properties and needs no configured value list.

Adds Feedback_Value to bind pFeedbackValue (BACnetBinaryPV on Binary Output, Unsigned on
Multi-state Output), and the event-configuration properties needed to commission it.
Event_Enable had only a read arm, so with every detector defaulting to event_enable 0 the
transition bits were stuck at (F,F,F) and no notification could reach the wire, against
"shall support (T, T, T) at a minimum"; Time_Delay and Notify_Type had no arm at all.
read_event_properties!/write_event_properties! are split into generic and analog halves so
the generic group can be wired without dragging analog-only limits along. Acked_Transitions
stays read-only across the split -- it is modified only by AcknowledgeAlarm, which ORs where
a property write would assign.

Event_Detection_Enable is added to both types defaulting FALSE, making intrinsic reporting
opt-in. With detection unconditional, the first command to either type latched OFFNORMAL and
IN_ALARM for the rest of uptime, since nothing updates Feedback_Value on a device with no
physical I/O. Both clauses make the property group conditional on the object supporting
intrinsic reporting, so support is optional and always-on was the wrong reading. The standard
specifies no default and Clause 15.3 makes uninitialized values a local matter. The disabled
state is an invariant rather than an edge-triggered action, and holds by construction: these
types have no public set_event_state seeder and no set_event_state_internal override, so the
gated detector is the only route to Event_State.

Multi-state Output's Feedback_Value is deliberately not range-checked against
Number_Of_States -- Clause 12.19 treats an out-of-range value as a condition to report
(CONFIGURATION_ERROR, #226) rather than a write to refuse -- but is checked for
representability, since a bare cast wrapped a large BACnet Unsigned back into the valid state
range and silently suppressed the transition.

Deferred and tracked: #225, #135, #229, #230, #226, #228.

Verified: 2315 tests pass across 34 suites; 14 mutants killed with no survivors; a
differential over the macro split confirms all 9 write and 12 read arms behave identically
before and after. Two rounds of adversarial review, 15 findings, all addressed.
…bility (#219) (#233)

FaultDetector::evaluate swept every Analog Input, Analog Output and Analog Value,
recomputed Reliability from Min_Pres_Value/Max_Pres_Value, and wrote the result without
consulting Out_Of_Service, so a value written to simulate a fault was reset within one
evaluation interval. Since #167 the 1-second re-derivation then saw the reset value and
emitted a spurious TO_NORMAL notification.

The standard already defines when a client owns the property, so no ownership heuristic was
needed. Clauses 12.2(b) and 12.3(b) require Reliability to be "decoupled from the physical
input" (respectively output) while Out_Of_Service is TRUE, and 12.2(c)/12.3(c) that it
"shall be writable to allow simulating specific conditions or for testing purposes"; Clause
12.4(b) states the writability half for Analog Value, which has no decoupling item because
it has no physical point. Out-of-service objects are now skipped entirely -- not compared,
not written, and absent from the returned Vec<ReliabilityChange>.

The check fails open, so an object that does not report Out_Of_Service or reports it at an
unexpected type is still evaluated. Event-state-detection is deliberately not skipped: Clause
12.2(d) requires other functions to respond to a written Reliability "as if those changes had
occurred in the physical input", so a simulated value still drives the object to FAULT. Only
the overwrite stops.

Reliability_Evaluation_Inhibit is not the fix and is tracked as #232 -- TRUE forces
NO_FAULT_DETECTED rather than freezing the value, which would make the clobber permanent.
Two larger defects in the same evaluator are tracked as #231: the derivation of
OVER_RANGE/UNDER_RANGE from Min_Pres_Value/Max_Pres_Value is unauthorized (those describe the
engineering range; the standard's mechanism is the FAULT_OUT_OF_RANGE fault algorithm with
its own limits), and Clause 12.3 gives Analog Output no authorization to apply a fault
algorithm at all.

Verified: 2319 tests pass across 34 suites; three mutants killed with no survivors. Four new
tests use NO_SENSOR as the written value because the limit logic can only produce OVER_RANGE,
UNDER_RANGE or NO_FAULT_DETECTED, making preserved-versus-recomputed distinguishable.
…fault value (#217) (#234)

Clause 13.2.2.1's Fault state defines a ToFault transition -- "If reliability-evaluation
indicates a different Reliability value and the new Reliability value is not
NO_FAULT_DETECTED ... then perform the corresponding transition actions and re-enter the Fault
state" -- and the transition actions apply "even if the transition does not change the event
state".

fault_precedence reduced reliability to a boolean on its first line and no detector retained
the previous value, so a change from OVER_RANGE to NO_SENSOR while already in FAULT held
silently and produced no CHANGE_OF_RELIABILITY notification. This was code from #215, merged
earlier the same day; the enum simply had no variant for "still faulted, different value".

FaultPrecedence gains ReenterFault, and each of the three detectors gains
fault_reliability: Option<u32>, the value in force at the last entry to FAULT.

Whether FAULT holds remains a standing condition re-derived every evaluation; only the
transition is edge-detected. That distinction is load-bearing: fault_step runs at the head of
both probe and tick and the server drives tick once per second, so deciding re-entry from the
standing condition would emit a FAULT notification every second for as long as any object
stayed faulted. A test asserts an unchanged non-normal Reliability fires exactly once, and a
mutant attacks precisely that.

The in-FAULT-without-a-record case is decided explicitly rather than by wildcard
fallthrough. Re-entering is self-healing -- it stores the value and the invariant holds
afterwards -- whereas holding stores nothing, so the field would stay None forever and every
later genuine change would hold again, permanently disabling re-entry for that detector.

fire() needed no change: it never compared from to to, EventStateChange::event_type already
returns CHANGE_OF_RELIABILITY when either end is FAULT, and
EventTransition::for_target_state(FAULT) already yields ToFault.

Breaking: fault_reliability is a new public field on OutOfRangeDetector,
ChangeOfStateDetector and CommandFailureDetector, none of which is #[non_exhaustive].

Only the first disjunct of the ToFault condition is implemented; the second has no source in
this codebase. #166 is the same requirement in the Event Enrollment evaluator, shares no code
path, and is blocked on the change baseline from #137.

Verified: 2322 tests across 34 suites; five mutants killed with no survivors; two rounds of
adversarial review, 13 findings, all addressed.
…trinsic-reporting types (#216) (#236)

Analog Input/Output/Value, Binary Input/Value and Multi-state Input/Value
gained Event_Detection_Enable (ASHRAE 135-2020 Clauses 12.2, 12.3, 12.4,
12.6, 12.8, 12.18, 12.20). Binary Output and Multi-state Output already had
it, so all nine intrinsically-reporting types now model the property.

The property gates both intrinsic-reporting entry points through a new
four-ident arm of impl_intrinsic_reporting!, and a write of FALSE
establishes the Clause 13.2.2.1 initial conditions for the state each type
carries. The analog types reset Event_Time_Stamps and Event_Message_Texts
as well, being the only three that model them; the other four do not model
those properties at all, which is a pre-existing gap tracked as #235.

The default is TRUE, and deliberately differs from Binary Output and
Multi-state Output, which stay FALSE. The standard specifies no default.
The argument is not that the detectors are wholly inert: fault_step() runs
ahead of the event algorithm in both probe() and tick(), keyed on
Reliability alone, and is gated by neither LimitEnable::NONE nor an empty
alarm_values. It is that these seven previously used the *ungated* macro
arm, so that path always ran. TRUE preserves exactly the prior behavior,
where FALSE would have silently suppressed a fault path that works today on
Analog Input/Output/Value. A FALSE default would also have removed all
seven from GetAlarmSummary, GetEnrollmentSummary and GetEventInformation,
since Clause 13.12 makes absence mean included but a present FALSE mean
excluded.

binary/mod.rs is split into input.rs, output.rs and value.rs behind a thin
module root, matching how analog/ and multistate/ are already laid out. The
file had reached 705 LOC against the repo's 700 cap; it was also the only
one of the three modules keeping every object type in a single file.
Behavioral equivalence was checked by a normalized differential of the
extracted match arms, property lists and writability disjuncts, not by
inspection.

BREAKING: the three-ident ungated arm of the exported
impl_intrinsic_reporting! macro is removed. It had no callers left and
exporting an ungated form offers downstream implementors a supported way
to wire detection permanently on.

Fixes #216

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…across all nine intrinsic-reporting types (#218) (#239)

Reliability was handled three ways across the nine intrinsic-reporting
types. Analog Input/Output/Value accepted an unconditional WriteProperty;
Binary Input/Output/Value and Multi-state Input/Output/Value denied it
outright while still advertising Reliability in Property_List. All nine now
follow one rule.

The six that denied it were non-conformant. Every one of the nine object
clauses carries the same sentence in its Out_Of_Service description, under
the lead-in "When Out_Of_Service is TRUE:" — "the Present_Value property
and the Reliability property, if present and capable of taking on values
other than NO_FAULT_DETECTED, shall be writable to allow simulating
specific conditions or for testing purposes" (Clauses 12.2, 12.3, 12.4,
12.6, 12.7, 12.8, 12.18, 12.19, 12.20). The standard names Present_Value
and Reliability together under one condition, and this codebase already
gates Present_Value that way, so treating Reliability differently was the
anomaly rather than the fix.

OWNERSHIP IS SYMMETRIC, and enforced at the boundary rather than by
convention. While Out_Of_Service is TRUE the client owns Reliability: the
network write is accepted and internal evaluation is refused. While it is
FALSE the object's reliability-evaluation owns it: the network write is
refused with PROPERTY / WRITE_ACCESS_DENIED and only the internal route may
write. Returning to service clears a simulated value back to
NO_FAULT_DETECTED, because each clause decouples Reliability from the point
only while out of service; for the six types that have no reliability-
evaluation process, the re-coupled value is NO_FAULT_DETECTED. Without that
reset, three ordinary WriteProperty requests would leave those six
permanently faulted with no way to clear them — a state unreachable before
this change, since they refused every Reliability write.

Written values are validated against BACnetReliability rather than stored
as a raw u32; out-of-range values are refused with PROPERTY /
VALUE_OUT_OF_RANGE per Clause 15.9.1.3.

BREAKING: adds BACnetObject::set_reliability_internal, mirroring
set_event_state_internal from #130. The server's FaultDetector previously
wrote Reliability through the public write_property route, which the gate
would have broken since its purpose is writing to in-service objects. The
trait default returns OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, so the other 56
object types are unaffected — but bacnet-objects is a public dependency,
and a downstream implementor registered as an analog type must override the
method or lose fault detection. That case now logs a warning rather than
failing silently.

The persistence rule from #219 is unchanged. Resolves the open question in
#238.

Fixes #218

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(types): resolve enumeration, bit-string

* fix(types): resolve car-door-status as BACnetDoorStatus, harden resolver edge cases

Review fixes for #244:
- car-door-status (450) is BACnetARRAY[N] of BACnetDoorStatus; 135-2020
  defines no lift-specific door status enumeration, so the LiftCarDoorStatus
  variant is gone and DoorStatus gains the production's remaining
  constants (door-fault..limited-opened, 3-9).
- segmentation-supported values past u8 stay Unknown instead of wrapping
  into a valid-looking named variant.
- status-flags decodes by MSB-first wire position like every other
  bit-string type, so a wrong declared length no longer corrupts flags;
  bitstring now genuinely re-exports StatusFlags as its docs claimed.
- tracking-value's mapping rule exception is documented: its only
  ENUMERATED form is BACnetLifeSafetyState.
- ObjectTypesSupported sizing rationale now cites the BACnetObjectType
  range rather than the bit-string production.

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

---------

Co-authored-by: Justin Scott <jscott3201@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e 20.2.10 (#203) (#247)

Every ≤8-bit bit string crossing the wire now packs its first defined bit
into bit 7 of the octet, as Clause 20.2.10 requires. Previously
Event_Enable/Acked_Transitions (property reads/writes and
GetEventInformation ACKs) and Recipient_List valid_days/transitions were
packed LSB-first within the octet: TO_OFFNORMAL and TO_NORMAL arrived
swapped and the valid_days week was reversed for conformant peers. The
mirror-image decode hid the defect from every internal round trip.

The conversion lives in one place: bacnet_types::bitstring::pack_octet /
unpack_octet (byte reversal, which is its own inverse and left-aligns any
width), with typed to_bacnet() on EventTransitionBits and LimitEnable,
pinned by asymmetric spec vectors that a round trip cannot fake.

Already MSB-first and unchanged: Status_Flags, Limit_Enable, Ack_Required,
Protocol_Object_Types_Supported, Protocol_Services_Supported — issue
#203's limit_enable claim was stale. Raw logged bitstrings (trend/event
log) pass through byte-faithful as before.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…atch table (#192) (#249)

The Device object advertised a hardcoded six-byte constant whose comment
mislabeled three bits: twenty executed services were undeclared (Who-Is
and Who-Has among them), four initiate-only bits were declared
(Clause 12.11 ties the property to services *executed*), and the 41-bit
string could not represent subscribe-cov-property-multiple (41).

One truth chain now feeds everything: choice consts colocated with the
dispatch arms map through the new ServiceSupported::from_confirmed_choice/
from_unconfirmed_choice (the choice and bit numberings diverge for every
post-1995 service) into device::EXECUTED_SERVICES, which builds the
property (full production through you-Are(48), 7 octets) and the PICS
executor column. A cross-check test fails if any link drifts. The
byte-level device test is replaced with named-bit assertions, README
Server rows for AcknowledgeAlarm/ReadRange/AddListElement/
RemoveListElement corrected, and DeviceObject::set_services_supported
added for deployments with a different dispatch surface.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#250)

ASHRAE 135-2020 defines no lift-car-door-status production. The Lift
object's Car_Door_Status (Clause 12.59) is BACnetARRAY[N] of
BACnetDoorStatus, whose Clause 21 production DoorStatus already models
value-for-value, and resolve_value already routes property 450 through
DoorStatus. The removed type assigned incompatible numbers to eight of
the production's ten named values (CLOSED=3 vs the standard's closed=0)
and did not name safety-locked (8) or limited-opened (9) at all.

BREAKING CHANGE: bacnet_types::enums::LiftCarDoorStatus is removed.
It was glob-exported public API, though nothing in this workspace
referenced it. Use DoorStatus for Car_Door_Status values.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…246, #241) (#251)

* fix(types): add missing BackupAndRestoreState and Reliability values (#246, #241)

BACnetBackupState defines backup-failure (5) and restore-failure (6) —
states a device legitimately reports during Clause 19.1 backup/restore
procedures — and BACnetReliability defines multi-state-out-of-range
(25). Neither was named, so both open enums displayed the raw number
(a device reporting backup-failure showed as "5", including through
resolve_value on Backup_And_Restore_State). Purely additive: raw
values already round-tripped, no wire-format change, ALL_NAMED and
Display pick the constants up from the macro.

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

* docs(types): align the Reliability value-11 comment with the production

The production's own comment is "enumeration value 11 is reserved for
a future addendum", not "removed from standard" — flagged by both
reviewers once the enum otherwise matched the production end to end.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…I and MSV (#229) (#254)

* fix(objects,server): make event commissioning reachable on BI, BV, MSI and MSV (#229)

Event_Enable was readable but not writable on Binary Input, Binary
Value, Multi-state Input and Multi-state Value, and Time_Delay/
Notify_Type were absent entirely. Every detector defaults its
transition bits to (F,F,F), so with no write path no event
notification could ever be distributed from these types — Clauses
12.6, 12.8, 12.18 and 12.20 require (T,T,T) support at a minimum.

Wire the four types into the shared generic event property macros
(the #227 split already carried by Binary Output and Multi-state
Output), delete the inline read arms the macros shadow, advertise
the event set in Property_List, and route PICS writability through
is_generic_event_property_writable. Event_State stays read-only;
Acked_Transitions writes stay denied (AcknowledgeAlarm's job).
Inline Event_Detection_Enable arms are kept — the macros
deliberately do not cover EDE.

Distribution is pinned at the wire with single-bit Event_Enable
fixtures on Multi-state Input (the only one of the four whose
detector can be fed until #228): the TO_OFFNORMAL bit alone emits
the CHANGE_OF_STATE notification; TO_FAULT-only, TO_NORMAL-only and
empty masks emit nothing.

Implemented by an Opus 5 lane from the grounded work order; gates
and commit by the orchestrator.

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

* fix(objects,server): address #229 review findings — prose accuracy and test coverage

Codex spec review (CONCUR x7 on implementation, DISPUTE on prose):
- The four object clauses require the supported Event_Enable value set
  to include (T,T,T); they do not mandate network writability. Comments,
  CHANGELOG and commit prose now separate the normative requirement from
  the repo fact (detectors default (F,F,F) with no commissioning path).
- Acked_Transitions is maintained by the alarm-acknowledgment process
  (AcknowledgeAlarm or a local means, Clause 13.2.3), not exclusively by
  the AcknowledgeAlarm service. Reworded everywhere, including the
  pre-existing common.rs and #227 CHANGELOG wording.

Opus adversarial review (3 lenses, no blockers):
- Add the missing TO_NORMAL positive direction to the wire tests: a gate
  written `event_enable & 0x01` instead of `& transition_bit` survived
  the previous suite. Both directions now decode to_state/from_state and
  notify_type off the wire.
- Pin refusals to PROPERTY/WRITE_ACCESS_DENIED instead of .is_err().
- Wire-order literal anchor (0x20) added to the multistate test file.
- Fix the distribution test docstring that wrongly claimed the alarm
  state was established through network entry points.
- Correct module-header clause ranges (binary: 12.6-12.8; multistate:
  12.18-12.20), resolve the #227/#229 CHANGELOG contradiction, mount
  binary's test file like its siblings, drop a redundant import.

Filed from findings: #255 (macro write-validation gaps, pre-existing).

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

* docs(objects): sweep the remaining disputed normative paraphrases (#229 re-verdict)

The re-verdict found five residual sites the list-driven fix pass
missed; this sweep is phrase-driven and grep-proven empty. Three
"only AcknowledgeAlarm may change it" comments and one causal
(T,T,T)-requires-a-write-path formulation from commit 3ede5a9, plus
the same #222-era formulation on the BO/MSO assertions two screens
away. The alarm-acknowledgment wording now names both maintenance
inputs Clause 13.2.3 defines: event-state transitions, and
acknowledgment indications from AcknowledgeAlarm or a local means.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…le (#228) (#256)

ChangeOfStateDetector.alarm_values defaulted empty with no network route
to populate it, so OFFNORMAL was unreachable on BI/BV/MSI/MSV.

Binary Input drops the invented plural Alarm_Values arm for Table 12-6's
singular Alarm_Value of BACnetBinaryPV; Binary Value gains the same.
Both default to ACTIVE(1) with the detector armed (owner ruling — the
standard defines no default): Event_State reacts to Present_Value out
of the box while distribution waits on Event_Enable. Multi-state
Input/Value Alarm_Values (BACnetLIST of Unsigned, Table 12-21/12-23) is
configurable via AddListElement/RemoveListElement with readback served
from the detector-owned list; whole-list WriteProperty stays blocked by
the single-primitive write decoder (#182) and is pinned as such. List
writes reject an array index (PROPERTY_IS_NOT_AN_ARRAY) and cap at
1,024 elements; the cap maps to NO_SPACE_TO_ADD_LIST_ELEMENT on the
ALE path per Clause 15.1.

Removed as invented or unimplemented surface: Multi-state Output's
Alarm_Values/Fault_Values (Table 12-22 defines neither; COMMAND_FAILURE
uses Feedback_Value), and Fault_Values on MSI/MSV (parameterizes the
Clause 13.4 FAULT_STATE algorithm this codebase lacks; the property is
optional, so omission is conformant).

BREAKING CHANGE: MultiStateInputObject::set_fault_values removed;
Multi-state Output no longer serves Alarm_Values/Fault_Values; Binary
Input returns UNKNOWN_PROPERTY for plural Alarm_Values; ResolvedEnum
gains a BinaryPV variant (property 6), breaking downstream exhaustive
matches.

Gates: Codex spec grounding before implementation; Codex implementation
lane; Opus adversarial review (3 lenses; blocker and all should-fixes
addressed); fresh Codex spec review CONCUR after two correction rounds;
CI green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…257)

The engine was a second, uncalled intrinsic-reporting evaluator with
different conformance behavior than the production path: no production
caller (only its own tests exercised it), no Event_Detection_Enable
gate (Clause 13.2.2.1 suppresses the state machine entirely when
FALSE), and it keyed CHANGE_OF_STATE off the plural Alarm_Values
identifier — always empty, and not served at all by Binary Input/Value
since the singular correction (#228). Two evaluators with divergent
conformance is the defect; deleting the dead one closes the class.

The deleted tests lose no live coverage: every assertion either maps
1:1 to the detector suites in bacnet-objects or exercised algorithms
(FLOATING_LIMIT, CHANGE_OF_BITSTRING, CHANGE_OF_VALUE) with no
intrinsic detector — those run on the separate Event Enrollment and
COV paths with their own suites. Also fixes the architecture doc's
task table, which misattributed intrinsic reporting to the 10s
event-enrollment task and omitted the 1s intrinsic_reporting_task.

BREAKING CHANGE: the public bacnet_server::intrinsic_reporting module
(IntrinsicReportingEngine, IntrinsicEvent) is removed. Drive objects
through the BACnetObject trait path, evaluate_intrinsic_reporting and
tick_intrinsic_reporting, gated on Event_Detection_Enable — the route
the production server uses.

Gates: Codex implementation lane (BLOCKED honestly on sandboxed socket
binds; orchestrator re-ran 34/34 workspace suites green); Opus
adversarial review (2 lenses, full lost-coverage audit, both findings
fixed); fresh Codex spec check CONCUR; CI green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…six remaining event types (#235, #230, #258) (#267)

* fix(objects): model Event_Time_Stamps and Event_Message_Texts on the six remaining event types (#235, #230, #258)

Binary Input/Output/Value and Multi-state Input/Output/Value now store
both arrays in a shared single-owner EventHistory, list them in
Property_List, and restore them to initial conditions on the
Event_Detection_Enable FALSE reset. The analog trio migrates onto the
same struct with no behavior change, and the hand-inlined read arms
leave read_analog_event_properties!, shrinking common.rs.

Spec gate (dual-agent, 12/12 concur): Event_Time_Stamps is required on
all six types; Event_Message_Texts is optional and added for parity
with the analog three. Array-index semantics stay with #171; transition
stamping stays with #123.

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

* fix(objects,server): address adversarial review of the EventHistory slice

EventHistory::read now implements Clause 12.1.5.1 array-index semantics
(count at index 0, single element at 1-3, INVALID_ARRAY_INDEX beyond)
for both properties, closing the regression where indexed reads of the
newly modeled properties answered with nonconforming ACKs and
SubscribeCOVProperty accepted out-of-range indexes. Element encoding
stays in the interim flattened form pending #171.

Also from the review: analog delegation and lossy-projection coverage
(new analog/tests/event_history_read.rs), detection-disable reset
wiring tests on all six new types, full-value decoding in the two
handler tests, the CHANGELOG contradiction with the earlier #229 entry,
stale macro rustdoc, restored slot-order docs, and a same-type cast.

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

* docs(changelog): correct the retained footnote claims the spec review disputed

The Event_Detection_Enable entry claimed Event_Time_Stamps and
Event_Message_Texts sit in the same required-if footnote group; the
dual spec gate found Event_Message_Texts carries only the
presence-restriction footnote on all six tables. Also move the entry's
partial-invariant paragraph and the Multi-state Output aside to
historical tense now that the storage entry above closes both gaps.

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

* docs(changelog): stale-after-first-event, not always-wrong, in the MSO aside

Initial zero sequence-number timestamps are correct until the first
corresponding transition per Clause 12.19, as the same paragraph's
closing clause already says.

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

* test(server),docs(changelog): close the verify-pass residuals

Handler-level indexed-read coverage: ReadProperty index 0 returns the
count with the index echoed, index 4 yields PROPERTY /
INVALID_ARRAY_INDEX, RPM ALL decodes Event_Time_Stamps symmetrically
with Event_Message_Texts, and an explicit indexed RPM reference decodes
exactly one element with the echoed index.

CHANGELOG: the Changed bullet now states the real analog delta -
unindexed reads and resets unchanged, indexed reads previously ignored
the index and now follow Clause 12.1.5.1 - and the macro-split entry no
longer claims the analog half keeps the event-history reads.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ier (#268)

* feat(types): impl serde::Deserialize for ObjectType & PropertyIdentifier

* feat: make serde feature-gated

* feat: impl FromStr in bacnet_enum
…arameters, Recipient_List, and unified BACnetTimeStamp (#154, #152, #259) (#271)

* fix(services)!: unify BACnetTimeStamp on the shared primitives codec (#259)

GetEventInformationAck encoded time [0] as opening-tag-0 / application Time /
closing-tag-0 while the primitives codec used the Clause 20.2.1.5-conformant
form (primitive context tag 0, length 4, raw Time octets); each decoder
rejected the other's bytes. Clause 20.2.1 rules for a context tag on the
primitive base type Time settle the form: the primitives codec wins.

- Split the primitives codec into a bare-CHOICE pair
  (encode_timestamp_choice / decode_timestamp_choice) plus the existing
  wrapped pair built on them, so bare-CHOICE contexts (the ACK's
  eventTimeStamps SEQUENCE OF) and tagged contexts (audit, alarm ack,
  event notification, COV multiple, notification parameters) share one
  encoding. Existing wrapped call sites are byte-identical.
- Migrate GetEventInformationAck encode/decode to the shared codec; its
  encode and COVNotificationMultipleRequest::encode are now fallible.
- Enforce the Clause 21 range Unsigned (0..65535) for sequence-number on
  encode and decode; wrap the server's seconds-of-epoch notification
  timestamps into the valid window instead of emitting non-conformant
  >65535 sequence numbers.

Tests: golden byte vectors for all three alternatives (bare and wrapped),
boundary 0/65535 accept + 65536 reject on both encode and decode (incl. the
3-octet overflow form), a cross-call-site matrix proving primitives-encoded
bytes decode through GetEventInformationAck and vice versa, and negatives
(wrong outer tag, wrong class bit, truncated, opening-tag-0 time form).

* feat(encoding)!: full ASN.1 framing for Event_Parameters/Fault_Parameters (#154)

The Event_Parameters/Fault_Parameters wire form was a simplified flat
application-tagged list with a leading Unsigned discriminant — internally
consistent but not interoperable with the Clause 21 productions of
BACnetEventParameter / BACnetFaultParameter. Encode with full ASN.1 framing:

- Every modeled CHOICE alternative over a SEQUENCE is emitted as an
  opening/closing context tag pair with members context-tagged per the
  production (change-of-bitstring [0], change-of-state [1], change-of-value
  [2], floating-limit [4], out-of-range [5], extended [9]; fault-none [0]
  NULL, fault-characterstring [1], fault-extended [2], fault-life-safety
  [3], fault-state [4], fault-status-flags [5], fault-out-of-range [6],
  fault-listed [7]).
- Inner tagged CHOICEs (cov-criteria [1], min/max-normal-value [0]/[1]) are
  explicitly tagged around the alternative's own tag; fault-out-of-range
  inner alternatives are discovered by application tag (REAL/Unsigned/
  Double/INTEGER) and the f64 model encodes as Double.
- BACnetPropertyStates elements are framed with the 135-2020 CHOICE tags,
  which corrects the legacy flat codec's invented tags for the door
  (15..=19, was 14..=18), timer (43/44, was 38/39) and lift (52/53, was
  40/42) variants; restart-reason [14] is unmodeled.
- BACnetDeviceObjectPropertyReference members are framed as their Clause 21
  SEQUENCE body.
- Unmodeled alternatives (command-failure [3], buffer-ready [10], none [20]
  NULL, etc.) round-trip byte-for-byte through Opaque via a raw scanner
  (bacnet_encoding::tags::extract_raw_context); the omitted/deprecated/
  reserved tags 6, 7, 12, 19 are rejected on decode.

A new PropertyValue::ApplicationData(Vec<u8>) carries the framed bytes
through the object read/write paths (verbatim on encode; produced by
decode_application_value for context-tagged elements). The EventEnrollment
read arms emit the framed form; the write arms and the server evaluator
accept framed (primary) plus the legacy flat form and the legacy
OctetString-to-Opaque path (fallback decode preserved per #129). The
services-side notification_parameters codec is deliberately untouched
(Tranche C); its pre-2020 property-state tags diverge from this codec — to
be resolved when the notification_params production is migrated.

Tests: golden byte vectors (ChangeOfState, ChangeOfValue bitmask+increment,
FloatingLimit, OutOfRange, Extended, FaultNone, FaultOutOfRange, FaultState,
FaultCharacterString, FaultExtended, FaultLifeSafety, FaultStatusFlags,
FaultListed), property-state goldens + all-modeled-variant round-trips,
reserved/deprecated tag rejection, truncated/unbalanced negatives, framed
write/read round-trips on the object with byte-identical re-emission.

* feat(objects)!: full ASN.1 framing for Recipient_List (#152)

Recipient_List was encoded as a flat PropertyValue::List of 7-field entries
with the recipient discriminated by application tag — internally consistent
but not interoperable with the Clause 12.21 BACnetLIST of BACnetDestination.
Encode with full ASN.1 framing per the Clause 21 productions:

- BACnetDestination members are untagged, so each entry is seven
  application-tagged elements in order (Bit String valid-days, Time,
  Time, recipient, Unsigned process-identifier, Boolean
  issue-confirmed-notifications, Bit String transitions); the BACnetLIST is
  their concatenation with no wrapper.
- BACnetRecipient CHOICE: device [0] as a primitive context tag 0 holding
  the 4-octet ObjectIdentifier; address [1] as an opening tag 1 /
  application-Unsigned16 network-number / application-OctetString
  mac-address / closing tag 1 (BACnetAddress is constructed).

The framed codecs live in bacnet_encoding::constructed::recipient (shared
destination/recipient/list encode-decode helpers). The NotificationClass
read arm emits the framed bytes via PropertyValue::ApplicationData; the
write arm accepts the framed form strictly plus the legacy flat form as a
fallback, and the recipient filter decodes both forms through one shared
flat-entry helper (destination_from_flat_fields) so the write and filter
decoders can no longer drift apart — the duplicate decode is gone.

Tests: golden wire vectors for the device-form, address-form (nonzero
network), and broadcast (zero-length MAC) destinations; an 8-entry list
round-trip at both the codec and object level (Annex K.2.25 minimum);
negatives for recipient context tag [2], truncated members, unbalanced
address recipient, and Unsigned16 overflow of the network number.

* docs(conformance): add framed-parameter/Recipient_List/timestamp ledger rows (#154, #152, #259)

Split clause-backed child rows out of BACNET-12-OBJECT-MODEL and
BACNET-21-FORMAL-APDUS now that golden-vector and negative tests provide
the evidence: BACNET-12-RECIPIENT-LIST-FRAMING (Clause 12.21 + Clause 21,
Annex K.2.25 AE-CRL-B posture), BACNET-12-EVENT-PARAMETERS-FRAMING
(Clause 12.12 + Clause 21 modeled alternatives with Opaque preservation
and legacy-fallback notes), and BACNET-21-TIMESTAMP-CHOICE (Clause 21
BACnetTimeStamp + Clause 20.2.1.5 tag forms). support-summary.md is
regenerated from the JSON by scripts/generate-conformance-docs.py
(--check passes). Also corrects two Annex references in test comments
(K.2.25 AE-CRL-B, not K.2.15).

* fix(server): hand complete framed Recipient_List content to the write arm (#152)

handle_write_property/handle_write_property_multiple decode a write's
propertyValue bytes with the single-element decode_application_value. For
a BACnetLIST property the framed form is a concatenation of destinations
whose members are application-tagged, so the generic decode consumed only
the first member (the valid_days bit string) and discarded the rest — the
whole-list decode gap already pinned in-tree as #182. Route RECIPIENT_LIST
writes through a helper that hands the object the complete content as
PropertyValue::ApplicationData; the framed context-tagged CHOICE
properties (Event_Parameters, Fault_Parameters) keep working through the
generic path because a CHOICE is exactly one context-tagged element.

Adds wire-level integration tests: framed WriteProperty (+ one WPM) of
Event_Parameters, Fault_Parameters, and Recipient_List round-trip through
handle_read_property byte-identically; a reserved-tag framed write is
rejected with a PROPERTY error.

* test(encoding): drop unused binding in recipient negative test (#152)

* fix(server): never route framed unmodeled event-parameter Opaques to the LE evaluator (#154)

Review blocker: a conformant framed Event_Parameters write of an UNMODELED
spec alternative (e.g. access-event [13]) surfaces as
BACnetEventParameter::Opaque — like the legacy raw-octet path — and the
evaluator fed its context-tagged TLV body to eval_legacy_le, reinterpreting
tag bytes as little-endian f32 limits and fabricating spurious
HIGH/LOW_LIMIT transitions (and notifications) from a peer's legitimate
configuration. Only true legacy raw-octet writes (sentinel tag 0xFF) route
to the little-endian evaluator now; framed unmodeled alternatives are
stored, read back byte-identical, and never evaluated, matching their
pre-framing posture.

Test: framed_unmodeled_alternative_is_never_le_evaluated writes a framed
access-event [13] whose TLV body would decode as a 0.0/0.0/0.0 LE band to
an OUT_OF_RANGE enrollment monitoring 85.0, asserts the value round-trips
verbatim, and asserts zero transitions (pre-fix: one HIGH_LIMIT).

* fix(server): wrap COVNotificationMultiple timeOfChange like the request timestamp (#259)

Review blocker: the request-level timestamp wrapped seconds-of-epoch mod
65536 while the per-value timeOfChange [3] content — built two statements
later — encoded raw seconds via encode_ctx_unsigned, reintroducing the
>65535 sequence-number the wrap exists to keep off the wire (Clause 21
constrains BACnetTimeStamp's sequence-number to Unsigned (0..65535), and
the shared codec rejects larger values on encode). Cov_multiple_timestamps
computes the wrapped value once for both fields.

Test: cov_multiple_timestamps_wrap_seconds_of_epoch_into_range decodes the
produced timeOfChange bytes through the shared timestamp codec and asserts
sequence-number ≤ 65535 at 2026-era seconds-of-epoch, 0/42/65535/65536
boundaries.

* fix(objects): reject indexed Recipient_List writes again (#152)

Review blocker: on dev @ 6b9ac4f an indexed Recipient_List write was
rejected with PROPERTY/INVALID_DATA_TYPE (verified empirically: the
single-entry wire shape never formed the List-of-entries the write arm
required). After the framing migration the arm ignored the array index
entirely, so an indexed framed write decoded fine and silently replaced
the whole list with the one written destination. Clause 15.5 permits an
array index only with array datatypes and Recipient_List is a BACnetLIST;
restore the rejection (indexed-array Recipient_List semantics belong to
the #260 family, Tranche K). Note: an indexed full-List-shaped in-process
write dev happened to accept is now rejected too — that acceptance was
itself accidental, not a semantic.

Test: recipient_list_indexed_write_rejected_list_unchanged covers the
framed and legacy-flat shapes at indices 1 and 2, asserts
PROPERTY/INVALID_DATA_TYPE, and asserts the stored three-entry list is
bytewise untouched.

* fix(server): edit framed Recipient_List by decoded destination, not empty fallback (#152)

Review blocker: AddListElement/RemoveListElement matched the read value as
PropertyValue::List; a framed Recipient_List reads back as ApplicationData,
so the handlers fell through to an empty Vec and the write-back of List([])
responded success while wiping every destination. Both handlers now branch
into a shared framed_destination_list_edit: stored list and listOfElements
are decoded with the strict framed destination codec (malformed input is a
determinate PROPERTY/INVALID_DATA_TYPE, never an empty fallback), element
matching works on decoded BACnetDestinations, and the merged list is
re-framed on write-back. Also restores handle_write_property's doc-block
attachment (decode_write_property_value's docs were spliced into it).

Tests: remove one entry leaves the rest (bytewise), removing a
non-matching entry is a byte-identical no-op, AddListElement appends two
entries (bytewise), malformed payload errors PROPERTY/INVALID_DATA_TYPE
with the list untouched.

* fix(objects): reject framed event/fault writes with trailing garbage (#154)

Packaging blocker: both framed write arms discarded the codec's consumed
offset, so a complete framed element followed by garbage was accepted on
write while read-back emitted only the first element (silent data loss
between the peer's write and our read-back). Both arms now require the
framed decode to consume the ENTIRE payload, else PROPERTY/INVALID_DATA_TYPE.

Tests: write_event_parameters_framed_trailing_garbage_rejected and
fault_parameters_framed_trailing_garbage_rejected append 1..=4 garbage
bytes after a valid framed element, assert PROPERTY/INVALID_DATA_TYPE, and
assert the stored value is untouched (default Opaque sentinel / Null
respectively).

* fix(encoding): validate fixed-width bit strings in BACnetDestination decode (#152)

Packaging blocker: valid-days and transitions were unpacked with no width
checks, so a zero-length or overlong bit string (or one with a mismatched
unused-bits count) decoded as all-zero — a silently dormant destination.
Per Clause 20.2.10 + the Clause 21 productions, BACnetDaysOfWeek (7 named
bits) is exactly 1 content octet with unused_bits 1, and
BACnetEventTransitionBits (3 named bits) is exactly 1 content octet with
unused_bits 5; anything else is now a decode error.

Tests: destination_wrong_width_valid_days_rejected and
destination_wrong_width_transitions_rejected cover zero content octets,
two content octets, and mismatched unused-bits counts; the device/address/
broadcast golden vectors pass unchanged.

* fix(objects,server): fail Recipient_List routing closed on malformed stored lists (#152)

Packaging blocker: the shared decode_destination_list_pv stopped at the
first malformed destination and kept the valid prefix, so routing would
notify only part of a configured Recipient_List. It is now strict (first
malformed destination or trailing bytes fail the whole decode, framed and
legacy forms alike); filter_recipient_list keeps its Vec signature but
yields nothing on decode failure, and routing uses the new
get_notification_recipients_strict -> Option<Vec<...>>: Some on
no-class/unreadable/valid (existing broadcast fallback preserved), None on
an undecodable stored list, which the event-notification sender now logs
and skips entirely (consistent with the encode-failure branches beside it).

Tests: recipient_list_malformed_tail_fails_whole_decode_no_prefix_delivery
(valid destination + truncated tail -> decode error; filter returns
empty), routing_skips_delivery_when_stored_recipient_list_is_malformed
(strict lookup None for a malformed stored list vs Some([]) for a missing
class).

* style: cargo fmt over the review-blocker fixes

* docs(conformance): refresh provenance to the framing tranche tip (#154, #152, #259)

Reviewed-at 2026-08-12; repo_sha 265b267 (the codex/encoding-asn1-framing
tip at time of writing — the last code commit; the docs-refresh commit
lands after it, matching how e6be3ea was recorded last tranche; the
convention is now stated in the ledger header). Review scope/addenda note
now cover this tranche's productions. Row wording fixes per the packaging
review: the Recipient_List row's claim is scoped to the modeled
NotificationClass path with precise strict-decode wording (fixed-width
bit-string members + fail-closed routing); the Event/Fault row's Opaque
preservation claim carries the none [20] primitive-vs-constructed caveat.
support-summary.md regenerated (check passes).

* test(integration): move the reviewed_at ledger pin with the provenance refresh (#154, #152, #259)

Same convention as 65fd871 (the Clause 5 tranche): the pinned
ledger-reviewed date in the schema test travels with the docs' provenance
refresh (now 2026-08-12).
…190, #260, #266) (#272)

* fix(server,objects): uniform array-index gating across RP/RPM/WP/WPM (#190, #260, #266)

Replace ReadProperty's identifier-static is_array_property whitelist with a
per-object BACnetObject::is_array_property query (default keyed by the
Clause 12 object tables) and apply the gate on all four object-access
service paths: an array index on a BACnetLIST property is rejected with
PROPERTY / PROPERTY_IS_NOT_AN_ARRAY (Clause 15.5.1.3, Clause 15.9.1.3).
RPM reports the rejection inline per property reference; WPM rejects in the
validation phase so the commit loop never starts.

The default classification admits identifier-stable BACnetARRAY properties
(OBJECT_LIST, PROPERTY_LIST, STATE_TEXT, PRIORITY, WEEKLY_SCHEDULE,
EXCEPTION_SCHEDULE, EVENT_TIME_STAMPS, EVENT_MESSAGE_TEXTS, PRIORITY_ARRAY,
TAGS, SUBORDINATE_LIST, SUBORDINATE_ANNOTATIONS, GROUP_MEMBERS,
GROUP_MEMBER_NAMES) and resolves the type-dependent identifiers by object
type: ALARM_VALUES/FAULT_VALUES are arrays on CharacterString/BitString
Value (lists elsewhere), LIST_OF_OBJECT_PROPERTY_REFERENCES is an array on
Channel (list on Schedule/Lighting Output), and PRESENT_VALUE is a
BACnetARRAY on Global Group.

Notification Class Recipient_List indexed writes move from the tranche-J
INVALID_DATA_TYPE stopgap to the same PROPERTY_IS_NOT_AN_ARRAY
classification. Direct PRIORITY_ARRAY writes with an omitted index now
surface PROPERTY / WRITE_ACCESS_DENIED (Clause 12.1.5.1 whole-array access,
unsupported) instead of an unmappable Error::Encoding; indexes 0 and 17
stay INVALID_ARRAY_INDEX.

Refs #171: the service-side gating half lands here; the object-side
Event_Time_Stamps/Event_Message_Texts datatype residue stays open.

* docs(conformance): add BACNET-15-ARRAY-INDEX-GATING ledger row (#190, #260, #266)

Clause-backed evidence row for the array-index gating across
ReadProperty/ReadPropertyMultiple/WriteProperty/WritePropertyMultiple
(Clause 15.5.1.3 / 15.9.1.3, with the Clause 12.1.5 array-vs-BACnetLIST
classification), refreshing the scope/provenance fields to this tranche.
repo_sha names the branch tip's last code commit (6c3d6a8) per the
documented convention.

* fix(objects,server): complete array classifier + test hardening (PR #272 review)

Review follow-ups on the array-index gate:

- Complete the classifier for the remaining modeled arrays: Command ACTION
  (Table 12-12) and Staging STAGES/STAGE_NAMES/TARGET_REFERENCES
  (Table 12-80) admit an index; ACTION_TEXT stays rejected (array in the
  standard but unmodeled in-tree), as do Channel's EXECUTION_DELAY /
  CONTROL_GROUPS (unmodeled). Correct the citation: the other BACnetLIST
  carrier of LIST_OF_OBJECT_PROPERTY_REFERENCES is Timer (Table 12-75), not
  Lighting Output. Unmodeled array identifiers are documented as rejected
  until their object-side modeling lands.
- Split analog/tests/input_output.rs (707 LOC) into input.rs + output.rs
  under the 700-LOC CI cap.
- Add WPM atomicity proof: a mixed request (valid DESCRIPTION write +
  gated indexed RECIPIENT_LIST write) fails as a whole and applies nothing.
- Pin exact class/code in the Recipient_List legacy-flat arm; add the RP
  wire-path acceptance for Notification Class PRIORITY (BACnetARRAY[3],
  Table 12-24) indexes 0 and 1..=3; rename the Schedule test to the RP+WP
  surface it actually covers.

* docs(conformance): refresh array-gating row after review fixes; repo_sha=db07098

Extend BACNET-15-ARRAY-INDEX-GATING with the completed classifier
(Command ACTION, Staging STAGES/STAGE_NAMES/TARGET_REFERENCES), qualify
coverage as the in-tree modeled set (unmodeled array identifiers stay
rejected until modeled), correct the LIST_OF_OBJECT_PROPERTY_REFERENCES
list citation to Timer (Table 12-75), and point provenance at the last
code commit of the review-fix round.
…e + feat: writable Relinquish_Default (#252, #240, #255, #270) (#276)

* refactor(objects): derive reliability validity from enum (#252)

is_reliability_value_valid restated the BACnetReliability named set as a
hand-written literal (0..=10 | 12..=25 | 64..=65535). The literal was
spec-derived when the enum was short (#241 added the missing consts);
with Reliability::ALL_NAMED generated from the const list, restating the
set is a pure drift hazard: the next addendum constant would be silently
rejected by the write path until someone remembered the literal.

Derive the named set from Reliability::ALL_NAMED; keep the reserved-11
gap and the 26..=63 ASHRAE-reserved span implicit, and the 64..=65535
vendor-proprietary range explicit (Clause 21). The nine call sites are
untouched.

Tests: exhaustive 0..=65536 equivalence sweep (predicate == ALL_NAMED
membership ∪ vendor range), plus a derived-boundary assertion that the
value past the enum ceiling stays rejected until its constant lands; the
existing 11/25/26/63/64/65535/65536 boundary matrix stays green.

* fix(objects): gate in-service Reliability writes on Loop and other OOS carriers (#240)

Loop accepted any Enumerated into Reliability unconditionally. Clause
12.17 Table 12-20 lists Reliability O7, whose footnote reads 'These
properties are required to be writable when Out_Of_Service is TRUE'; the
Out_Of_Service property text grants the writability for simulation
(extract lines 18670-18678). Loop now mirrors the nine intrinsic-
reporting objects: in-service writes refuse PROPERTY /
WRITE_ACCESS_DENIED, out-of-service writes are validated against
BACnetReliability, Out_Of_Service saves and restores the evaluated value,
and set_reliability_internal gains the complementary ownership guard. A
mirroring is_writable_property override keeps the PICS aligned with
dispatch.

Sweep of the other OOS+Reliability carriers:

- Schedule (12.24 Table 12-28): Reliability is plain R with no writable
  footnote, but the Reliability_Evaluation_Inhibit text anticipates the
  simulation write ('...unless Out_Of_Service is TRUE and an alternate
  value has been written to the Reliability property', extract line
  21984) and the class previously accepted unvalidated stores. Gated the
  same way.
- Trend Log (12.25 Table 12-29) and Trend Log Multiple (12.30 Table
  12-35): Reliability O with no writability footnote, and their
  Reliability_Evaluation_Inhibit paragraphs end without the
  OOS-write provision (extract lines 22638, 25448) - the log owns the
  property. Trend Log's arm now refuses PROPERTY / WRITE_ACCESS_DENIED
  in and out of service (behavior change); Trend Log Multiple already
  fell through to the default denial. Both pinned with cited tests.

The shared reliability_gate_test! macro drives Loop and Schedule as the
tenth and eleventh carriers; wire-level pins cover the gate over
WriteProperty (handlers/tests/write_validation.rs).

* fix(objects)!: validate Notify_Type and Event_Enable/Limit_Enable bit widths on writes (#255)

Two input-validation gaps in the shared event-property write path, both
present on every object type carrying the family macros:

1. Notify_Type accepted any u32. BACnetNotifyType is a closed
   {alarm(0), event(1), ack-notification(2)} production (Clause 21), so
   a stored Enumerated(99) read back as 99 and reached the wire in the
   notification's notifyType field. Writes outside the production now
   refuse PROPERTY / VALUE_OUT_OF_RANGE (Clause 15.9.1.3: 'The value
   provided is outside the range of values that the property can take
   on', extract line 53209). Membership derives from
   NotifyType::ALL_NAMED so a future addendum constant widens the gate
   without a second edit.

2. Event_Enable and Limit_Enable destructured the written BitString
   ignoring its declared unused_bits, then masked the first octet - a
   nonconformant shape (e.g. an 8-bit string where the production
   defines 3 or 2 bits) was silently normalized. BACnetEventTransitionBits
   (3 bits) and BACnetLimitEnable (2 bits, Clause 21, extract lines
   65661/66351) each have exactly one canonical shape - one content
   octet with 8-N unused bits, the form the read path emits - and a
   write declaring any other shape now refuses PROPERTY /
   INVALID_DATA_ENCODING (Clause 15.9.1.3: 'The encoding is not valid
   for the datatype of the property', extract line 53214) via a shared
   objects-layer helper (check_fixed_width_bit_string); an empty content
   now reports INVALID_DATA_ENCODING instead of INVALID_DATA_TYPE.

EventEnrollment's own NOTIFY_TYPE / EVENT_ENABLE arms and the sibling
AlertEnrollment EVENT_ENABLE arm carried the identical defects and get
the identical validation.

BREAKING: previously-accepted invalid writes (out-of-production
Notify_Type values, non-canonical Event_Enable/Limit_Enable BitString
shapes) now fail with the protocol errors above.

Tests: one analog (AI), one binary (BI), one multistate (MSO) object
plus EventEnrollment and AlertEnrollment at object level with exact
class/code and state-unchanged asserts; all four paths pinned end-to-end
over confirmed WriteProperty in handlers/tests/write_validation.rs.

* feat(objects): writable Relinquish_Default on commandable types (#270)

Relinquish_Default was fixed at construction with no arm and no setter,
so a site-configured safe default was unreachable. The conformance
tables carry it R on the commandable outputs (AO Table 12-3, BO Table
12-8, MSO Table 12-22, Lighting Output Table 12-64, Binary Lighting
Output Table 12-65, Access Door Table 12-30) and O on the commandable
value types - permitted writability, implemented here, not a
conformance upgrade.

Per type: a RELINQUISH_DEFAULT write arm routed through a validated
local set_relinquish_default, is_writable_property advertising it, and
recalculate_present_value() after the store so an empty priority array
falls back to the new default immediately. Validation mirrors the PV
path: finite Real (AO/AV), BinaryPV 0/1 (BO/BV), Unsigned
1..=Number_Of_States (MSO/MSV), Real 0..=100 (Lighting Output),
BinaryLightingPV 0..=4, DoorValue 0..=2 (Access Door), and per-type
extraction plus a finite check for LargeAnalog on the value types.

- The shared is_commandable_property_writable predicate covers
  AO/AV/BO/BV/MSO/MSV in one edit; its doc stops claiming
  RELINQUISH_DEFAULT is read-only.
- The 12 commandable value types gain the arm through
  define_value_object_commandable! with per-type validators; the two
  datetime-paired types stay network-read-only (their BACnetDateTime
  wire form needs multi-element decode - follow-up #182) while keeping
  the uniform local setter.
- Lighting Output, Binary Lighting Output, and Access Door gain
  is_writable_property overrides mirroring their write arms.
- Number_Of_States shrink interplay on MSO/MSV: clause 12.19/12.22 make
  stored-value adjustment a local matter and this implementation does
  not auto-adjust, pinned in a test comment.
- Pins asserting non-writability flipped to behavior tests
  (analog/tests/output.rs, analog/tests/value.rs,
  bacnet-server pics/tests.rs).

Wire-level end-to-end (handlers/tests/write_validation.rs): an AO
Relinquish_Default write holds while a command occupies priority 8,
NaN refuses PROPERTY / VALUE_OUT_OF_RANGE with state unchanged, and
relinquishing the command resolves PV to the written default.

* docs(objects): cite Table 12-69 for Binary Lighting Relinquish_Default (#270)

Corrects the conformance-table citation added in 37b234f: the Binary
Lighting Output property table is 12-69 (Clause 12.55); Relinquish_Default
carries BACnetBinaryLightingPV / R there. Comment-only change.

* docs(conformance): add write-validation ledger rows (#252, #240, #255, #270)

Three clause-backed rows with test evidence, repo_sha pinned to the
tranche's last code commit (6fac224):

- BACNET-12-OOS-RELIABILITY-WRITABILITY: the Out_Of_Service-gated
  Reliability writability contract across the carriers (12.17 Table
  12-20 footnote 7 for Loop; the Clause 12 Out_Of_Service property
  texts; Schedule's Reliability_Evaluation_Inhibit provision; the
  Trend Log / Trend Log Multiple no-writability pins; Clause 21
  BACnetReliability named set derived from the enum).
- BACNET-12-RELINQUISH-DEFAULT-WRITABILITY: permitted writability
  implemented on the commandable types, per-type validation paired
  with the Present_Value arms, datetime-paired value types excluded
  pending #182.
- BACNET-15-WP-EVENT-FIELD-VALIDATION: 15.9.1.3 pairings for
  Notify_Type production membership (VALUE_OUT_OF_RANGE) and the
  fixed-width Event_Enable/Limit_Enable encodings
  (INVALID_DATA_ENCODING).

* style(server): fix clippy needless-borrow/to_vec in write-validation tests

New wire-test helpers in handlers/tests/write_validation.rs tripped
clippy::needless_borrow (read_wire(&mut db, ...)) and
clippy::unnecessary_to_owned (ack_buf.to_vec()). No behavior change.

* docs(conformance): refresh provenance to the tranche tip; repo_sha=66e05fe

* fix(objects): validate Access Door Relinquish_Default against BACnetDoorValue 0..=3 (#270)

Table 12-30 types both Present_Value and Relinquish_Default as
BACnetDoorValue, and the Clause 21 production at extract line 64024 is a
closed set of four: lock(0), unlock(1), pulse-unlock(2),
extended-pulse-unlock(3). The #270 tranche validated the write against
0..=2 taken from the wrong enum's names (a DoorStatus-shaped comment);
the correct domain is 0..=3, which now matches the priority-slot
Present_Value arm's acceptance of 3 - the asymmetry is resolved.

Tests pin all four named values accepted (including
extended-pulse-unlock), Enumerated(4) / large values / wrong types
refused PROPERTY / VALUE_OUT_OF_RANGE (INVALID_DATA_TYPE for a Real),
and the stored default byte-identical after each refusal.

* refactor(objects): extract lighting Relinquish_Default setters (#270)

Lighting Output and Binary Lighting Output validated their RD writes
inline in write_property while AO/AV/BO/BV/MSO/MSV/Access Door and the
value-type macro route through an inherent validated
set_relinquish_default. Lift both inline blocks into matching setters
(same checks: finite Real 0..=100; BinaryLightingPV 0..=4) so every
commandable type shares one local API, and pin the runtime gap: BLO
Enumerated(5) and out-of-production values refuse PROPERTY /
VALUE_OUT_OF_RANGE with the stored default byte-identical.

* docs(changelog): record the PR #276 write-validation set under Unreleased

Prior tranches record behavior changes under [Unreleased]; the
write-path tranche had none. Four compact entries matching the existing
entry style: the #252 enum-derived Reliability predicate (Changed),
writable Relinquish_Default on the commandable types with the
permitted-writability wording (Added), in-service Reliability write
refusals on Loop/Schedule plus the Trend Log refusal (Fixed), and the
Notify_Type / Event_Enable / Limit_Enable validation refusals flagged
breaking (Fixed). (#252, #240, #255, #270)

* docs(conformance): refresh provenance after panel fixes; repo_sha=926559c

repo_sha now names 926559c, the last
code commit of the tranche after the panel round: the Access Door
Relinquish_Default domain correction (BACnetDoorValue 0..=3) and the
lighting setter extraction.

* docs(conformance): correct the Access Door domain in the Relinquish_Default row

Panel correction: the row text said the Access Door validated against a
Door set of 0..=2; Table 12-30 types Relinquish_Default as
BACnetDoorValue whose Clause 21 production is lock..extended-pulse-unlock
(0..=3), matching the code after 32e1e31.

* docs(conformance): sync hand-maintained ledger.md to the write-path tranche

standard-135-2020-ledger.md is NOT generated by
scripts/generate-conformance-docs.py (which emits only support-summary /
pics-draft / bibbs-draft), so its Scope header stayed at the array-index
tranche's provenance (repo_sha db07098...) while the JSON and
support-summary moved on - the --check gate cannot see this file, which
is exactly the drift the packaging review caught. Previous tranches hit
the same path and synced by hand (bfcc220, dd4dd6e).

Sync: repo_sha 926559c (last code
commit of codex/write-path-validation), reviewed_at 2026-08-12
(unchanged), write-path-validation scope + addenda note matching the
JSON fields, and the three new rows mirrored into the Clauses 12-19
section in the existing prose style (OOS Reliability writability,
Relinquish_Default writability, and WP event-field validation after the
array-index row). Mirror now complete: 42/42 JSON rows present.
conformance_ledger integration tests pass (10/10); its pin asserts
repo_sha shape, not the string, so no .rs change.
…roperty/WPM (#182) (#278)

* feat(server): loop-decode multi-element WriteProperty values with full consumption (#182)

decode_write_property_value consumed exactly one application-tagged
element and silently dropped the rest, so a structured or BACnetLIST
value could never reach an object write arm whole. It now loops
decode_application_value until the payload is exhausted — the mirror of
encode_property_value's List flattening: exactly one element yields the
scalar PropertyValue as before, more than one yields PropertyValue::List.

Full consumption is required: an element that does not decode (including
a partial tail) is PROPERTY / INVALID_DATA_ENCODING per the Clause
15.9.1.3 pairing, never a silent drop, and an empty payload is refused.
Context-tagged behavior is unchanged: a single framed CHOICE element
(Event_Parameters/Fault_Parameters) still arrives as one ApplicationData
scalar, and context-tagged member productions (the Loop/Accumulator
reference properties) arrive as one ApplicationData element per context
tag for the object arm to reassemble. The RECIPIENT_LIST verbatim
special-case keeps its precedence.

A true TLV-truncated tail is refused earlier, by the service request's
own [3] framing walk; the new wire tests pin both layers plus the arm's
wrong-shape INVALID_DATA_TYPE for a well-formed extra element, each with
the stored value proven unchanged. Note this changes one envelope: value
payloads that the old code failed on map to PROPERTY/INVALID_DATA_ENCODING
instead of the SERVICES/OTHER that a propagated raw decoding error
produced.

* feat(encoding,objects): BACnetObjectPropertyReference wire codec + reference write arms (#182)

The Loop reference properties (Controlled_Variable_Reference,
Manipulated_Variable_Reference, Setpoint_Reference — Clause 12.17) and the
Pulse Converter Input_Reference (Clause 12.10) are
BACnetObjectPropertyReference, whose members are primitive context tags
[0]/[1]/[2] on the wire; the multi-element decode hands them to the object
arm as ApplicationData element(s), so the arms previously rejected every
network write as INVALID_DATA_TYPE.

bacnet-encoding gains a strict typed codec under constructed/ for the
Clause 21 production, built on the tranche-J DOPR body decoder and then
narrowed: full consumption is required, and a device-qualifying member [3]
is rejected — unlike BACnetDeviceObjectPropertyReference, these references
are local-device only. A companion codec covers Loop Setpoint_Reference's
outer BACnetSetpointReference production (the reference inside an
opening/closing context tag 0 frame).

bacnet-objects gains a shared arm helper (reference.rs) so the four arms
accept both the legacy local List form (in-process compatibility — exact
shape, no silent member dropping) and the framed network form via one or
more concatenated ApplicationData elements. Error pairings follow Clause
15.9.1.3: wrong value datatype → PROPERTY / INVALID_DATA_TYPE; malformed
framing, device qualification, or an unknown trailing context tag →
PROPERTY / INVALID_DATA_ENCODING with the stored reference untouched. The
read arms now carry the reference's optional array index as a third list
element. Setpoint_Reference additionally accepts the [0]-framed production
while staying strict about unbalanced frames.

Covered by codec golden vectors + negatives, an arm-helper shape matrix,
per-arm object tests, and wire-level WriteProperty tests including
WritePropertyMultiple in-order commit and rollback of a failed
reference+setpoint request.

* style: cargo fmt over the #182 reference-codec files

* feat(objects): unlock datetime-paired writes + whole-list writes over WP (#182, #270)

With the multi-element decode in place, flip the two tranche pins and the
last tranche-L1 exclusions:

- MSI ALARM_VALUES whole-list writes (handlers/list.rs
  whole_list_write_property_decodes_all_elements, flipped): the consecutive
  application-tagged elements arrive as PropertyValue::List and the arm's
  per-element Unsigned validation applies to each member — a mixed write is
  INVALID_DATA_TYPE with the stored list untouched.
- DateTime Value / DateTime Pattern Value Relinquish_Default: rd_access
  moves from readonly to writable — the BACnetDateTime wire form (a
  date+time application-tagged pair) now decodes to the List([Date, Time])
  the arm expects, completing #270's permitted writability for all twelve
  commandable value types. temporal.rs's pinned-denial test flips to
  datetime_value_relinquish_default_write_recaptures_present_value.

New wire-level coverage in handlers/tests/multi_element_writes.rs:
application-tagged Date+Time lands DateTime Value Present_Value, an indexed
priority-array ENTRY write wins over a lower-priority command, and
Relinquish_Default recaptures Present_Value on both datetime-paired types;
a date+date mispairing is refused INVALID_DATA_TYPE with state preserved.

* docs(conformance): clause-backed ledger rows + changelog for #182

Ledger (hand-maintained md + JSON, SHA 1f29ca8 / reviewed 2026-08-12):

- BACNET-15-STRUCTURED-WRITE-DECODE (Clause 15.9.1.2/15.9.1.3, 15.10,
  20.2.1): the WP/WPM loop decoder with full-consumption requirement —
  supported-with-clause-evidence with the five-test wire matrix and the
  flipped MSI ALARM_VALUES pin.
- BACNET-12-REFERENCE-PROPERTY-WRITABILITY (Clause 12.17 Table 12-20,
  12.10 Input_Reference, Clause 21 BACnetObjectPropertyReference /
  BACnetSetpointReference): the strict codec, the both-forms arm helper,
  [3]-device rejection, WPM rollback evidence.
- BACNET-12-RELINQUISH-DEFAULT-WRITABILITY: datetime unlock folded in —
  the pinned-denial negative is replaced by
  datetime_value_relinquish_default_write_recaptures_present_value plus
  the over-the-wire Date/Time pair evidence; the readonly rd_access note
  is retired.

support-summary/pics/bibbs regenerated per the generator (--check green).
CHANGELOG [Unreleased]: decode behavior change under Changed; structured
value writes + reference properties under Added; the stale 'stay
network-read-only' tail of the #270 entry corrected to point at #182.

* docs: correct Pulse Converter citation to Clause 12.23 / Table 12-27 (#182)

Pulse Converter (and its Input_Reference property) is Clause 12.23 with
Table 12-27 in 135-2020 — my #182 comments and ledger rows initially said
Clause 12.10. Verified against the local extract (clause heading at line
21100, Table 12-27 at line ~21148). Note the pre-existing accumulator.rs
header claim (Clauses 12.1/12.2) is also wrong but left for a follow-up —
out of this PR's scope.

* fix(objects)!: strict reference-arm decode — exact shape, >u32 guards, absent BACnetSetpointReference member (#182)

Panel blocker: the Averaging OBJECT_PROPERTY_REFERENCE arm accepted
items.len() >= 2 silently dropping trailing members, retyped a non-Unsigned
third member to None-index, and truncated 64-bit numerics with 'as u32' —
reachable over the network for the first time via the multi-element decode.
The arm now routes through the shared reference.rs decode: exact 2-or-3
member shape, u32::try_from on the Unsigned members, framed-form strictness
including INVALID_DATA_ENCODING on a device-qualified member [3] (Clause
12.5's BACnetDeviceObjectPropertyReference typing leaves remote sampling
optional — 'Optionally, the object property to be sampled may exist in a
different BACnet device' — and this object samples local objects only).

Flat-form member typing decision (documented in reference.rs): the property
member is accepted as Unsigned OR Enumerated — Enumerated is the Loop/Pulse
flat convention and matches the framed form's Clause 21 tag [1] semantics;
Unsigned is Averaging's existing flat form and stays read-back compatible.

Runtime+dataflow item: the BACnetSetpointReference frame with its OPTIONAL
member absent (0x0E 0x0F) is a syntactically valid encoding of the Clause
12.17 'fixed setpoint' state — decode_setpoint_reference now returns
Option and the frame clears the reference exactly like a Null write,
instead of INVALID_DATA_ENCODING on a conformant peer.

Tests: averaging exact-shape matrix (2/3 members, Unsigned+Enumerated
typings, read-back fidelity), refusal matrix (4-member, wrong-typed third,
non-OID first, >u32 member, device-qualified frame — state preserved each
time), framed-local-form acceptance; codec golden vector for
encode_setpoint_reference with new_indexed; empty-frame pins at codec,
helper, and loop arm level.

* fix(objects,server): mirror PICS writability advertising to the Pulse Converter write arms (#182)

The historical is_writable_property default reported Pulse Converter's
INPUT_REFERENCE read-only while the arm accepted writes, and OBJECT_NAME
writable while no arm routes it — both halves drifted from write reality.
The override now matches the arms exactly: PRESENT_VALUE, SCALE_FACTOR,
ADJUST_VALUE, INPUT_REFERENCE, plus the shared DESCRIPTION /
OUT_OF_SERVICE / COV_INCREMENT routes; OBJECT_NAME drops off (a PICS
correction, flagged, mirroring the Loop/Schedule corrections in #276).
The Averaging override (OBJECT_PROPERTY_REFERENCE + DESCRIPTION +
OUT_OF_SERVICE) rode in with the arm-hardening commit 715803e — flagged
here since no amend/rewrite per tranche rules.

pins: pics::truth_source_tests::is_writable_property_matches_write_property_on_pulse_converter_and_averaging
asserts advertised<=>arm agreement in both directions (accepted with a
canonical good value, rejected with a plausible one), including the
OBJECT_NAME non-advertisement on both objects.

* test(server): split reference wire tests into reference_writes.rs + indexed-read pins (#182)

- multi_element_writes.rs crossed the 700-LOC cap; the seven reference-
  property wire tests move to a self-contained reference_writes.rs (same
  convention as framed_properties.rs: local write/read/assert helpers),
  and multi_element_writes keeps the full-consumption matrix + datetime
  unlocks.
- New pins: the empty-frame and Null clears of SETPOINT_REFERENCE over
  WriteProperty (both paths, set-then-clear each); Averaging
  Object_Property_Reference over the wire (framed local write lands with
  historical Unsigned read-back; device-qualified [3] and 4-member flat
  writes refused with state preserved).
- array_index_gating.rs: the five reference-typed properties (Loop CVR /
  MVR / SR, Pulse Converter INPUT_REFERENCE, Averaging OPR) reject an
  indexed ReadProperty with PROPERTY / PROPERTY_IS_NOT_AN_ARRAY — closes
  the loop between the #182 read arms and the tranche-K gate.

* docs(conformance): review-round ledger sync + changelog for #182 panel fixes

Ledger (hand-maintained md + JSON, repo_sha 0c7be97 / reviewed 2026-08-12):
- BACNET-12-REFERENCE-PROPERTY-WRITABILITY: anchor gains Clause 12.5
  Table 12-5 (Averaging Object_Property_Reference, typed
  BACnetDeviceObjectPropertyReference); evidence gains the adversary-
  blocker fix (shared strict decode on the averaging arm), the
  absent-member BACnetSetpointReference acceptance, the Unsigned OR
  Enumerated flat-member tolerance, the PICS writability overrides with
  their truth-source pin, and the review-round notes; wire-test paths
  follow the multi_element_writes -> reference_writes split.
- BACNET-15-ARRAY-INDEX-GATING: indexed-read pins on the five
  reference-typed properties.
- BACNET-15-STRUCTURED-WRITE-DECODE: evidence path note for the split.
- Header SHA advances to the review-round code tip; scope sentence
  records the panel round.
CHANGELOG [Unreleased]: PICS-override correction under Changed, empty
BACnetSetpointReference acceptance under Added, averaging arm hardening
under Fixed.
#253) (#282)

* feat(types): complete BACnetLifeSafetyState with values 24-34 (#253)

The Clause 21 production runs through test-oeo-unaffected (34); the
crate stopped at test-supervisory (23). Append the eleven tail values
(non-default-mode through the test-oeo-* states) without renumbering
existing constants.

* feat(types): add BACnetEscalatorOperationDirection enum (#253)

The production (Clause 21: unknown, stopped, up-rated-speed,
up-reduced-speed, down-rated-speed, down-reduced-speed) had no Rust
enum; the Escalator object stores operation_direction as a bare u32
and resolve.rs has no arm for operation-direction (477). This is the
enum half; the resolve arm lands with the other resolve additions.
EscalatorObject's raw u32 field typing is untouched on purpose.

* feat(types): add BACnetBinaryLightingPV and BACnetLightingTransition (#253)

Both Clause 21 productions were missing from the crate. Also correct the
Binary Lighting Output docs and test labels that cited a 'fade-on' value:
the production has warn-relinquish (4) and stop (5); there is no fade-on.
Write-validation behavior is unchanged (still accepts 0..=4).

* feat(types): add access-family enums; wire door RD validation (#253)

Add the missing Clause 21 productions BACnetAuthenticationStatus,
BACnetAuthorizationExemption, BACnetAccessZoneOccupancyState, and
BACnetDoorValue. AccessDoorObject::set_relinquish_default now bounds
against DoorValue::EXTENDED_PULSE_UNLOCK instead of a literal 0..=3 so
the write domain cannot drift from the production, and the
relinquish-default test iterates DoorValue::ALL_NAMED. Also correct the
CredentialDataInput present_value comment: 1 is ready, not waiting.

* feat(types): add BACnetProgramError/RestartReason/Maintenance/Relationship (#253)

Four more missing Clause 21 productions. BACnetRelationship mirrors the
production's even/odd forward/reverse pairing note in its docs, and the
test asserts the n^1 structure on top of the value table. ProgramError
and RestartReason join the object-level file beside ProgramState and
DeviceStatus; Maintenance and Relationship join misc (Maintenance_Required
serves both life-safety and access-door objects; Relationship belongs
with NodeType's structured-view cluster). All new enums are added to the
Display->parse round-trip invariant list.

* feat(types): resolve arms for the new enumerations + comment fixes (#253)

Map reason-for-halt (100), maintenance-required (158), last-restart-
reason (196), authentication-status (260), occupancy-state (296),
authorization-exemptions (364), transition (385), operation-direction
(477), and subordinate-relationships/default-subordinate-relationship
(489/490) to the new types. The two list-valued properties (364 a
BACnetLIST, 489 a BACnetARRAY) resolve element-wise through the existing
resolve_value recursion, and both are covered by an element-level test.
Unknown wire values continue to display as bare numbers; the tests share
one production-table macro so the ten value tables stay terse.

Represents (491) deliberately gets no arm: it is typed
BACnetDeviceObjectReference in the Structured View object, never an
ENUMERATED, so an arm would be dead and misleading.

Also correct two comments: BACnetNetworkType's non-bacnet (8) was
removed in version 1 revision 18 (not "protocol revision 16"), and
BACnetEventType's gap note now mirrors the production (tag 6 kept clear
for the complex-event-type CHOICE of BACnetNotificationParameters; tag 7
deprecated).

* docs(changelog): Unreleased entries for the #253 enum additions (#253)

* docs(objects): correct field/rustdoc type attributions from PR #282 panel (#253)

- Escalator operation_direction: drop the invented 0/1/2/3 value mapping
  and name the real BACnetEscalatorOperationDirection production
  (0=unknown, 1=stopped, 2..5 speed-qualified up/down); point the raw-u32
  retyping at #284.
- Access Door: Present_Value is BACnetDoorValue, so 0 is lock, not
  "closed" — fix the struct doc, the field comment, and both default
  initializers. Note on set_relinquish_default that the types-layer test
  door_value_values_match_clause_21 pins the production's closed-set
  length, guarding the DoorValue-derived write bound from silent drift.
- CredentialDataInput: Present_Value is a BACnetAuthenticationFactor
  structure (Clause 12.36), not an authentication status; the
  BACnetAuthenticationStatus values (0=not-ready, 1=ready, ...) belong to
  the Access Point object's Authentication_Status (Clause 12.31).
- Relink the BinaryLightingOutput stop(5) gap comment from #253 to its
  dedicated tracker #283.
…ented StagingState (#273, #274, #275) (#285)

* fix(types)!: correct FileAccessMethod value assignments (#273)

The Clause 21 production is BACnetFileAccessMethod ::= ENUMERATED {
record-access (0), stream-access (1) }; the stack had STREAM_ACCESS = 0 /
RECORD_ACCESS = 1, so every File object reported the other access method
in File_Access_Method (interop-blocking) and ResolvedEnum mislabeled both
values. Swap the constants, mirror the production order in the enum, and
correct the clause attribution (File object is Clause 12.13; the
enumeration itself is the Clause 21 production).

Consumer audit (grep of FileAccessMethod / STREAM_ACCESS / RECORD_ACCESS
/ file_access_method across the workspace): no compensating swaps found.

- bacnet-types resolve.rs arm for file-access-method (41) uses
  FileAccessMethod::from_raw and is correct automatically after the swap.
- bacnet-objects file.rs restated the values as literals (default 0,
  '1 = record' gating); it now derives both from the enum so the
  production is the single truth, and its default/recordaccess pins and
  docs are updated. Its module doc's clause cite (12.11) is fixed to
  12.13 at the same time.
- bacnet-services file.rs selects stream vs record by the
  AtomicReadFile/AtomicWriteFile CHOICE tags [0]/[1], a separate
  Clause 15.6/15.7 production unrelated to the enumeration values;
  server handlers, clients, and the CLI pass that CHOICE through and are
  unaffected.

Tests: production-pinned values + resolve-arm names in bacnet-types, a
wire-value test pinning record-access -> 0x91 0x00 and stream-access ->
0x91 0x01 in bacnet-encoding, and File object property reads showing
stream-access = 1 and record-access = 0.

* fix(types)!: rename DoorAlarmState LOCK_FAULT to LOCK_DOWN (#274)

The Clause 21 BACnetDoorAlarmState production names value 6 lock-down;
there is no lock-fault member (Tranche Q audit). The wire value was
always right, but the constant name, Display, and FromStr text were
invented, so Access Door Door_Alarm_State value 6 decoded and displayed
under a name the standard does not define.

LockStatus::LOCK_FAULT (value 2 of the BACnetLockStatus production) is
correct and is deliberately untouched; the prior grep inventory shows no
in-tree consumer named the old constant (door state is carried as raw
u32 and resolved via from_raw), so the rename is source-local to the
enum plus its new production-pinned test, which also guards the
LockStatus conflation path.

* fix(types)!: remove invented StagingState enum (#275)

No StagingState production exists anywhere in 135-2020: the Staging
object's Present_Stage is 'of type Unsigned ... the array index (1 to
Nstages) that corresponds to the current active stage' (Clause 12.62,
Table 12-80), not an enumeration. The audit.rs module drops back to the
real 135-2020 audit productions.

Grep inventory shows zero in-tree consumers (no resolve arm, no object
state, no test referenced it) and the Staging object already models its
stage state as raw Unsigned, so the deletion is source-local. A staging
object test now pins Present_Stage as PropertyValue::Unsigned through a
write so a future enumerated regression fails in the suite.

* docs(types): correct AuthenticationStatus attribution

Authentication_Status is a property of the Access Point object
(Table 12-36); the rustdoc wrongly attributed the production to
Credential Data Input.

* style(types): rustfmt the added enum tests

* docs(changelog): relocate Tranche-Q entries to [Unreleased], restore [0.7.1] entry

The prior three commits' changelog hunks were applied under the released
## [0.7.1] heading, replacing its '- **Fixed** maturin wheel build'
entry. Restore that entry verbatim and move the three breaking entries
(FileAccessMethod swap #273, DoorAlarmState rename #274, StagingState
removal #275) to ## [Unreleased] -> ### Fixed where they belong.

Also correct the AtomicReadFile/AtomicWriteFile clause citations:
the services are Clause 14.1/14.2 (file access services) with their
ASN.1 productions in Clause 21, not Clause 15.6/15.7. Commit 30c2e23's
message carries the wrong citation and cannot be amended (no
force-push); this corrects the record in-tree and the PR body notes the
erratum.
…ction on intrinsic reporters (#225) (#288)

* feat(objects): add Time_Delay_Normal with normal-direction delay selection on intrinsic reporters (#225)

* docs(conformance): ledger row BACNET-12-TIME-DELAY-NORMAL with per-table conformance codes (#225)
…sition actions, COV baseline (#163, #166, #137) (#290)

* feat(server,objects): Event Enrollment evaluator honors Time_Delay + Time_Delay_Normal (#163)

Every algorithm arm of the EE evaluator discarded the variant's time_delay,
so a condition first observed by an evaluation pass transitioned immediately
even with a nonzero delay configured. Honor both delays per Clause 13.3:

- pTimeDelay (Event_Parameters.Time_Delay, Table 12-15) gates transitions
  into OFFNORMAL states; pTimeDelayNormal gates transitions to NORMAL.
- The EE object gains the optional Table 12-14 Time_Delay_Normal property
  (Unsigned, conformance O; writable per the Clause 12.1.2 option, mirroring
  the intrinsic types): read-back falls back to the Event_Parameters
  Time_Delay ('if no value is available for this parameter, then it takes
  on the value of the pTimeDelay parameter'), and the PICS writability set
  and Property_List advertise it.
- The pending countdown is owned by the EE object (in-memory only, like the
  intrinsic detectors' pending state) behind the new
  enrollment_eval_state_internal / set_enrollment_eval_state_internal trait
  channel, mirroring the set_event_state_internal precedent (#130). The
  Clause 13.2.2.1 detection-disabled reset clears it, and the write path
  refuses while detection is disabled so the invariant holds by
  construction.
- Semantics mirror the intrinsic probe/tick (#120/#225) without sharing
  code across the objects/server boundary: revert cancels, redundant
  qualifying observations never re-seed, a changed target re-seeds with the
  current target's direction delay. One countdown tick is one
  event_enrollment_task interval (event_enrollment_interval_secs, 10s
  default): delay N gates N evaluation passes.
- Parameter change mid-pending (Event_Parameters or Time_Delay_Normal)
  cancels the countdown and re-gates from the current parameters; no
  partial countdown resumes (fingerprint = framed params encoding +
  effective normal delay + event type).
- Legacy Opaque (0xFF) layouts carry no Time_Delay slot and keep their
  historical immediate transitions.

The evaluator's dispatch moves pure evaluation to the new algorithms.rs
(700-LOC cap) behind an Option<Indication> spine so 'no condition true'
(Clause 13.3 intro) is explicit ahead of the #166/#137 follow-ups; the
same-state skip itself is unchanged here.

Tests: per-arm delay gating, TDN-vs-TD asymmetry and fallback, cancel on
revert, no-restart cadence, target-change re-seed, parameter- and TDN-
change regates; a start_paused tokio lifecycle test proving the spawned
event_enrollment_task advances and fires the countdown on the configured
interval (#133); object tests for the TDN read/write/validation/PICS/
property-list and the eval-state channel incl. the disable reset.

* fix(server): execute transition actions on same-state EE transitions per 13.2.2.1.4 (#166)

The evaluator dropped every evaluation whose result equaled the current
state, but Clause 13.2.2.1.4 requires the actions 'even if the transition
does not change the event state (e.g., a transition from the OFFNORMAL
event state to the OFFNORMAL event state)'. Removing the guard alone would
have re-fired every poll: the evaluators could not distinguish a genuine
same-state indication from 'nothing changed'. The indication spine
 distinguishes the two; this change makes it real:

- CHANGE_OF_STATE condition (c) is implemented via a new retained slot on
  the EE object: the value that caused the last transition to OFFNORMAL
  (object-owned, in-memory, internal-channel only, cleared by the
  detection-disabled reset). An enrollment whose monitored value moves
  between listed alarm values now re-indicates OFFNORMAL -> OFFNORMAL,
  gated by pTimeDelay ('remains equal to that value for pTimeDelay' — the
  pending condition identity discriminates by matched value). Condition
  (c) is marked 'Optional:' in 13.3.2; implementing it is conformant, and
  without it the 13.2.2.1.4 example transition is unreachable. With the
  causing value UNKNOWN (state seeded by the test/setup helper), (c)
  declines rather than fabricating a re-entry per pass.
- CHANGE_OF_BITSTRING condition (c) stays deliberately unimplemented (also
  'Optional:') — no bitstring baseline is retained; documented at the arm.
- Every fired transition (state-changing or same-state) now also executes
  the alarm-acknowledgment half of the fourth action: Acked_Transitions is
  maintained per Clause 13.2.3 through the new
  set_acked_transitions_internal trait channel — the bit is cleared when
  the referenced Notification Class's Ack_Required says ack is owed,
  otherwise set; an unresolvable Notification Class means not-required,
  matching the clause's 'otherwise it is set' fallback. The intrinsic
  detectors' missing counterpart stays tracked under #123.
- Event_Enable remains distribution-scoped (Clause 12.12 / 13.2.5): a
  same-state transition is emitted with distribute=false when its bit is
  cleared, actions intact.
- CHANGE_OF_VALUE's own same-state indication (Figure 13-10's
  NORMAL->NORMAL) remains inert until #137 supplies the baseline.

Tests: COS (c) emission + no-refire + delayed + value-discriminated; OORs
no-same-state pin and across-band specific-state storage; Acked_Transitions
clear/set against a real NotificationClass incl. the no-class fallback;
Event_Enable suppression covers the transition but never its actions;
object tests for the ack channel and the extended eval state.

* feat(server): track COV baseline for Event Enrollment evaluations (#137)

The CHANGE_OF_VALUE arm compared the absolute monitored magnitude against
the increment with no retained prior value, so a stable large value kept
classifying itself as changed (and it indicated OFFNORMAL — a transition
Figure 13-10 shows the algorithm cannot indicate at all).

Per Clause 13.3.3 the algorithm detects CHANGE against the value held
'when a transition to NORMAL is indicated ... until the next transition to
NORMAL is indicated':

- The EE object gains the baseline as a third object-owned evaluation slot
  (in-memory only, internal channel, cleared by the detection-disabled
  reset), stored as the sampled PropertyValue.
- Both criteria compare against it: ReferencedPropertyIncrement fires when
  |current - baseline| >= pIncrement (a positive REAL, as the clause
  requires); Bitmask fires when any significant (masked) bit differs from
  the baseline's masked value. The only indication is NORMAL -> NORMAL
  (condition (a) in both forms), which is exactly the 13.2.2.1.4
  same-state case #166 made reportable.
- The baseline advances when the NORMAL transition FIRES (after the
  pTimeDelayNormal — only — countdown: 13.3.3's conditions reference no
  pTimeDelay), to the sample observed at the indication.
- First-sample policy: the first observed value initializes the baseline
  and NEVER indicates a transition. This is the clause's explicit local
  matter ('the initialization of the value used in evaluation before the
  first transition to NORMAL is indicated is a local matter'); the
  alternative (zero-initializing) would false-fire on any object whose
  opening value exceeds the increment.
- A criterion-type change that leaves the stored baseline incomparable
  re-establishes it from the first comparable sample (same local matter),
  never fabricating a transition.
- The legacy Opaque 0xFF byte layout is untouched: no baseline slot predates
  structured parameters, so it keeps its historical absolute-magnitude
  behavior.

Tests: first-sample no-fire; sub-increment silence; threshold crossing
emits NORMAL->NORMAL with actions; baseline advancement after each
indicated transition; repeated crossings; non-positive increment never
indicates; the change condition gated by the normal-direction delay;
bitmask significance against the baseline. The two pre-#137 assertions
(|10|>=5 -> OFFNORMAL; masked-bit-set -> OFFNORMAL) are rewritten to the
conformant behavior they were pinning the absence of.

* docs(changelog,conformance): Event Enrollment evaluator tranche ledger row + changelog (#163, #166, #137)

Add BACNET-13-EVENT-ENROLLMENT-EVALUATOR (supported-with-clause-evidence):
Clause 12.12/Table 12-14 TDN property + Table 12-15 pTimeDelay mapping +
13.2.2.1.4 same-state transition actions + 13.2.3 Acked_Transitions
maintenance + 13.3 direction rules and the 13.3.3 COV baseline, with the
extract line anchors, the killed-mutant evidence, the per-cycle tick model
(#133 cadence), the fences honored, and the remaining gaps. Sync the
generated support summary and refresh the hand-maintained ledger scope for
the tranche; repo_sha names the last code commit (8680203) per convention.
CHANGELOG [Unreleased]: Added the EE Time_Delay_Normal property + the
object-owned evaluation state channel; Fixed: delay honoring, same-state
transition actions, and the COV baseline.

* fix(server): EE delay countdown honors wall-clock seconds, not pass count (#163)

PR-#290 review blocker 1 (both lanes): pTimeDelay/pTimeDelayNormal are
SECONDS in Clause 13.3 ('the time, in seconds, that the offnormal
conditions must exist'), but the countdown counted evaluation passes — at
the default event_enrollment_interval_secs=10 a Time_Delay=5 gated ~50s,
and interval=3600 would gate delay=2 for 2 hours. Now:

- evaluate_event_enrollments takes the driving interval in seconds
  (lifecycle passes its actual clamped period) and seeds the countdown as
  ceil(delay_secs / interval_secs) — never-fire-early ceiling semantics,
  saturating at u32::MAX. delay=0 still fires without seeding.
- Residual behavior documented: the interval is builder-config only and
  pending is in-memory, so no runtime rescale exists; restart re-evaluates
  from the confirmed state with a fresh conversion.

Review fixes folded in:

- F4 (cancel loss): the fingerprint-mismatch cancellation is flushed to
  the object BEFORE any later exit, so a params round-trip A→B→A (with B
  failing to evaluate, e.g. monitored object missing) can no longer resume
  the stale A countdown. The unreadable-reference exit deliberately stays
  BEFORE the check (transient unreachability retains the countdown).
- F5: the monitored object identifier + property identifier are folded
  into params_fingerprint (references carried inside Event_Parameters
  — e.g. the FLOATING_LIMIT setpoint — were already covered by the framed
  encoding).

Tests: passes_for_delay ceiling/saturating unit pins; interval=10
conversion driven through the evaluator (TD=25 gates 3 passes); four
start_paused lifecycle tests at the DEFAULT 10s interval (TD=5 fires at
the t~10s tick, TD=15 at t~20s, TD=25 at t~30s, TD=0 on the immediate
first tick); params A→B-with-failed-read→A regates the full delay;
fingerprint differs across retargets + a transplant-driven retarget
regates. Existing suites pass interval=1 (identity conversion), so their
per-tick cadences are unchanged.

* fix(server): recover EE algorithm arms from foreign Event_State; zero-pad COBS comparison (#163 #166)

PR-#290 review blocker 2: a foreign Event_State (left over after
Event_Parameters were rewritten to a different algorithm, e.g. HIGH_LIMIT
under new CHANGE_OF_STATE parameters) wedged every structured arm — the
indication-spine conditions require current to be in the arm's reachable
set, so no condition ever matched and the ghost state persisted forever.
The stateless base evaluators computed unconditionally and therefore
recovered; the regression was introduced with the indication spine.

- Reachable sets are now explicit per arm (ArmEvaluation doc note):
  OUT_OF_RANGE/FLOATING_LIMIT {NORMAL, HIGH_LIMIT, LOW_LIMIT};
  CHANGE_OF_STATE/CHANGE_OF_BITSTRING {NORMAL, OFFNORMAL};
  CHANGE_OF_VALUE {NORMAL}. Outside the set the arm evaluates as from
  NORMAL and INDICATES the computed state (necessarily different from the
  foreign current), flowing through the ordinary actions path including
  direction-selected delay gating — a NORMAL recovery waits
  pTimeDelayNormal, it does not snap instantly. COV's recovery installs
  the current sample as the detection baseline on fire, exactly 13.3.3's
  'value when a transition to NORMAL is indicated'.
- F2: the structured COBS matcher truncated the comparison to
  min(mask, alarm, value), so an alarm bit set beyond the monitored
  bitstring's width was never examined (false OFFNORMAL on a prefix
  match). Comparisons now span max(mask, value) with zero-filled missing
  bytes — matching the pending-condition hash and the legacy evaluator.
- N2 (comment precision): the pending-condition value identity is driven
  by 13.3.2 (c)'s 'remains equal to THAT value'; (a) says 'ANY of the
  values', so per-value identity on (a) is now documented as the
  deliberate stricter choice on both the Indication field and the COS arm.

Tests (new foreign_state.rs): HIGH_LIMIT->NORMAL recovery under rewritten
COS params, gated by the normal-direction delay until it elapses;
OFFNORMAL->NORMAL under rewritten OOR params; OFFNORMAL under a COV
criterion recovers to NORMAL and establishes the baseline (then a +increment
move indicates NORMAL->NORMAL against it); HIGH_LIMIT recovery under COBS;
mask wider than the monitored value is not a match; the out-of-service
gate still precedes evaluation, wedged state or not.

* fix(objects,server): EventEnrollment alarms are acknowledgeable via AcknowledgeAlarm

PR-#290 review blocker 3: #166 made EE transitions maintain
Acked_Transitions per Clause 13.2.3, but AcknowledgeAlarm could never
succeed on an EE — the acknowledge_alarm trait default rejects with
OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED — so a transition whose Notification
Class requires acknowledgment held the bit cleared forever.

Override acknowledge_alarm on EventEnrollmentObject, mirroring the analog
pattern (unconditional, idempotent |= on the masked bit, per Clause
13.2.3's 'the corresponding bit in Acked_Transitions is set'), with the
EE-specific refusal: detection-DISABLED enrollments answer OBJECT /
NO_ALARM_CONFIGURED — Table 13-10's 'The object exists but does not
support or is not configured for event generation' — which doubles as the
invariant guard for Clause 12.12's initial-condition Acked_Transitions
while disabled. Out_Of_Service does not gate the ack (no clause bars
acknowledging a notification already issued).

Service-level proofs (handlers/tests/acknowledge_alarm_ee.rs): evaluated
ack-required transition fires -> GetEventInformation shows the
TO_OFFNORMAL bit cleared -> AcknowledgeAlarm succeeds -> GEI shows it set;
duplicate ack idempotent per 13.2.3; TO_NORMAL ack path; and the
detection-disabled refusal leaves Acked_Transitions untouched.

Also pinned while in the file, per the review's runtime residuals: the
evaluator's ack maintenance is direction-complete (TO_NORMAL clears when
ack-required) and independent of Event_Enable (a distribution-suppressed
transition still clears its ack-owed bit).

* docs(changelog,conformance): refresh for the PR-#290 review round

FIX 8 + the review-round changelog entries: addenda_errata_status now
describes THIS tranche's extract review (was the #225 leftovers);
BACNET-13-EVENT-ENROLLMENT-EVALUATOR notes record the four review fixes
(B1 wall-clock seconds conversion, B2 foreign-state recovery with the
per-arm reachable sets, B3 acknowledgeable EE alarms incl. the Table
13-10 NO_ALARM_CONFIGURED refusal, F4 persisted cancel, F5 monitored
reference in the fingerprint, F2 zero-padded COBS width) with their new
evidence files; hand-ledger scope + row synced; generated summaries
regenerated. CHANGELOG: the #163 entry now describes the
seconds-with-ceiling conversion (was per-pass); new Fixed entries for
the wedge recovery, the COBS comparison width, and acknowledgeable EE
alarms.

* style(server): drop unnecessary u32 cast in params_fingerprint (clippy)

* docs(conformance,server): seconds-semantics ledger notes + FL/COS-matched foreign-state pins

Re-verification micro-round on PR #290:

- runtime N1: the BACNET-13-EVENT-ENROLLMENT-EVALUATOR row notes still
  described the pre-fix per-pass semantics ('Time_Delay N gates N passes'
  was issue #163's ORIGINAL suggested direction, superseded in review) —
  rewritten to the seconds + ceil(delay_secs / interval_secs) reality
  with the never-fire-early examples; the hand-ledger's addenda bullet
  now carries Clause 13.2.4, the seconds phrasing of both delay
  parameters, and Clause 13.9 + Table 13-10's Result(-) mapping, synced
  with the JSON (regenerated summaries; --check clean).
- runtime N2: two pins in foreign_state.rs — (a) FLOATING_LIMIT's
  {NORMAL, HIGH_LIMIT, LOW_LIMIT} normalization recovering HIGH_LIMIT to
  NORMAL when the value sits inside the setpoint band; (b) the COS
  foreign + matched-alarm branch: HIGH_LIMIT under COS params whose list
  contains the value indicates OFFNORMAL through the actions path, gated
  by pTimeDelay.

Tests: foreign_high_limit_recovers_under_floating_limit_params,
foreign_high_limit_with_matching_alarm_indicates_offnormal.
…209, #289) (#292)

* fix(server,objects): preserve event state across WPM rollback (#205, #209, #289)

* docs(conformance): record C3 event rollback evidence

* fix(server): harden WPM rollback contracts

* docs(conformance): advance C3 review evidence

* fix(server): scope WPM residual reconciliation

* docs(conformance): finalize C3 rollback evidence

* test(conformance): guard ledger evidence revision

* fix(objects): preserve derived state across WPM rollback

* docs(conformance): expand C3 rollback evidence

* perf(objects): avoid cloning log rollback buffers

* docs(conformance): anchor optimized rollback evidence
* fix(objects): derive enrollment status flags from event state

* test(objects): cover enrollment reliability status flag
Closes #296. Tracks nested legacy member validation separately in #298.
Add an unpublished endpoint-core crate, move single-owner ingress into the shared boundary, and introduce a device-wide invoke ID coordinator with owner-aware terminal routing.

Refs #431
Make endpoint-core publishable, add dependency-safe release ordering, and preserve existing requester acknowledgment behavior for the upcoming role adapters.

Refs #431
Use endpoint-core for device-wide client invoke ID leasing, response admission, and exact transaction cleanup while preserving standalone APIs and legacy TSM behavior.

Refs #431
Use endpoint-core for device-wide confirmed COV and event-notification leasing, routed terminal admission, exact retry cleanup, and lifecycle draining while preserving standalone server APIs.

Refs #431
Add a single-owner endpoint session with bounded egress, injected requester/notification adapters, responder reuse, and same-invoke-ID Loopback composition proof.

Refs #431
Preserve complete endpoint network-service delivery forms, raw link-group provenance, and routed responder replies while enforcing unconfirmed-only effective group APDUs.

Refs #431
* feat(device): add truthful clock synchronization

* fix(device): preserve clockless delivery truth
* fix: preserve event array timestamp choices

* fix: preserve timestamp choices in event summaries
* feat(events): commit intrinsic transitions atomically

* fix(events): preserve intrinsic notification policy
* feat(event-enrollment): add atomic reliability fault evaluation

* fix(event-enrollment): preserve report compatibility
Apply the repository-local D4 reset/fresh-baseline policy for valid unavailable Event Enrollment observations while preserving public transition coordinates and CONFIGURATION_ERROR handling.
Route committed Event Enrollment normal and reliability transitions through the shared notification lifecycle while preserving commit order, committed timestamps, acknowledgment policy, and qualified payload/Event Log boundaries. Closes #127.
Add a canonical typed Notification Class recipient lookup, retain source-compatible wrappers, and migrate event delivery to exhaustive fail-closed outcomes without implicit broadcast. Closes #124.
Add bounded configured and passively observed Device recipient bindings, preserve routed peer identity, and suppress stale observed retries.

Refs #125
Select deterministic identity/state text before intrinsic transition commit, preserve it in transition history, and reuse the committed snapshot for delivery and retries.

Refs #135
Project committed detector and Event Enrollment outputs into exact structured notification values while preserving immutable delivery snapshots and ACK field exclusions.

Closes #135.
* feat(audit): add persistent AuditLog storage foundation

* fix(audit): preserve recovery and WPM rollback
@jscott3201

Copy link
Copy Markdown
Owner Author

Temporary qualification PR only. Exact head d111f48 completed the heavy main-target matrix in run 33284712673: Cargo Audit, Cargo Deny, MSRV 1.93, Ubuntu, macOS, Windows, Clippy, Rustfmt, file-size, and no-secret checks all passed. The first Windows attempt hit an isolated request-timer timing failure; the user-authorized failed-job rerun passed the unchanged head in 7m33s. Closing unmerged.

@jscott3201 jscott3201 closed this Aug 30, 2026
@jscott3201
jscott3201 deleted the codex/sec-rustsec-2026-0258-h2 branch August 30, 2026 02:06
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.

2 participants