Measured, not asserted

Does obfuscation break emissions rolling average limits?

An emission limit is two things, not one: a number, and the period the number is measured over. Software that gets the first right and the second wrong produces an average that is arithmetically perfect and legally meaningless. This test renamed the option naming the period, alongside the check written to catch exactly that, and a boiler whose 30-day average was 0.1515 lb/MMBtu against a 0.150 limit reported IN COMPLIANCE at 0.1180 -- computed over 24 real hours, at 100% data availability, with nothing on the line reading as an error.

A limit is a number and a period, and only one of them is a number

Every continuous emissions monitoring system does the same work. Collect hourly block averages. Drop the hours the permit exempts, typically startup, shutdown and malfunction. Drop the hours the analyzer itself flagged invalid. Weight what is left by heat input, average it over the period the permit names, and compare the result against the limit. If the average is over, an excess emissions report goes to the regulator.

Two of those inputs are plain numbers: the limit, and each hour's emission rate. Everything else decides which hours the average is taken over. The averaging period is a choice. The exempt operating modes are a choice. So is the data availability floor, the rule that says a period built from too few valid hours may not be reported as compliant at all -- because an average over the good hours of a bad month is not an average of that month.

The fixture used here is configured the way a permit would configure it: a 30-day rolling average, startup and shutdown exempt, a 75% availability floor, and a 0.150 lb/MMBtu limit. Two units are measured. Boiler 2 ran all month, with six exempt startup hours and a 41-hour stretch the analyzer flagged bad; its average over the remaining 673 hours is 0.1515, which is an exceedance. Boiler 5's analyzer was down for all but the last 34 hours, so its availability is 4.7% and no average may be reported for it at all.

The averaging engine is copied in unprotected, as an installed dependency would be. Its own defaults are the permissive ones any library would ship: a one-day window, no exempt modes, no availability floor, no limit.

Protection on its own moved nothing

Before any renaming, all five areas of this pass were run through five presets: the ES5 default path, the modern target, the emit gate's own option set on both targets, and the string transforms. Twenty-five comparisons, every one byte-identical to the unprotected run, with the guard that runs each original twice and refuses to compare anything against a fixture that will not reproduce itself.

So nothing below is caused by protecting the code. Every result in this article required member renaming aimed at a property name that both the plant's own code and the installed averaging engine read. That is a configuration question, not a verdict on protection.

A one-day average of twenty-four genuine hours, reported as a month

Renaming the averaging period on its own was caught. The plant's rule of record refuses an average that was not taken over the permit's period, and it said so: average-rejected(period-check(the permit is a 30-day rolling average, 24h was averaged)). Nothing is reported, somebody investigates. That is the mitigation working exactly as designed.

Now lose the enabling flag in front of that rule in the same pattern -- one adjacent line in the same options object -- and the outcome inverts. Boiler 2 reports window=24h blocks=697..720 hours-used=24 excluded=0 invalid=0 availability=100.0% rate=0.1180 limit=0.15 => IN COMPLIANCE.

Read that line as a regulator would. Twenty-four hours were averaged and twenty-four hours are reported. Every one of those readings is genuine; they are the last day of the month, and the last day was clean. Data availability is 100%, which is better than the correct run's 94.3%. The limit is printed correctly. The only wrong thing on the line is the word 24h where the permit says 720, and it is stated openly. A month that owed an excess emissions report at 0.1515 now certifies at 0.1180.

Boiler 5 goes the same way. Its last 23 hours average 0.1210 and it reports IN COMPLIANCE too, on a month whose analyzer was down for 96% of the hours.

The availability floor is printed, correct, and consulted by nothing

The data availability floor deserves its own paragraph because of how its failure looks. Renamed on its own, the plant's rule caught it: average-rejected(period-check(34 valid hour(s) of 720 is below the 75% monitor availability)). Renamed together with the flag in front of that rule, Boiler 5 reports hours-used=34 excluded=0 invalid=686 availability=4.7% rate=0.1210 limit=0.15 => IN COMPLIANCE.

The number that should have stopped the report is on the same line as the report. availability=4.7% is computed, formatted and printed, and it is correct. It is simply not compared against anything any more, because the value it was to be compared against reverted to the library's default of zero and the rule that would have checked it independently was switched off by the same edit.

This is the third pass running in which a quiet name switches off a loud one. The enabling flag is checked first, so the strict control behind it is never consulted -- and any reasoning of the form "if this broke we would know" only holds when the thing that would tell you is not downstream of the thing that broke.

What failed loudly, and why both operands did

The plain numbers failed closed and said so. Renaming the hourly emission rate produced non-finite-emissions(refused before averaging) on both units. So did renaming the heat input. That is worth stating precisely, because a previous pass in this series found a ratio whose numerator was guarded and whose denominator was not: there, the numerator was summed into an accumulator the engine checked for finiteness and the denominator was never summed into anything, so losing it produced a silent NaN comparison rather than a refusal.

A heat-input-weighted average has no such gap. Both operands accumulate -- one into the weighted sum, one into the total heat input -- so a single finiteness check on the accumulators covers both. That is a property of this shape of arithmetic, not a general reassurance. The rule that generalises is the narrow one: a finiteness check written on an accumulator protects the operands that accumulate, and it is worth knowing which of your operands do not.

The validity flag also failed loudly, and in the strictest direction: with it renamed, every hour reads invalid, availability drops to 0.0% and both units report NOT REPORTED. Renaming the limit itself produced limit=Infinity and a silent IN COMPLIANCE on a 0.1515 average, which is the one option in the area whose loss is both quiet and total.

The exempt modes fail in the safe direction, and that is not luck

Renaming the exempt operating modes reverted the engine to an empty exemption set, so the six startup hours were counted rather than dropped. Boiler 2 still reported an exceedance. Renaming the mode field on the hourly block did the same thing by the other route: no hour matched an exemption, so none was dropped.

Both directions err toward including more hours in the average, which is the conservative direction for an emissions limit. That is a consequence of where the vendor's default points, not of the exemption being an unimportant name. The same structure with the exemption list pointing the other way -- a default that exempts rather than one that exempts nothing -- would fail silently and in the operator's favour, and there would be nothing in the code to tell the two apart.

That is the practical reading of every arm in this article. The severity of losing a name is not a property of the name or of how important the option sounds. It is whichever direction the library's default happens to point, and the options on one object routinely point opposite ways.

What this means if you protect a monitoring or compliance front end

The exposure is narrow and nameable. Renaming inside your own module is not the problem. The problem is a rename pattern wide enough to reach the keys of an options object you hand to an installed library, or the fields of a record that library reads.

Three things follow. First, keep member renaming off the boundary: use RenameMembers with a MemberRegexp that matches your internal names and excludes every option key and record field a dependency reads. Second, treat the averaging period, the exemption set and the availability floor as the highest-value names in the file, ahead of the limit itself -- the limit is the number a reviewer checks and the period is the one that decides what the number means. Third, assert the outcome rather than the configuration: a smoke test that runs one month of known hourly data and asserts the reported average to four decimals fails in every arm in this article, and a test that checks the averaging period is configured passes in most of them.

None of this argues against protecting a monitoring front end, and the same reasoning underpins the control narrative in IEC 62443 and industrial JavaScript. The protected build reported both units correctly on all five presets. It argues for knowing which names in the file are shared with somebody who is not rebuilt with you.

Frequently asked questions

Did obfuscation change any reported average on its own?

No. Five areas were run through five presets and every protected output matched the unprotected run -- twenty-five comparisons, no differences. Every failure in this article required member renaming aimed at a property name the installed averaging engine also reads.

What exactly happened when the averaging period was renamed?

On its own it was caught: the plant's own rule refused an average taken over 24 hours when the permit names 720. Renamed together with the flag that enables that rule -- one adjacent line in the same options object -- a 30-day average of 0.1515 lb/MMBtu became a one-day average of 0.1180 reading IN COMPLIANCE.

Would anything on the report look wrong?

Not to a reviewer checking the numbers. Every hourly reading in the shortened window is genuine, the limit prints correctly, and data availability reads 100% because the last day was a clean day. The only tell is the window length itself, printed as 24h where the permit says 720h.

What happened to the data availability floor?

Renamed with the same flag, a unit whose analyzer ran 4.7% of the month certified as compliant -- and the 4.7% figure was computed, printed and correct on the same line as the compliance finding. It was simply no longer compared against anything.

Which names failed safely?

The plain numbers. Renaming the hourly emission rate or the heat input refused the whole period with a non-finite emissions error, because both operands of a heat-input-weighted average accumulate and one finiteness check covers both. Renaming the validity flag made every hour invalid and reported nothing.

Did the exempt startup and shutdown hours fail dangerously?

No, and it is worth knowing why. Losing them reverted to an empty exemption set, so more hours entered the average, which is the conservative direction. That is where this library's default points, not a property of the name. A library defaulting the other way would fail silently in the operator's favour.

What is the cheapest test that would have caught all of this?

Run one month of known hourly data end to end and assert the reported average to four decimal places and the reported window length. That fails in every arm in this article. A test that checks the averaging period is configured passes in most of them.

Related reading