reploid 0.0.0-stage → 0.1.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 (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 +534 -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 +83 -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,156 @@
1
+ /** Mechanical citation checks for one answer against its retrieval record. Parsing and mapping are exact;
2
+ * whether a source supports a claim is delegated to a host support checker and is never inferred here. */
3
+ import { sumMicroUsd } from './costs.js';
4
+ import { abortable } from '../cancellation.js';
5
+
6
+ export const CITATION_VERIFICATION_SCHEMA = 'reploid.citation-verification/v1';
7
+ export const SUPPORT_VERDICTS = Object.freeze(['supported', 'unsupported', 'unverified']);
8
+
9
+ const MARKER = /\[(\d{1,3}(?:\s*[,–-]\s*\d{1,3})*)\]/g;
10
+ const MARKERS_ONLY = /^(?:\s*\[(\d{1,3}(?:\s*[,–-]\s*\d{1,3})*)\])+\s*[.,;:!?]?\s*$/;
11
+ // A sentence ends at terminal punctuation followed by space or line end; markers after the period stay with it.
12
+ const SENTENCE = /\S[^]*?(?:[.!?]+["'’”)\]]*(?:\s*\[\d{1,3}(?:\s*[,–-]\s*\d{1,3})*\])*(?=\s|$)|$)/g;
13
+ const URL_TEXT = /\bhttps?:\/\/[^\s<>"'`\]\[)(]+/g;
14
+ // "Sources:", "**References**" and similar labels open a source list; "Sources disagree" does not.
15
+ const SOURCE_LABEL = /^\s*(?:\*\*|__)?(?:sources|references|citations)(?:\*\*|__)?\s*(?::(?:\*\*|__)?|$)/i;
16
+ const MAX_RANGE = 20;
17
+
18
+ function markerNumbers(group) {
19
+ return group.split(',').flatMap(part => {
20
+ const [from, to] = part.split(/[–-]/).map(value => Number(value.trim()));
21
+ if (to === undefined) return [from];
22
+ return to >= from && to - from <= MAX_RANGE ? Array.from({ length: to - from + 1 }, (_, index) => from + index) : [from, to];
23
+ });
24
+ }
25
+ const markersIn = text => [...new Set([...text.matchAll(MARKER)].flatMap(match => markerNumbers(match[1])))];
26
+ const withoutMarkers = text => text.replace(MARKER, '').replace(/\s+([.,;:!?])/g, '$1').replace(/\s+/g, ' ').trim();
27
+ const trimUrl = url => url.replace(/[.,;:!?]+$/, '');
28
+ const sameUrl = (a, b) => a === b || a.replace(/\/$/, '') === b.replace(/\/$/, '');
29
+
30
+ /**
31
+ * Splits an answer into claims (sentences outside code, headings and a trailing source list) with their
32
+ * citation markers, reference-list entries, and every URL the answer names.
33
+ */
34
+ export function parseCitations(answer) {
35
+ const claims = [], references = [], urls = [];
36
+ let offset = 0, fenced = false, referenceList = false;
37
+ for (const raw of String(answer).split('\n')) {
38
+ const start = offset; offset += raw.length + 1;
39
+ if (/^\s*(```|~~~)/.test(raw)) { fenced = !fenced; continue; }
40
+ if (fenced) continue;
41
+ for (const match of raw.matchAll(URL_TEXT)) urls.push({ url: trimUrl(match[0]), start: start + match.index });
42
+ if (!raw.trim()) continue;
43
+ if (/^\s{0,3}#{1,6}\s/.test(raw)) { referenceList = /^\s{0,3}#{1,6}\s+(sources|references|citations)\b/i.test(raw); continue; }
44
+ const listed = raw.match(/^(\s*(?:[-*+•]|\d{1,3}[.)])\s+)/)?.[1] || '';
45
+ const body = raw.slice(listed.length), opensWithMarker = /^\s*\[\d{1,3}\]/.test(body);
46
+ // A "Sources:" line starts a source list that continues through list items and marker-led lines.
47
+ // A marker-led line naming a URL is a reference entry anywhere.
48
+ const label = !listed && SOURCE_LABEL.test(raw);
49
+ if (label) referenceList = true;
50
+ else if (referenceList && !(listed || opensWithMarker)) referenceList = false;
51
+ if (referenceList || opensWithMarker && /\bhttps?:\/\//.test(body)) {
52
+ const text = (label ? raw.replace(SOURCE_LABEL, '') : body).trim();
53
+ if (text) references.push({ start, end: start + raw.length, markers: markersIn(text), text });
54
+ continue;
55
+ }
56
+ for (const match of body.matchAll(SENTENCE)) {
57
+ const sentence = match[0].trimEnd(), text = withoutMarkers(sentence);
58
+ const at = start + listed.length + match.index;
59
+ if (MARKERS_ONLY.test(sentence) && claims.length) {
60
+ // A marker group separated from its sentence belongs to the preceding claim.
61
+ const previous = claims.at(-1);
62
+ previous.markers = [...new Set([...previous.markers, ...markersIn(sentence)])]; previous.end = at + sentence.length;
63
+ continue;
64
+ }
65
+ if (!/[\p{L}\p{N}]/u.test(text)) continue;
66
+ const markers = markersIn(sentence);
67
+ if (!markers.length && /:\s*$/.test(text)) continue; // A lead-in such as "The sources say:" asserts nothing.
68
+ claims.push({ index: claims.length, start: at, end: at + sentence.length, text, markers });
69
+ }
70
+ }
71
+ return { claims, references, urls };
72
+ }
73
+
74
+ const unverified = reason => ({ verdict: 'unverified', reason });
75
+ const STOPPED = 'Support checking stopped before this citation.';
76
+ /** A checker that ignores its signal cannot hold the attempt past it. */
77
+ function untilStopped(operation, signal) {
78
+ if (!signal) return Promise.resolve().then(operation);
79
+ return abortable(operation, signal).catch(error => {
80
+ if (signal.aborted && error === signal.reason) return unverified(STOPPED);
81
+ throw error;
82
+ });
83
+ }
84
+ function judged(value) {
85
+ if (!value || !SUPPORT_VERDICTS.includes(value.verdict) || typeof value.reason !== 'string' || !value.reason.trim()) {
86
+ return unverified('The support checker returned an invalid result.');
87
+ }
88
+ return { verdict: value.verdict, reason: value.reason.trim().slice(0, 500) };
89
+ }
90
+
91
+ /**
92
+ * Maps every citation marker to the record's numbered sources, flags uncited claims, unresolved markers and URLs
93
+ * absent from the record, and asks the support checker about each resolved citation. Without a checker, or past
94
+ * the check limit, support is unverified. Nothing here grades the answer; flags are evidence for review.
95
+ */
96
+ export async function verifyCitations({ answer, record, checker = null, signal = null, maxSupportChecks = 256 }) {
97
+ const parsed = parseCitations(answer), byNumber = new Map((record?.sources || []).map(source => [source.number, source]));
98
+ const known = (record?.sources || []).map(source => source.url);
99
+ let checks = 0;
100
+ // Each check's cost as the checker reports it; unreported, failed or stopped checks are unknown, not free.
101
+ const checkCosts = [];
102
+ const support = async (claim, source) => {
103
+ if (!checker) return unverified('No support checker is configured.');
104
+ if (signal?.aborted) return unverified(STOPPED);
105
+ if (checks >= maxSupportChecks) return unverified('The support check limit was reached.');
106
+ checks++;
107
+ try {
108
+ const value = await untilStopped(() => checker.check({ claim, signal, source: { number: source.number,
109
+ sourceId: source.sourceId, title: source.title, url: source.url, passage: source.passage } }), signal);
110
+ checkCosts.push(Number.isSafeInteger(value?.costMicroUsd) && value.costMicroUsd >= 0 ? value.costMicroUsd : null);
111
+ return judged(value);
112
+ } catch (error) {
113
+ checkCosts.push(null);
114
+ return unverified(`The support checker failed: ${String(error?.message || error).slice(0, 200)}`);
115
+ }
116
+ };
117
+ const claims = [], flags = [];
118
+ for (const claim of parsed.claims) {
119
+ const citations = [];
120
+ for (const marker of claim.markers) {
121
+ const source = byNumber.get(marker);
122
+ if (!source) { citations.push({ marker, sourceId: null, resolved: false, support: null }); continue; }
123
+ citations.push({ marker, sourceId: source.sourceId, resolved: true, support: await support(claim.text, source) });
124
+ }
125
+ claims.push({ ...claim, cited: citations.length > 0, citations });
126
+ if (!citations.length) flags.push({ kind: 'uncited-claim', claim: claim.index });
127
+ for (const citation of citations) {
128
+ if (!citation.resolved) flags.push({ kind: 'unresolved-citation', claim: claim.index, marker: citation.marker });
129
+ else if (citation.support.verdict === 'unsupported') flags.push({ kind: 'unsupported-citation', claim: claim.index,
130
+ marker: citation.marker, reason: citation.support.reason });
131
+ else if (citation.support.verdict === 'unverified') flags.push({ kind: 'unverified-support', claim: claim.index, marker: citation.marker });
132
+ }
133
+ }
134
+ const references = parsed.references.map(entry => ({ ...entry, unresolved: entry.markers.filter(marker => !byNumber.has(marker)) }));
135
+ for (const entry of references) {
136
+ for (const marker of entry.unresolved) flags.push({ kind: 'unresolved-reference', marker });
137
+ // "[2] https://..." must name source 2's own URL, not another retrieved source's.
138
+ const source = entry.markers.length === 1 ? byNumber.get(entry.markers[0]) : null;
139
+ for (const match of source ? entry.text.matchAll(URL_TEXT) : []) {
140
+ if (!sameUrl(source.url, trimUrl(match[0]))) flags.push({ kind: 'mismatched-reference', marker: source.number, url: trimUrl(match[0]) });
141
+ }
142
+ }
143
+ const unrecordedUrls = [...new Set(parsed.urls.map(item => item.url).filter(url => !known.some(source => sameUrl(source, url))))];
144
+ for (const url of unrecordedUrls) flags.push({ kind: 'unrecorded-url', url });
145
+ const all = claims.flatMap(claim => claim.citations), verdict = name => all.filter(item => item.support?.verdict === name).length;
146
+ return {
147
+ schema: CITATION_VERIFICATION_SCHEMA,
148
+ checker: checker ? { id: checker.id, version: checker.version } : null,
149
+ usage: { checks, costMicroUsd: sumMicroUsd(checkCosts) },
150
+ claims, references, unrecordedUrls, flags,
151
+ summary: { claims: claims.length, citedClaims: claims.filter(claim => claim.cited).length,
152
+ uncitedClaims: claims.filter(claim => !claim.cited).length, citations: all.length,
153
+ unresolvedCitations: all.filter(item => !item.resolved).length, supported: verdict('supported'),
154
+ unsupported: verdict('unsupported'), unverified: verdict('unverified'), unrecordedUrls: unrecordedUrls.length }
155
+ };
156
+ }
@@ -0,0 +1,113 @@
1
+ /** Claim-to-span mapping and deterministic citation validity for one answer against its retrieval record.
2
+ * Everything here is mechanical: a valid citation names a fetched source whose retained passage still hashes to
3
+ * its record and contains the mapped span. Whether a span supports its claim is never decided here; only an
4
+ * independent reviewer (support-review.js) decides that, so every reviewable claim stays unverified until then. */
5
+ import defaults from './policy.json' with { type: 'json' };
6
+ import { parseCitations } from './citations.js';
7
+ import { renderSourceContext } from './sources.js';
8
+ import { SHA256, contentWords, sha256Text } from './text.js';
9
+
10
+ export const CLAIM_SUPPORT_SCHEMA = 'reploid.claim-support/v1';
11
+ export const SPAN_ALIGNMENT = 'reploid.lexical-span-alignment/v1';
12
+ /** Why a citation is not deterministically valid. */
13
+ export const CITATION_PROBLEMS = Object.freeze(['unresolved-marker', 'mismatched-reference', 'not-retrieved', 'passage-hash-mismatch',
14
+ 'document-offsets-invalid', 'context-digest-mismatch', 'span-invalid']);
15
+
16
+ const URL_TEXT = /\bhttps?:\/\/[^\s<>"'`\]\[)(]+/g;
17
+ const trimUrl = url => url.replace(/[.,;:!?]+$/, '');
18
+ const sameUrl = (a, b) => a === b || a.replace(/\/$/, '') === b.replace(/\/$/, '');
19
+ const webUrl = value => { try { return ['http:', 'https:'].includes(new URL(value).protocol); } catch { return false; } };
20
+
21
+ /** Sentence spans of a passage, as [start, end) offsets without surrounding whitespace. */
22
+ export function passageSentences(passage) {
23
+ const spans = [];
24
+ // A period ends a sentence only before whitespace or the end (after any closing quote), so 3.5 stays inside.
25
+ for (const match of String(passage).matchAll(/(?:[^\n.!?]|[.!?](?!["'’”)\]]*(?:\s|$)))*(?:[.!?]+["'’”)\]]*|\n|$)/g)) {
26
+ const raw = match[0], lead = raw.length - raw.trimStart().length, text = raw.trim();
27
+ if (text && /[\p{L}\p{N}]/u.test(text)) spans.push({ start: match.index + lead, end: match.index + lead + text.length });
28
+ if (!raw) break;
29
+ }
30
+ return spans;
31
+ }
32
+
33
+ /** The passage sentence sharing the most content words with the claim (earliest on ties); the whole passage when none does.
34
+ * This only points the reviewer at evidence; overlap is not support. */
35
+ export function alignClaimSpan(claim, passage) {
36
+ const words = new Set(contentWords(claim));
37
+ let best = null;
38
+ for (const span of passageSentences(passage)) {
39
+ const score = contentWords(passage.slice(span.start, span.end)).filter(word => words.has(word)).length;
40
+ if (score > (best?.score ?? 0)) best = { ...span, score };
41
+ }
42
+ return best ? { start: best.start, end: best.end, alignment: 'sentence' } : { start: 0, end: passage.length, alignment: 'whole-passage' };
43
+ }
44
+
45
+ /** A span is valid when it lies inside the passage, its text is exactly that slice and its hash is the slice's sha256. */
46
+ export async function validSpan(passage, span) {
47
+ return !!span && Number.isSafeInteger(span.start) && Number.isSafeInteger(span.end) && span.start >= 0 && span.end > span.start
48
+ && span.end <= passage.length && span.text === passage.slice(span.start, span.end)
49
+ && SHA256.test(span.sha256) && span.sha256 === await sha256Text(span.text);
50
+ }
51
+
52
+ /** Integrity of one retrieved source, independent of any claim. */
53
+ async function sourceProblems(record, source) {
54
+ const problems = [];
55
+ // The cited URL was fetched and extracted, not merely named by a search result.
56
+ if (record.outcome !== 'retrieved' || source.passageOrigin !== 'extract' || !webUrl(source.url)
57
+ || source.document && !webUrl(source.document.url)) problems.push('not-retrieved');
58
+ if (await sha256Text(source.passage) !== source.passageHash) problems.push('passage-hash-mismatch');
59
+ if (source.document && !(Number.isSafeInteger(source.document.start) && source.document.end - source.document.start === source.passage.length
60
+ && source.document.end <= source.document.characters)) problems.push('document-offsets-invalid');
61
+ return problems;
62
+ }
63
+
64
+ /**
65
+ * Maps every claim of `answer` to spans of the passages it cites and checks each citation deterministically:
66
+ * the marker resolves to a numbered source, any reference entry names that source's URL, the source was fetched,
67
+ * its passage still matches its hash and offsets, the numbered context still matches the digest that was sent, and
68
+ * the span is an exact, hashed slice of the passage. Uncited claims and claims citing only unknown markers are
69
+ * unsupported; claims with a valid citation await independent review; claims whose citations all fail integrity
70
+ * checks are unverified.
71
+ */
72
+ export async function mapClaimSpans({ answer, record, policy = defaults }) {
73
+ const parsed = parseCitations(answer), sources = record?.sources || [];
74
+ const byNumber = new Map(sources.map(source => [source.number, source]));
75
+ const contextValid = !!record?.context && sources.length > 0
76
+ && await sha256Text(renderSourceContext(sources, policy)) === record.context.digest;
77
+ const integrity = new Map();
78
+ for (const source of sources) integrity.set(source.number, [...await sourceProblems(record, source), ...(contextValid ? [] : ['context-digest-mismatch'])]);
79
+ // "[2] https://..." in a source list must name source 2's own URL.
80
+ const mismatched = new Set();
81
+ for (const entry of parsed.references) {
82
+ const source = entry.markers.length === 1 ? byNumber.get(entry.markers[0]) : null;
83
+ for (const match of source ? entry.text.matchAll(URL_TEXT) : []) if (!sameUrl(source.url, trimUrl(match[0]))) mismatched.add(source.number);
84
+ }
85
+ const claims = [];
86
+ for (const claim of parsed.claims) {
87
+ const citations = [];
88
+ for (const marker of claim.markers) {
89
+ const source = byNumber.get(marker);
90
+ if (!source) { citations.push({ marker, sourceId: null, url: null, valid: false, problems: ['unresolved-marker'], span: null }); continue; }
91
+ const aligned = alignClaimSpan(claim.text, source.passage), text = source.passage.slice(aligned.start, aligned.end);
92
+ const span = { start: aligned.start, end: aligned.end, text, sha256: await sha256Text(text), alignment: aligned.alignment };
93
+ const problems = [...integrity.get(marker), ...(mismatched.has(marker) ? ['mismatched-reference'] : []),
94
+ ...(await validSpan(source.passage, span) ? [] : ['span-invalid'])];
95
+ citations.push({ marker, sourceId: source.sourceId, url: source.url, passageHash: source.passageHash,
96
+ ...(source.document ? { document: { sha256: source.document.sha256, start: source.document.start + span.start,
97
+ end: source.document.start + span.end } } : {}), valid: !problems.length, problems, span });
98
+ }
99
+ const valid = citations.filter(citation => citation.valid);
100
+ const [verdict, reason] = !citations.length ? ['unsupported', 'uncited']
101
+ : citations.every(citation => citation.problems.includes('unresolved-marker')) ? ['unsupported', 'unresolved-citation']
102
+ : valid.length ? ['unverified', 'awaiting-review'] : ['unverified', 'invalid-evidence'];
103
+ claims.push({ id: `claim-${claim.index + 1}`, index: claim.index, text: claim.text, start: claim.start, end: claim.end,
104
+ markers: claim.markers, citations, reviewable: valid.length > 0, verdict, reason });
105
+ }
106
+ const all = claims.flatMap(claim => claim.citations), tally = name => claims.filter(claim => claim.verdict === name).length;
107
+ return { schema: CLAIM_SUPPORT_SCHEMA, alignment: SPAN_ALIGNMENT, query: record?.query ?? null,
108
+ contextDigest: record?.context?.digest ?? null, contextValid, claims,
109
+ summary: { claims: claims.length, citedClaims: claims.filter(claim => claim.citations.length).length,
110
+ reviewableClaims: claims.filter(claim => claim.reviewable).length, citations: all.length,
111
+ validCitations: all.filter(citation => citation.valid).length, invalidCitations: all.filter(citation => !citation.valid).length,
112
+ supported: tally('supported'), unsupported: tally('unsupported'), unverified: tally('unverified') } };
113
+ }
@@ -0,0 +1,101 @@
1
+ /** Per-answer costs of a search-backed answer, kept in its retrieval record and never in conversation text. Retrieval
2
+ * (search queries and page fetches), verification (support checks and entailment reviews) and fallback are separate
3
+ * entries so service economics can sum each one. Prices come only from explicit configuration reported by the
4
+ * providers and reviewers: an unpriced or unmeasured component is null, never zero, and a null makes every sum above
5
+ * it null. Zero means the work did not happen. Amounts are integer micro-USD. */
6
+
7
+ export const RETRIEVAL_COSTS_SCHEMA = 'reploid.retrieval-costs/v1';
8
+ /** Who pays a route's retrieval: the operator (a Reploid-paid provider) or the requester (their own device and tools). */
9
+ export const COST_BEARERS = Object.freeze(['operator', 'requester']);
10
+ /** No fallback route exists yet, so every record says so explicitly rather than omitting the entry. */
11
+ export const NO_FALLBACK = Object.freeze({ path: 'none', costMicroUsd: 0 });
12
+
13
+ const amount = value => Number.isSafeInteger(value) && value >= 0;
14
+ const known = value => amount(value) ? value : null;
15
+ /** The sum of the amounts, or null when any of them is unknown. */
16
+ export const sumMicroUsd = values => values.every(amount) ? values.reduce((total, value) => total + value, 0) : null;
17
+
18
+ /** A model's configured token prices, micro-USD per million tokens, or null when unpriced. */
19
+ export function validTokenPrice(price) {
20
+ return price === null || !!price && typeof price === 'object' && Object.keys(price).length === 2
21
+ && amount(price.inputMicroUsdPerMillionTokens) && amount(price.outputMicroUsdPerMillionTokens);
22
+ }
23
+ /** One model call's cost; null unless both token counts and the price are known. Rounded to the nearest micro-USD. */
24
+ export function tokenCostMicroUsd({ inputTokens, outputTokens, price }) {
25
+ if (!amount(inputTokens) || !amount(outputTokens) || !price || !validTokenPrice(price)) return null;
26
+ return Math.round((inputTokens * price.inputMicroUsdPerMillionTokens + outputTokens * price.outputMicroUsdPerMillionTokens) / 1e6);
27
+ }
28
+
29
+ const unknownSearch = service => ({ service, requests: null, costMicroUsd: null });
30
+ const UNKNOWN_EXTRACTION = Object.freeze({ attempted: null, documents: null, failed: null, bytes: null, costMicroUsd: null });
31
+ /** The retrieval entry of a record whose retrieval was not observed to completion (for example, interrupted by a reload). */
32
+ export const unknownRetrievalCosts = (bearer = null) => ({ bearer, search: unknownSearch(null), extraction: { ...UNKNOWN_EXTRACTION }, costMicroUsd: null });
33
+
34
+ /**
35
+ * The retrieval entry from what one retrieval observed. `meter`: whether the provider was called and the usage it
36
+ * reported; extraction attempts, failures, fetched bytes (null when an extractor did not report them) and each
37
+ * extracted passage's reported cost (null when unreported). A failed fetch may still have transferred bytes, so any
38
+ * failure leaves the extraction cost unknown.
39
+ */
40
+ export function retrievalCostEntry({ bearer = null, service = null, meter }) {
41
+ const search = !meter.searched ? { service: null, requests: 0, costMicroUsd: 0 }
42
+ : meter.usage ? { service: meter.usage.provider, requests: meter.usage.requests, costMicroUsd: known(meter.usage.costMicroUsd) }
43
+ : unknownSearch(service);
44
+ const extraction = { attempted: meter.attempted, documents: meter.documents, failed: meter.failed, bytes: meter.bytes,
45
+ costMicroUsd: meter.attempted === 0 ? 0 : meter.failed > 0 ? null : sumMicroUsd(meter.costs) };
46
+ return { bearer: COST_BEARERS.includes(bearer) ? bearer : null, search, extraction,
47
+ costMicroUsd: sumMicroUsd([search.costMicroUsd, extraction.costMicroUsd]) };
48
+ }
49
+
50
+ /** One review's cost: a model review from its reported tokens and configured price; a human review unpriced unless declared. */
51
+ export function reviewCostEntry(judgments) {
52
+ if (judgments?.schema !== 'reploid.claim-support-judgments/v1' || !judgments.reviewer) throw new Error('Invalid support judgments');
53
+ const base = { kind: judgments.reviewer.kind, packet: judgments.packet?.id ?? null, reviewer: judgments.reviewer.id };
54
+ if (judgments.reviewer.kind === 'model') {
55
+ const usage = judgments.usage ?? {}, price = validTokenPrice(usage.price ?? null) ? usage.price ?? null : null;
56
+ const inputTokens = known(usage.inputTokens), outputTokens = known(usage.outputTokens);
57
+ return { ...base, inputTokens, outputTokens, price: price && { ...price }, costMicroUsd: tokenCostMicroUsd({ inputTokens, outputTokens, price }) };
58
+ }
59
+ return amount(judgments.declaredCostMicroUsd) ? { ...base, pricing: 'declared', costMicroUsd: judgments.declaredCostMicroUsd }
60
+ : { ...base, pricing: 'unpriced', costMicroUsd: null };
61
+ }
62
+
63
+ /** The verification entry: the configured support checker's checks during verification, plus any entailment reviews. */
64
+ function verificationEntry(verification, reviews = []) {
65
+ const checker = verification?.checker ? { id: verification.checker.id, version: verification.checker.version,
66
+ checks: verification.usage?.checks ?? null, costMicroUsd: verification.usage ? known(verification.usage.costMicroUsd) : null } : null;
67
+ return { checker, reviews: structuredClone(reviews),
68
+ costMicroUsd: sumMicroUsd([...(checker ? [checker.costMicroUsd] : []), ...reviews.map(review => review.costMicroUsd)]) };
69
+ }
70
+ const NO_VERIFICATION = Object.freeze({ checker: null, reviews: [], costMicroUsd: 0 });
71
+
72
+ /** The complete cost record: three separate entries, their total, and the components whose cost is unknown. */
73
+ export function retrievalCostRecord({ retrieval, verification = NO_VERIFICATION, fallback = NO_FALLBACK }) {
74
+ const unknown = [
75
+ ...(retrieval.search.costMicroUsd === null ? ['retrieval.search'] : []),
76
+ ...(retrieval.extraction.costMicroUsd === null ? ['retrieval.extraction'] : []),
77
+ ...(verification.checker?.costMicroUsd === null ? ['verification.checker'] : []),
78
+ ...verification.reviews.filter(review => review.costMicroUsd === null).map(review => `verification.review:${review.kind}:${review.reviewer}`),
79
+ ...(fallback.costMicroUsd === null ? ['fallback'] : [])];
80
+ return { schema: RETRIEVAL_COSTS_SCHEMA, unit: 'micro-usd', retrieval: structuredClone(retrieval), verification: structuredClone(verification),
81
+ fallback: { ...fallback }, totalMicroUsd: sumMicroUsd([retrieval.costMicroUsd, verification.costMicroUsd, fallback.costMicroUsd]), unknown };
82
+ }
83
+
84
+ /** The record with its citation verification attached and the verification cost entry updated from it. */
85
+ export function withVerification(record, verification) {
86
+ const costs = record.costs;
87
+ return { ...record, verification, costs: retrievalCostRecord({ retrieval: costs?.retrieval ?? unknownRetrievalCosts(),
88
+ verification: verificationEntry(verification, costs?.verification?.reviews), fallback: costs?.fallback ?? NO_FALLBACK }) };
89
+ }
90
+
91
+ /** The record with entailment review costs added from judgment files; a reviewer's later file for the same packet replaces its earlier one. */
92
+ export function withReviewCosts(record, judgmentFiles) {
93
+ if (!Array.isArray(judgmentFiles)) throw new Error('Review cost needs judgment files');
94
+ const reviews = [...(record.costs?.verification?.reviews ?? [])];
95
+ for (const entry of judgmentFiles.map(reviewCostEntry)) {
96
+ const index = reviews.findIndex(item => item.kind === entry.kind && item.reviewer === entry.reviewer && item.packet === entry.packet);
97
+ if (index >= 0) reviews[index] = entry; else reviews.push(entry);
98
+ }
99
+ return { ...record, costs: retrievalCostRecord({ retrieval: record.costs?.retrieval ?? unknownRetrievalCosts(),
100
+ verification: verificationEntry(record.verification, reviews), fallback: record.costs?.fallback ?? NO_FALLBACK }) };
101
+ }
@@ -0,0 +1,34 @@
1
+ /** Deterministic search and extraction over fixed documents, for tests and harnesses. It makes no network
2
+ * request, needs no key and discloses the query to no one. It is not a search provider. */
3
+ import { contentWords, sha256Text, wordTokens } from './text.js';
4
+
5
+ const assert = (ok, message) => { if (!ok) throw new Error(message); };
6
+
7
+ export function createFixedSearchProvider({ documents, now = Date.now, id = 'reploid.fixed-documents', version = '1' }) {
8
+ assert(Array.isArray(documents) && documents.every(doc => doc && ['id', 'url', 'title', 'text'].every(key => typeof doc[key] === 'string' && doc[key]))
9
+ && new Set(documents.map(doc => doc.id)).size === documents.length, 'Fixed documents need unique id, url, title and text');
10
+ const fixed = documents.map(doc => Object.freeze({ ...doc, words: new Set(wordTokens(`${doc.title} ${doc.text}`)) }));
11
+ const hashes = new Map();
12
+ const hash = doc => { if (!hashes.has(doc.id)) hashes.set(doc.id, sha256Text(doc.text)); return hashes.get(doc.id); };
13
+ // The paragraph naming the most query words, first on ties.
14
+ const passage = (doc, terms) => doc.text.split(/\n\s*\n/).map(part => part.trim()).filter(Boolean)
15
+ .map((part, index) => ({ part, index, score: (words => terms.filter(term => words.has(term)).length)(new Set(wordTokens(part))) }))
16
+ .sort((a, b) => b.score - a.score || a.index - b.index)[0].part;
17
+ return Object.freeze({
18
+ id, version,
19
+ async search(query, limits, signal) {
20
+ signal?.throwIfAborted();
21
+ const terms = contentWords(query);
22
+ const ranked = fixed.map(doc => ({ doc, score: terms.filter(term => doc.words.has(term)).length }))
23
+ .filter(row => row.score > 0).sort((a, b) => b.score - a.score || a.doc.id.localeCompare(b.doc.id)).slice(0, limits.maxResults);
24
+ return Promise.all(ranked.map(async ({ doc }) => ({ sourceId: doc.id, url: doc.url, title: doc.title,
25
+ excerpt: passage(doc, terms), retrievedAt: now(), contentHash: await hash(doc) })));
26
+ },
27
+ async extract({ source, query, signal }) {
28
+ signal?.throwIfAborted();
29
+ const doc = fixed.find(item => item.id === source.sourceId);
30
+ assert(doc, 'Unknown fixed document');
31
+ return { sourceId: doc.id, url: doc.url, text: passage(doc, contentWords(query)), retrievedAt: now(), contentHash: await hash(doc) };
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,20 @@
1
+ export { createRetrieval } from './retrieval.js';
2
+ export { RETRIEVAL_RECORD_SCHEMA, RETRIEVAL_OUTCOMES, normalizeRetrievalLimits, normalizeRetrievalRequest, retrieveSources,
3
+ renderSourceContext, withSourceContext, retrievalFailureMessage, interruptRetrievalRecord, RETRIEVAL_SUPPLIES } from './sources.js';
4
+ export { CITATION_VERIFICATION_SCHEMA, SUPPORT_VERDICTS, parseCitations, verifyCitations } from './citations.js';
5
+ export { createLexicalSupportChecker } from './lexical-support.js';
6
+ export { createFixedSearchProvider } from './fixed-provider.js';
7
+ export { CLAIM_SUPPORT_SCHEMA, SPAN_ALIGNMENT, CITATION_PROBLEMS, mapClaimSpans, alignClaimSpan, passageSentences, validSpan } from './claim-support.js';
8
+ export { SUPPORT_REVIEW_PACKET_SCHEMA, SUPPORT_JUDGMENTS_SCHEMA, SUPPORT_RESULT_SCHEMA, REVIEW_VERDICTS, SUPPORT_REVIEW_INSTRUCTIONS,
9
+ createSupportReviewPacket, renderSupportReviewPacketMarkdown, applySupportJudgments, validateSupportJudgments,
10
+ assertIndependentReviewer, validateGenerator, supportPacketHash, canonicalJson } from './support-review.js';
11
+ export { REVIEW_QUEUE_SCHEMA, createHumanReviewQueue } from './review-queue.js';
12
+ export { MODEL_REVIEWER_SYSTEM, createModelSupportReviewer, renderModelReviewPrompt } from './model-reviewer.js';
13
+ export { RETRIEVAL_COSTS_SCHEMA, COST_BEARERS, NO_FALLBACK, sumMicroUsd, validTokenPrice, tokenCostMicroUsd, reviewCostEntry,
14
+ retrievalCostRecord, withVerification, withReviewCosts } from './costs.js';
15
+ export { decodeEntities, htmlFragmentText, detectCharset, extractDocumentText } from './page-text.js';
16
+ export { selectPassage } from './passage.js';
17
+ export { sha256Text } from './text.js';
18
+ import pageFetchPolicy from './page-fetch.json' with { type: 'json' };
19
+ /** Page fetch limits shared by the server fetcher and device-side extractors. */
20
+ export const PAGE_FETCH_POLICY = pageFetchPolicy;
@@ -0,0 +1,27 @@
1
+ /** Test-only support checker. It compares words, not meaning, and is not qualified to judge support.
2
+ * It misses paraphrase (false unsupported) and accepts reworded falsehoods that reuse the source's words
3
+ * (false supported); scope, attribution and most negation errors pass undetected. */
4
+ import { contentWords, wordTokens } from './text.js';
5
+
6
+ const NEGATIONS = new Set(['not', 'no', 'never', 'none', 'nor', 'without', "isn't", "aren't", "wasn't", "weren't", "doesn't",
7
+ "don't", "didn't", "cannot", "can't", "won't"]);
8
+ const numeric = token => /^\p{N}/u.test(token);
9
+
10
+ export function createLexicalSupportChecker({ supportedAt = 0.8, unsupportedBelow = 0.5 } = {}) {
11
+ if (!(unsupportedBelow > 0 && unsupportedBelow <= supportedAt && supportedAt <= 1)) throw new Error('Invalid lexical support thresholds');
12
+ return Object.freeze({
13
+ id: 'reploid.lexical-overlap-test', version: '1', qualified: false,
14
+ async check({ claim, source }) {
15
+ const words = contentWords(claim), passage = new Set(wordTokens(source.passage));
16
+ if (!words.length) return { verdict: 'unverified', reason: 'The claim has no checkable words.' };
17
+ const missingNumbers = words.filter(word => numeric(word) && !passage.has(word));
18
+ if (missingNumbers.length) return { verdict: 'unsupported', reason: `The source does not contain ${missingNumbers.join(', ')}.` };
19
+ const negation = words.find(word => NEGATIONS.has(word) && !passage.has(word));
20
+ if (negation) return { verdict: 'unverified', reason: `The claim says "${negation}" and the source does not.` };
21
+ const found = words.filter(word => passage.has(word)).length, share = found / words.length;
22
+ const reason = `${found} of ${words.length} claim words appear in the source.`;
23
+ if (share >= supportedAt) return { verdict: 'supported', reason };
24
+ return { verdict: share < unsupportedBelow ? 'unsupported' : 'unverified', reason };
25
+ }
26
+ });
27
+ }
@@ -0,0 +1,67 @@
1
+ /** A stronger-model entailment reviewer. Off unless a host configures it: the host names the reviewer's provider and
2
+ * model, the generating models it is declared stronger than, and a `complete` port that reaches that model.
3
+ * Sending a packet to it discloses the question, the answer and the cited passages to that provider; this is a
4
+ * separate disclosure from search and from execution contributors, so hosts use it only for evaluation material
5
+ * they may send there. It never reviews an answer from its own model or from a model it is not declared stronger than. */
6
+ import { SUPPORT_JUDGMENTS_SCHEMA, assertIndependentReviewer, supportPacketHash, validateGenerator } from './support-review.js';
7
+ import { tokenCostMicroUsd, validTokenPrice } from './costs.js';
8
+
9
+ const assert = (ok, message) => { if (!ok) throw new Error(message); };
10
+ const named = value => typeof value === 'string' && value.trim() && value.length <= 256;
11
+ const normalized = value => String(value).toLowerCase().replace(/[^a-z0-9]/g, '');
12
+
13
+ export const MODEL_REVIEWER_SYSTEM = 'You review whether cited source passages support claims in an answer. You judge only against the recorded passages, never your own knowledge. You reply with JSON only.';
14
+
15
+ export function renderModelReviewPrompt(packet) {
16
+ return [...packet.instructions, '',
17
+ 'Reply with one JSON object: {"judgments":[{"claim":"<claim id>","verdict":"supported|unsupported|unverified","basis":"<one or two sentences>","spans":[{"source":<number>,"quote":"<exact text copied from that passage>"}]}]}.',
18
+ 'Include every claim id once. For supported, quote the exact passage text you relied on; quotes must match the passage character for character.', '',
19
+ `Question:\n${packet.question}`, '', `Answer:\n${packet.answer}`, '',
20
+ 'Sources:', ...packet.sources.map(source => `[${source.number}] ${source.title}\n${source.url}\n<<<\n${source.passage}\n>>>`), '',
21
+ 'Claims:', ...packet.claims.map(claim => `${claim.id}: ${claim.text}\n cites: ${claim.citations.map(citation =>
22
+ `[${citation.source}] suggested span "${citation.suggestedSpan.text}"`).join('; ')}`)].join('\n');
23
+ }
24
+
25
+ function parseJudgments(reply) {
26
+ const body = String(reply), start = body.indexOf('{'), end = body.lastIndexOf('}');
27
+ assert(start >= 0 && end > start, 'The reviewer model returned no JSON judgments');
28
+ const parsed = JSON.parse(body.slice(start, end + 1));
29
+ assert(Array.isArray(parsed?.judgments), 'The reviewer model returned no judgments list');
30
+ return parsed.judgments.map(item => ({ claim: item?.claim, verdict: item?.verdict, basis: item?.basis,
31
+ spans: Array.isArray(item?.spans) ? item.spans.map(span => ({ source: span?.source, quote: span?.quote })) : [] }));
32
+ }
33
+
34
+ const tokens = value => Number.isSafeInteger(value) && value >= 0 ? value : null;
35
+
36
+ /**
37
+ * `identity`: `{ provider, model, version?, identity? }` of the reviewer. `strongerThan`: generating model ids or
38
+ * content identities this reviewer is explicitly declared stronger than. `complete({ system, prompt, signal })`
39
+ * returns the model's text reply, or `{ text, usage: { inputTokens, outputTokens } }` when the endpoint reports tokens.
40
+ * `price`: the configured `{ inputMicroUsdPerMillionTokens, outputMicroUsdPerMillionTokens }`, or null when unpriced.
41
+ * Each judgment file carries the review's token usage and cost (null when tokens or price are unknown).
42
+ */
43
+ export function createModelSupportReviewer({ identity, strongerThan, complete, price = null, maxReplyCharacters = 200000 } = {}) {
44
+ assert(identity && named(identity.provider) && named(identity.model), 'A model reviewer is off unless its provider and model are configured');
45
+ assert(Array.isArray(strongerThan) && strongerThan.length && strongerThan.every(named),
46
+ 'A model reviewer must name the generating models it is stronger than');
47
+ assert(typeof complete === 'function', 'A model reviewer needs a completion port');
48
+ assert(validTokenPrice(price), 'A model reviewer price is null or both token prices in micro-USD per million tokens');
49
+ const reviewer = Object.freeze({ kind: 'model', id: `${identity.provider}/${identity.model}`,
50
+ model: { provider: identity.provider, name: identity.model, version: identity.version ?? null, ...(identity.identity ? { identity: identity.identity } : {}) } });
51
+ return Object.freeze({
52
+ id: 'reploid.model-support-reviewer', version: '1', kind: 'model', reviewer,
53
+ async review(packet, { generator, signal = null } = {}) {
54
+ const made = validateGenerator(generator);
55
+ assertIndependentReviewer(reviewer, made);
56
+ assert(strongerThan.some(value => value === made.modelIdentity || normalized(value) === normalized(made.modelId)),
57
+ 'This reviewer is not configured as stronger than the generating model');
58
+ const returned = await complete({ system: MODEL_REVIEWER_SYSTEM, prompt: renderModelReviewPrompt(packet), signal });
59
+ const reply = typeof returned === 'string' ? returned : returned?.text;
60
+ assert(typeof reply === 'string' && reply.length <= maxReplyCharacters, 'The reviewer model reply is missing or too long');
61
+ const inputTokens = tokens(returned?.usage?.inputTokens), outputTokens = tokens(returned?.usage?.outputTokens);
62
+ return { schema: SUPPORT_JUDGMENTS_SCHEMA, packet: { id: packet.id, sha256: await supportPacketHash(packet) },
63
+ reviewer: structuredClone(reviewer), judgments: parseJudgments(reply),
64
+ usage: { inputTokens, outputTokens, price: price && { ...price }, costMicroUsd: tokenCostMicroUsd({ inputTokens, outputTokens, price }) } };
65
+ }
66
+ });
67
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "reploid.page-fetch-policy",
3
+ "version": "1.0.0",
4
+ "maxBytes": 2000000,
5
+ "timeoutMs": 8000,
6
+ "maxRedirects": 3,
7
+ "maxUrlCharacters": 2048,
8
+ "contentTypes": ["text/html", "application/xhtml+xml", "text/plain"],
9
+ "unsupportedContentTypes": ["application/pdf"]
10
+ }