yakmesh 2.8.2 → 3.0.0

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 (232) hide show
  1. package/CHANGELOG.md +637 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/Caddyfile +77 -0
  4. package/README.md +119 -29
  5. package/adapters/adapter-mlv-bible/README.md +124 -0
  6. package/adapters/adapter-mlv-bible/index.js +400 -0
  7. package/adapters/chat-mod-adapter.js +532 -0
  8. package/adapters/content-adapter.js +273 -0
  9. package/content/api.js +50 -41
  10. package/content/index.js +2 -2
  11. package/content/store.js +355 -173
  12. package/dashboard/index.html +19 -3
  13. package/database/replication.js +117 -37
  14. package/docs/CRYPTO-AGILITY.md +204 -0
  15. package/docs/MTLS-RESEARCH.md +367 -0
  16. package/docs/NAMCHE-SPEC.md +681 -0
  17. package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
  18. package/docs/PRECISION-DISCLOSURE.md +96 -0
  19. package/docs/README.md +76 -0
  20. package/docs/ROADMAP-2.4.0.md +447 -0
  21. package/docs/ROADMAP-2.5.0.md +244 -0
  22. package/docs/SECURITY-AUDIT-REPORT.md +306 -0
  23. package/docs/SST-INTEGRATION.md +712 -0
  24. package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
  25. package/docs/TERNARY-AUDIT-REPORT.md +247 -0
  26. package/docs/TME-FAQ.md +221 -0
  27. package/docs/WHITEPAPER.md +623 -0
  28. package/docs/adapters.html +1001 -0
  29. package/docs/advanced-systems.html +1045 -0
  30. package/docs/annex.html +1046 -0
  31. package/docs/api.html +970 -0
  32. package/docs/business/response-templates.md +160 -0
  33. package/docs/c2c.html +1225 -0
  34. package/docs/cli.html +1332 -0
  35. package/docs/configuration.html +1248 -0
  36. package/docs/darshan.html +1085 -0
  37. package/docs/dharma.html +966 -0
  38. package/docs/docs-bundle.html +1075 -0
  39. package/docs/docs.css +3120 -0
  40. package/docs/docs.js +556 -0
  41. package/docs/doko.html +969 -0
  42. package/docs/geo-proof.html +858 -0
  43. package/docs/getting-started.html +840 -0
  44. package/docs/gumba-tutorial.html +1144 -0
  45. package/docs/gumba.html +1098 -0
  46. package/docs/index.html +914 -0
  47. package/docs/jhilke.html +1312 -0
  48. package/docs/karma.html +1100 -0
  49. package/docs/katha.html +1037 -0
  50. package/docs/lama.html +978 -0
  51. package/docs/mandala.html +1067 -0
  52. package/docs/mani.html +964 -0
  53. package/docs/mantra.html +967 -0
  54. package/docs/mesh.html +1409 -0
  55. package/docs/nakpak.html +869 -0
  56. package/docs/namche.html +928 -0
  57. package/docs/nav-order.json +53 -0
  58. package/docs/prahari.html +1043 -0
  59. package/docs/prism-bash.min.js +1 -0
  60. package/docs/prism-javascript.min.js +1 -0
  61. package/docs/prism-json.min.js +1 -0
  62. package/docs/prism-tomorrow.min.css +1 -0
  63. package/docs/prism.min.js +1 -0
  64. package/docs/privacy.html +699 -0
  65. package/docs/quick-reference.html +1181 -0
  66. package/docs/sakshi.html +1402 -0
  67. package/docs/sandboxing.md +386 -0
  68. package/docs/seva.html +911 -0
  69. package/docs/sherpa.html +871 -0
  70. package/docs/studio.html +860 -0
  71. package/docs/stupa.html +995 -0
  72. package/docs/tailwind.min.css +2 -0
  73. package/docs/tattva.html +1332 -0
  74. package/docs/terms.html +686 -0
  75. package/docs/time-server-deployment.md +166 -0
  76. package/docs/time-sources.html +1392 -0
  77. package/docs/tivra.html +1127 -0
  78. package/docs/trademark-policy.html +686 -0
  79. package/docs/tribhuj.html +1183 -0
  80. package/docs/trust-security.html +1029 -0
  81. package/docs/tutorials/backup-recovery.html +654 -0
  82. package/docs/tutorials/dashboard.html +604 -0
  83. package/docs/tutorials/domain-setup.html +605 -0
  84. package/docs/tutorials/host-website.html +456 -0
  85. package/docs/tutorials/mesh-network.html +505 -0
  86. package/docs/tutorials/mobile-access.html +445 -0
  87. package/docs/tutorials/privacy.html +467 -0
  88. package/docs/tutorials/raspberry-pi.html +600 -0
  89. package/docs/tutorials/security-basics.html +539 -0
  90. package/docs/tutorials/share-files.html +431 -0
  91. package/docs/tutorials/troubleshooting.html +637 -0
  92. package/docs/tutorials/trust-karma.html +419 -0
  93. package/docs/tutorials/yak-protocol.html +456 -0
  94. package/docs/tutorials.html +1034 -0
  95. package/docs/vani.html +1270 -0
  96. package/docs/webserver.html +809 -0
  97. package/docs/yak-protocol.html +940 -0
  98. package/docs/yak-timeserver-design.md +475 -0
  99. package/docs/yakapp.html +1015 -0
  100. package/docs/ypc27.html +1069 -0
  101. package/docs/yurt.html +1344 -0
  102. package/embedded-docs/bundle.js +334 -74
  103. package/gossip/protocol.js +247 -27
  104. package/identity/key-resolver.js +262 -0
  105. package/identity/machine-seed.js +632 -0
  106. package/identity/node-key.js +669 -368
  107. package/identity/tribhuj-ratchet.js +506 -0
  108. package/knowledge-base.js +37 -8
  109. package/launcher/yakmesh.bat +62 -0
  110. package/launcher/yakmesh.sh +70 -0
  111. package/mesh/annex.js +462 -108
  112. package/mesh/beacon-broadcast.js +113 -1
  113. package/mesh/darshan.js +1718 -0
  114. package/mesh/gumba.js +1567 -0
  115. package/mesh/jhilke.js +651 -0
  116. package/mesh/katha.js +1012 -0
  117. package/mesh/nakpak-routing.js +8 -5
  118. package/mesh/network.js +724 -34
  119. package/mesh/pulse-sync.js +4 -1
  120. package/mesh/rate-limiter.js +127 -15
  121. package/mesh/seva.js +526 -0
  122. package/mesh/sherpa-discovery.js +89 -8
  123. package/mesh/sybil-defense.js +19 -5
  124. package/mesh/temporal-encoder.js +4 -3
  125. package/mesh/vani.js +1364 -0
  126. package/mesh/yurt.js +1340 -0
  127. package/models/entropy-sentinel.onnx +0 -0
  128. package/models/karma-trust.onnx +0 -0
  129. package/models/manifest.json +43 -0
  130. package/models/sakshi-anomaly.onnx +0 -0
  131. package/oracle/code-proof-protocol.js +7 -6
  132. package/oracle/codebase-lock.js +257 -28
  133. package/oracle/index.js +74 -15
  134. package/oracle/ma902-snmp.js +678 -0
  135. package/oracle/module-sealer.js +5 -3
  136. package/oracle/network-identity.js +16 -0
  137. package/oracle/packet-checksum.js +201 -0
  138. package/oracle/sst.js +579 -0
  139. package/oracle/ternary-144t.js +714 -0
  140. package/oracle/ternary-ml.js +481 -0
  141. package/oracle/time-api.js +239 -0
  142. package/oracle/time-source.js +137 -47
  143. package/oracle/validation-oracle-hardened.js +1111 -1071
  144. package/oracle/validation-oracle.js +4 -2
  145. package/oracle/ypc27.js +211 -0
  146. package/package.json +20 -3
  147. package/protocol/yak-handler.js +35 -9
  148. package/protocol/yak-protocol.js +28 -13
  149. package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
  150. package/reference/cpp/yakmesh_ypc27.cpp +179 -0
  151. package/sbom.json +87 -0
  152. package/scripts/security-audit.mjs +264 -0
  153. package/scripts/update-docs-nav.js +194 -0
  154. package/scripts/update-docs-sidebar.cjs +164 -0
  155. package/security/crypto-config.js +4 -3
  156. package/security/dharma-moderation.js +517 -0
  157. package/security/doko-identity.js +193 -143
  158. package/security/domain-consensus.js +86 -85
  159. package/security/fs-hardening.js +620 -0
  160. package/security/hardware-attestation.js +5 -3
  161. package/security/hybrid-trust.js +227 -87
  162. package/security/karma-rate-limiter.js +692 -0
  163. package/security/khata-protocol.js +22 -21
  164. package/security/khata-trust-integration.js +277 -150
  165. package/security/memory-safety.js +635 -0
  166. package/security/mesh-auth.js +11 -10
  167. package/security/mesh-revocation.js +373 -5
  168. package/security/namche-gateway.js +298 -69
  169. package/security/sakshi.js +460 -3
  170. package/security/sangha.js +770 -0
  171. package/security/secure-config.js +473 -0
  172. package/security/silicon-parity.js +13 -10
  173. package/security/steadywatch.js +1142 -0
  174. package/security/strike-system.js +32 -3
  175. package/security/temporal-signing.js +488 -0
  176. package/security/trit-commitment.js +464 -0
  177. package/server/crypto/annex.js +247 -0
  178. package/server/darshan-api.js +343 -0
  179. package/server/index.js +3259 -362
  180. package/server/komm-api.js +668 -0
  181. package/utils/accel.js +2273 -0
  182. package/utils/ternary-id.js +79 -0
  183. package/utils/verify-worker.js +57 -0
  184. package/webserver/index.js +95 -5
  185. package/assets/yakmesh-logo.png +0 -0
  186. package/assets/yakmesh-logo.svg +0 -80
  187. package/assets/yakmesh-logo2.png +0 -0
  188. package/assets/yakmesh-logo2sm.png +0 -0
  189. package/assets/ymsm.png +0 -0
  190. package/website/assets/silhouettes/adapters.svg +0 -107
  191. package/website/assets/silhouettes/api-endpoints.svg +0 -115
  192. package/website/assets/silhouettes/atomic-clock.svg +0 -83
  193. package/website/assets/silhouettes/base-camp.svg +0 -81
  194. package/website/assets/silhouettes/bridge.svg +0 -69
  195. package/website/assets/silhouettes/docs-bundle.svg +0 -113
  196. package/website/assets/silhouettes/doko-basket.svg +0 -70
  197. package/website/assets/silhouettes/fortress.svg +0 -93
  198. package/website/assets/silhouettes/gateway.svg +0 -54
  199. package/website/assets/silhouettes/gears.svg +0 -93
  200. package/website/assets/silhouettes/globe-satellite.svg +0 -67
  201. package/website/assets/silhouettes/karma-wheel.svg +0 -137
  202. package/website/assets/silhouettes/lama-council.svg +0 -141
  203. package/website/assets/silhouettes/mandala-network.svg +0 -169
  204. package/website/assets/silhouettes/mani-stones.svg +0 -149
  205. package/website/assets/silhouettes/mantra-wheel.svg +0 -116
  206. package/website/assets/silhouettes/mesh-nodes.svg +0 -113
  207. package/website/assets/silhouettes/nakpak.svg +0 -56
  208. package/website/assets/silhouettes/peak-lightning.svg +0 -73
  209. package/website/assets/silhouettes/sherpa.svg +0 -69
  210. package/website/assets/silhouettes/stupa-tower.svg +0 -119
  211. package/website/assets/silhouettes/tattva-eye.svg +0 -78
  212. package/website/assets/silhouettes/terminal.svg +0 -74
  213. package/website/assets/silhouettes/webserver.svg +0 -145
  214. package/website/assets/silhouettes/yak.svg +0 -78
  215. package/website/assets/yakmesh-logo.png +0 -0
  216. package/website/assets/yakmesh-logo.webp +0 -0
  217. package/website/assets/yakmesh-logo128x140.webp +0 -0
  218. package/website/assets/yakmesh-logo2.png +0 -0
  219. package/website/assets/yakmesh-logo2.svg +0 -51
  220. package/website/assets/yakmesh-logo40x44.webp +0 -0
  221. package/website/assets/yakmesh.gif +0 -0
  222. package/website/assets/yakmesh.ico +0 -0
  223. package/website/assets/yakmesh.jpg +0 -0
  224. package/website/assets/yakmesh.pdf +0 -0
  225. package/website/assets/yakmesh.png +0 -0
  226. package/website/assets/yakmesh.svg +0 -70
  227. package/website/assets/yakmesh128.webp +0 -0
  228. package/website/assets/yakmesh32.png +0 -0
  229. package/website/assets/yakmesh32.svg +0 -65
  230. package/website/assets/yakmesh32o.ico +0 -2
  231. package/website/assets/yakmesh32o.svg +0 -65
  232. package/website/assets/yakmesh32o.svgz +0 -0
@@ -0,0 +1,1142 @@
1
+ /**
2
+ * STEADYWATCH — Quantum-Hardware-Validated Entropy Seed Module
3
+ *
4
+ * Loads Hurwitz quaternion satellite seeds (256-bit each) generated on
5
+ * IBM ibm_marrakesh (156-qubit Heron r2) via STEADYWATCH circuits.
6
+ *
7
+ * Math: Satellites = 24 * (p + 1) for prime p
8
+ * p=5 -> 144 nodes
9
+ * p=13 -> 336 nodes
10
+ * p=17 -> 432 nodes
11
+ *
12
+ * Integration: XOR quantum seed with local CSPRNG output to create
13
+ * hybrid entropy for ML-KEM-768 keygen. Two-source extractor pattern
14
+ * ensures keys remain secure even if one entropy source is compromised.
15
+ *
16
+ * @module security/steadywatch
17
+ * @license MIT
18
+ * @copyright 2026 YAKMESH Contributors
19
+ */
20
+
21
+ import { randomBytes } from 'crypto';
22
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
23
+ import { join, dirname } from 'path';
24
+ import { fileURLToPath } from 'url';
25
+ import { createLogger } from '../utils/logger.js';
26
+ import { sha3_256, sha3_256hex } from '../utils/accel.js';
27
+
28
+ // ═══ TRIBHUJ — Balanced ternary quality verdicts ═══
29
+ import { Trit, TritArray, POSITIVE, NEUTRAL, NEGATIVE, TritState } from '../oracle/tribhuj.js';
30
+
31
+ // ═══ SST — Synergy Sequence Theory for family grouping & Fibonacci rotation ═══
32
+ import {
33
+ digitalRoot, getFamily, getFamilyOf, toFamilyTrit,
34
+ fibonacciRoot, fibonacciFamily, fibonacciFamilyTrit,
35
+ SSTFamily, FAMILY_TO_TRIT, FIBONACCI_CYCLE_24,
36
+ } from '../oracle/sst.js';
37
+
38
+ const log = createLogger('security:steadywatch');
39
+
40
+ // ===================================================================
41
+ // CONSTANTS
42
+ // ===================================================================
43
+
44
+ /** Hurwitz quaternion satellite count per prime */
45
+ const SATELLITE_COUNTS = Object.freeze({
46
+ 5: 144, // 24 * (5 + 1) — validated Feb 1 2026
47
+ 13: 336, // 24 * (13 + 1) — future batch
48
+ 17: 432, // 24 * (17 + 1) — future batch
49
+ });
50
+
51
+ /** Expected seed size in bytes */
52
+ const SEED_BYTES = 32; // 256-bit per satellite seed
53
+ /** Required output for ML-KEM-768 keygen */
54
+ const KEYGEN_SEED_BYTES = 64;
55
+ /** Minimum acceptable min-entropy ratio (bits of entropy / total bits) */
56
+ const MIN_ENTROPY_RATIO = 0.75;
57
+
58
+ // ═══ TERNARY-144 CONSTANTS ═══
59
+
60
+ /**
61
+ * 6-trit address space for satellite indexing.
62
+ * 144 in balanced ternary = "1TT100" (6 trits).
63
+ * 3^5 = 243 > 144, so 5 trits suffice, but 6 trits are used for
64
+ * balanced addressing that encodes the satellite's numeric position.
65
+ */
66
+ const TRIT_ADDRESS_LENGTH = 6;
67
+
68
+ /**
69
+ * SST family distribution for 144 satellites.
70
+ * Indices are classified by their digital root's SST family:
71
+ * Family A (1,4,7): ~48 satellites — Physical Negative (descending)
72
+ * Family B (2,5,8): ~48 satellites — Physical Positive (ascending)
73
+ * Family C (3,6,9): ~48 satellites — Governing Source (singularity)
74
+ *
75
+ * DR(144) = 9 → Family C → The 144 constellation itself is a NEUTRAL singularity.
76
+ */
77
+ const FAMILY_GROUPS = Object.freeze({
78
+ [SSTFamily.A]: [], // Populated at load time
79
+ [SSTFamily.B]: [],
80
+ [SSTFamily.C]: [],
81
+ });
82
+
83
+ // ===================================================================
84
+ // TELEMETRY
85
+ // ===================================================================
86
+
87
+ const telemetry = {
88
+ seedsLoaded: 0,
89
+ hybridSeeds: 0,
90
+ rotations: 0,
91
+ biasRejections: 0,
92
+ sentinelChecks: 0,
93
+ sentinelPasses: 0,
94
+ sentinelFails: 0,
95
+ lastSeedIndex: -1,
96
+ // Ternary-144 telemetry
97
+ familySelections: { A: 0, B: 0, C: 0 },
98
+ fibonacciCyclePosition: 0,
99
+ ternaryQualityVerdicts: { positive: 0, neutral: 0, negative: 0 },
100
+ tritAddressLookups: 0,
101
+ batchConsensusRuns: 0,
102
+ };
103
+
104
+ // ===================================================================
105
+ // SEED STORE
106
+ // ===================================================================
107
+
108
+ /**
109
+ * STEADYWATCH Seed Store.
110
+ * Manages the 144+ satellite seeds and provides hybrid entropy generation.
111
+ */
112
+ class SteadywatchSeedStore {
113
+ constructor() {
114
+ /** @type {Uint8Array[]} Array of 256-bit satellite seeds */
115
+ this.seeds = [];
116
+ /** Current rotation index */
117
+ this.rotationIndex = 0;
118
+ /** Source prime used to generate this seed set */
119
+ this.prime = 0;
120
+ /** IBM Quantum job metadata */
121
+ this.metadata = null;
122
+ /** Whether seeds are loaded and validated */
123
+ this.initialized = false;
124
+ /** Node-specific seed assignment (index into seeds array) */
125
+ this.nodeAssignment = -1;
126
+ /** Entropy quality scores per seed (from Sentinel) */
127
+ this.qualityScores = new Map();
128
+
129
+ // ═══ TERNARY-144 STATE ═══
130
+
131
+ /**
132
+ * SST family groups — indices organized by digital root family.
133
+ * @type {{ A: number[], B: number[], C: number[] }}
134
+ */
135
+ this.familyGroups = { A: [], B: [], C: [] };
136
+
137
+ /**
138
+ * 6-trit balanced ternary address for each satellite.
139
+ * @type {Map<number, TritArray>} index → TritArray("1TT100"-range)
140
+ */
141
+ this.tritAddresses = new Map();
142
+
143
+ /**
144
+ * Reverse lookup: trit string → seed index.
145
+ * @type {Map<string, number>}
146
+ */
147
+ this.tritIndex = new Map();
148
+
149
+ /**
150
+ * Ternary quality verdicts per seed.
151
+ * POSITIVE = excellent entropy, NEUTRAL = acceptable, NEGATIVE = biased/rejected.
152
+ * @type {Map<number, Trit>}
153
+ */
154
+ this.qualityVerdicts = new Map();
155
+
156
+ /**
157
+ * Fibonacci cycle position for rotation.
158
+ * Advances through the 24-position cycle, selecting seeds family-aware.
159
+ */
160
+ this.fibCyclePos = 0;
161
+ }
162
+
163
+ /**
164
+ * Load seeds from a JSON file.
165
+ * Expected format:
166
+ * {
167
+ * "prime": 5,
168
+ * "satelliteCount": 144,
169
+ * "backend": "ibm_marrakesh",
170
+ * "validated": "2026-02-01",
171
+ * "seeds": ["hex-encoded-32-bytes", ...]
172
+ * }
173
+ *
174
+ * @param {string} seedFilePath — path to seeds JSON
175
+ * @returns {boolean} — true if loaded and validated
176
+ */
177
+ load(seedFilePath) {
178
+ try {
179
+ if (!existsSync(seedFilePath)) {
180
+ log.warn('STEADYWATCH seed file not found:', seedFilePath);
181
+ return false;
182
+ }
183
+
184
+ const raw = readFileSync(seedFilePath, 'utf-8');
185
+ const data = JSON.parse(raw);
186
+
187
+ // Validate structure
188
+ if (!data.seeds || !Array.isArray(data.seeds)) {
189
+ log.error('Invalid seed file: missing seeds array');
190
+ return false;
191
+ }
192
+
193
+ this.prime = data.prime || 5;
194
+ const expectedCount = SATELLITE_COUNTS[this.prime];
195
+ if (expectedCount && data.seeds.length !== expectedCount) {
196
+ log.warn('Seed count mismatch: expected ' + expectedCount + ' for p=' + this.prime + ', got ' + data.seeds.length);
197
+ }
198
+
199
+ // Parse and validate each seed
200
+ this.seeds = [];
201
+ const fingerprints = new Set();
202
+
203
+ for (let i = 0; i < data.seeds.length; i++) {
204
+ const hex = data.seeds[i];
205
+ if (hex.length !== SEED_BYTES * 2) {
206
+ log.warn('Seed ' + i + ': invalid length ' + hex.length + ', expected ' + (SEED_BYTES * 2));
207
+ continue;
208
+ }
209
+
210
+ const seedBytes = hexToUint8(hex);
211
+
212
+ // Check uniqueness via SHA3 fingerprint
213
+ const fp = sha3_256hex(seedBytes);
214
+ if (fingerprints.has(fp)) {
215
+ log.warn('Seed ' + i + ': duplicate detected (fingerprint collision)');
216
+ telemetry.biasRejections++;
217
+ continue;
218
+ }
219
+ fingerprints.add(fp);
220
+
221
+ // Bias check: ensure reasonable byte distribution
222
+ if (!this._checkBias(seedBytes, i)) {
223
+ telemetry.biasRejections++;
224
+ continue;
225
+ }
226
+
227
+ this.seeds.push(seedBytes);
228
+ }
229
+
230
+ this.metadata = {
231
+ prime: this.prime,
232
+ backend: data.backend || 'unknown',
233
+ validated: data.validated || 'unknown',
234
+ jobIds: data.jobIds || [],
235
+ loadedAt: new Date().toISOString(),
236
+ seedCount: this.seeds.length,
237
+ };
238
+
239
+ telemetry.seedsLoaded = this.seeds.length;
240
+ this.initialized = this.seeds.length > 0;
241
+
242
+ // ═══ Build ternary-144 structures ═══
243
+ if (this.initialized) {
244
+ this._buildTernaryStructures();
245
+ }
246
+
247
+ log.info('STEADYWATCH loaded: ' + this.seeds.length + ' satellite seeds (p=' + this.prime + ', backend=' + this.metadata.backend + ')');
248
+ return this.initialized;
249
+
250
+ } catch (err) {
251
+ log.error('Failed to load STEADYWATCH seeds:', err.message);
252
+ return false;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Generate initial seeds from quantum circuit parameters.
258
+ * Used when no seed file exists — creates deterministic seeds from
259
+ * Hurwitz quaternion phase rotations.
260
+ *
261
+ * theta_i = 2*pi*i / N where N = 24(p+1)
262
+ * seed_i = SHA3-256(quaternion_coordinate || circuit_measurement)
263
+ *
264
+ * NOTE: This is a simulation. Real seeds come from IBM Quantum hardware.
265
+ * Use this only for development/testing.
266
+ *
267
+ * @param {number} prime — Hurwitz prime (5, 13, or 17)
268
+ * @param {string} [outputPath] — save generated seeds to file
269
+ * @returns {boolean}
270
+ */
271
+ generateTestSeeds(prime = 5, outputPath = null) {
272
+ const count = SATELLITE_COUNTS[prime];
273
+ if (!count) {
274
+ log.error('Invalid prime ' + prime + '. Use 5, 13, or 17.');
275
+ return false;
276
+ }
277
+
278
+ log.info('Generating ' + count + ' test seeds for p=' + prime + ' (SIMULATION)');
279
+
280
+ const seeds = [];
281
+ for (let i = 0; i < count; i++) {
282
+ // Hurwitz quaternion phase rotation
283
+ const theta = (2 * Math.PI * i) / count;
284
+
285
+ // Simulate quaternion coordinate: q = cos(theta) + sin(theta)*i + ...
286
+ // In real STEADYWATCH, this parameterizes an 8-qubit circuit
287
+ const qReal = Math.cos(theta);
288
+ const qImag = Math.sin(theta);
289
+
290
+ // Create deterministic but unique input for SHA3
291
+ const input = Buffer.alloc(48);
292
+ input.writeDoubleBE(qReal, 0);
293
+ input.writeDoubleBE(qImag, 8);
294
+ input.writeUInt32BE(prime, 16);
295
+ input.writeUInt32BE(i, 20);
296
+ input.writeDoubleBE(theta, 24);
297
+ // Add satellite-specific label
298
+ const label = 'STEADYWATCH-SAT-' + prime + '-' + i;
299
+ Buffer.from(label).copy(input, 32);
300
+
301
+ // SHA3-256 of quaternion parameters -> 256-bit seed
302
+ const seed = sha3_256(input);
303
+ seeds.push(Buffer.from(seed).toString('hex'));
304
+ }
305
+
306
+ const data = {
307
+ prime,
308
+ satelliteCount: count,
309
+ backend: 'simulation',
310
+ validated: new Date().toISOString().split('T')[0],
311
+ note: 'TEST SEEDS — generated from Hurwitz quaternion simulation, not IBM Quantum hardware',
312
+ seeds,
313
+ };
314
+
315
+ if (outputPath) {
316
+ writeFileSync(outputPath, JSON.stringify(data, null, 2));
317
+ log.info('Test seeds written to ' + outputPath);
318
+ }
319
+
320
+ // Load seeds in-memory
321
+ this.seeds = seeds.map(h => hexToUint8(h));
322
+ this.prime = prime;
323
+ this.metadata = {
324
+ prime,
325
+ backend: 'simulation',
326
+ validated: data.validated,
327
+ seedCount: count,
328
+ loadedAt: new Date().toISOString(),
329
+ };
330
+ this.initialized = true;
331
+ telemetry.seedsLoaded = count;
332
+
333
+ // Build ternary-144 structures (family groups, trit addresses)
334
+ if (this.initialized) {
335
+ this._buildTernaryStructures();
336
+ }
337
+
338
+ return true;
339
+ }
340
+
341
+ /**
342
+ * Assign this node a specific satellite seed index.
343
+ * In production: provisioned by DOKO identity ceremony.
344
+ *
345
+ * @param {number} index — satellite index [0, seedCount)
346
+ */
347
+ assignNode(index) {
348
+ if (index < 0 || index >= this.seeds.length) {
349
+ log.error('Invalid node assignment: ' + index + ' (have ' + this.seeds.length + ' seeds)');
350
+ return;
351
+ }
352
+ this.nodeAssignment = index;
353
+ this.rotationIndex = index;
354
+ log.info('Node assigned to satellite ' + index + ' (seed fingerprint: ' + sha3_256hex(this.seeds[index]).slice(0, 16) + '...)');
355
+ }
356
+
357
+ /**
358
+ * Get a hybrid seed suitable for ML-KEM-768 keygen.
359
+ *
360
+ * Algorithm:
361
+ * 1. Select satellite seed (rotation or assigned)
362
+ * 2. Generate 64 bytes from OS CSPRNG
363
+ * 3. Expand satellite seed to 64 bytes: SHA3-256(seed || "EXPAND-0") || SHA3-256(seed || "EXPAND-1")
364
+ * 4. XOR: hybridSeed = expandedSatellite XOR csprng
365
+ * 5. Return 64-byte hybrid seed for ml_kem768.keygen()
366
+ *
367
+ * Security property: Even if one source is fully compromised (weak VM CSPRNG
368
+ * or faulty quantum hardware), the other source ensures the hybrid seed
369
+ * retains at least 256 bits of entropy.
370
+ *
371
+ * @returns {Uint8Array} — 64-byte hybrid seed
372
+ */
373
+ /**
374
+ * Get a hybrid seed suitable for ML-KEM-768 keygen.
375
+ *
376
+ * Algorithm (Ternary-144 enhanced):
377
+ * 1. Advance Fibonacci 24-cycle position
378
+ * 2. Determine SST family for this position
379
+ * 3. Select satellite from matching family group (family-aware rotation)
380
+ * 4. Generate 64 bytes from OS CSPRNG
381
+ * 5. Expand satellite seed to 64 bytes: SHA3-256(seed || "EXPAND-0") || SHA3-256(seed || "EXPAND-1")
382
+ * 6. XOR: hybridSeed = expandedSatellite XOR csprng
383
+ * 7. Return 64-byte hybrid seed for ml_kem768.keygen()
384
+ *
385
+ * @returns {Uint8Array} — 64-byte hybrid seed
386
+ */
387
+ getHybridSeed() {
388
+ if (!this.initialized || this.seeds.length === 0) {
389
+ log.trace('STEADYWATCH not initialized, falling back to pure CSPRNG');
390
+ return randomBytes(KEYGEN_SEED_BYTES);
391
+ }
392
+
393
+ // ═══ TERNARY-144: Fibonacci-cycle family-aware seed selection ═══
394
+ let seedIndex;
395
+
396
+ if (this.nodeAssignment >= 0) {
397
+ // Node has a fixed assignment — use it, but still track Fibonacci position
398
+ seedIndex = this.nodeAssignment;
399
+ } else if (this.familyGroups.A.length > 0) {
400
+ // Use Fibonacci 24-cycle to pick the SST family, then round-robin within family
401
+ const fibRoot = fibonacciRoot(this.fibCyclePos);
402
+ const family = getFamily(fibRoot);
403
+ const familyGroup = this.familyGroups[family];
404
+
405
+ if (familyGroup.length > 0) {
406
+ // Round-robin within the selected family
407
+ const familyOffset = this.rotationIndex % familyGroup.length;
408
+ seedIndex = familyGroup[familyOffset];
409
+ telemetry.familySelections[family]++;
410
+ } else {
411
+ // Fallback: linear rotation if family group is empty
412
+ seedIndex = this.rotationIndex;
413
+ }
414
+
415
+ this.fibCyclePos = (this.fibCyclePos + 1) % 24;
416
+ telemetry.fibonacciCyclePosition = this.fibCyclePos;
417
+ } else {
418
+ // Fallback: simple linear rotation (pre-ternary behavior)
419
+ seedIndex = this.rotationIndex;
420
+ }
421
+
422
+ const satelliteSeed = this.seeds[seedIndex % this.seeds.length];
423
+
424
+ // Rotate for next call (even if assigned, rotate for re-key diversity)
425
+ this.rotationIndex = (this.rotationIndex + 1) % this.seeds.length;
426
+ telemetry.rotations++;
427
+
428
+ // Expand 32-byte satellite seed to 64 bytes using SHA3 (ACCEL: native SHA-NI)
429
+ const expand0 = sha3_256(Buffer.concat([
430
+ Buffer.from(satelliteSeed),
431
+ Buffer.from('EXPAND-0'),
432
+ ]));
433
+ const expand1 = sha3_256(Buffer.concat([
434
+ Buffer.from(satelliteSeed),
435
+ Buffer.from('EXPAND-1'),
436
+ ]));
437
+ const expandedSeed = new Uint8Array(KEYGEN_SEED_BYTES);
438
+ expandedSeed.set(expand0, 0);
439
+ expandedSeed.set(expand1, 32);
440
+
441
+ // CSPRNG contribution
442
+ const csprng = randomBytes(KEYGEN_SEED_BYTES);
443
+
444
+ // XOR: two-source extractor
445
+ const hybridSeed = new Uint8Array(KEYGEN_SEED_BYTES);
446
+ for (let i = 0; i < KEYGEN_SEED_BYTES; i++) {
447
+ hybridSeed[i] = expandedSeed[i] ^ csprng[i];
448
+ }
449
+
450
+ telemetry.hybridSeeds++;
451
+ telemetry.lastSeedIndex = seedIndex;
452
+
453
+ log.trace('Hybrid seed generated (satellite ' + seedIndex +
454
+ ', family ' + getFamilyOf(seedIndex) +
455
+ ', fibPos ' + this.fibCyclePos +
456
+ ', rotation ' + this.rotationIndex + ')');
457
+ return hybridSeed;
458
+ }
459
+
460
+ /**
461
+ * ═══ TERNARY SEED QUALITY ═══
462
+ *
463
+ * Evaluate a seed's entropy quality as a balanced ternary verdict.
464
+ * Replaces the old boolean _checkBias with three-valued logic:
465
+ * POSITIVE (+1): Excellent entropy — diverse bytes, no bias patterns
466
+ * NEUTRAL ( 0): Acceptable entropy — passes minimum thresholds
467
+ * NEGATIVE (-1): Biased/rejected — fails quality checks
468
+ *
469
+ * @param {Uint8Array} seedBytes — 256-bit seed
470
+ * @param {number} index — satellite index
471
+ * @returns {Trit} — ternary quality verdict
472
+ */
473
+ _checkBiasTernary(seedBytes, index) {
474
+ let zeros = 0;
475
+ let ones = 0;
476
+ const byteSet = new Set();
477
+
478
+ for (const b of seedBytes) {
479
+ if (b === 0x00) zeros++;
480
+ if (b === 0xFF) ones++;
481
+ byteSet.add(b);
482
+ }
483
+
484
+ // NEGATIVE: Hard reject — severe bias
485
+ if (zeros > SEED_BYTES * 0.5) {
486
+ log.warn('Seed ' + index + ': NEGATIVE — excessive zero bytes (' + zeros + '/' + SEED_BYTES + ')');
487
+ telemetry.ternaryQualityVerdicts.negative++;
488
+ return new Trit(NEGATIVE);
489
+ }
490
+ if (ones > SEED_BYTES * 0.5) {
491
+ log.warn('Seed ' + index + ': NEGATIVE — excessive 0xFF bytes (' + ones + '/' + SEED_BYTES + ')');
492
+ telemetry.ternaryQualityVerdicts.negative++;
493
+ return new Trit(NEGATIVE);
494
+ }
495
+ if (byteSet.size < SEED_BYTES * 0.25) {
496
+ log.warn('Seed ' + index + ': NEGATIVE — low byte diversity (' + byteSet.size + ' unique)');
497
+ telemetry.ternaryQualityVerdicts.negative++;
498
+ return new Trit(NEGATIVE);
499
+ }
500
+
501
+ // POSITIVE: Excellent — high diversity, balanced distribution
502
+ if (byteSet.size >= SEED_BYTES * 0.75 && zeros <= 2 && ones <= 2) {
503
+ telemetry.ternaryQualityVerdicts.positive++;
504
+ return new Trit(POSITIVE);
505
+ }
506
+
507
+ // NEUTRAL: Acceptable but not exceptional
508
+ telemetry.ternaryQualityVerdicts.neutral++;
509
+ return new Trit(NEUTRAL);
510
+ }
511
+
512
+ /**
513
+ * Backward-compatible wrapper — _checkBias still returns boolean for load().
514
+ * Internally uses ternary verdict.
515
+ * @private
516
+ */
517
+ _checkBias(seedBytes, index) {
518
+ const verdict = this._checkBiasTernary(seedBytes, index);
519
+ this.qualityVerdicts.set(index, verdict);
520
+ return !verdict.isNegative; // POSITIVE and NEUTRAL both pass
521
+ }
522
+
523
+ // ═══════════════════════════════════════════════════════════════════════
524
+ // TERNARY-144: SST FAMILY SATELLITE GROUPING
525
+ // ═══════════════════════════════════════════════════════════════════════
526
+
527
+ /**
528
+ * Classify all loaded satellite seeds into SST family groups.
529
+ *
530
+ * Each satellite index i is assigned to a family based on digitalRoot(i+1):
531
+ * Family A (DR 1,4,7) ← Physical Negative
532
+ * Family B (DR 2,5,8) ← Physical Positive
533
+ * Family C (DR 3,6,9) ← Governing Source
534
+ *
535
+ * For 144 satellites: each family gets exactly 48 members (perfectly balanced).
536
+ * This reflects 144's digital root (9) — a governing singularity that divides
537
+ * evenly across all three polarities.
538
+ */
539
+ _buildFamilyGroups() {
540
+ this.familyGroups = { A: [], B: [], C: [] };
541
+
542
+ for (let i = 0; i < this.seeds.length; i++) {
543
+ // Use i+1 so satellite 0 → DR(1) = Family A, satellite 1 → DR(2) = Family B, etc.
544
+ const family = getFamilyOf(i + 1);
545
+ this.familyGroups[family].push(i);
546
+ }
547
+
548
+ log.info('SST family groups: A=' + this.familyGroups.A.length +
549
+ ' B=' + this.familyGroups.B.length +
550
+ ' C=' + this.familyGroups.C.length);
551
+ }
552
+
553
+ // ═══════════════════════════════════════════════════════════════════════
554
+ // TERNARY-144: 6-TRIT SATELLITE ADDRESSES
555
+ // ═══════════════════════════════════════════════════════════════════════
556
+
557
+ /**
558
+ * Assign each satellite a 6-trit balanced ternary address.
559
+ *
560
+ * 144 in balanced ternary = "1TT100" (6 trits):
561
+ * 1×243 + (-1)×81 + (-1)×27 + 1×9 + 0×3 + 0×1 = 243 - 81 - 27 + 9 = 144
562
+ *
563
+ * Each satellite gets its index converted to a fixed-width 6-trit address.
564
+ * This enables ternary trie routing for seed lookups instead of array indexing.
565
+ *
566
+ * Satellite 0 → "000000" (0)
567
+ * Satellite 1 → "00000+1" ("000001")
568
+ * Satellite 72 → "10T100" (midpoint)
569
+ * Satellite 143 → just below "1TT100"
570
+ */
571
+ _buildTritAddresses() {
572
+ this.tritAddresses.clear();
573
+ this.tritIndex.clear();
574
+
575
+ for (let i = 0; i < this.seeds.length; i++) {
576
+ const addr = TritArray.fromDecimal(i, TRIT_ADDRESS_LENGTH);
577
+ this.tritAddresses.set(i, addr);
578
+ this.tritIndex.set(addr.toString(), i);
579
+ }
580
+
581
+ log.info('Trit addresses assigned: ' + this.tritAddresses.size +
582
+ ' satellites in 6-trit space (capacity: 3^5=' + Math.pow(3, 5) + ')');
583
+ }
584
+
585
+ /**
586
+ * Look up a satellite seed by its 6-trit address.
587
+ *
588
+ * @param {TritArray | string} address — 6-trit balanced ternary address
589
+ * @returns {Uint8Array | null} — seed bytes, or null if not found
590
+ */
591
+ getSeedByTritAddress(address) {
592
+ telemetry.tritAddressLookups++;
593
+ const key = address instanceof TritArray ? address.toString() : address;
594
+ const index = this.tritIndex.get(key);
595
+ if (index === undefined) return null;
596
+ return { index, seed: this.seeds[index] ?? null };
597
+ }
598
+
599
+ /**
600
+ * Get the 6-trit address for a satellite index.
601
+ * @param {number} index
602
+ * @returns {TritArray | null}
603
+ */
604
+ getTritAddress(index) {
605
+ return this.tritAddresses.get(index) ?? null;
606
+ }
607
+
608
+ // ═══════════════════════════════════════════════════════════════════════
609
+ // TERNARY-144: BATCH SEED CONSENSUS
610
+ // ═══════════════════════════════════════════════════════════════════════
611
+
612
+ /**
613
+ * Validate a batch of seed quality verdicts using TritArray consensus.
614
+ *
615
+ * After loading, each seed has a ternary quality verdict (from _checkBiasTernary).
616
+ * This method aggregates them using TritArray.majority() to determine the
617
+ * overall quality of the seed batch.
618
+ *
619
+ * A batch with POSITIVE majority indicates high-quality quantum entropy.
620
+ * A batch with NEUTRAL majority suggests mixed quality (acceptable).
621
+ * A batch with NEGATIVE majority triggers a warning — seeds may be compromised.
622
+ *
623
+ * @returns {{ majority: Trit, counts: Object, balanced: boolean }}
624
+ */
625
+ batchQualityConsensus() {
626
+ telemetry.batchConsensusRuns++;
627
+
628
+ const verdicts = [];
629
+ for (let i = 0; i < this.seeds.length; i++) {
630
+ const v = this.qualityVerdicts.get(i);
631
+ verdicts.push(v ? v.value : NEUTRAL);
632
+ }
633
+
634
+ if (verdicts.length === 0) {
635
+ return { majority: new Trit(NEUTRAL), counts: { negative: 0, neutral: 0, positive: 0 }, total: 0, balanced: true };
636
+ }
637
+
638
+ const tritArr = new TritArray(verdicts);
639
+ const majority = tritArr.majority();
640
+ const counts = tritArr.count();
641
+ const balanced = tritArr.isBalanced();
642
+
643
+ if (majority.isNegative) {
644
+ log.warn('STEADYWATCH batch consensus: NEGATIVE — seed quality may be compromised');
645
+ } else if (majority.isPositive) {
646
+ log.info('STEADYWATCH batch consensus: POSITIVE — excellent seed quality');
647
+ }
648
+
649
+ return { majority, counts, total: verdicts.length, balanced };
650
+ }
651
+
652
+ // ═══════════════════════════════════════════════════════════════════════
653
+ // TERNARY-144: FAMILY-AWARE SEED SELECTION
654
+ // ═══════════════════════════════════════════════════════════════════════
655
+
656
+ /**
657
+ * Select a seed from a specific SST family.
658
+ *
659
+ * @param {string} family — 'A', 'B', or 'C'
660
+ * @param {number} [offset=0] — offset within the family group
661
+ * @returns {{ index: number, seed: Uint8Array, address: TritArray, quality: Trit } | null}
662
+ */
663
+ selectFromFamily(family, offset = 0) {
664
+ const group = this.familyGroups[family];
665
+ if (!group || group.length === 0) return null;
666
+
667
+ const index = group[offset % group.length];
668
+ return {
669
+ index,
670
+ seed: this.seeds[index],
671
+ family,
672
+ address: this.tritAddresses.get(index) || null,
673
+ quality: this.qualityVerdicts.get(index) || new Trit(NEUTRAL),
674
+ };
675
+ }
676
+
677
+ /**
678
+ * Select a seed using the Fibonacci 24-cycle family mapping.
679
+ * Each call advances the cycle position and picks from the indicated family.
680
+ *
681
+ * The SST family at Fibonacci position n determines which pool to draw from:
682
+ * fibRoot(0) = 1 → Family A → pick from NEGATIVE polarity satellites
683
+ * fibRoot(1) = 1 → Family A → same
684
+ * fibRoot(2) = 2 → Family B → pick from POSITIVE polarity satellites
685
+ * fibRoot(3) = 3 → Family C → pick from GOVERNING satellites
686
+ * ...repeats every 24 positions (the Fibonacci digital root cycle)
687
+ *
688
+ * At cycle positions 12 and 24: fibRoot = 9 → Family C → singularity marker
689
+ *
690
+ * @returns {{ index: number, seed: Uint8Array, family: string, fibPosition: number, fibRoot: number }}
691
+ */
692
+ selectByFibonacciCycle() {
693
+ const pos = this.fibCyclePos;
694
+ const root = fibonacciRoot(pos);
695
+ const family = getFamily(root);
696
+ const familyTrit = toFamilyTrit(root);
697
+
698
+ const group = this.familyGroups[family];
699
+ if (!group || group.length === 0) {
700
+ // Fallback to linear
701
+ return {
702
+ index: this.rotationIndex % this.seeds.length,
703
+ seed: this.seeds[this.rotationIndex % this.seeds.length],
704
+ family,
705
+ fibPosition: pos,
706
+ fibRoot: root,
707
+ };
708
+ }
709
+
710
+ // Use rotation index modulo family group size
711
+ const familyOffset = this.rotationIndex % group.length;
712
+ const index = group[familyOffset];
713
+
714
+ // Advance cycle
715
+ this.fibCyclePos = (this.fibCyclePos + 1) % 24;
716
+ telemetry.familySelections[family]++;
717
+ telemetry.fibonacciCyclePosition = this.fibCyclePos;
718
+
719
+ return {
720
+ index,
721
+ seed: this.seeds[index],
722
+ family,
723
+ fibPosition: pos,
724
+ fibRoot: root,
725
+ familyTrit: familyTrit.value,
726
+ address: this.tritAddresses.get(index)?.toString() || null,
727
+ };
728
+ }
729
+
730
+ /**
731
+ * Build all ternary-144 structures after seeds are loaded.
732
+ * Called automatically at end of load() and generateTestSeeds().
733
+ * @private
734
+ */
735
+ _buildTernaryStructures() {
736
+ this._buildFamilyGroups();
737
+ this._buildTritAddresses();
738
+
739
+ // Log the mathematical harmony
740
+ const totalDR = digitalRoot(this.seeds.length);
741
+ const totalFamily = getFamily(totalDR);
742
+ const totalTrit = toFamilyTrit(this.seeds.length);
743
+
744
+ log.info('Ternary-144: DR(' + this.seeds.length + ')=' + totalDR +
745
+ ' → Family ' + totalFamily +
746
+ ' → TRIT ' + totalTrit.toChar() +
747
+ ' | constellation address: ' + TritArray.fromDecimal(this.seeds.length, TRIT_ADDRESS_LENGTH).toString());
748
+ }
749
+
750
+ /**
751
+ * Get status report
752
+ */
753
+ getStatus() {
754
+ // Quality consensus (if seeds loaded)
755
+ let consensus = null;
756
+ if (this.qualityVerdicts.size > 0) {
757
+ consensus = this.batchQualityConsensus();
758
+ consensus.majority = consensus.majority.toChar(); // serializable
759
+ }
760
+
761
+ return {
762
+ initialized: this.initialized,
763
+ prime: this.prime,
764
+ seedCount: this.seeds.length,
765
+ nodeAssignment: this.nodeAssignment,
766
+ rotationIndex: this.rotationIndex,
767
+ metadata: this.metadata,
768
+ telemetry: { ...telemetry },
769
+ // Ternary-144 status
770
+ ternary: {
771
+ familyGroups: {
772
+ A: this.familyGroups.A.length,
773
+ B: this.familyGroups.B.length,
774
+ C: this.familyGroups.C.length,
775
+ },
776
+ fibCyclePos: this.fibCyclePos,
777
+ tritAddressCount: this.tritAddresses.size,
778
+ constellationDR: this.seeds.length > 0 ? digitalRoot(this.seeds.length) : 0,
779
+ constellationFamily: this.seeds.length > 0 ? getFamily(digitalRoot(this.seeds.length)) : null,
780
+ constellationAddress: this.seeds.length > 0
781
+ ? TritArray.fromDecimal(this.seeds.length, TRIT_ADDRESS_LENGTH).toString()
782
+ : null,
783
+ consensus,
784
+ },
785
+ };
786
+ }
787
+ }
788
+
789
+ // ===================================================================
790
+ // ENTROPY SENTINEL — NPU-Accelerated Quality Monitor
791
+ // ===================================================================
792
+
793
+ /**
794
+ * Entropy Sentinel uses the ACCEL InferenceEngine (NPU/GPU/CPU) to
795
+ * score the quality of seed material before it enters keygen.
796
+ *
797
+ * Novel use: the AMD NPU or NVIDIA GPU runs a small ONNX model that
798
+ * detects patterns in byte sequences that indicate weak randomness:
799
+ * - Low bit-entropy (information density per byte)
800
+ * - Repeating patterns (autocorrelation)
801
+ * - Byte frequency bias
802
+ * - Sequential runs
803
+ *
804
+ * If no ONNX model is loaded, falls back to software statistical tests.
805
+ */
806
+ class EntropySentinel {
807
+ constructor() {
808
+ this._inferenceEngine = null;
809
+ this._modelLoaded = false;
810
+ this._modelName = 'entropy-sentinel';
811
+ }
812
+
813
+ /**
814
+ * Initialize with ACCEL inference engine reference.
815
+ * @param {import('../utils/accel.js').InferenceEngine} engine
816
+ */
817
+ async initialize(engine) {
818
+ this._inferenceEngine = engine;
819
+
820
+ if (engine && engine.hasModel(this._modelName)) {
821
+ this._modelLoaded = true;
822
+ log.info('Entropy Sentinel: NPU/GPU model loaded');
823
+ } else {
824
+ log.debug('Entropy Sentinel: using software statistical tests (no ONNX model)');
825
+ }
826
+ }
827
+
828
+ /**
829
+ * Convert a numeric score to a TRIBHUJ ternary verdict.
830
+ * ≥ 0.85 → POSITIVE (excellent entropy)
831
+ * ≥ 0.50 → NEUTRAL (acceptable entropy)
832
+ * < 0.50 → NEGATIVE (poor entropy — reject)
833
+ *
834
+ * @param {number} score — 0.0 to 1.0
835
+ * @returns {import('../../oracle/tribhuj.js').Trit}
836
+ */
837
+ _scoreToVerdict(score) {
838
+ if (score >= 0.85) return new Trit(POSITIVE);
839
+ if (score >= 0.50) return new Trit(NEUTRAL);
840
+ return new Trit(NEGATIVE);
841
+ }
842
+
843
+ /**
844
+ * Score entropy quality of a byte sequence.
845
+ * Returns a score from 0.0 (terrible) to 1.0 (excellent),
846
+ * plus a TRIBHUJ ternary verdict (POSITIVE/NEUTRAL/NEGATIVE).
847
+ *
848
+ * NPU path: runs ONNX model for pattern detection
849
+ * CPU path: bit-entropy + byte frequency chi-square
850
+ *
851
+ * @param {Uint8Array} data — seed or key material
852
+ * @returns {Promise<{ score: number, verdict: Trit, method: 'npu'|'gpu'|'cpu', details: Object }>}
853
+ */
854
+ async score(data) {
855
+ telemetry.sentinelChecks++;
856
+
857
+ // Try NPU/GPU inference first
858
+ if (this._modelLoaded && this._inferenceEngine) {
859
+ try {
860
+ // Model expects exactly 32 features — bin input bytes into 32 segments
861
+ const MODEL_INPUTS = 32;
862
+ const inputTensor = new Float32Array(MODEL_INPUTS);
863
+ if (data.length <= MODEL_INPUTS) {
864
+ // Short input: use directly, pad remainder with 0
865
+ for (let i = 0; i < data.length; i++) inputTensor[i] = data[i] / 255.0;
866
+ } else {
867
+ // Longer input: average each bin for representative features
868
+ const binSize = data.length / MODEL_INPUTS;
869
+ for (let b = 0; b < MODEL_INPUTS; b++) {
870
+ const start = Math.floor(b * binSize);
871
+ const end = Math.floor((b + 1) * binSize);
872
+ let sum = 0;
873
+ for (let i = start; i < end; i++) sum += data[i];
874
+ inputTensor[b] = (sum / (end - start)) / 255.0;
875
+ }
876
+ }
877
+
878
+ const result = await this._inferenceEngine.infer(this._modelName, {
879
+ seed_bytes: inputTensor,
880
+ });
881
+
882
+ if (result && result.quality_score) {
883
+ const score = result.quality_score[0];
884
+ telemetry.sentinelPasses += score >= MIN_ENTROPY_RATIO ? 1 : 0;
885
+ telemetry.sentinelFails += score < MIN_ENTROPY_RATIO ? 1 : 0;
886
+
887
+ const provider = this._inferenceEngine.provider || '';
888
+ return {
889
+ score,
890
+ verdict: this._scoreToVerdict(score),
891
+ method: provider.includes('Dml') ? 'npu'
892
+ : provider.includes('CUDA') ? 'gpu' : 'cpu',
893
+ details: { raw: result },
894
+ };
895
+ }
896
+ } catch (err) {
897
+ log.trace('Sentinel NPU inference failed, falling back to software:', err.message);
898
+ }
899
+ }
900
+
901
+ // Software fallback: bit-entropy + chi-square
902
+ return this._softwareScore(data);
903
+ }
904
+
905
+ /**
906
+ * Software statistical scoring (no NPU required).
907
+ * @private
908
+ */
909
+ _softwareScore(data) {
910
+ const len = data.length;
911
+ if (len === 0) return { score: 0, verdict: new Trit(NEGATIVE), method: 'cpu', details: {} };
912
+
913
+ // 1. Bit-entropy (information density per byte, max 8.0 bits)
914
+ const freq = new Uint32Array(256);
915
+ for (const b of data) freq[b]++;
916
+
917
+ let bitEntropy = 0;
918
+ for (let i = 0; i < 256; i++) {
919
+ if (freq[i] === 0) continue;
920
+ const p = freq[i] / len;
921
+ bitEntropy -= p * Math.log2(p);
922
+ }
923
+ const entropyNorm = bitEntropy / 8.0; // Normalized to [0, 1]
924
+
925
+ // 2. Chi-square test for uniform distribution
926
+ const expected = len / 256;
927
+ let chiSquare = 0;
928
+ for (let i = 0; i < 256; i++) {
929
+ const diff = freq[i] - expected;
930
+ chiSquare += (diff * diff) / expected;
931
+ }
932
+ // Normalize: chi-square for 255 DOF, p=0.05 critical ~ 293.25
933
+ const chiNorm = Math.max(0, 1 - chiSquare / 600);
934
+
935
+ // 3. Run length test (consecutive identical bytes)
936
+ let maxRun = 0;
937
+ let currentRun = 1;
938
+ for (let i = 1; i < len; i++) {
939
+ if (data[i] === data[i - 1]) {
940
+ currentRun++;
941
+ if (currentRun > maxRun) maxRun = currentRun;
942
+ } else {
943
+ currentRun = 1;
944
+ }
945
+ }
946
+ const runNorm = Math.max(0, 1 - maxRun / 8);
947
+
948
+ // 4. Autocorrelation at lag 1
949
+ let mean = 0;
950
+ for (const b of data) mean += b;
951
+ mean /= len;
952
+ let num = 0, den = 0;
953
+ for (let i = 0; i < len - 1; i++) {
954
+ num += (data[i] - mean) * (data[i + 1] - mean);
955
+ den += (data[i] - mean) ** 2;
956
+ }
957
+ const autocorr = den === 0 ? 0 : Math.abs(num / den);
958
+ const autoNorm = Math.max(0, 1 - autocorr * 5);
959
+
960
+ // Combined score (weighted)
961
+ const score = entropyNorm * 0.4 + chiNorm * 0.25 + runNorm * 0.15 + autoNorm * 0.2;
962
+
963
+ telemetry.sentinelPasses += score >= MIN_ENTROPY_RATIO ? 1 : 0;
964
+ telemetry.sentinelFails += score < MIN_ENTROPY_RATIO ? 1 : 0;
965
+
966
+ return {
967
+ score: Math.round(score * 1000) / 1000,
968
+ verdict: this._scoreToVerdict(score),
969
+ method: 'cpu',
970
+ details: {
971
+ bitEntropyPerByte: Math.round(bitEntropy * 1000) / 1000,
972
+ entropyNorm: Math.round(entropyNorm * 1000) / 1000,
973
+ chiSquare: Math.round(chiSquare * 10) / 10,
974
+ chiNorm: Math.round(chiNorm * 1000) / 1000,
975
+ maxRunLength: maxRun,
976
+ runNorm: Math.round(runNorm * 1000) / 1000,
977
+ autocorrelation: Math.round(autocorr * 1000) / 1000,
978
+ autoNorm: Math.round(autoNorm * 1000) / 1000,
979
+ },
980
+ };
981
+ }
982
+ }
983
+
984
+ // ===================================================================
985
+ // UTILITIES
986
+ // ===================================================================
987
+
988
+ function hexToUint8(hex) {
989
+ const bytes = new Uint8Array(hex.length / 2);
990
+ for (let i = 0; i < bytes.length; i++) {
991
+ bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
992
+ }
993
+ return bytes;
994
+ }
995
+
996
+ // ===================================================================
997
+ // SINGLETONS & EXPORTS
998
+ // ===================================================================
999
+
1000
+ /** Global STEADYWATCH seed store */
1001
+ export const seedStore = new SteadywatchSeedStore();
1002
+
1003
+ /** Global Entropy Sentinel */
1004
+ export const sentinel = new EntropySentinel();
1005
+
1006
+ /**
1007
+ * Initialize STEADYWATCH subsystem.
1008
+ *
1009
+ * @param {Object} options
1010
+ * @param {string} [options.seedFile] — path to satellite seeds JSON
1011
+ * @param {number} [options.nodeIndex] — assigned satellite index for this node
1012
+ * @param {number} [options.prime=5] — Hurwitz prime for test seed generation
1013
+ * @param {boolean} [options.generateTest=false] — generate test seeds if no file
1014
+ * @param {import('../utils/accel.js').InferenceEngine} [options.inferenceEngine] — ACCEL inference engine
1015
+ * @returns {Promise<{ initialized: boolean, seedCount: number, sentinel: boolean }>}
1016
+ */
1017
+ export async function initialize(options = {}) {
1018
+ const {
1019
+ seedFile,
1020
+ nodeIndex,
1021
+ prime = 5,
1022
+ generateTest = false,
1023
+ inferenceEngine = null,
1024
+ } = options;
1025
+
1026
+ // Load seeds
1027
+ let loaded = false;
1028
+ if (seedFile) {
1029
+ loaded = seedStore.load(seedFile);
1030
+ }
1031
+
1032
+ if (!loaded && generateTest) {
1033
+ const defaultPath = join(
1034
+ dirname(fileURLToPath(import.meta.url)),
1035
+ '../data/steadywatch-seeds-p' + prime + '.json'
1036
+ );
1037
+ seedStore.generateTestSeeds(prime, defaultPath);
1038
+ loaded = seedStore.initialized;
1039
+ }
1040
+
1041
+ // Assign node
1042
+ if (loaded && typeof nodeIndex === 'number') {
1043
+ seedStore.assignNode(nodeIndex);
1044
+ }
1045
+
1046
+ // Initialize Sentinel
1047
+ if (inferenceEngine) {
1048
+ await sentinel.initialize(inferenceEngine);
1049
+ }
1050
+
1051
+ const status = {
1052
+ initialized: seedStore.initialized,
1053
+ seedCount: seedStore.seeds.length,
1054
+ sentinel: sentinel._modelLoaded,
1055
+ };
1056
+
1057
+ log.info('STEADYWATCH initialized:', status);
1058
+ return status;
1059
+ }
1060
+
1061
+ /**
1062
+ * Get a hybrid quantum+CSPRNG seed for ML-KEM-768 keygen.
1063
+ * This is the primary integration point for ANNEX.
1064
+ *
1065
+ * @returns {Uint8Array} — 64-byte seed
1066
+ */
1067
+ export function getHybridSeed() {
1068
+ return seedStore.getHybridSeed();
1069
+ }
1070
+
1071
+ /**
1072
+ * Score entropy quality of arbitrary byte data.
1073
+ * Uses NPU/GPU if available, software fallback otherwise.
1074
+ * Returns numeric score + TRIBHUJ ternary verdict.
1075
+ *
1076
+ * @param {Uint8Array} data
1077
+ * @returns {Promise<{ score: number, verdict: Trit, method: string, details: Object }>}
1078
+ */
1079
+ export async function scoreEntropy(data) {
1080
+ return sentinel.score(data);
1081
+ }
1082
+
1083
+ /**
1084
+ * Select a seed using Fibonacci-cycle family-aware rotation.
1085
+ * Advances the 24-position Fibonacci cycle, determines the SST
1086
+ * family for the current position, and selects from matching satellites.
1087
+ *
1088
+ * @returns {{ seed: Uint8Array, index: number, family: string, fibPos: number, address: TritArray|null }}
1089
+ */
1090
+ export function selectByFibonacciCycle() {
1091
+ return seedStore.selectByFibonacciCycle();
1092
+ }
1093
+
1094
+ /**
1095
+ * Get seed by 6-trit balanced ternary address.
1096
+ * @param {string|TritArray} address — e.g. "1TT100" or TritArray instance
1097
+ * @returns {{ seed: Uint8Array, index: number }|null}
1098
+ */
1099
+ export function getSeedByTritAddress(address) {
1100
+ return seedStore.getSeedByTritAddress(address);
1101
+ }
1102
+
1103
+ /**
1104
+ * Run batch quality consensus across all satellite seeds.
1105
+ * Returns TritArray majority verdict and per-family counts.
1106
+ *
1107
+ * @returns {{ majority: Trit, counts: Object, total: number }}
1108
+ */
1109
+ export function batchQualityConsensus() {
1110
+ return seedStore.batchQualityConsensus();
1111
+ }
1112
+
1113
+ /**
1114
+ * Get STEADYWATCH status and telemetry.
1115
+ */
1116
+ export function getStatus() {
1117
+ return {
1118
+ ...seedStore.getStatus(),
1119
+ sentinel: {
1120
+ modelLoaded: sentinel._modelLoaded,
1121
+ provider: sentinel._inferenceEngine?.provider || 'none',
1122
+ },
1123
+ };
1124
+ }
1125
+
1126
+ export {
1127
+ SteadywatchSeedStore, EntropySentinel,
1128
+ SATELLITE_COUNTS, MIN_ENTROPY_RATIO,
1129
+ TRIT_ADDRESS_LENGTH, FAMILY_GROUPS,
1130
+ };
1131
+
1132
+ export default {
1133
+ initialize,
1134
+ getHybridSeed,
1135
+ scoreEntropy,
1136
+ selectByFibonacciCycle,
1137
+ getSeedByTritAddress,
1138
+ batchQualityConsensus,
1139
+ getStatus,
1140
+ seedStore,
1141
+ sentinel,
1142
+ };