ci: validate AuditLog persistence on release matrix - #459
Closed
jscott3201 wants to merge 304 commits into
Closed
Conversation
…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.
…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
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.
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Temporary validation-only draft PR for PR #458. This exists solely to run the main-target hosted platform matrix, including Windows. Do not merge. It will be closed after the exact-head checks complete.