File size: 20,851 Bytes
87bb7d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
/**
 * Adversarial integration tests.
 *     node dev/constraints/integration.test.js
 *
 * The engine test suite proves the state machine. This one attacks everything
 * around it: malformed rulebooks, hostile contexts, corrupt artefacts, absurd
 * numbers, and every path where a module could quietly produce a confident
 * answer it has not earned.
 *
 * The bar throughout: a module may return a correct answer or refuse. It may
 * never guess, default, impute, or crash.
 *
 * Zero dependencies, zero network.
 */
'use strict';

const CE = require('./engine');
const { maneuverRulebook, classify, rightOfWay, screeningInterval, LIMITS } = require('./rulebooks/orbital');
const { reentryRulebook } = require('./rulebooks/reentry');
const { releaseRulebook, SCENARIOS } = require('./rulebooks/release-gate');
const CV = require('./voting');
const { PropellantLedger } = require('./ledger');
const RE = require('./reentry');
const CF = require('./conformal');
const A = require('./authoring');
const FL = require('./flares');
const SW = require('./spaceweather');

let pass = 0, fail = 0;
const G = '\x1b[32m', R = '\x1b[31m', B = '\x1b[1m', X = '\x1b[0m';
const ok = (n, c, x) => { if (c) { pass++; console.log(`  ${G}βœ“${X} ${n}`); } else { fail++; console.log(`  ${R}βœ—${X} ${n}${x ? '  β†’ ' + x : ''}`); } };
const eq = (n, a, b) => ok(n, a === b, `got ${JSON.stringify(a)}, want ${JSON.stringify(b)}`);
const sec = (t) => console.log(`\n${B}${t}${X}`);
/** Assert a call does not throw, whatever it is fed. */
const noThrow = (n, fn) => { try { fn(); ok(n, true); } catch (e) { ok(n, false, 'threw: ' + e.message); } };

const NOW = Date.parse('2026-08-22T12:00:00Z');
const H = 3600000;

// ---------------------------------------------------------------------------
sec('ENGINE β€” hostile rulebooks and contexts');

noThrow('rule with no evaluate() does not crash the engine', () => {
  const r = CE.evaluate({ rulebook: { id: 'x', rules: [{ id: 'A', title: 'no evaluator', class: 'HARD' }] }, context: {}, now: NOW });
  if (r.rules[0].state !== CE.STATE.UNEVALUATED) throw new Error('became ' + r.rules[0].state);
});
noThrow('evaluate() returning garbage becomes UNEVALUATED', () => {
  const r = CE.evaluate({ rulebook: { id: 'x', rules: [{ id: 'A', class: 'HARD', evaluate: () => 42 }] }, context: {}, now: NOW });
  if (r.rules[0].state !== CE.STATE.UNEVALUATED) throw new Error('became ' + r.rules[0].state);
});
noThrow('evaluate() returning an unknown state string is tolerated', () => {
  CE.evaluate({ rulebook: { id: 'x', rules: [{ id: 'A', class: 'HARD', evaluate: () => ({ state: 'BANANA' }) }] }, context: {}, now: NOW });
});
noThrow('null context does not crash', () => CE.evaluate({ rulebook: maneuverRulebook, context: null, now: NOW }));
noThrow('undefined context does not crash', () => CE.evaluate({ rulebook: maneuverRulebook, context: undefined, now: NOW }));
noThrow('context with circular reference does not crash', () => {
  const c = { conj: {} }; c.self = c;
  CE.evaluate({ rulebook: maneuverRulebook, context: c, now: NOW });
});
noThrow('waivers as a non-array is tolerated', () => CE.evaluate({ rulebook: maneuverRulebook, context: {}, waivers: 'nope', now: NOW }));
noThrow('empty rulebook is tolerated', () => CE.evaluate({ rulebook: { id: 'e', rules: [] }, context: {}, now: NOW }));
{
  const r = CE.evaluate({ rulebook: { id: 'e', rules: [] }, context: {}, now: NOW });
  eq('empty rulebook signals COMPLETE', r.signal, CE.SIGNAL.COMPLETE);
  eq('...with progress 0 (nothing applicable)', r.progress, 0);
}
{
  let threw = false;
  try { CE.evaluate({ rulebook: { id: 'x' }, context: {} }); } catch { threw = true; }
  ok('rulebook without a rules array throws rather than passing', threw);
}
{
  // A rule that mutates the context must not corrupt other rules' view of it.
  const ctx = { conj: { min_range_km: 5 } };
  CE.evaluate({
    rulebook: { id: 'm', rules: [
      { id: 'A', class: 'SOFT', evaluate: (c) => { c.conj.min_range_km = 999; return CE.R.pass('ok'); } },
      { id: 'B', class: 'SOFT', evaluate: (c) => CE.R.pass('saw ' + c.conj.min_range_km) },
    ] }, context: ctx, now: NOW,
  });
  ok('a mutating rule is visible but does not crash later rules', true);
}

sec('ENGINE β€” absurd numbers');
for (const v of [NaN, Infinity, -Infinity, null, undefined, '', 'abc', {}, [], -0]) {
  noThrow(`miss_range = ${JSON.stringify(v)} handled`, () => {
    CE.evaluate({ rulebook: maneuverRulebook, context: { conj: { min_range_km: v, probability: v, tca_ms: v, tle_age_days: v } }, now: NOW });
  });
}
{
  const r = CE.evaluate({ rulebook: maneuverRulebook, context: { conj: { min_range_km: NaN, probability: NaN } }, now: NOW });
  ok('NaN inputs never produce SATISFIED on the rules that read them',
    !['FR-01', 'FR-12'].some((id) => r.rules.find((x) => x.id === id).state === CE.STATE.SATISFIED));
}

sec('ENGINE β€” rollup edge cases');
noThrow('rollup(null)', () => CE.rollup(null));
noThrow('rollup([null, undefined])', () => CE.rollup([null, undefined]));
eq('rollup of nothing has 0 items', CE.rollup([]).items, 0);
{
  const one = CE.evaluate({ rulebook: { id: 'x', rules: [{ id: 'A', class: 'HARD', evaluate: () => CE.R.fail('no') }] }, context: {}, now: NOW });
  eq('rollup takes the worst state', CE.rollup([one, one]).signal, CE.SIGNAL.BLOCKED);
}

// ---------------------------------------------------------------------------
sec('CLASSIFY / RIGHT OF WAY β€” hostile names');
for (const n of [null, undefined, '', '   ', 123, {}, [], '', 'A'.repeat(5000)]) {
  noThrow(`classify(${JSON.stringify(n)?.slice(0, 20)}) does not crash`, () => classify(n));
}
eq('empty name is unknown, not a guess', classify(''), null);
eq('whitespace name is unknown', classify('   '), null);
noThrow('rightOfWay with nulls', () => rightOfWay(null, null, null, null));
{
  const rw = rightOfWay('A', 'B', 'NONMANEUVERABLE', 'NONMANEUVERABLE');
  ok('debris x debris names nobody as the mover', rw.mover === null && rw.resolved === true);
  const rw2 = rightOfWay('A', 'B', 'MANEUVERABLE', 'MANEUVERABLE');
  ok('same class is explicitly UNRESOLVED, not arbitrated', rw2.resolved === false);
}

// ---------------------------------------------------------------------------
sec('VOTING β€” degenerate clusters');
{
  const blocked = CE.evaluate({ rulebook: { id: 'x', rules: [{ id: 'FR-03', title: 't', class: 'HARD', waivable: false, evaluate: () => CE.R.fail('no') }] }, context: {}, now: NOW });
  noThrow('poll with no nodes', () => CV.poll([], blocked, 3));
  noThrow('poll with null nodes', () => CV.poll(null, blocked, 3));
  noThrow('poll with unknown node ids', () => CV.poll([{ id: 999, name: 'X', online: true }], blocked, 3));
  eq('empty cluster cannot approve', CV.poll([], blocked, 3).status, 'DENIED');
  const p = CV.poll([{ id: 1, online: true }], blocked, 3);
  eq('single node cannot reach quorum 3', p.status, 'DENIED');
  ok('every vote carries a reason even for unknown nodes',
    CV.poll([{ id: 999, name: 'X', online: true }], blocked, 3).votes.every((v) => v.reason && v.reason.length));
  eq('offline nodes are excluded from the poll', CV.poll([{ id: 1, online: false }, { id: 2, online: true }], blocked, 3).votes.length, 1);
}
{
  // Emergency must refuse a non-negotiable violation and refuse unknowns.
  const nn = CE.evaluate({ rulebook: { id: 'x', rules: [{ id: 'FR-03', title: 't', class: 'HARD', waivable: false, evaluate: () => CE.R.fail('no') }] }, context: {}, now: NOW });
  ok('emergency refuses a non-negotiable violation', CV.emergencyWaivers(nn).allowed === false);
  const un = CE.evaluate({ rulebook: { id: 'x', rules: [{ id: 'Z', title: 't', class: 'HARD', waivable: true, evaluate: () => CE.R.unknown('no data') }] }, context: {}, now: NOW });
  ok('emergency refuses while a rule is unevaluated', CV.emergencyWaivers(un).allowed === false);
  const cl = CE.evaluate({ rulebook: { id: 'x', rules: [{ id: 'Z', title: 't', class: 'HARD', waivable: true, evaluate: () => CE.R.pass('fine') }] }, context: {}, now: NOW });
  ok('emergency on a clean report allows, with no waivers', CV.emergencyWaivers(cl).allowed === true && CV.emergencyWaivers(cl).waivers.length === 0);
}

// ---------------------------------------------------------------------------
sec('LEDGER β€” arithmetic cannot be cheated');
{
  const L = new PropellantLedger();
  noThrow('get("") is safe', () => L.get(''));
  eq('get("") is null, not a phantom asset', L.get(''), null);
  noThrow('debit with NaN', () => L.debit('SAT', NaN));
  ok('NaN debit refused', L.debit('SAT', NaN).ok === false);
  ok('negative debit does not create propellant', (() => {
    const s = new PropellantLedger(); s.seed('S', { used_ms: 5 });
    const before = s.get('S').ca_remaining_ms;
    s.debit('S', -100);
    return s.get('S').ca_remaining_ms <= before;
  })());
  const s2 = new PropellantLedger(); s2.seed('S', { used_ms: 14.9 });
  eq('a burn beyond the allocation is refused', s2.debit('S', 5).ok, false);
  ok('...and the balance is untouched after a refusal', s2.get('S').ca_used_ms === 14.9);
  ok('the disposal reserve is never reachable', (() => {
    const s = new PropellantLedger(); s.seed('S', { used_ms: 0 });
    for (let i = 0; i < 100; i++) s.debit('S', 1);
    return s.get('S').ca_used_ms <= s.get('S').ca_allocation_ms;
  })());
  ok('seeded history is stable across instances',
    new PropellantLedger().get('STARLINK-1008').ca_used_ms === new PropellantLedger().get('STARLINK-1008').ca_used_ms);
}

// ---------------------------------------------------------------------------
sec('RE-ENTRY β€” determinism and refusal');
{
  const o = { entry_lat: -20, entry_lon: -140, inclination_deg: 53, ballistic_coefficient: 80, kp: 2, seed: 'fixed' };
  const a = RE.footprint(o), b = RE.footprint(o);
  eq('identical inputs give an identical footprint', a.span_km, b.span_km);
  eq('...and identical population exposure', a.mean_population_density, b.mean_population_density);
  const c = RE.footprint({ ...o, seed: 'other' });
  ok('a different seed gives a different sample path', c.span_km !== a.span_km || c.mean_population_density !== a.mean_population_density);

  ok('missing Kp refuses rather than assuming quiet', RE.footprint({ ...o, kp: undefined }).unresolved === true);
  ok('null Kp refuses', RE.footprint({ ...o, kp: null }).unresolved === true);
  noThrow('NaN Kp', () => RE.footprint({ ...o, kp: NaN }));

  eq('casualty of an unresolved footprint is null', RE.casualtyExpectancy({ unresolved: true }, 3.5), null);
  eq('casualty with an unknown casualty area is null', RE.casualtyExpectancy(a, null), null);
  eq('casualty with NaN area is null', RE.casualtyExpectancy(a, NaN), null);
  eq('consequence of an unresolved footprint is null', RE.consequenceProfile({ unresolved: true }), null);

  for (const [lat, lon] of [[91, 0], [-91, 0], [0, 181], [0, -181], [NaN, NaN]]) {
    noThrow(`cellAt(${lat}, ${lon}) clamps rather than throwing`, () => RE.cellAt(lat, lon));
  }
  ok('an extreme ballistic coefficient still produces a finite span',
    Number.isFinite(RE.footprint({ ...o, ballistic_coefficient: 100000 }).span_km));
}

// ---------------------------------------------------------------------------
sec('CONFORMAL β€” refuses to fabricate a guarantee');
{
  ok('too few pairs is refused', CF.calibrate([{ predicted: 1, actual: 1 }]).ok === false);
  ok('empty is refused', CF.calibrate([]).ok === false);
  noThrow('null input', () => CF.calibrate(null));
  const pairs = Array.from({ length: 200 }, (_, i) => ({ predicted: 1 + (i % 7) * 0.1, actual: 1 + (i % 5) * 0.12 }));
  const cal = CF.calibrate(pairs);
  ok('200 pairs calibrate', cal.ok === true);
  ok('coverage is reported', cal.coverage === 0.95);
  eq('interval on a bad calibrator is null', CF.interval({ ok: false }, 1), null);
  eq('interval on a non-positive value in log space is null', CF.interval(cal, 0), null);
  const iv = CF.interval(cal, 1);
  ok('the interval brackets the point estimate', iv.lower <= iv.point && iv.point <= iv.upper);
  eq('a straddling interval is UNEVALUATED', CF.decide({ point: 1, lower: 0.5, upper: 2, coverage: 0.95 }, 1, 'below').verdict, 'UNEVALUATED');
  eq('an interval entirely below the limit is SATISFIED', CF.decide({ point: 0.1, lower: 0.05, upper: 0.2, coverage: 0.95 }, 1, 'below').verdict, 'SATISFIED');
  eq('an interval entirely above is VIOLATED', CF.decide({ point: 9, lower: 8, upper: 10, coverage: 0.95 }, 1, 'below').verdict, 'VIOLATED');
  eq('decide(null) is UNEVALUATED', CF.decide(null, 1).verdict, 'UNEVALUATED');

  // Wired lookups
  ok('a calibrated age returns an interval', !!screeningInterval(0.8));
  eq('an uncalibrated age returns null, not a neighbour', screeningInterval(30), null);
  eq('NaN age returns null', screeningInterval(NaN), null);
}

// ---------------------------------------------------------------------------
sec('AUTHORING β€” the model does not get to decide');
{
  const base = { id: 'ZZ-1', key: 'K', title: 'A title', field: 'conj.min_range_km', op: 'lte', value: 5, class: 'SOFT', waivable: true, authority: 'operator-declared', rationale: 'x'.repeat(40) };
  ok('a well-formed rule validates', A.validate(base, []).ok === true);
  const bad = {
    'field not in the allow-list': { ...base, field: 'process.env.SECRET' },
    'nested traversal field': { ...base, field: '__proto__.polluted' },
    'unsupported operator': { ...base, op: 'exec' },
    'operator as code': { ...base, op: '(()=>{})' },
    'missing authority': { ...base, authority: '' },
    'rationale too short': { ...base, rationale: 'no' },
    'class not HARD/SOFT': { ...base, class: 'MEDIUM' },
    'waivable not boolean': { ...base, waivable: 'yes' },
    'value not numeric': { ...base, value: 'five' },
    'bad id shape': { ...base, id: 'DROP TABLE' },
    'boolean field with numeric op': { ...base, field: 'plan.clear_vs_catalogue', op: 'lte', value: 1 },
  };
  for (const [name, d] of Object.entries(bad)) ok(`refused: ${name}`, A.validate(d, []).ok === false);
  ok('a model declining honestly is surfaced as such', A.validate({ error: 'cannot express that' }, []).model_declined === true);
  noThrow('validate(null)', () => A.validate(null, []));
  noThrow('validate on an array', () => A.validate([], []));

  // Prototype pollution must be impossible.
  const store = new A.AuthoredRules();
  store.add({ ...base, id: 'ZZ-2', field: 'conj.min_range_km' }, []);
  const compiled = store.rules[0];
  noThrow('compiled rule runs on an empty context', () => compiled.evaluate({ now: NOW }));
  eq('...and reports UNEVALUATED, not a pass', compiled.evaluate({ now: NOW }).state, CE.STATE.UNEVALUATED);
  ok('Object.prototype was not polluted', ({}).polluted === undefined);

  // Probe must expose an inverted rule.
  const inv = A.probe(A.compile({ ...base, id: 'ZZ-3', field: 'cluster.online_nodes', op: 'lt', value: 3 }));
  const cor = A.probe(A.compile({ ...base, id: 'ZZ-4', field: 'cluster.online_nodes', op: 'gte', value: 3 }));
  ok('probing distinguishes an inverted rule from a correct one',
    JSON.stringify(inv.map((p) => p.result)) !== JSON.stringify(cor.map((p) => p.result)));
  ok('TLA+ is emitted for an authored rule', A.toTla(compiled).includes('status = "APPROVED"'));
  ok('a non-negotiable authored rule also emits a never-waived invariant',
    A.toTla(A.compile({ ...base, id: 'ZZ-5', waivable: false })).includes('NeverWaived'));
}

// ---------------------------------------------------------------------------
sec('FLARES β€” parser robustness');
{
  eq('classify(0) is null', FL.classify(0), null);
  eq('classify(-1) is null', FL.classify(-1), null);
  eq('classify(NaN) is null', FL.classify(NaN), null);
  ok('an X-class flux classifies as X', FL.classify(2e-4).startsWith('X'));
  ok('an M-class flux classifies as M', FL.classify(5e-5).startsWith('M'));
  eq('R-scale of an X1 flare is R3', FL.rScale(1.2e-4).scale, 'R3');
  eq('R-scale below M1 is R0', FL.rScale(1e-6).scale, 'R0');
  eq('rScale(NaN) is null', FL.rScale(NaN), null);

  eq('align([]) is empty', FL.align([]).length, 0);
  noThrow('align on malformed records', () => FL.align([{ time_tag: 'nonsense', flux: 'x', energy: 'z' }, null, {}]));
  ok('records missing one band are dropped, not half-used',
    FL.align([{ time_tag: '2026-01-01T00:00:00Z', flux: 1e-6, energy: '0.1-0.8nm' }]).length === 0);

  ok('forecast on a short series refuses', FL.forecast([{ t: 1, soft: 1e-6, hard: 1e-8 }]).available === false);
  const flat = Array.from({ length: 120 }, (_, i) => ({ t: NOW + i * 60000, soft: 1e-6, hard: 1e-8 }));
  const f = FL.forecast(flat);
  ok('a flat quiet series forecasts no blackout', f.available && f.blackout_expected === false);
  eq('...and predicts no impact', FL.predictedImpact(f).expected, false);
  eq('predictedImpact(null) is null', FL.predictedImpact(null), null);
  eq('nowcast on a flat series finds no flare', FL.nowcast(flat).length, 0);
  noThrow('nowcast on an empty series', () => FL.nowcast([]));
}

// ---------------------------------------------------------------------------
sec('SPACE WEATHER β€” derived zones');
{
  eq('gScale below storm', SW.gScale(2), 'G0');
  eq('gScale Kp 5 is G1', SW.gScale(5), 'G1');
  eq('gScale Kp 9 is G5', SW.gScale(9), 'G5');
  eq('gScale is clamped above 9', SW.gScale(99), 'G5');
  eq('gScale(NaN) is G0', SW.gScale(NaN), 'G0');
  eq('zones(null) is null', SW.zones(null), null);
  eq('groundSegment(null) is null', SW.groundSegment(null), null);
  const quiet = SW.groundSegment({ kp: 1, scale_s: 'S0' });
  eq('all stations reachable when quiet', quiet.stations_available, quiet.stations_assigned);
  const storm = SW.groundSegment({ kp: 9, scale_s: 'S3' });
  ok('a radiation storm takes polar stations offline', storm.stations_available < storm.stations_assigned);
  ok('...and never takes them all, at these latitudes', storm.stations_available > 0);
  ok('every offline station states a reason', storm.stations.filter((s) => !s.available).every((s) => s.reason));
  eq('an unknown replay is null', SW.replay('nope'), null);
  ok('the gannon replay is complete', !!SW.replay('gannon').groundSegment);
}

// ---------------------------------------------------------------------------
sec('CROSS-RULEBOOK β€” invariants that must hold everywhere');
{
  const books = [maneuverRulebook, reentryRulebook, releaseRulebook];
  const all = books.flatMap((b) => b.rules);
  ok('every rule has a unique id within its book',
    books.every((b) => new Set(b.rules.map((r) => r.id)).size === b.rules.length));
  ok('every rule cites an authority', all.every((r) => r.authority && r.authority.length > 4));
  ok('every rule explains itself', all.every((r) => r.rationale && r.rationale.length > 20));
  ok('every rule states its requirement', all.every((r) => r.requirement && r.requirement.length > 10));
  ok('no SOFT rule is marked non-negotiable', all.every((r) => !(r.class === 'SOFT' && r.waivable === false)));
  ok('every rule has an evaluate()', all.every((r) => typeof r.evaluate === 'function'));

  // Every rule, fed an empty context, must report UNEVALUATED or NOT_APPLICABLE.
  // Never SATISFIED β€” that would be a pass earned from no evidence at all.
  for (const b of books) {
    const rep = CE.evaluate({ rulebook: b, context: {}, now: NOW });
    const wrong = rep.rules.filter((r) => r.state === CE.STATE.SATISFIED);
    ok(`${b.id}: no rule is SATISFIED on an empty context`, wrong.length === 0, wrong.map((r) => r.id).join(','));
    ok(`${b.id}: an empty context is never authorised`, rep.authorised === false);
  }

  // A waiver on a non-negotiable rule must never take effect, in any book.
  for (const b of books) {
    const nn = b.rules.filter((r) => r.waivable === false);
    if (!nn.length) continue;
    const waivers = nn.map((r) => ({ rule_id: r.id, party: 'attacker', reason: 'because' }));
    const rep = CE.evaluate({ rulebook: b, context: {}, waivers, now: NOW });
    ok(`${b.id}: non-negotiable rules cannot be waived`,
      rep.rules.filter((r) => r.waivable === false).every((r) => r.state !== CE.STATE.WAIVED));
  }
}

sec('RELEASE GATE β€” the portability proof still holds');
{
  const seen = new Set();
  for (const [id, s] of Object.entries(SCENARIOS)) {
    const r = CE.evaluate({ rulebook: releaseRulebook, context: s.context, now: NOW });
    seen.add(r.signal);
  }
  ok('the non-space rulebook reaches every one of the four states', seen.size === 4, [...seen].join(','));
  const w = [{ rule_id: 'RG-02', party: 'release manager', reason: 'ship it' }];
  eq('waiving a critical CVE does not unblock a release',
    CE.evaluate({ rulebook: releaseRulebook, context: SCENARIOS.critical_cve.context, waivers: w, now: NOW }).signal,
    CE.SIGNAL.BLOCKED);
}

console.log(`\n${fail === 0 ? G : R}${B}${pass} passed, ${fail} failed${X}\n`);
process.exit(fail === 0 ? 0 : 1);