Skip to content

Commit 6a1ce0f

Browse files
jscott3201claude
andauthored
Formalize fault-ID bands; move SYS-FC-100/101 to SYS-FC-058/059; band-grouped sidebar (#38)
The numbering bands (001-049 reference-derived, 050-099 expansion, 100-149 advanced statistical, 150-199 ML) existed as one undocumented SCHEMA line; this makes them a stated contract. SYS-FC-100/101 (flatline/spike) are deterministic rules that landed in the advanced-statistical band — renamed to SYS-FC-058/059 with all cross-references swept (clusters, sensor-drift playbook, sibling sys cards). The CXF does not embed the fault ID, so content_id is unchanged and both rules re-verify green at the pin. tools/lint/id_bands.py enforces the checkable direction (100-149 must be method: statistical; id must match dir) in CI; 050-099 deliberately admits any method — 13 reference-derived statistical rules live there. The book sidebar now groups a family's rules under band separators when it spans bands (draft chapters), and family index pages carry a band legend linking to the Schema. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 979abc2 commit 6a1ce0f

18 files changed

Lines changed: 132 additions & 40 deletions

File tree

.github/workflows/verify.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ jobs:
2828
working-directory: cxf-library
2929
run: python3 tools/lint/svg_fitment.py
3030

31+
- name: id band lint
32+
working-directory: cxf-library
33+
run: python3 tools/lint/id_bands.py
34+
3135
- uses: dtolnay/rust-toolchain@stable
3236

3337
- uses: Swatinem/rust-cache@v2

SCHEMA.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,21 @@ cxf-library/
2323
```
2424

2525
Equipment family keys: `ahu`, `vav`, `rtu`, `hp`, `fcu`, `chw`, `hw`, `erv`,
26-
`pmp`, `vfd`, `sys`, `tower`. Fault IDs follow `{EQUIP}-FC-{NNN}` (001–049 G36-derived,
27-
050–099 research-backed, 100–149 advanced statistical, 150–199 ML) per the HVAC
28-
FDD Reference; folder name is the fault ID, uppercase.
26+
`pmp`, `vfd`, `sys`, `tower`. Fault IDs follow `{EQUIP}-FC-{NNN}`; the folder
27+
name is the fault ID, uppercase. The number encodes the rule's provenance
28+
band (numbering within a band is arbitrary and IDs are never reused):
29+
30+
| Band | Meaning |
31+
|---|---|
32+
| 001–049 | Transcribed from the HVAC FDD Reference / G36-derived; IDs preserve the source's numbering. |
33+
| 050–099 | Library expansion — research-, standards-, or simulation-backed rules of **any** method (threshold `rule` or `statistical`). |
34+
| 100–149 | Advanced statistical — sequential/accumulating (e.g. CUSUM) or fleet-relative (e.g. neighbor-median) methods. `method: statistical` required (lint-enforced). |
35+
| 150–199 | ML-based methods (reserved). |
36+
37+
Band placement is set at authoring time and IDs are stable identifiers —
38+
renames are exceptional (the CXF does not embed the fault ID, so a rename
39+
never churns `content_id`, but it does break external links and every
40+
cross-reference).
2941

3042
## Design stance (why the pieces split this way)
3143

clusters/clusters.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@
116116
"AHU-FC-002",
117117
"AHU-FC-003",
118118
"SYS-FC-055",
119-
"SYS-FC-100",
120-
"SYS-FC-101"
119+
"SYS-FC-058",
120+
"SYS-FC-059"
121121
],
122122
"playbook": "sensor-drift",
123123
"prevalence": "~15%",

faults/sys/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ this chapter:
77
schedule faults, fully specified in HVAC FDD Reference v1.0 ch.16
88
(pdf pages 138–147). All eight are verified — this completed the
99
reference's entire fully-specified fault set.
10-
- **Sensor-health rules** (SYS-FC-054 + SYS-FC-100/101): the library's
10+
- **Sensor-health rules** (SYS-FC-054 + SYS-FC-058/059): the library's
1111
cross-equipment sensor-integrity family per the accepted design
1212
(internal sensor-health design note, local-only). SYS-FC-054 is BOTH — the
1313
reference's own paired-sensor cross-validation card is exactly the
@@ -33,12 +33,12 @@ Point dictionary: [`points/sys.points.json`](../../points/sys.points.json).
3333
| SYS-FC-055 | Virtual sensor drift detection | 3 | statistical | **verified** |
3434
| SYS-FC-056 | Zone heating active during summer / warm weather | 3 | rule | **verified** |
3535
| SYS-FC-057 | Exhaust fan schedule misalignment with AHU | 3 | rule | **verified** |
36-
| SYS-FC-100 | Sensor flatline while equipment active | 3 | rule | **verified** |
37-
| SYS-FC-101 | Sensor spike / rate-of-change violation | 3 | rule | **verified** |
36+
| SYS-FC-058 | Sensor flatline while equipment active | 3 | rule | **verified** |
37+
| SYS-FC-059 | Sensor spike / rate-of-change violation | 3 | rule | **verified** |
3838

3939
All severities/methods are re-verified against the chapter text (SYS-FC-052
4040
is severity 4/info per the chapter, correcting the provisional row).
41-
SYS-FC-100/101 are library-authored (design doc + public sources: Yang et
41+
SYS-FC-058/059 are library-authored (design doc + public sources: Yang et
4242
al. 2008, Liao et al. 2021, Dey & Dong 2016). Naming note: the sys
4343
dictionary's host-derived schedule boolean is `occ_scheduled`; the ahu
4444
dictionary spells the same concept `occ_schedule` — a known inconsistency,

faults/sys/SYS-FC-054/card.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ suppressed_by: []
2323
adjudicates:
2424
points: [sensor_value_a, sensor_value_b]
2525
verdict: ambiguous
26-
related: [SYS-FC-055, SYS-FC-100, SYS-FC-101, AHU-FC-062, RTU-FC-052]
26+
related: [SYS-FC-055, SYS-FC-058, SYS-FC-059, AHU-FC-062, RTU-FC-052]
2727
playbooks: [sensor-drift]
2828
operating_states: "all, within the binding's own validity window — the two sensors must be measuring the same physical quantity at the moment of comparison, which for a stream-mixing pair is true only in particular damper or mode states (host-enforced; see preconditions)"
2929
preconditions: "sensor_value_a and sensor_value_b are ROLE points, not canonical names: the host's instance configuration records which real point each is bound to, and that record is also what resolves this card's adjudicates target. Both must be bound to the same quantity kind in the same units — the rule subtracts two numbers and converts nothing, so a pair trended in °C against °F reads as a permanent 30-unit divergence and alarms forever. drift_threshold ships as a temperature placeholder and MUST be retuned to the binding (see Deviations); a percent-quantity pair left at the shipped 2.0 gets a band the reference never intended. The pair must genuinely see the same quantity during evaluation, which is a per-binding claim the graph cannot check: erv_oa_entering_temp against oat holds whenever both are in the outdoor air stream, but mat against rat holds only with the outdoor air damper shut and mat against oat only at full economizer, so the host must gate those bindings on damper position and exclude the minutes after a changeover exactly as AHU-FC-062 does. Delivery quality is resolved before this rule runs, not by it: a value the host held over from twenty minutes ago reads as a divergence, and the rule is right about the number it was given and wrong about the sensor. Per the design doc's normative constraint, no other card may list SYS-FC-054 in its suppresses — an equipment fault silencing the sensor rule that invalidates it is a cycle with a wrong answer at both ends."
@@ -136,7 +136,7 @@ The reference's four, in its order:
136136
outdoor sensor on a sunlit wall, a probe downstream of a leak, a pair split
137137
across a mixing plane), which is a binding correction, not a work order
138138
4. Sensor failure — a transmitter drifting toward a rail, on its way to the
139-
flatline SYS-FC-100 will catch when it arrives
139+
flatline SYS-FC-058 will catch when it arrives
140140

141141
Every one of the four names a single sensor and this rule cannot say which of
142142
the two it is; the playbook's Step 3.4 settles it by taking a reference
@@ -200,7 +200,7 @@ emits nothing. The quantity is entirely cascade, which is why
200200
both are kept and chained, the VFD-FC-050 shape. A single 5400 s delay behaves
201201
identically as shipped; the chain is what lets a site keep a 30-minute drift
202202
window and a two-hour alarm hold, or the reverse, without re-authoring.
203-
- **No activity gate, deliberately.** SYS-FC-100 needs `equip_active` because a
203+
- **No activity gate, deliberately.** SYS-FC-058 needs `equip_active` because a
204204
signal that is not moving on idle equipment is not evidence of anything; a
205205
bias test needs no such permission, since two thermometers in the same air
206206
disagree when one is wrong whether or not a fan runs. What this rule does need
@@ -211,7 +211,7 @@ emits nothing. The quantity is entirely cascade, which is why
211211
are combinational, and so is `GreaterThreshold` at the shipped `h = 0` — it
212212
takes a state word only when hysteresis is enabled — so tick one compares two
213213
live readings and means it. `Discrete.UnitDelay`'s tick-one artifact and the
214-
ban on `Reals.Derivative` belong to SYS-FC-101; this card's only state is the
214+
ban on `Reals.Derivative` belong to SYS-FC-059; this card's only state is the
215215
two timers.
216216
- **`TrueDelay` asserts at exactly `T + delayTime`,** verified against the engine
217217
at the pin rather than assumed: with `delayOnInit` the timer is zero on the
@@ -260,7 +260,7 @@ common false positive. Real drift opens slowly and does not close; a placement
260260
mismatch opens and closes with the weather, the schedule, or the damper, and the
261261
repair is a bracket rather than a calibration.
262262

263-
The family's three members answer different questions: SYS-FC-100 catches the
264-
transmitter that has stopped moving, SYS-FC-101 the one that jumps further than
263+
The family's three members answer different questions: SYS-FC-058 catches the
264+
transmitter that has stopped moving, SYS-FC-059 the one that jumps further than
265265
the process can, and this one the one that is quietly wrong. A sensor that trips
266-
this rule and later trips SYS-FC-100 has finished failing.
266+
this rule and later trips SYS-FC-058 has finished failing.

faults/sys/SYS-FC-055/card.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ source:
1414
- "HVAC FDD Reference v1.0 §16, SYS-FC-055 (pdf pp. 144-145) — the residual equation, both published thresholds, the four diagnoses, the whole impact profile, and the Koo & Yoon note"
1515
- "The reference's own provenance line for that card: Koo & Yoon 2022; Sun et al. 2024 (virtual sensor RMSE 0.30 °C, bias > 1 °C detected reliably)"
1616
- "Accepted design: internal sensor-health design note (local-only, not distributed) (§2 stance, §2.3 the adjudicates contract, §4.3 the MovingAverage ring floor, §4.4 vector strategy)"
17-
- "Library precedent: SYS-FC-054 (the pair form of the same question), SYS-FC-100/101 (the role-point sensor family), AHU-FC-056 (Reals.MovingAverage at a 64-checkpoint ring), HP-FC-050 and VAV-FC-050 (host-fitted baselines consumed as ordinary points)"
17+
- "Library precedent: SYS-FC-054 (the pair form of the same question), SYS-FC-058/059 (the role-point sensor family), AHU-FC-056 (Reals.MovingAverage at a 64-checkpoint ring), HP-FC-050 and VAV-FC-050 (host-fitted baselines consumed as ordinary points)"
1818
g36: null
1919
clusters: [CLU-09]
2020
suppresses: []
2121
suppressed_by: []
2222
adjudicates:
2323
points: [physical_sensor]
2424
verdict: invalid_while_active
25-
related: [SYS-FC-054, SYS-FC-100, SYS-FC-101, AHU-FC-062, RTU-FC-052]
25+
related: [SYS-FC-054, SYS-FC-058, SYS-FC-059, AHU-FC-062, RTU-FC-052]
2626
playbooks: [sensor-drift]
2727
operating_states: "all, within the operating envelope the virtual sensor was trained on. The graph has no gate and evaluates whenever the host publishes a prediction, so the envelope is the host's to enforce: a Ridge model fitted over a summer learning period is extrapolating in January, and its extrapolation error arrives here as a residual indistinguishable from sensor drift. Where the host cannot vouch for the prediction it should stop publishing virtual_value rather than publish a guess."
2828
preconditions: "physical_sensor and virtual_value are a ROLE PAIR, not canonical names: the host's instance configuration records which real point physical_sensor is bound to, and that record is what resolves this card's adjudicates target. Both thresholds are in the BOUND point's units — the reference's 1.5 and 3.0 are its temperature defaults and MUST be retuned for any other quantity kind. Four host obligations decide whether this rule means anything. (1) The model must never take the accused sensor as one of its own features: a regression that can see physical_sensor predicts it perfectly, the residual collapses to zero, and the rule goes permanently silent while reporting health. (2) The learning period must be known-good. A model trained while the sensor was already 2 K high learns the bias as truth, and the drift becomes invisible from the moment it is fitted — this rule cannot detect a fault that predates its own baseline. (3) Model health is a separate question with a separate rule: the reference's META-FC-050 (statistical model confidence degradation) is what says the Ridge fit has stopped tracking, and a host running it should read a degraded model as NO_EVAL here rather than as sensor drift. (4) Delivery quality is resolved before this rule runs, not by it — a value held over from a dead subscription reads as a residual, and the rule is right about the number it was given and wrong about the sensor (design doc §2.2). Recommended: report NO_EVAL for the first `window` after load, where both statistics are computed over a partial window; the graph will still produce a verdict there, and `bias_present_at_load` pins what that verdict looks like."
@@ -153,7 +153,7 @@ The reference's four, in its order:
153153
identical output with the accused transmitter in perfect health
154154

155155
Diagnosis 4 is the residual ambiguity to weigh against `invalid_while_active`.
156-
The cheap discriminator is the family: run SYS-FC-054, SYS-FC-100 and SYS-FC-101
156+
The cheap discriminator is the family: run SYS-FC-054, SYS-FC-058 and SYS-FC-059
157157
on the model's *input* sensors, and a clean bill on the features turns this
158158
rule's finding from a suspicion into an accusation. The expensive one is a
159159
reference instrument, which is where the playbook ends up anyway.
@@ -244,7 +244,7 @@ emits nothing. The quantity is entirely cascade, which is why
244244
never means NO_EVAL — this rule has no evaluability output, because its
245245
evaluability question (is the model still fit?) lives outside the graph.
246246
- **Role points, and the thresholds are in the bound point's units.** Same
247-
documented exception as SYS-FC-054 and SYS-FC-100 (SCHEMA.md points contract):
247+
documented exception as SYS-FC-054 and SYS-FC-058 (SCHEMA.md points contract):
248248
one graph deploys against many real points, so the host's instance
249249
configuration records each binding. Both published thresholds are the
250250
reference's temperature numbers; a humidity or pressure binding left at 1.5
@@ -260,7 +260,7 @@ emits nothing. The quantity is entirely cascade, which is why
260260
SYS-FC-055 as a member with SYS-FC-054 as trigger, and
261261
`playbooks/sensor-drift.md` already names this rule in its Applies-To row and
262262
in step 1.2. Both files predate this card and neither needs an edit.
263-
- **`category: COMFORT_ENERGY` transcribed, not argued.** SYS-FC-100 departs to
263+
- **`category: COMFORT_ENERGY` transcribed, not argued.** SYS-FC-058 departs to
264264
`PROTECTIVE` on the grounds that a sensor gate delivers avoided false alarms
265265
rather than energy, and the argument applies word for word here; it is not
266266
taken because the reference publishes a profile for *this* card and it says
@@ -295,7 +295,7 @@ calibration, never before, and treat a fault that cleared without a work order
295295
as a retraining event to be explained.
296296

297297
The family's members answer different questions about the same transmitter:
298-
SYS-FC-100 catches the sensor that has stopped moving, SYS-FC-101 the one that
298+
SYS-FC-058 catches the sensor that has stopped moving, SYS-FC-059 the one that
299299
jumps further than the process can, SYS-FC-054 the one that disagrees with a
300300
partner in the same air stream, and this one the one that disagrees with
301301
everything else at once. The

faults/sys/SYS-FC-056/card.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ should follow the actual heating source (the same caveat VAV-FC-055 carries).
179179
- **`oat` drift is the standing false positive and this card does not solve
180180
it.** A sun-baked or drifted outdoor sensor reading 3 K high manufactures the
181181
fault across every zone at once, which is also the tell. SYS-FC-054,
182-
SYS-FC-100 and SYS-FC-101 adjudicate `oat` directly; where a host runs them,
182+
SYS-FC-058 and SYS-FC-059 adjudicate `oat` directly; where a host runs them,
183183
an active sensor finding on the bound `oat` makes this rule NO_EVAL through
184184
the `adjudicates` fan-out.
185185
- **`clusters: [CLU-05]` is a declaration, not an edit.** CLU-05 already lists
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
schema: cxf-library/fault-card/v1
3-
id: SYS-FC-100
3+
id: SYS-FC-058
44
name: Sensor flatline while equipment active
55
equipment: sys
66
status: verified
@@ -24,7 +24,7 @@ suppressed_by: []
2424
adjudicates:
2525
points: [sensor_value]
2626
verdict: invalid_while_active
27-
related: [SYS-FC-054, SYS-FC-101, AHU-FC-062, RTU-FC-052]
27+
related: [SYS-FC-054, SYS-FC-059, AHU-FC-062, RTU-FC-052]
2828
playbooks: [sensor-drift]
2929
operating_states: "all — the rule evaluates only while equip_active is true, and its own yWindowOk reports whether a complete window of running time has accumulated"
3030
preconditions: "Host delivery quality must be resolved before this rule runs, and this is the load-bearing precondition rather than boilerplate: a value the host is re-serving from cache because the subscription died presents to the graph as a perfectly frozen sensor, and the rule will report flatline and be right about the number it was given and wrong about the transmitter. Freshness, PointStatus and gap handling stay where the reference's ch.4 puts them — host-side, ahead of this rule (design doc §2.2). sensor_value must be bound to a live measurement: a setpoint, a configured constant, a schedule output or a host-derived aggregate that only refreshes hourly will all read as flatline and none of them is a sensor. equip_active must be bound to the run status of the equipment whose process actually drives the bound sensor — sf_status for an AHU supply-air temperature, comp_status for a suction line, pump_status for a loop reading. A VAV box has no run status of its own; bind the parent AHU's fan status or zone_airflow > 0 and record which. Both flatline_band and flatline_window are per-binding site configuration in the bound point's units and MUST be set for this instance before the rule is trusted; the shipped defaults are a worked example for a supply-air temperature, not a site value. Where yWindowOk is false the verdict is NO_EVAL, not a healthy sensor."
@@ -103,7 +103,7 @@ yFault = (still AND equip_active) held continuously for flatline_window,
103103

104104
Block graph (`rule.cxf.jsonld`):
105105

106-
![SYS-FC-100 block graph](diagram.svg)
106+
![SYS-FC-058 block graph](diagram.svg)
107107

108108
Eight blocks. `sensRef` is a `Discrete.Sampler` on the window period, so the
109109
comparison is always against where the reading sat at the start of the current
@@ -225,7 +225,7 @@ heating fault or an electric cooling one, and the rule does not know which.
225225
reporting twice the actual slope at a 300 s tick with `T = 300 s`.
226226
- **`Discrete.Sampler` emits the live input on its first tick,** so there is no
227227
startup artifact of the kind `Discrete.UnitDelay` produces with `y_start = 0`
228-
(that is SYS-FC-101's problem). This is pinned by an arrival time rather than
228+
(that is SYS-FC-059's problem). This is pinned by an arrival time rather than
229229
asserted: the alarm lands at `flatline_window + alarm_delay` from t = 0, which
230230
is only reachable if the first baseline was the live reading.
231231
- **The sampler grid is anchored to absolute model time, not to controller
@@ -244,7 +244,7 @@ heating fault or an electric cooling one, and the rule does not know which.
244244
`flatline_band` per `flatline_window` satisfies the test. That is a sensor
245245
finding either way, but the *name* is wrong and this rule cannot supply the
246246
right one — naming drift is SYS-FC-054's job, and a host running both should
247-
read a simultaneous SYS-FC-100 and SYS-FC-054 as drift, not two faults.
247+
read a simultaneous SYS-FC-058 and SYS-FC-054 as drift, not two faults.
248248
- **Strict `<` on the band.** CDL `Reals` has no `LessEqual` and the
249249
disagreement is measure-zero on a real-valued signal, so the comparison errs
250250
toward silence. Edge cases use dyadic values (14.0 against 14.25) so the

0 commit comments

Comments
 (0)