reploid 0.0.0-stage → 0.1.1

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 (233) hide show
  1. package/apps/poolday/config/chat-files.json +27 -0
  2. package/apps/poolday/config/partition-generation.js +21 -0
  3. package/apps/poolday/config/partition-policy.json +74 -0
  4. package/apps/poolday/config/partition-releases.json +3 -0
  5. package/apps/poolday/config/work-profile.json +122 -0
  6. package/apps/poolday/host/chat-view.js +47 -0
  7. package/apps/poolday/host/earning/client.js +199 -0
  8. package/apps/poolday/host/earning/contribution.js +168 -0
  9. package/apps/poolday/host/partition-release.js +33 -0
  10. package/apps/poolday/host/work-partition-artifact-source.js +37 -0
  11. package/apps/poolday/host/work-partition-capsule.js +39 -0
  12. package/apps/poolday/host/work-partition-plan.js +167 -0
  13. package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
  14. package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
  15. package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
  16. package/apps/poolday/pool/pack-operation.js +161 -0
  17. package/apps/poolday/pool/pack-release-policy.js +44 -0
  18. package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
  19. package/apps/poolday/providers/work-adapter.js +67 -0
  20. package/apps/poolday/providers/work-device.js +33 -0
  21. package/apps/poolday/providers/work-generation-completion.js +55 -0
  22. package/apps/poolday/providers/work-provider.js +209 -0
  23. package/apps/poolday/providers/work-resident-provider.js +113 -0
  24. package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
  25. package/docs/fleet-service.md +214 -0
  26. package/docs/native-participant.md +542 -0
  27. package/native-delivery.json +1174 -0
  28. package/package.json +29 -4
  29. package/packages/reploid/src/artifacts/binary.js +25 -0
  30. package/packages/reploid/src/artifacts/canonical-json.js +23 -0
  31. package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
  32. package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
  33. package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
  34. package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
  35. package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
  36. package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
  37. package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
  38. package/packages/reploid/src/artifacts/identity.js +91 -0
  39. package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
  40. package/packages/reploid/src/artifacts/receipt.js +131 -0
  41. package/packages/reploid/src/artifacts/signatures.js +94 -0
  42. package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
  43. package/packages/reploid/src/cancellation.js +44 -0
  44. package/packages/reploid/src/config/defaults.json +110 -0
  45. package/packages/reploid/src/config/index.js +209 -0
  46. package/packages/reploid/src/config/request-overrides.json +9 -0
  47. package/packages/reploid/src/config/transport-defaults.json +7 -0
  48. package/packages/reploid/src/hashing.js +30 -0
  49. package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
  50. package/packages/reploid/src/measurement/index.js +5 -0
  51. package/packages/reploid/src/measurement/readiness-records.js +310 -0
  52. package/packages/reploid/src/mesh/computer-identity.js +50 -0
  53. package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
  54. package/packages/reploid/src/mesh/contribution.js +141 -0
  55. package/packages/reploid/src/mesh/generation-recovery.js +58 -0
  56. package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
  57. package/packages/reploid/src/mesh/gpu-signature.js +136 -0
  58. package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
  59. package/packages/reploid/src/mesh/index.js +66 -0
  60. package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
  61. package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
  62. package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
  63. package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
  64. package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
  65. package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
  66. package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
  67. package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
  68. package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
  69. package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
  70. package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
  71. package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
  72. package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
  73. package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
  74. package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
  75. package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
  76. package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
  77. package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
  78. package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
  79. package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
  80. package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
  81. package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
  82. package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
  83. package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
  84. package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
  85. package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
  86. package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
  87. package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
  88. package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
  89. package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
  90. package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
  91. package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
  92. package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
  93. package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
  94. package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
  95. package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
  96. package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
  97. package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
  98. package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
  99. package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
  100. package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
  101. package/packages/reploid/src/mesh/peer-generation.js +896 -0
  102. package/packages/reploid/src/mesh/peer-identity.js +87 -0
  103. package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
  104. package/packages/reploid/src/mesh/preparation-progress.js +46 -0
  105. package/packages/reploid/src/mesh/provider-hardware.js +11 -0
  106. package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
  107. package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
  108. package/packages/reploid/src/mesh/web-access-network.js +112 -0
  109. package/packages/reploid/src/mesh/web-access-policy.json +17 -0
  110. package/packages/reploid/src/retrieval/citations.js +156 -0
  111. package/packages/reploid/src/retrieval/claim-support.js +113 -0
  112. package/packages/reploid/src/retrieval/costs.js +101 -0
  113. package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
  114. package/packages/reploid/src/retrieval/index.js +20 -0
  115. package/packages/reploid/src/retrieval/lexical-support.js +27 -0
  116. package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
  117. package/packages/reploid/src/retrieval/page-fetch.json +10 -0
  118. package/packages/reploid/src/retrieval/page-text.js +182 -0
  119. package/packages/reploid/src/retrieval/passage.js +37 -0
  120. package/packages/reploid/src/retrieval/policy.json +14 -0
  121. package/packages/reploid/src/retrieval/retrieval.js +23 -0
  122. package/packages/reploid/src/retrieval/review-queue.js +76 -0
  123. package/packages/reploid/src/retrieval/sources.js +263 -0
  124. package/packages/reploid/src/retrieval/support-review.js +155 -0
  125. package/packages/reploid/src/retrieval/text.js +21 -0
  126. package/packages/reploid/src/rules/contribution.rules.json +8 -0
  127. package/packages/reploid/src/rules/routing.rules.json +24 -0
  128. package/packages/reploid/src/transport/assignment.js +571 -0
  129. package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
  130. package/packages/reploid/src/transport/index.js +11 -0
  131. package/packages/reploid/src/transport/introduction-admission.js +24 -0
  132. package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
  133. package/packages/reploid/src/transport/introduction-policy.json +12 -0
  134. package/packages/reploid/src/transport/introduction-records.js +90 -0
  135. package/packages/reploid/src/transport/introductions.js +209 -0
  136. package/packages/reploid/src/transport/partition-data-channel.js +421 -0
  137. package/packages/reploid/src/transport/partition-errors.json +16 -0
  138. package/packages/reploid/src/transport/retry-policy.js +36 -0
  139. package/packages/reploid/src/transport/room.js +502 -0
  140. package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
  141. package/packages/reploid/src/transport/signaling.js +571 -0
  142. package/packages/reploid/src/transport/swarm.js +1247 -0
  143. package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
  144. package/server/browser-roots.js +42 -0
  145. package/server/browser-web-worker.js +94 -0
  146. package/server/chat-completions.js +45 -0
  147. package/server/fleet/browser-bridge.js +145 -0
  148. package/server/fleet/contract.js +42 -0
  149. package/server/fleet/credit-evidence.js +103 -0
  150. package/server/fleet/credit-recovery.js +175 -0
  151. package/server/fleet/credits.js +178 -0
  152. package/server/fleet/execution-admission.js +32 -0
  153. package/server/fleet/native-host.js +817 -0
  154. package/server/fleet/placement.js +132 -0
  155. package/server/fleet/requester.js +346 -0
  156. package/server/fleet/service.js +311 -0
  157. package/server/fleet/terminal-policy.js +83 -0
  158. package/server/fleet/text-api.js +94 -0
  159. package/server/hashing.js +35 -0
  160. package/server/http-request-owner.js +49 -0
  161. package/server/messages.js +57 -0
  162. package/server/native-canonical-epochs.js +90 -0
  163. package/server/native-change-log.js +40 -0
  164. package/server/native-earning.js +203 -0
  165. package/server/native-generation-journal.js +46 -0
  166. package/server/native-model-config.js +57 -0
  167. package/server/native-model-files.js +382 -0
  168. package/server/native-model-origin.js +54 -0
  169. package/server/native-model-origins.json +12 -0
  170. package/server/native-node.js +85 -0
  171. package/server/native-owner/controls.js +162 -0
  172. package/server/native-owner/monitor.js +251 -0
  173. package/server/native-owner/policy.js +273 -0
  174. package/server/native-owner/rate.js +92 -0
  175. package/server/native-participant.js +866 -0
  176. package/server/native-readiness-log.js +30 -0
  177. package/server/native-release-storage.js +64 -0
  178. package/server/native-shader-cache.js +369 -0
  179. package/server/native-supply-budget.js +66 -0
  180. package/server/native-whole-earning.js +67 -0
  181. package/server/native-whole-model-fill.js +182 -0
  182. package/server/native-whole-model.js +130 -0
  183. package/server/public-web-worker.js +66 -0
  184. package/server/responses.js +92 -0
  185. package/server/search/errors.js +47 -0
  186. package/server/search/page-fetch.js +185 -0
  187. package/server/text-api.js +83 -0
  188. package/server/text-recovery.js +45 -0
  189. package/server/text-request-options.js +23 -0
  190. package/server/text-requester.js +231 -0
  191. package/server/token-auth.js +9 -0
  192. package/server/whole-model-requester.js +99 -0
  193. package/shared/config/chat-models.json +4685 -0
  194. package/shared/config/doppler-execution-contracts.js +32 -0
  195. package/shared/config/doppler-execution-contracts.json +43 -0
  196. package/shared/config/doppler-generation-contract.js +223 -0
  197. package/shared/config/doppler-local-models.js +79 -0
  198. package/shared/config/doppler-package.json +7 -0
  199. package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
  200. package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
  201. package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
  202. package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
  203. package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
  204. package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
  205. package/shared/config/reploid-library.json +49 -0
  206. package/shared/config/swarm-bootstrap.json +21 -0
  207. package/shared/infrastructure/doppler-runtime-service.js +261 -0
  208. package/shared/pool/executable-pack.js +87 -0
  209. package/shared/pool/pack-operation-adapters.js +124 -0
  210. package/shared/pool/pack-operation-policy.js +75 -0
  211. package/shared/pool/pool-config.json +2280 -0
  212. package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
  213. package/shared/vendor/reploid/artifacts/binary.js +25 -0
  214. package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
  215. package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
  216. package/shared/vendor/reploid/artifacts/identity.js +91 -0
  217. package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
  218. package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
  219. package/shared/vendor/reploid/artifacts/signatures.js +94 -0
  220. package/shared/vendor/reploid/config/defaults.json +110 -0
  221. package/shared/vendor/reploid/config/index.js +209 -0
  222. package/shared/vendor/reploid/config/request-overrides.json +9 -0
  223. package/shared/vendor/reploid/hashing.js +30 -0
  224. package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
  225. package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
  226. package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
  227. package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
  228. package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
  229. package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
  230. package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
  231. package/shared/vendor/reploid/rules/routing.rules.json +24 -0
  232. package/shared/vendor/reploid/transport/partition-errors.json +16 -0
  233. package/README.md +0 -3
@@ -0,0 +1,179 @@
1
+ import { assertPartition as assert, canonicalPartitionJson, canonicalPartitionRoute } from './partition-contract.js';
2
+
3
+ const REQUEST = 'reploid.partition-channel-route/v1';
4
+ const ACCEPT = 'reploid.partition-channel-route-accepted/v1';
5
+ /** Auxiliary channel label for ordered routes; the swarm transport must admit it. */
6
+ export const ORDERED_PARTITION_CHANNEL = 'reploid-partitions-ordered';
7
+ /** Route-bound channels inside the existing authenticated partition network. */
8
+ export function createRoutePartitionNetwork({ transport, verifyPeer, createEndpoint, maxPeers, timeoutMs,
9
+ ordered, onPeer = () => {}, label }) {
10
+ assert(label === ORDERED_PARTITION_CHANNEL && typeof ordered?.validateRoute === 'function'
11
+ && Number.isSafeInteger(ordered.maxRouteBytes) && ordered.maxRouteBytes > 0
12
+ && typeof transport?.onDataChannel === 'function' && typeof transport.getPeerBinding === 'function'
13
+ && typeof verifyPeer === 'function' && typeof createEndpoint === 'function'
14
+ && Number.isSafeInteger(maxPeers) && maxPeers > 0 && Number.isSafeInteger(timeoutMs) && timeoutMs > 0
15
+ && timeoutMs <= 2147483647, 'Ordered network identity, route and channel budgets required');
16
+ const { maxRouteBytes, validateRoute } = ordered;
17
+ const entries = new Map();
18
+ let closed = false, closing = null;
19
+ const routeSnapshot = route => {
20
+ const value = canonicalPartitionRoute(route);
21
+ assert(new TextEncoder().encode(canonicalPartitionJson(value)).byteLength <= maxRouteBytes, 'Ordered route exceeds handshake allocation');
22
+ return value;
23
+ };
24
+ const keyFor = (peerId, route) => JSON.stringify([peerId, canonicalPartitionJson(route)]);
25
+ const sameCertificate = (peerId, original) => {
26
+ const current = transport.getPeerBinding(peerId);
27
+ return !!original && original.local === current?.local && original.remote === current?.remote;
28
+ };
29
+ function handshake(entry, incoming) {
30
+ return new Promise((resolve, reject) => {
31
+ let finished = false;
32
+ const finish = (error, value) => {
33
+ if (finished) return; finished = true; clearTimeout(timer);
34
+ entry.channel.removeEventListener('message', receive);
35
+ entry.channel.removeEventListener('close', lost);
36
+ entry.controller.signal.removeEventListener('abort', lost);
37
+ error ? reject(error) : resolve(value);
38
+ };
39
+ const lost = () => finish(new Error('Ordered channel closed during authentication'));
40
+ const receive = event => {
41
+ try {
42
+ // Bound text before parsing; envelope overhead is fixed, route budget explicit.
43
+ assert(typeof event.data === 'string' && event.data.length <= maxRouteBytes + 128
44
+ && new TextEncoder().encode(event.data).byteLength <= maxRouteBytes + 128, 'Ordered handshake allocation exceeded');
45
+ const message = JSON.parse(event.data);
46
+ assert(message.schema === (incoming ? REQUEST : ACCEPT)
47
+ && Object.keys(message).length === 2, 'Ordered channel handshake version mismatch');
48
+ const route = routeSnapshot(message.route);
49
+ if (!incoming) assert(canonicalPartitionJson(route) === canonicalPartitionJson(entry.route), 'Ordered handshake route changed');
50
+ finish(null, route);
51
+ } catch (error) { finish(error); }
52
+ };
53
+ const timer = setTimeout(() => finish(new Error('Ordered channel authentication timed out')), timeoutMs);
54
+ entry.channel.addEventListener('message', receive);
55
+ entry.channel.addEventListener('close', lost, { once: true });
56
+ entry.controller.signal.addEventListener('abort', lost, { once: true });
57
+ if (entry.controller.signal.aborted || entry.channel.readyState === 'closed') lost();
58
+ });
59
+ }
60
+ function opened(entry) {
61
+ if (entry.channel.readyState === 'open') return Promise.resolve();
62
+ return new Promise((resolve, reject) => {
63
+ const done = error => { entry.channel.removeEventListener('open', ready);
64
+ entry.controller.signal.removeEventListener('abort', abort); error ? reject(error) : resolve(); };
65
+ const ready = () => done(), abort = () => done(new Error('Ordered channel opening cancelled'));
66
+ entry.channel.addEventListener('open', ready, { once: true });
67
+ entry.controller.signal.addEventListener('abort', abort, { once: true });
68
+ if (entry.controller.signal.aborted) abort();
69
+ });
70
+ }
71
+ async function guarded(entry, operation) {
72
+ entry.controller.signal.throwIfAborted();
73
+ let abort;
74
+ const cancelled = new Promise((_, reject) => {
75
+ abort = () => reject(new Error('Ordered authentication or route validation cancelled'));
76
+ entry.controller.signal.addEventListener('abort', abort, { once: true });
77
+ });
78
+ try { return await Promise.race([Promise.resolve().then(() => {
79
+ entry.controller.signal.throwIfAborted(); return operation();
80
+ }), cancelled]); }
81
+ finally { entry.controller.signal.removeEventListener('abort', abort); }
82
+ }
83
+ function retireEntry(entry) {
84
+ if (entry.retirement) return entry.retirement;
85
+ entry.retiring = true;
86
+ const operation = Promise.resolve().then(() => entry.endpoint?.close()).then(() => {
87
+ entry.controller.abort(); entry.channel.close();
88
+ if (entries.get(entry.key) === entry) entries.delete(entry.key);
89
+ });
90
+ entry.retirement = operation;
91
+ operation.catch(() => { if (entry.retirement === operation) entry.retirement = null; });
92
+ return operation;
93
+ }
94
+ function attach(peerId, channel, incoming, route) {
95
+ assert(!closed && entries.size < maxPeers, 'Ordered channel allocation exhausted');
96
+ const key = route ? keyFor(peerId, route) : Symbol('unbound-route');
97
+ assert(!entries.has(key), 'Ordered route channel already exists');
98
+ const entry = { peerId, key, route, channel, controller: new AbortController(), endpoint: null,
99
+ promise: null, retirement: null, retiring: false, lost: false };
100
+ entries.set(key, entry);
101
+ channel.addEventListener('close', () => {
102
+ entry.lost = true; entry.controller.abort();
103
+ // A closed socket alone never clears resident ownership.
104
+ Promise.resolve(entry.promise).catch(() => {}).then(() => retireEntry(entry)).catch(() => {});
105
+ }, { once: true });
106
+ const received = handshake(entry, incoming); received.catch(() => {});
107
+ entry.promise = (async () => {
108
+ const timer = setTimeout(() => entry.controller.abort(), timeoutMs);
109
+ try {
110
+ const observedCertificate = transport.getPeerBinding(peerId);
111
+ assert(observedCertificate && typeof observedCertificate.local === 'string' && typeof observedCertificate.remote === 'string',
112
+ 'Authenticated WebRTC certificate binding required');
113
+ const certificate = {local: observedCertificate.local, remote: observedCertificate.remote};
114
+ const remoteParticipantId = await guarded(entry, () => verifyPeer(peerId, entry.controller.signal));
115
+ entry.controller.signal.throwIfAborted();
116
+ assert(/^peer:[a-f0-9]{24}$/.test(remoteParticipantId) && sameCertificate(peerId, certificate), 'Ordered peer identity or certificate changed');
117
+ await opened(entry); entry.controller.signal.throwIfAborted();
118
+ if (!incoming) channel.send(JSON.stringify({ schema: REQUEST, route: entry.route }));
119
+ const bound = await received; entry.controller.signal.throwIfAborted();
120
+ assert(await guarded(entry, () => validateRoute(bound, { remoteParticipantId, remoteTransportId: peerId, incoming,
121
+ signal: entry.controller.signal })) === true, 'Ordered route validation declined');
122
+ entry.controller.signal.throwIfAborted();
123
+ assert(sameCertificate(peerId, certificate), 'Ordered peer certificate changed during route validation');
124
+ if (incoming) {
125
+ const boundKey = keyFor(peerId, bound);
126
+ assert(!entries.has(boundKey), 'Ordered route channel already exists');
127
+ entries.delete(entry.key); entry.key = boundKey; entry.route = bound; entries.set(boundKey, entry);
128
+ }
129
+ entry.endpoint = createEndpoint({ channel, remoteParticipantId, remoteTransportId: peerId, route: entry.route });
130
+ assert(entry.endpoint && typeof entry.endpoint.close === 'function', 'Ordered endpoint cleanup owner required');
131
+ if (incoming) channel.send(JSON.stringify({ schema: ACCEPT, route: entry.route }));
132
+ onPeer(peerId, entry.endpoint, structuredClone(entry.route));
133
+ return entry.endpoint;
134
+ } catch (error) {
135
+ entry.controller.abort();
136
+ try { await retireEntry(entry); } catch { /* Explicit retire/close retries retained ownership. */ }
137
+ throw error;
138
+ } finally { clearTimeout(timer); }
139
+ })();
140
+ return entry.promise;
141
+ }
142
+ const unsubscribe = transport.onDataChannel(label, (peerId, channel) => {
143
+ try { attach(peerId, channel, true).catch(() => {}); } catch { channel.close(); }
144
+ });
145
+ return Object.freeze({
146
+ connect(peerId, { route } = {}) {
147
+ assert(!closed, 'Ordered partition network closed');
148
+ const snapshot = routeSnapshot(route), key = keyFor(peerId, snapshot), existing = entries.get(key);
149
+ if (existing) {
150
+ assert(!existing.retiring && !existing.lost, 'Previous ordered route cleanup remains charged');
151
+ return existing.promise;
152
+ }
153
+ assert(entries.size < maxPeers, 'Ordered channel allocation exhausted');
154
+ const channel = transport.openDataChannel(peerId, label);
155
+ try { return attach(peerId, channel, false, snapshot); } catch (error) { channel.close(); throw error; }
156
+ },
157
+ async retire(peerId, { route }) {
158
+ const entry = entries.get(keyFor(peerId, routeSnapshot(route)));
159
+ if (!entry) return;
160
+ entry.retiring = true;
161
+ if (!entry.endpoint) entry.controller.abort();
162
+ await entry.promise.catch(() => {});
163
+ await retireEntry(entry);
164
+ },
165
+ getState() { return { closed, channels: entries.size, retiring: [...entries.values()].filter(entry=>entry.retiring).length }; },
166
+ close() {
167
+ if (closing) return closing;
168
+ closed = true; unsubscribe();
169
+ const snapshot = [...entries.values()]; snapshot.forEach(entry=>entry.controller.abort());
170
+ closing = (async () => {
171
+ await Promise.allSettled(snapshot.map(entry=>entry.promise));
172
+ const results = await Promise.allSettled(snapshot.map(retireEntry));
173
+ const failures = results.filter(result=>result.status === 'rejected').map(result=>result.reason);
174
+ if (failures.length) throw new AggregateError(failures, 'Ordered network cleanup remains unsettled');
175
+ })().catch(error => { closing = null; throw error; });
176
+ return closing;
177
+ }
178
+ });
179
+ }
@@ -0,0 +1,268 @@
1
+ import { assertPartition as assert } from './partition-contract.js';
2
+
3
+ /** Expected-cost ranking of eligible prepared routes (DR-6; Launch 3, opt-in). It ranks complete
4
+ * assignments of distinct prepared stages by expected cost per completed answer under an explicit,
5
+ * versioned `reploid.route-cost-policy/v1`; nothing here grants, reserves or moves an attempt.
6
+ * Peer identity is the deterministic tie-breaker, never the placement criterion.
7
+ *
8
+ * For a route, with workload P prompt tokens and T expected tokens, per stage i:
9
+ * latency = prefill_i * P + decode_i * T (stage execution; prices.latencyPerMs)
10
+ * transfer = transfer_i * T (inbound activation transfer; prices.transferPerMs)
11
+ * queueing = (active_i + queued_i) * (latency_i + transfer_i) (advertised capacity; prices.queueingPerMs)
12
+ * opening = 0 for a route already open or the local stage, else its measured channel opening
13
+ * (prices.preparationPerMs). Stage residents are always prepared: unprepared offers are
14
+ * not eligible, so routing never waits on stage preparation.
15
+ * refusal r_i = (refusals_i + readiness.priorRefusals) / (admissions_i + readiness.priorAttempts)
16
+ * reserve = max(0, reserve.spareSlots - (availableSlots_i - 1)) slots below the spare reserve
17
+ * and per route f = (failures + failure.priorFailures) / (executions + failure.priorAttempts).
18
+ * With a = prod(1 - r_i), retries on the same route give N = 1 / (a * (1 - f)) attempts per
19
+ * completed answer, service S = latency + queueing + transfer costs and penalty F = refund + retry:
20
+ * readiness = N * (1 - a) * F failure = N * a * f * (S + F)
21
+ * total = S + preparation + readiness + failure + reserve
22
+ * Unmeasured or stale latency/opening use the policy's declared priors and are labeled as such;
23
+ * outcome probabilities always include the declared prior counts. Totals are compared in units of
24
+ * `resolution`, so routes equal within it tie and fall back to participant ids. Answer quality is
25
+ * route-independent under one model, plan and numerical tolerance and is not a ranking input. */
26
+ export const ROUTE_COST_POLICY_SCHEMA = 'reploid.route-cost-policy/v1';
27
+ export const ROUTE_SELECTION_SCHEMA = 'reploid.route-selection/v1';
28
+ export const ROUTE_RANKING_WORK_LIMIT = 'Route ranking work limit exceeded';
29
+ const RUNTIME = 'executionMs';
30
+ const POLICY_GROUPS = Object.freeze({
31
+ prices: ['latencyPerMs', 'queueingPerMs', 'transferPerMs', 'preparationPerMs', 'refundPerFailure', 'retryPerFailure'],
32
+ unknown: ['prefillMsPerToken', 'decodeMsPerToken', 'transferMsPerToken', 'openingMs', 'maxObservationAgeMs'],
33
+ readiness: ['priorAttempts', 'priorRefusals'],
34
+ failure: ['priorAttempts', 'priorFailures'],
35
+ reserve: ['spareSlots', 'perMissingSlot']
36
+ });
37
+ const COMPONENTS = Object.freeze(['latency', 'queueing', 'transfer', 'preparation', 'readiness', 'failure', 'reserve']);
38
+ const finite = value => typeof value === 'number' && Number.isFinite(value) && value >= 0;
39
+ const count = value => Number.isSafeInteger(value) && value >= 0;
40
+ const mean = row => row && row.count > 0 && finite(row.total) ? row.total / row.count : null;
41
+ const exact = (value, keys) => !!value && typeof value === 'object' && !Array.isArray(value)
42
+ && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
43
+ const freeze = value => { if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); } return value; };
44
+ const stageKey = stages => JSON.stringify(stages.map(({ participantId, transportId, residentId, executionId }) =>
45
+ [participantId, transportId ?? null, residentId ?? null, executionId ?? null]));
46
+
47
+ /** Validates one complete cost policy and returns a frozen copy; a missing or unknown member fails. */
48
+ export function validateRouteCostPolicy(policy) {
49
+ const fail = path => { throw new Error('Explicit route cost policy required: ' + path); };
50
+ if (!exact(policy, ['schema', 'id', 'unit', 'resolution', ...Object.keys(POLICY_GROUPS)])) fail('members');
51
+ if (policy.schema !== ROUTE_COST_POLICY_SCHEMA) fail('schema');
52
+ for (const key of ['id', 'unit']) if (typeof policy[key] !== 'string' || !policy[key].length || policy[key].length > 128) fail(key);
53
+ if (!finite(policy.resolution) || policy.resolution <= 0) fail('resolution');
54
+ for (const [group, keys] of Object.entries(POLICY_GROUPS)) {
55
+ if (!exact(policy[group], keys)) fail(group);
56
+ for (const key of keys) if (!finite(policy[group][key])) fail(group + '.' + key);
57
+ }
58
+ const { unknown, readiness, failure, reserve } = policy;
59
+ // Unmeasured computation is never free; transfer and opening priors may be zero only by declaration.
60
+ if (unknown.prefillMsPerToken <= 0) fail('unknown.prefillMsPerToken');
61
+ if (unknown.decodeMsPerToken <= 0) fail('unknown.decodeMsPerToken');
62
+ if (!Number.isSafeInteger(unknown.maxObservationAgeMs) || unknown.maxObservationAgeMs <= 0) fail('unknown.maxObservationAgeMs');
63
+ if (readiness.priorAttempts <= 0 || readiness.priorRefusals >= readiness.priorAttempts) fail('readiness');
64
+ if (failure.priorAttempts <= 0 || failure.priorFailures >= failure.priorAttempts) fail('failure');
65
+ if (!count(reserve.spareSlots)) fail('reserve.spareSlots');
66
+ return freeze(structuredClone(policy));
67
+ }
68
+
69
+ /** Bounded per (plan, participant, stage) estimates and per-route execution outcomes, measured by this
70
+ * coordinator. Latency and opening fold by exponential smoothing so a recovered or slowed contributor is
71
+ * reflected without unbounded history; admission and execution outcomes are counts of distinct attempts. */
72
+ export function createRouteObservations({ maxEntries, smoothing }) {
73
+ assert(Number.isSafeInteger(maxEntries) && maxEntries > 0 && finite(smoothing) && smoothing > 0 && smoothing <= 1,
74
+ 'Explicit observation limits required');
75
+ const rows = new Map(), routes = new Map();
76
+ const key = (planId, participantId, index) => JSON.stringify([planId, participantId, index]);
77
+ const routeKey = (planId, participants) => JSON.stringify([planId, ...participants]);
78
+ const fold = (previous, value) => value === null ? previous ?? null : previous == null ? value : previous + smoothing * (value - previous);
79
+ const touch = (map, id, value) => { map.delete(id); map.set(id, value); while (map.size > maxEntries) map.delete(map.keys().next().value); };
80
+ const row = (planId, stage) => rows.get(key(planId, stage.participantId, stage.index)) ?? { planId, participantId: stage.participantId,
81
+ index: stage.index, at: null, samples: 0, decodeMs: null, transferMs: null, prefillMsPerToken: null,
82
+ openingMs: null, openedAt: null, admissions: 0, refusals: 0 };
83
+ const bound = (planId, route, at) => assert(typeof planId === 'string' && Array.isArray(route?.stages) && route.stages.length > 0
84
+ && route.stages.every((stage, index) => stage?.index === index && typeof stage.participantId === 'string')
85
+ && Number.isSafeInteger(at), 'Observation binding required');
86
+ return Object.freeze({
87
+ /** Folds one completed attempt's ordered phases (partition-summary.js) into per-stage latency. */
88
+ record({ planId, route, phases, at }) {
89
+ bound(planId, route, at);
90
+ if (!phases?.prompt || !phases.decode) return 0;
91
+ let recorded = 0;
92
+ for (const stage of route.stages) {
93
+ const prompt = phases.prompt.stages?.find(item => item.partitionIndex === stage.index);
94
+ const decode = phases.decode.stages?.find(item => item.partitionIndex === stage.index);
95
+ const decodeMs = mean(decode?.elapsedMs), executionMs = mean(decode?.runtime?.[RUNTIME]);
96
+ const tokens = phases.prompt.inputTokens;
97
+ const prefillMsPerToken = finite(prompt?.elapsedMs) && Number.isSafeInteger(tokens) && tokens > 0 ? prompt.elapsedMs / tokens : null;
98
+ if (decodeMs === null && prefillMsPerToken === null) continue;
99
+ const previous = row(planId, stage);
100
+ touch(rows, key(planId, stage.participantId, stage.index), { ...previous, at, samples: previous.samples + 1,
101
+ decodeMs: fold(previous.decodeMs, decodeMs),
102
+ transferMs: fold(previous.transferMs, decodeMs !== null && executionMs !== null ? Math.max(decodeMs - executionMs, 0) : null),
103
+ prefillMsPerToken: fold(previous.prefillMsPerToken, prefillMsPerToken) });
104
+ recorded++;
105
+ }
106
+ return recorded;
107
+ },
108
+ /** One finished attempt: stages [0, admitted) admitted it; with `refused`, stage `admitted` refused it
109
+ * (readiness). `execution` is the outcome after complete admission, or null when cancelled or never
110
+ * fully admitted. Cancellation is never a refusal or failure. */
111
+ recordOutcome({ planId, route, admitted, refused, execution, at }) {
112
+ bound(planId, route, at);
113
+ const all = route.stages.length;
114
+ assert(Number.isSafeInteger(admitted) && admitted >= 0 && admitted <= all && typeof refused === 'boolean'
115
+ && (!refused || admitted < all) && [null, 'completed', 'failed'].includes(execution)
116
+ && (execution === null || admitted === all), 'Route outcome binding required');
117
+ route.stages.slice(0, admitted + (refused ? 1 : 0)).forEach((stage, position) => {
118
+ const previous = row(planId, stage);
119
+ touch(rows, key(planId, stage.participantId, stage.index), { ...previous, admissions: previous.admissions + 1,
120
+ refusals: previous.refusals + (position === admitted ? 1 : 0) });
121
+ });
122
+ if (execution === null) return;
123
+ const participants = route.stages.map(stage => stage.participantId), id = routeKey(planId, participants);
124
+ const previous = routes.get(id) ?? { planId, participants, at: null, executions: 0, failures: 0 };
125
+ touch(routes, id, { ...previous, at, executions: previous.executions + 1, failures: previous.failures + (execution === 'failed' ? 1 : 0) });
126
+ },
127
+ /** Measured time to open this coordinator's channel to a stage for a new route. */
128
+ recordOpening({ planId, stage, openingMs, at }) {
129
+ assert(typeof planId === 'string' && typeof stage?.participantId === 'string' && Number.isSafeInteger(stage.index)
130
+ && stage.index >= 0 && finite(openingMs) && Number.isSafeInteger(at), 'Opening observation required');
131
+ const previous = row(planId, stage);
132
+ touch(rows, key(planId, stage.participantId, stage.index), { ...previous, openingMs: fold(previous.openingMs, openingMs), openedAt: at });
133
+ },
134
+ get: (planId, participantId, index) => structuredClone(rows.get(key(planId, participantId, index)) ?? null),
135
+ getRoute: (planId, participants) => structuredClone(routes.get(routeKey(planId, participants)) ?? null),
136
+ snapshot: () => structuredClone([...rows.values()]),
137
+ snapshotRoutes: () => structuredClone([...routes.values()])
138
+ });
139
+ }
140
+
141
+ const fresh = (at, now, maxAge) => Number.isSafeInteger(at) && now - at <= maxAge;
142
+
143
+ /** Expected per-stage cost inputs for a new attempt on this candidate, each with its provenance. */
144
+ function stageInputs(candidate, observation, policy, workload, now) {
145
+ const { unknown, readiness, reserve } = policy, maxAge = unknown.maxObservationAgeMs;
146
+ const measuredLatency = observation?.samples > 0 && observation.decodeMs !== null && observation.prefillMsPerToken !== null;
147
+ const latencySource = !measuredLatency ? 'prior' : fresh(observation.at, now, maxAge) ? 'measured' : 'stale';
148
+ const measured = latencySource === 'measured';
149
+ // Measured decode includes the stage's inbound transfer; split only where its execution time was reported.
150
+ const split = measured && observation.transferMs !== null;
151
+ const prefillMsPerToken = measured ? observation.prefillMsPerToken : unknown.prefillMsPerToken;
152
+ const decodeMsPerToken = !measured ? unknown.decodeMsPerToken : split ? Math.max(observation.decodeMs - observation.transferMs, 0) : observation.decodeMs;
153
+ const transferMsPerToken = !measured ? unknown.transferMsPerToken : split ? observation.transferMs : null;
154
+ const latencyMs = prefillMsPerToken * workload.promptTokens + decodeMsPerToken * workload.expectedTokens;
155
+ const transferMs = (transferMsPerToken ?? 0) * workload.expectedTokens;
156
+ const { active, availableSlots } = candidate.capacity, queued = candidate.capacity.queued ?? 0;
157
+ const openingSource = candidate.local ? 'local' : observation?.openingMs == null ? 'prior'
158
+ : fresh(observation.openedAt, now, maxAge) ? 'measured' : 'stale';
159
+ const openingMs = candidate.local ? 0 : openingSource === 'measured' ? observation.openingMs : unknown.openingMs;
160
+ const admissions = observation?.admissions ?? 0, refusals = observation?.refusals ?? 0;
161
+ return { latencyMs, transferMs, queueingMs: (active + queued) * (latencyMs + transferMs), openingMs,
162
+ refusalProbability: (refusals + readiness.priorRefusals) / (admissions + readiness.priorAttempts),
163
+ missingSlots: Math.max(0, reserve.spareSlots - (availableSlots - 1)),
164
+ inputs: { index: candidate.index, participantId: candidate.participantId,
165
+ latency: { source: latencySource, samples: observation?.samples ?? 0, observedAt: observation?.at ?? null,
166
+ prefillMsPerToken, decodeMsPerToken, transfer: split ? 'measured' : measured ? 'within-decode' : latencySource, transferMsPerToken },
167
+ capacity: { active, queued, availableSlots }, opening: { source: openingSource, ms: openingMs },
168
+ admission: { admissions, refusals } } };
169
+ }
170
+
171
+ /** Ranks complete assignments of distinct participants to every stage by expected cost (see above).
172
+ * Full, closed or memory-short candidates are inadmissible; a stage without an admissible candidate
173
+ * yields no route, so a slow device is avoided only by another complete route, never by leaving
174
+ * layers uncovered. `open` lists stage identities of routes this coordinator already opened. */
175
+ export function rankPartitionRoutes({ planId, candidates, observations, workload, limits, policy, now, open = [] }) {
176
+ assert(typeof planId === 'string' && Array.isArray(candidates) && candidates.length > 0 && candidates.every(Array.isArray)
177
+ && typeof observations?.get === 'function' && typeof observations.getRoute === 'function' && Array.isArray(open), 'Route ranking input required');
178
+ const costs = validateRouteCostPolicy(policy);
179
+ assert(Number.isSafeInteger(now) && now >= 0, 'Route ranking clock required');
180
+ assert(Number.isSafeInteger(workload?.promptTokens) && workload.promptTokens > 0
181
+ && Number.isSafeInteger(workload.expectedTokens) && workload.expectedTokens > 0
182
+ && Number.isSafeInteger(workload.allocationBytes) && workload.allocationBytes >= 0, 'Explicit ranking workload required');
183
+ assert(Number.isSafeInteger(limits?.maxParticipants) && Number.isSafeInteger(limits.maxRoutes) && limits.maxRoutes > 0
184
+ && Number.isSafeInteger(limits.maxSelectionSteps) && limits.maxSelectionSteps > 0, 'Explicit ranking limits required');
185
+ const admitted = candidate => candidate.capacity.availableSlots !== 0 && candidate.capacity.closed !== true
186
+ && (candidate.memory == null || finite(candidate.memory.availableBytes) && candidate.memory.availableBytes >= workload.allocationBytes);
187
+ const rows = candidates.map((stage, index) => stage.map(candidate => {
188
+ assert(candidate.index === index && typeof candidate.participantId === 'string', 'Candidate stage mismatch');
189
+ assert(count(candidate.capacity?.active) && count(candidate.capacity.availableSlots)
190
+ && (candidate.capacity.queued === undefined || count(candidate.capacity.queued)), 'Candidate capacity required');
191
+ return structuredClone(candidate);
192
+ }).filter(admitted).map(candidate => ({ candidate, ...stageInputs(candidate, observations.get(planId, candidate.participantId, index), costs, workload, now) }))
193
+ .sort((a, b) => a.candidate.participantId < b.candidate.participantId ? -1 : a.candidate.participantId > b.candidate.participantId ? 1 : 0));
194
+ const participants = [...new Set(rows.flat().map(row => row.candidate.participantId))].sort();
195
+ assert(participants.length <= limits.maxParticipants && participants.length <= 30, 'Route ranking participant limit exceeded');
196
+ const bit = new Map(participants.map((id, index) => [id, 1 << index]));
197
+ const opened = new Set(open.map(stageKey));
198
+ const { prices } = costs, penalty = prices.refundPerFailure + prices.retryPerFailure;
199
+ function price(chosen) {
200
+ const stages = chosen.map(row => row.candidate), ids = stages.map(stage => stage.participantId);
201
+ const prepared = opened.has(stageKey(stages));
202
+ const sum = name => chosen.reduce((total, row) => total + row[name], 0);
203
+ const latencyMs = sum('latencyMs'), queueingMs = sum('queueingMs'), transferMs = sum('transferMs');
204
+ const preparationMs = prepared ? 0 : sum('openingMs'), reserveSlots = sum('missingSlots');
205
+ const admit = chosen.reduce((product, row) => product * (1 - row.refusalProbability), 1);
206
+ const outcome = observations.getRoute(planId, ids), executions = outcome?.executions ?? 0, failures = outcome?.failures ?? 0;
207
+ const failureProbability = (failures + costs.failure.priorFailures) / (executions + costs.failure.priorAttempts);
208
+ const attempts = 1 / (admit * (1 - failureProbability));
209
+ const service = latencyMs * prices.latencyPerMs + queueingMs * prices.queueingPerMs + transferMs * prices.transferPerMs;
210
+ const components = { latency: latencyMs * prices.latencyPerMs, queueing: queueingMs * prices.queueingPerMs,
211
+ transfer: transferMs * prices.transferPerMs, preparation: preparationMs * prices.preparationPerMs,
212
+ readiness: attempts * (1 - admit) * penalty, failure: attempts * admit * failureProbability * (service + penalty),
213
+ reserve: reserveSlots * costs.reserve.perMissingSlot };
214
+ const cost = COMPONENTS.reduce((total, name) => total + components[name], 0);
215
+ return { stages, participants: ids, cost, units: Math.round(cost / costs.resolution), prepared,
216
+ measured: chosen.every(row => row.inputs.latency.source === 'measured'), components,
217
+ expected: { latencyMs, queueingMs, transferMs, preparationMs, reserveSlots, refusalProbability: 1 - admit,
218
+ failureProbability, attemptsPerAnswer: attempts },
219
+ inputs: { stages: chosen.map(row => row.inputs), execution: { executions, failures } } };
220
+ }
221
+ // Enumerate distinct assignments stage by stage in participant order; the bound keeps work explicit.
222
+ const routes = [];
223
+ let steps = 0;
224
+ const walk = (index, used, chosen) => {
225
+ assert(++steps <= limits.maxSelectionSteps, ROUTE_RANKING_WORK_LIMIT);
226
+ if (index === rows.length) { routes.push(price(chosen)); return; }
227
+ for (const row of rows[index]) {
228
+ const mask = bit.get(row.candidate.participantId);
229
+ if (used & mask) continue;
230
+ walk(index + 1, used | mask, [...chosen, row]);
231
+ }
232
+ };
233
+ walk(0, 0, []);
234
+ // Ties compare participant ids stage by stage in code-unit order, like first-valid matching;
235
+ // locale collation would ignore separators and differ between hosts.
236
+ const byIds = (a, b) => { for (let i = 0; i < a.participants.length; i++) {
237
+ const x = a.participants[i], y = b.participants[i];
238
+ if (x !== y) return x < y ? -1 : 1;
239
+ } return 0; };
240
+ return freeze(routes.sort((a, b) => a.units - b.units || byIds(a, b)).slice(0, limits.maxRoutes));
241
+ }
242
+
243
+ /** New attempts move to a better-ranked route only when its expected cost is lower by the margin;
244
+ * attempts already running are never moved, and the current route is kept while any attempt on it is
245
+ * active. A current route that is no longer admissible (a full, closed or memory-short stage) takes no
246
+ * new attempts, so they go to the best admissible route; its active attempts stay where they are. */
247
+ export function preferRoute({ current, ranked, active, margin }) {
248
+ assert(finite(margin) && margin < 1 && Array.isArray(ranked), 'Explicit route preference margin required');
249
+ const best = ranked[0] ?? null;
250
+ if (!current || !best) return best ?? current ?? null;
251
+ const ids = route => JSON.stringify(route.stages.map(stage => stage.participantId));
252
+ const mine = ranked.find(route => ids(route) === ids(current));
253
+ if (!mine) return best;
254
+ if (active || ids(best) === ids(current)) return current;
255
+ return best.cost <= mine.cost * (1 - margin) ? best : current;
256
+ }
257
+
258
+ /** The auditable record of one ranking decision: the full policy, workload, the chosen route's inputs
259
+ * and cost breakdown, and the best `maxRoutes` alternatives. Unsigned coordinator observations. */
260
+ export function routeSelectionRecord({ policy, workload, ranked, chosen, reason, decidedAt, maxRoutes }) {
261
+ assert(typeof reason === 'string' && Number.isSafeInteger(decidedAt) && Number.isSafeInteger(maxRoutes) && maxRoutes > 0
262
+ && Array.isArray(ranked), 'Route selection record input required');
263
+ const summary = route => ({ participants: [...route.participants], cost: route.cost, units: route.units, measured: route.measured,
264
+ prepared: route.prepared, components: { ...route.components }, expected: { ...route.expected } });
265
+ return freeze(structuredClone({ schema: ROUTE_SELECTION_SCHEMA, policy: validateRouteCostPolicy(policy), reason, decidedAt,
266
+ workload: { promptTokens: workload.promptTokens, expectedTokens: workload.expectedTokens }, considered: ranked.length,
267
+ chosen: chosen ? { ...summary(chosen), inputs: chosen.inputs } : null, alternatives: ranked.slice(0, maxRoutes).map(summary) }));
268
+ }