zeropoint-node 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/CITATION.cff +1 -1
  3. package/README.md +50 -10
  4. package/dist/legacy-BEa7tljT.js.map +1 -1
  5. package/dist/legacy-CV8oTnKM.cjs.map +1 -1
  6. package/package.json +10 -2
  7. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.breathe.ts +15 -1
  8. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.cleanup.ts +15 -1
  9. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.cmyk.ts +7 -1
  10. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.generate.matrix.ts +15 -1
  11. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.browse.ts +15 -1
  12. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.click.ts +15 -1
  13. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.hear.ts +15 -1
  14. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.heat.ts +15 -1
  15. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.pulse.ts +15 -1
  16. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.speak.ts +15 -1
  17. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.i.tap.ts +15 -1
  18. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.math.ts +14 -6
  19. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.overlay.visual.controller.ts +0 -3
  20. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.rodin.ts +15 -1
  21. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.self.ts +15 -1
  22. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.service.worker.ts +30 -7
  23. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.terminal.ts +15 -1
  24. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.test.ts +15 -1
  25. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.trinity.ts +0 -3
  26. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.vbm.path.ts +6 -2
  27. package/src/0/3/6/9/1/2/4/8/7/5/1/a432.vbm.relations.ts +15 -1
  28. package/src/0/index.ts +16 -0
  29. package/src/index.ts +4 -4
  30. package/src/kernel/import-graph.ts +58 -0
  31. package/src/multidimensional-vortex-demo.ts +1 -1
  32. package/src/quantum/superposition-execution.ts +9 -4
  33. package/src/thermo/free-energy.ts +279 -0
  34. package/src/thermo/wastewater-energy.ts +204 -0
  35. package/src/vbm-math.ts +15 -1
  36. package/src/verification/lean-bridge.ts +384 -0
  37. package/src/zero-entropy-demo.ts +15 -1
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Free energy of the water reaction — Gibbs, exactly.
3
+ *
4
+ * "Free energy" is not a loose phrase. It is ΔG, the portion of a reaction's
5
+ * enthalpy change that can be taken out as work at constant temperature and
6
+ * pressure, with TΔS the part that cannot because it is owed to entropy:
7
+ *
8
+ * ΔG = ΔH − TΔS
9
+ *
10
+ * Its sign is the whole answer to whether a process can be run for gain. A
11
+ * reaction with ΔG < 0 releases work and runs on its own; one with ΔG > 0 must
12
+ * be paid for. For splitting water, ΔG is POSITIVE, and the size of it is the
13
+ * bill: 237.14 kJ per mole, or 1.229 V across an electrolysis cell. Nothing in
14
+ * this module is an opinion about that — every figure recomputes from three
15
+ * tabulated standard quantities.
16
+ *
17
+ * ARITHMETIC. All values are exact integers in units of 10⁻⁵ J/mol, so nothing
18
+ * here is a float literal and no rounding happens until a value is rendered.
19
+ * The constants are standard-state (298.15 K, 1 bar) CODATA/NIST values.
20
+ *
21
+ * WHAT THIS SETTLES. The forward and reverse reactions share one bond ledger,
22
+ * so what splitting costs is exactly what burning returns — at best. Real
23
+ * devices take a cut at every step, which `roundTrip` reports. There is no
24
+ * arrangement of the two that yields more out than in, and that is not an
25
+ * engineering limit to be improved on: it is the sign of ΔG.
26
+ */
27
+
28
+ // ============================================================================
29
+ // SCALE — exact integer arithmetic, no float literals
30
+ // ============================================================================
31
+
32
+ /** Every energy below is an integer count of 10⁻⁵ J/mol. */
33
+ export const SCALE = 100000
34
+
35
+ /** Render a scaled energy as kJ/mol, rounding once, at the boundary. */
36
+ export function toKilojoulesPerMole(scaled: number): number {
37
+ return round(scaled, SCALE * 1000)
38
+ }
39
+
40
+ /** Round a/b to the nearest integer without Math.*, ties away from zero. */
41
+ function round(a: number, b: number): number {
42
+ const neg = a < 0
43
+ const x = neg ? -a : a
44
+ const q = (2 * x + b) / (2 * b)
45
+ const f = q - (q % 1)
46
+ return neg ? -f : f
47
+ }
48
+
49
+ // ============================================================================
50
+ // STANDARD STATE (298.15 K, 1 bar) — CODATA / NIST
51
+ // ============================================================================
52
+
53
+ /** 298.15 K, held as hundredths of a kelvin. */
54
+ const T_CENTIKELVIN = 29815
55
+
56
+ /**
57
+ * ΔH° of formation for liquid water, −285.83 kJ/mol.
58
+ * H₂(g) + ½O₂(g) → H₂O(l)
59
+ */
60
+ export const ENTHALPY_FORMATION = -285830 * SCALE
61
+
62
+ /**
63
+ * ΔS° of that formation, −163.305 J/(mol·K), from the absolute entropies
64
+ * S°(H₂O,l) 69.95, S°(H₂) 130.68, S°(O₂) 205.15 J/(mol·K):
65
+ * 69.95 − 130.68 − 205.15/2 = −163.305
66
+ * Held as thousandths of a J/(mol·K).
67
+ */
68
+ const ENTROPY_FORMATION_MILLI = -163305
69
+
70
+ /** Faraday constant, C/mol. */
71
+ export const FARADAY = 96485
72
+
73
+ /** Electrons transferred per water molecule. */
74
+ export const ELECTRONS = 2
75
+
76
+ // ============================================================================
77
+ // THE FREE ENERGY
78
+ // ============================================================================
79
+
80
+ /**
81
+ * TΔS for the formation, in units of 10⁻⁵ J/mol.
82
+ *
83
+ * (T_CENTIKELVIN/100) × (ENTROPY_FORMATION_MILLI/1000) J/mol, and the two
84
+ * denominators multiply to exactly SCALE, so the product is already an integer
85
+ * in our units — no rounding enters here.
86
+ */
87
+ export const ENTROPY_TERM_FORMATION = T_CENTIKELVIN * ENTROPY_FORMATION_MILLI
88
+
89
+ /** ΔG° of formation: ΔH − TΔS. Negative — water forms and releases work. */
90
+ export const GIBBS_FORMATION = ENTHALPY_FORMATION - ENTROPY_TERM_FORMATION
91
+
92
+ /** Splitting is the reverse reaction, so every quantity changes sign. */
93
+ export const ENTHALPY_SPLITTING = -ENTHALPY_FORMATION
94
+ export const GIBBS_SPLITTING = -GIBBS_FORMATION
95
+
96
+ // ============================================================================
97
+ // CELL POTENTIALS — the same numbers, in volts
98
+ // ============================================================================
99
+
100
+ /** An exact rational, so no precision is lost before it is rendered. */
101
+ export interface ExactPotential {
102
+ /** Microvolts, as numerator/denominator. */
103
+ readonly numerator: number
104
+ readonly denominator: number
105
+ }
106
+
107
+ /**
108
+ * Reversible cell potential E° = ΔG/(nF) as an EXACT fraction of a microvolt.
109
+ *
110
+ * The rounded form below cannot be inverted exactly — quantising to whole
111
+ * microvolts throws away what the inverse would need. Returning the fraction
112
+ * keeps ΔG recoverable with no drift at all, which is what lets
113
+ * `every_model_inverts` demand equality rather than a tolerance.
114
+ */
115
+ export function reversiblePotentialExact(): ExactPotential {
116
+ return { numerator: GIBBS_SPLITTING * 10, denominator: ELECTRONS * FARADAY }
117
+ }
118
+
119
+ /** Thermoneutral potential ΔH/(nF), likewise exact. */
120
+ export function thermoneutralPotentialExact(): ExactPotential {
121
+ return { numerator: ENTHALPY_SPLITTING * 10, denominator: ELECTRONS * FARADAY }
122
+ }
123
+
124
+ /**
125
+ * Recover the energy an exact potential came from, in 10⁻⁵ J/mol.
126
+ *
127
+ * E is `numerator/denominator` microvolts and energy = E·nF/10, where the
128
+ * denominator IS nF — so it cancels. The cancellation is done here rather than
129
+ * by multiplying it out: `numerator × denominator` is about 4.6e16, past the
130
+ * 2^53 where doubles stop counting exactly, and multiplying it out reintroduced
131
+ * the very drift these exact potentials exist to remove.
132
+ */
133
+ export function energyFromPotential(p: ExactPotential): number {
134
+ if (p.denominator !== ELECTRONS * FARADAY) {
135
+ throw new RangeError(`potential denominator must be nF = ${ELECTRONS * FARADAY}, got ${p.denominator}`)
136
+ }
137
+ return p.numerator / 10
138
+ }
139
+
140
+ /**
141
+ * Reversible cell potential, E° = ΔG/(nF), in whole microvolts.
142
+ *
143
+ * The least voltage that can split water at all. Below it the reaction does not
144
+ * run, however the cell is built. Rounded for display; use
145
+ * `reversiblePotentialExact` when the value will be computed with further.
146
+ */
147
+ export function reversiblePotentialMicrovolts(): number {
148
+ const { numerator, denominator } = reversiblePotentialExact()
149
+ return round(numerator, denominator)
150
+ }
151
+
152
+ /**
153
+ * Thermoneutral potential, ΔH/(nF), in microvolts.
154
+ *
155
+ * At this voltage the cell neither warms nor cools: the entropy term is being
156
+ * supplied electrically instead of drawn from the surroundings. It is strictly
157
+ * above the reversible potential, and the gap is TΔS.
158
+ */
159
+ export function thermoneutralPotentialMicrovolts(): number {
160
+ const { numerator, denominator } = thermoneutralPotentialExact()
161
+ return round(numerator, denominator)
162
+ }
163
+
164
+ // ============================================================================
165
+ // THE CYCLE
166
+ // ============================================================================
167
+
168
+ export interface RoundTrip {
169
+ /** Energy that must be supplied to split, 10⁻⁵ J/mol. */
170
+ readonly inputRequired: number
171
+ /** Energy recoverable by burning the hydrogen back, 10⁻⁵ J/mol. */
172
+ readonly outputRecovered: number
173
+ /** outputRecovered − inputRequired. Never positive. */
174
+ readonly net: number
175
+ /** Efficiency as a percentage, rounded once. */
176
+ readonly percent: number
177
+ /** True only if the cycle returns more than it took. */
178
+ readonly gainsEnergy: boolean
179
+ }
180
+
181
+ /**
182
+ * A split-then-burn cycle, at whatever efficiencies you give it.
183
+ *
184
+ * Each efficiency is a percentage, 0..100, as an integer. Passing 100 for all
185
+ * three describes the thermodynamic ideal — the case most favourable to a
186
+ * closed loop, and the one that matters, because if it fails there it fails
187
+ * everywhere.
188
+ */
189
+ export function roundTrip(
190
+ electrolysisPercent: number,
191
+ enginePercent: number,
192
+ generatorPercent: number,
193
+ ): RoundTrip {
194
+ for (const p of [electrolysisPercent, enginePercent, generatorPercent]) {
195
+ if (!Number.isInteger(p) || p < 0 || p > 100) {
196
+ throw new RangeError(`efficiency must be an integer percentage in 0..100, got ${p}`)
197
+ }
198
+ }
199
+
200
+ // Splitting must supply the full enthalpy, not merely ΔG: a real cell run at
201
+ // the reversible potential draws the entropy term from its surroundings and
202
+ // goes cold, which no engine cycle recovers.
203
+ const inputRequired = electrolysisPercent === 0
204
+ ? Number.POSITIVE_INFINITY
205
+ : round(ENTHALPY_SPLITTING * 100, electrolysisPercent)
206
+
207
+ // Burning returns the same enthalpy — the identical bond ledger, reversed —
208
+ // and then the engine and generator each take their cut.
209
+ const outputRecovered = round(
210
+ round(ENTHALPY_FORMATION * -1 * enginePercent, 100) * generatorPercent,
211
+ 100,
212
+ )
213
+
214
+ const net = outputRecovered - inputRequired
215
+ const percent = inputRequired === Number.POSITIVE_INFINITY
216
+ ? 0
217
+ : round(outputRecovered * 100, inputRequired)
218
+
219
+ return { inputRequired, outputRecovered, net, percent, gainsEnergy: net > 0 }
220
+ }
221
+
222
+ // ============================================================================
223
+ // SELF-CHECK
224
+ // ============================================================================
225
+
226
+ /** Facts this module must satisfy. Returns failures. */
227
+ export function selfTest(): string[] {
228
+ const fail: string[] = []
229
+
230
+ // ΔG = ΔH − TΔS must reproduce the tabulated ΔG°f of −237.14 kJ/mol.
231
+ const kj = toKilojoulesPerMole(GIBBS_FORMATION)
232
+ if (kj !== -237) fail.push(`ΔG°f rounds to ${kj} kJ/mol, expected -237`)
233
+
234
+ // Formation releases work; splitting costs it. This is the sign that decides
235
+ // whether a reaction can be run for gain, and it is the whole answer.
236
+ if (!(GIBBS_FORMATION < 0)) fail.push('ΔG of formation is not negative')
237
+ if (!(GIBBS_SPLITTING > 0)) fail.push('ΔG of splitting is not positive')
238
+
239
+ // The forward and reverse reactions are one ledger read in two directions.
240
+ if (ENTHALPY_SPLITTING !== -ENTHALPY_FORMATION) fail.push('enthalpy is not antisymmetric')
241
+ if (GIBBS_SPLITTING !== -GIBBS_FORMATION) fail.push('Gibbs is not antisymmetric')
242
+
243
+ // Cell potentials, to the millivolt.
244
+ const rev = round(reversiblePotentialMicrovolts(), 1000)
245
+ const thermo = round(thermoneutralPotentialMicrovolts(), 1000)
246
+ if (rev !== 1229) fail.push(`reversible potential ${rev} mV, expected 1229`)
247
+ if (thermo !== 1481) fail.push(`thermoneutral potential ${thermo} mV, expected 1481`)
248
+ if (!(thermo > rev)) fail.push('thermoneutral potential is not above the reversible one')
249
+
250
+ // The exact potentials must invert with NO drift — that is their purpose.
251
+ if (energyFromPotential(reversiblePotentialExact()) !== GIBBS_SPLITTING) {
252
+ fail.push('reversible potential does not invert exactly')
253
+ }
254
+ if (energyFromPotential(thermoneutralPotentialExact()) !== ENTHALPY_SPLITTING) {
255
+ fail.push('thermoneutral potential does not invert exactly')
256
+ }
257
+
258
+ // The ideal cycle breaks even and never better.
259
+ const ideal = roundTrip(100, 100, 100)
260
+ if (ideal.net !== 0) fail.push(`ideal cycle net ${ideal.net}, expected 0`)
261
+ if (ideal.gainsEnergy) fail.push('ideal cycle reports a gain')
262
+
263
+ // Every real cycle loses. Exhaustive over the efficiency grid in 5% steps,
264
+ // excluding only the ideal corner above.
265
+ for (let e = 5; e <= 100; e += 5) {
266
+ for (let m = 5; m <= 100; m += 5) {
267
+ for (let g = 5; g <= 100; g += 5) {
268
+ if (e === 100 && m === 100 && g === 100) continue
269
+ const r = roundTrip(e, m, g)
270
+ if (r.gainsEnergy) {
271
+ fail.push(`cycle at ${e}/${m}/${g}% reports a gain of ${r.net}`)
272
+ return fail
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ return fail
279
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Polluted water in, electricity and drinkable water out — the real version.
3
+ *
4
+ * The described machine exists. What makes it work is that the fuel is not the
5
+ * water: it is what is dissolved in it. H₂O carries no recoverable energy —
6
+ * `free-energy.ts` settles that, ΔG for splitting is +237 kJ/mol — but organic
7
+ * contamination does, and a litre of strong effluent can carry more chemical
8
+ * energy than it costs to clean the litre.
9
+ *
10
+ * So the architecture is:
11
+ *
12
+ * polluted water → anaerobic digestion → biogas (mostly CH₄)
13
+ * → combustion engine (CHP) → ELECTRICITY
14
+ * digester effluent → membrane treatment → DRINKABLE WATER
15
+ *
16
+ * The engine burns methane the bacteria made from the pollutants. Its exhaust
17
+ * is CO₂ and water vapour; the drinkable water comes from the membrane train,
18
+ * powered by part of the electricity. Everything you described happens, and
19
+ * nothing runs on the water itself.
20
+ *
21
+ * THE WHOLE QUESTION IS THE LOAD. Weak sewage does not carry enough energy to
22
+ * pay for its own treatment; strong industrial effluent carries several times
23
+ * over. There is therefore a THRESHOLD contamination, and this module computes
24
+ * it rather than asserting it. Below it the plant imports power; above it the
25
+ * plant exports.
26
+ *
27
+ * Distillation is deliberately not the water step. Evaporating a litre costs
28
+ * about 2.26 MJ in latent heat alone — far more than any realistic load carries
29
+ * — so a still cannot be paid for by the pollution. Membranes can.
30
+ *
31
+ * ARITHMETIC. Integers throughout, in decijoules per litre (dJ/L). No float
32
+ * literals; rounding happens once, where a value is rendered.
33
+ */
34
+
35
+ // ============================================================================
36
+ // CONSTANTS — measured quantities, not choices
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Chemical energy per unit of COD (chemical oxygen demand): 13.9 J per mg.
41
+ * COD is the standard measure of oxidisable load, so this converts "how dirty"
42
+ * directly into "how much energy is in it". Held as tenths of a J/mg.
43
+ */
44
+ const JOULES_PER_MG_COD_TENTHS = 139
45
+
46
+ /**
47
+ * The same figure as J/mg, for prose that quotes it. Derived from the tenths
48
+ * above rather than written as 13.9 — a decimal literal here would be a second
49
+ * copy of the constant, and the ratchet counts those for exactly that reason.
50
+ */
51
+ export const JOULES_PER_MG_COD = JOULES_PER_MG_COD_TENTHS / 10
52
+
53
+ /**
54
+ * Fraction of COD that anaerobic digestion converts to methane, as a
55
+ * percentage. Well-run mesophilic digesters on soluble organics reach the
56
+ * mid-sixties; the rest becomes biomass or leaves undigested.
57
+ */
58
+ const DIGESTION_CAPTURE_PERCENT = 65
59
+
60
+ /**
61
+ * Electrical efficiency of a gas engine in CHP service, as a percentage.
62
+ * Reciprocating engines in the hundreds-of-kW class sit near this. Heat is
63
+ * recovered too, but heat is not what was asked for, so it is not counted.
64
+ */
65
+ const ENGINE_ELECTRICAL_PERCENT = 38
66
+
67
+ /**
68
+ * Electricity to bring digester effluent to drinking standard by membranes:
69
+ * ultrafiltration, reverse osmosis, then polishing. 4 kWh per m³ = 4 Wh/L =
70
+ * 14 400 J/L = 144 000 dJ/L. The conservative end of the RO range, because a
71
+ * dirty feed fouls membranes and costs more than a clean one.
72
+ */
73
+ export const TREATMENT_DEMAND_DECIJOULES_PER_LITRE = 144000
74
+
75
+ // ============================================================================
76
+ // THE BALANCE
77
+ // ============================================================================
78
+
79
+ export interface WaterBalance {
80
+ /** Contamination as COD, mg/L — the input. */
81
+ readonly codMilligramsPerLitre: number
82
+ /** Chemical energy present in one litre, dJ/L. */
83
+ readonly energyInLoad: number
84
+ /** Electricity the engine actually delivers, dJ/L. */
85
+ readonly electricityGenerated: number
86
+ /** Electricity the membrane train consumes, dJ/L. */
87
+ readonly treatmentDemand: number
88
+ /** generated − demand. Positive means the plant exports power. */
89
+ readonly netElectricity: number
90
+ /** True when the water pays for its own cleaning and more. */
91
+ readonly selfPowering: boolean
92
+ }
93
+
94
+ /** Integer division rounded to nearest, ties away from zero, no Math.*. */
95
+ function round(a: number, b: number): number {
96
+ const neg = a < 0
97
+ const x = neg ? -a : a
98
+ const q = (2 * x + b) / (2 * b)
99
+ const f = q - (q % 1)
100
+ return neg ? -f : f
101
+ }
102
+
103
+ /**
104
+ * Energy balance for one litre at a given contamination.
105
+ *
106
+ * `cod` is chemical oxygen demand in mg/L: roughly 300–800 for municipal
107
+ * sewage, 2 000–6 000 for dairy or brewery effluent, 20 000–80 000 for manure
108
+ * slurry or food-processing waste.
109
+ */
110
+ export function balanceFor(cod: number): WaterBalance {
111
+ if (!Number.isInteger(cod) || cod < 0) {
112
+ throw new RangeError(`COD must be a non-negative integer in mg/L, got ${cod}`)
113
+ }
114
+
115
+ const energyInLoad = cod * JOULES_PER_MG_COD_TENTHS
116
+ const captured = round(energyInLoad * DIGESTION_CAPTURE_PERCENT, 100)
117
+ const electricityGenerated = round(captured * ENGINE_ELECTRICAL_PERCENT, 100)
118
+ const netElectricity = electricityGenerated - TREATMENT_DEMAND_DECIJOULES_PER_LITRE
119
+
120
+ return {
121
+ codMilligramsPerLitre: cod,
122
+ energyInLoad,
123
+ electricityGenerated,
124
+ treatmentDemand: TREATMENT_DEMAND_DECIJOULES_PER_LITRE,
125
+ netElectricity,
126
+ selfPowering: netElectricity > 0,
127
+ }
128
+ }
129
+
130
+ /**
131
+ * The lowest whole mg/L of COD at which the plant exports power.
132
+ *
133
+ * Found by scanning upward rather than by inverting the arithmetic, so the
134
+ * answer is the same quantity `balanceFor` reports and cannot drift from it.
135
+ */
136
+ export function breakEvenCod(): number {
137
+ for (let cod = 0; cod <= 100000; cod++) {
138
+ if (balanceFor(cod).selfPowering) return cod
139
+ }
140
+ throw new Error('no break-even COD below 100 000 mg/L')
141
+ }
142
+
143
+ /** Typical loads, for locating a real stream against the threshold. */
144
+ export const TYPICAL_LOADS: readonly { readonly name: string; readonly cod: number }[] = [
145
+ { name: 'municipal sewage, dilute', cod: 300 },
146
+ { name: 'municipal sewage, strong', cod: 800 },
147
+ { name: 'brewery effluent', cod: 3000 },
148
+ { name: 'dairy processing', cod: 5000 },
149
+ { name: 'manure slurry', cod: 40000 },
150
+ ]
151
+
152
+ // ============================================================================
153
+ // SELF-CHECK
154
+ // ============================================================================
155
+
156
+ /** Facts this module must satisfy. Returns failures. */
157
+ export function selfTest(): string[] {
158
+ const fail: string[] = []
159
+
160
+ // Clean water carries nothing. This is the point: H2O is not the fuel.
161
+ const clean = balanceFor(0)
162
+ if (clean.energyInLoad !== 0) fail.push('clean water reports non-zero energy')
163
+ if (clean.electricityGenerated !== 0) fail.push('clean water generates electricity')
164
+ if (clean.selfPowering) fail.push('clean water reports as self-powering')
165
+ if (clean.netElectricity !== -TREATMENT_DEMAND_DECIJOULES_PER_LITRE) {
166
+ fail.push('clean water does not owe exactly the treatment demand')
167
+ }
168
+
169
+ // A threshold exists, is positive, and is unique — monotone in the load.
170
+ const t = breakEvenCod()
171
+ if (!(t > 0)) fail.push(`break-even COD is ${t}, expected positive`)
172
+ if (balanceFor(t).netElectricity <= 0) fail.push('break-even point does not export')
173
+ if (balanceFor(t - 1).netElectricity > 0) fail.push('a lower COD also exports — not a threshold')
174
+
175
+ // Monotone: more load never yields less electricity.
176
+ let previous = -1
177
+ for (let cod = 0; cod <= 20000; cod += 100) {
178
+ const e = balanceFor(cod).electricityGenerated
179
+ if (e < previous) { fail.push(`electricity fell between loads at COD ${cod}`); break }
180
+ previous = e
181
+ }
182
+
183
+ // Municipal sewage must sit below the threshold and strong effluent above,
184
+ // or the model has lost contact with the streams it claims to describe.
185
+ const municipal = TYPICAL_LOADS.find((l) => l.name === 'municipal sewage, strong')
186
+ const dairy = TYPICAL_LOADS.find((l) => l.name === 'dairy processing')
187
+ if (municipal && balanceFor(municipal.cod).selfPowering) {
188
+ fail.push('strong municipal sewage reports as self-powering')
189
+ }
190
+ if (dairy && !balanceFor(dairy.cod).selfPowering) {
191
+ fail.push('dairy effluent does not report as self-powering')
192
+ }
193
+
194
+ // No arrangement recovers more than the load contains: capture and engine
195
+ // efficiency are both below 100%, so electricity < energy present, always.
196
+ for (const { cod } of TYPICAL_LOADS) {
197
+ const b = balanceFor(cod)
198
+ if (cod > 0 && b.electricityGenerated >= b.energyInLoad) {
199
+ fail.push(`COD ${cod} yields electricity at or above the energy present`)
200
+ }
201
+ }
202
+
203
+ return fail
204
+ }
package/src/vbm-math.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * Wave 10: digitalRoot bridges to kernel legacy adapter (0→0).
6
6
  */
7
7
 
8
+ import { pathToFileURL } from 'node:url'
8
9
  import { abs, pow } from './0/algebra.ts'
9
10
  import { legacyDigitalRoot } from './0/3/6/9/1/2/4/8/7/5/1/a432.roots.ts'
10
11
 
@@ -374,6 +375,19 @@ export class VBMNumberAnalyzer {
374
375
  }
375
376
 
376
377
  // Run examples if this file is executed directly
377
- if (typeof require !== 'undefined' && require.main === module) {
378
+ /**
379
+ * True when this file is the entry point Node was started with.
380
+ *
381
+ * ESM has no `require.main`. The CommonJS idiom did not merely fail to detect
382
+ * direct execution here — `require` is undefined in an ES module, so the guard
383
+ * THREW on import and made the whole module unloadable. Nobody importing this
384
+ * ever got far enough to notice the guard was wrong.
385
+ */
386
+ function isMainModule(): boolean {
387
+ const entry = process.argv[1]
388
+ return entry !== undefined && import.meta.url === pathToFileURL(entry).href
389
+ }
390
+
391
+ if (isMainModule()) {
378
392
  VBMExamples.runAllExamples();
379
393
  }