← trenyx.io / audits

verify-006 · building fault detection / Rust + SQL + Python · blind read, static

open-fdd: two rule flavors, two answers, fixed in a day.

A pre-registered, blind read of bbartling/open-fdd, a fault-detection platform for building HVAC whose README publishes 62 rules "in two parity-matched flavors, the same rules": one in SQL for production, one in Pandas for the notebook. Free and unsolicited. Verdict: two correctness findings against that claim, no security finding, and a maintainer who closed both before the day was out.

Blind read · pinned commit c20ab483 · read 2026-09-04 · reported 2026-09-08 · fixed 2026-09-08 in 3.3.41 · re-checked 2026-09-09

Two hand-maintained copies of sixty-two rules is the most falsifiable thing in the project, and I said so in the plan before I opened the code. The failure that matters for fault detection is the silent one: a fault that should fire and doesn't. So the plan pre-committed to reading the two catalogs side by side, rule id by rule id, looking for the place where the same id gives two answers.

result
pre-registered claims reached a cited verdict9 / 9
security findings0 (auth, MQTT ingest and the MCP write gate all held)
correctness findings2, both against the parity claim
maintainer reply5 minutes; fix merged the same day; both rules added to the parity harness

The first pair was FC3, the mixed-air temperature rule. In the notebook flavor its tolerance is a tunable parameter with a default of 1.15. In the production SQL it was the literal 1.15 with no parameter at all. An operator who tuned the tolerance changed what the notebook flagged without changing what production flagged, for the same rule id. The two flavors also picked the fan signal in opposite orders when both signals were present.

The second was VAV-2, the night-setback rule. SQL decided "unoccupied" from an exact list of strings. Pandas also accepted any numeric occupancy at or below 0.05. A reading of 0.03 was occupied in one flavor and unoccupied in the other, so the fault could fire in the notebook and stay silent in production. Neither flavor was wrong on its own. They disagreed, and nothing fenced the disagreement: the project's own parity harness covered about twenty-two of the sixty-two pairs at that commit, and these two were not among them.

Two implementations of "the same rules". This is a fault class the catalogue now carries under its own name: two hand-maintained flavors of one rule set drift in thresholds, units or null handling exactly where the parity harness doesn't reach. The harness is the fence. The drift lives in its gaps.
What happened next

Reported at 18:47. Answered at 18:52. Fixed that evening.

Because neither finding had a security consequence, the note went up as a public issue, with what held stated first: the JWT secret fails closed off loopback, MQTT ingest requires client certificates, MCP writes are gated behind an operator flag plus a per-call confirm. Ben Bartling replied within five minutes and shipped the fix the same day as release 3.3.41: the FC3 tolerance became three registry parameters, the fan-signal priority now matches on both sides with a comment citing the issue, VAV-2's SQL normalises numeric occupancy the way Pandas does, and the parity harness gained both rules, which is the part that keeps it fixed. The README's registry count was corrected while he was there.

I re-checked the merged fix against every point on the ninth, statically, and confirmed it on the issue. That's the loop closed: a claim, a disagreement found where the claim's own test didn't look, a fix, and a fence.

What this read did not do

It read the code. It never touched the tests.

A blind read is a static pass at one pinned commit: the plan is hashed first, then the code is read against it, and every verdict cites a line. It does not build or run the project, and it does not plant anything in the project's suite. The sealed engagement is the other kind of entry on this site: catalogue defects planted one at a time into a clean checkout, the project's own tests run against each, and the escapes counted. The analitiq-engine entry is what that looks like, with its kill count.

Verify it yourself

The pre-registration is the honesty proof.

Before reading a line of the implementation, I wrote the attack plan from the project's README alone, hashed it, and anchored the hash to the Bitcoin blockchain. A second, server-stamped clock was added at publication. Check the chain in about a minute:

  1. Clone the public engagement repo and hash the frozen attack plan:
    shasum -a 256 00-preregistration/ATTACK-PLAN.md
  2. Confirm it matches the recorded hash 5ee8a8db…38480, and verify its Bitcoin timestamp (block 965548): ots verify 00-preregistration/ATTACK-PLAN.md.ots.
  3. Read the findings against the pinned commit, and the maintainer's fix against the findings.

Open the engagement repo →   Read the note, the reply and the re-check

This is what an engagement looks like

Want your codebase read the same way?

Every engagement ships the pre-registration, the findings with their citations, and the maintainer's response, disclosed to you first, privately, with the same receipts you see here.

How to work with me →