2sagaco48 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/diagnostics.mjs +141 -0
  2. package/dist/compute.worker-j5ahIpF-.js +1 -0
  3. package/dist/graph.worker-DG6iGCB9.js +1 -0
  4. package/dist/index.html +2314 -0
  5. package/flatten-guide.md +248 -0
  6. package/index.html +12 -0
  7. package/package.json +38 -0
  8. package/src/App.orig.tsx +204 -0
  9. package/src/App.orig1.tsx +27 -0
  10. package/src/App.orig2.tsx +25 -0
  11. package/src/App.tsx +3 -0
  12. package/src/BaseApp.tsx +18 -0
  13. package/src/INTEGRATION_LEDGER.md +22 -0
  14. package/src/PERSIST_CANARY.orig.txt +1 -0
  15. package/src/PERSIST_CANARY.txt +1 -0
  16. package/src/REGRESSION_LEDGER.md +651 -0
  17. package/src/TEMPLATE_PIPELINE_AUDIT.md +208 -0
  18. package/src/WILLIAMS_PERSONA_GUIDE.md +62 -0
  19. package/src/chat/VeritasChat.tsx +69 -0
  20. package/src/chat/synthesis.ts +31 -0
  21. package/src/chat/tier.ts +45 -0
  22. package/src/components/AdaptersPage.orig.tsx +99 -0
  23. package/src/components/AdaptersPage.tsx +3 -0
  24. package/src/components/AdversarialPanel.tsx +136 -0
  25. package/src/components/AnchorBaselinePanel.tsx +75 -0
  26. package/src/components/AnswerPanel.tsx +60 -0
  27. package/src/components/ArtifactPanel.tsx +43 -0
  28. package/src/components/ChatApp.orig.tsx +1582 -0
  29. package/src/components/ChatApp.orig1.tsx +722 -0
  30. package/src/components/ChatApp.tsx +95 -0
  31. package/src/components/ChatAugmentPanels.tsx +73 -0
  32. package/src/components/CitationLedgerPanel.tsx +313 -0
  33. package/src/components/ComputeSandboxPanel.tsx +130 -0
  34. package/src/components/ConfigPanel.tsx +110 -0
  35. package/src/components/ControlPlanePage.tsx +195 -0
  36. package/src/components/CreativeTreeLifePage.tsx +234 -0
  37. package/src/components/CreativeTreeOfLifePanel.tsx +149 -0
  38. package/src/components/DebugTracePanel.tsx +137 -0
  39. package/src/components/DeepReasoningTrace.tsx +48 -0
  40. package/src/components/EntitySheetPanel.tsx +51 -0
  41. package/src/components/FailureModesPanel.tsx +104 -0
  42. package/src/components/GBSDashboard.tsx +239 -0
  43. package/src/components/GraphView.tsx +116 -0
  44. package/src/components/HUD.tsx +125 -0
  45. package/src/components/HypothesisPanel.tsx +67 -0
  46. package/src/components/InnovationGenomeEngine.tsx +792 -0
  47. package/src/components/InnovationPersonaGuide.tsx +348 -0
  48. package/src/components/InnovationPersonaPanel.tsx +289 -0
  49. package/src/components/LiveResourceHUD.tsx +109 -0
  50. package/src/components/LongWriterPanel.tsx +112 -0
  51. package/src/components/MainPipelineV10Bridge.tsx +72 -0
  52. package/src/components/MarkdownLite.tsx +63 -0
  53. package/src/components/MemoryInspector.tsx +38 -0
  54. package/src/components/MemoryMonitor.tsx +52 -0
  55. package/src/components/MemoryStressPanel.tsx +176 -0
  56. package/src/components/ModulesPage.tsx +171 -0
  57. package/src/components/PipelineDebugConsole.orig.tsx +1856 -0
  58. package/src/components/PipelineDebugConsole.tsx +3 -0
  59. package/src/components/PreFlightHUD.tsx +67 -0
  60. package/src/components/PrismaFetchTracePanel.tsx +87 -0
  61. package/src/components/ReportOSPanel.tsx +165 -0
  62. package/src/components/ResourceEstimatorPage.tsx +327 -0
  63. package/src/components/RichText.tsx +181 -0
  64. package/src/components/SagaOsPanel.tsx +605 -0
  65. package/src/components/SagaOsReference.tsx +421 -0
  66. package/src/components/SagaOsV2Panel.tsx +594 -0
  67. package/src/components/SagaStyleTab.tsx +382 -0
  68. package/src/components/SageBlueprintPanel.tsx +416 -0
  69. package/src/components/SharedChatInput.tsx +77 -0
  70. package/src/components/StatFinancePanel.tsx +190 -0
  71. package/src/components/StylePersonaPanel.tsx +128 -0
  72. package/src/components/TemplatesPage.tsx +300 -0
  73. package/src/components/TestPanel.tsx +63 -0
  74. package/src/components/TraceLog.tsx +89 -0
  75. package/src/components/V15CalibrationAugment.tsx +1134 -0
  76. package/src/components/V15CalibrationDialog.tsx +1871 -0
  77. package/src/components/V15Overlay.orig.tsx +120 -0
  78. package/src/components/V15Overlay.tsx +4 -0
  79. package/src/components/V15OverlayWrapper.tsx +362 -0
  80. package/src/components/V15Toggle.tsx +34 -0
  81. package/src/console/DiagnosticRecoveryPanel.tsx +358 -0
  82. package/src/console/EngineeringConsole.tsx +103 -0
  83. package/src/console/FictionSystemPanels.tsx +882 -0
  84. package/src/console/InspectorPanels.tsx +254 -0
  85. package/src/console/ManuscriptGraderPanel.tsx +182 -0
  86. package/src/console/RecursiveReviserPanel.tsx +245 -0
  87. package/src/console/SeedControlPanel.tsx +135 -0
  88. package/src/console/StoryControllerPanel.tsx +443 -0
  89. package/src/console/engineBridge.ts +258 -0
  90. package/src/console/gemini.ts +106 -0
  91. package/src/console/grading.ts +250 -0
  92. package/src/console/util.ts +45 -0
  93. package/src/fiction/continuity.ts +372 -0
  94. package/src/fiction/crash-protection.ts +111 -0
  95. package/src/fiction/diagnostics.ts +208 -0
  96. package/src/fiction/diff.ts +74 -0
  97. package/src/fiction/entity-state-machine.ts +214 -0
  98. package/src/fiction/nive-engine.ts +404 -0
  99. package/src/fiction/pipeline-selftest.ts +132 -0
  100. package/src/fiction/seed-control.ts +193 -0
  101. package/src/fiction/story-engine.ts +326 -0
  102. package/src/fiction/types.ts +68 -0
  103. package/src/flatten-guide.md +379 -0
  104. package/src/index.css +14 -0
  105. package/src/index.orig.css +13 -0
  106. package/src/lib/academic-sources.base.ts +188 -0
  107. package/src/lib/academic-sources.orig.ts +28 -0
  108. package/src/lib/academic-sources.ts +10 -0
  109. package/src/lib/advanced-math.ts +119 -0
  110. package/src/lib/adversarial-engine.base.ts +409 -0
  111. package/src/lib/adversarial-engine.ts +82 -0
  112. package/src/lib/ai-revision.ts +623 -0
  113. package/src/lib/app-state.tsx +426 -0
  114. package/src/lib/artifact-registry.ts +97 -0
  115. package/src/lib/artifacts.ts +180 -0
  116. package/src/lib/atlas-dr.ts +167 -0
  117. package/src/lib/browser-mtls.ts +127 -0
  118. package/src/lib/browser-queue.ts +221 -0
  119. package/src/lib/browser-search-scraper.base.ts +382 -0
  120. package/src/lib/browser-search-scraper.orig.ts +90 -0
  121. package/src/lib/browser-search-scraper.ts +10 -0
  122. package/src/lib/calc-interceptor.ts +82 -0
  123. package/src/lib/citation-lane-tap.ts +78 -0
  124. package/src/lib/citation-ledger-store.ts +369 -0
  125. package/src/lib/citation-ledger.ts +203 -0
  126. package/src/lib/compute-sandbox.ts +215 -0
  127. package/src/lib/compute.worker.ts +153 -0
  128. package/src/lib/connectors/gemini.ts +120 -0
  129. package/src/lib/connectors/jina.ts +233 -0
  130. package/src/lib/connectors/marketdata.ts +51 -0
  131. package/src/lib/connectors/prismafetch.base.ts +175 -0
  132. package/src/lib/connectors/prismafetch.ts +1 -0
  133. package/src/lib/connectors/serpapi.ts +65 -0
  134. package/src/lib/connectors/wikidata.ts +44 -0
  135. package/src/lib/constraints.ts +331 -0
  136. package/src/lib/continuation-detector.base.ts +153 -0
  137. package/src/lib/continuation-detector.ts +48 -0
  138. package/src/lib/contradraft.ts +168 -0
  139. package/src/lib/cors-proxy.ts +120 -0
  140. package/src/lib/coverage.ts +81 -0
  141. package/src/lib/debug/architecture-prescription.ts +556 -0
  142. package/src/lib/debug/covea-repair.ts +543 -0
  143. package/src/lib/debug/helios-ground.ts +319 -0
  144. package/src/lib/debug/intent-decomposer.ts +281 -0
  145. package/src/lib/debug/intent-lattice.ts +349 -0
  146. package/src/lib/debug/pipeline-diagnosis.ts +1149 -0
  147. package/src/lib/debug/pipeline-trace-bus.ts +424 -0
  148. package/src/lib/debug/prompt-forge.ts +678 -0
  149. package/src/lib/debug/repair-sites.ts +513 -0
  150. package/src/lib/debug/research-phase.ts +266 -0
  151. package/src/lib/debug/retrieval-context.ts +76 -0
  152. package/src/lib/debug/retrieval-hardener.ts +142 -0
  153. package/src/lib/debug/scraper-debug-runner.ts +576 -0
  154. package/src/lib/debug/scraper-forensics.ts +418 -0
  155. package/src/lib/debug/scraper-lane-roadmap.ts +136 -0
  156. package/src/lib/debug/self-test.ts +415 -0
  157. package/src/lib/debug/step-attribution.ts +211 -0
  158. package/src/lib/debug/template-rubric.ts +301 -0
  159. package/src/lib/debug/unified-innovation.ts +126 -0
  160. package/src/lib/debug/veritas-hybrid-scraper.ts +335 -0
  161. package/src/lib/defense-registry.ts +104 -0
  162. package/src/lib/defenses.ts +162 -0
  163. package/src/lib/deterministic-citation-ledger.ts +174 -0
  164. package/src/lib/elo-registry.base.ts +117 -0
  165. package/src/lib/elo-registry.ts +1 -0
  166. package/src/lib/entity-resolver.ts +291 -0
  167. package/src/lib/failure-modes.ts +267 -0
  168. package/src/lib/feature-registry.ts +69 -0
  169. package/src/lib/flaw-registry.ts +143 -0
  170. package/src/lib/flaws/_template.ts +52 -0
  171. package/src/lib/flaws/builtins.ts +63 -0
  172. package/src/lib/flaws/finance.ts +169 -0
  173. package/src/lib/flaws/fixers.ts +10 -0
  174. package/src/lib/flaws/index.ts +32 -0
  175. package/src/lib/flaws/legal.ts +157 -0
  176. package/src/lib/flaws/medical.ts +188 -0
  177. package/src/lib/flaws/original-defenses-pack.ts +81 -0
  178. package/src/lib/flaws/sample-declarative-pack.json +38 -0
  179. package/src/lib/flaws/selftest.ts +201 -0
  180. package/src/lib/flaws/software-extended.ts +125 -0
  181. package/src/lib/flaws/software-rn-webgl.ts +182 -0
  182. package/src/lib/flaws/statistics-advanced.ts +46 -0
  183. package/src/lib/flaws/statistics.ts +61 -0
  184. package/src/lib/gbse/config.ts +20 -0
  185. package/src/lib/gbse/engine.ts +226 -0
  186. package/src/lib/gbse/graph.ts +118 -0
  187. package/src/lib/gbse/graph.worker.ts +21 -0
  188. package/src/lib/gbse/tests.ts +117 -0
  189. package/src/lib/gbse/tiers.ts +65 -0
  190. package/src/lib/gbse/types.ts +94 -0
  191. package/src/lib/innovation-genome-engine-v2.orig.ts +1001 -0
  192. package/src/lib/innovation-genome-engine-v2.ts +3 -0
  193. package/src/lib/innovation-genome-engine.orig.ts +330 -0
  194. package/src/lib/innovation-genome-engine.ts +3 -0
  195. package/src/lib/innovation-genome-v10.orig.ts +314 -0
  196. package/src/lib/innovation-genome-v10.ts +2 -0
  197. package/src/lib/innovation-genome-v3.orig.ts +2015 -0
  198. package/src/lib/innovation-genome-v3.ts +2 -0
  199. package/src/lib/innovation-genome-v4.orig.ts +1959 -0
  200. package/src/lib/innovation-genome-v4.ts +2 -0
  201. package/src/lib/innovation-genome-v5.orig.ts +1210 -0
  202. package/src/lib/innovation-genome-v5.ts +2 -0
  203. package/src/lib/innovation-genome-v7.orig.ts +2549 -0
  204. package/src/lib/innovation-genome-v7.ts +2 -0
  205. package/src/lib/innovation-genome-v8.orig.ts +1485 -0
  206. package/src/lib/innovation-genome-v8.ts +2 -0
  207. package/src/lib/innovation-genome-v9.orig.ts +643 -0
  208. package/src/lib/innovation-genome-v9.ts +2 -0
  209. package/src/lib/jina.base.ts +293 -0
  210. package/src/lib/jina.ts +1 -0
  211. package/src/lib/live-telemetry.ts +61 -0
  212. package/src/lib/longwriter.ts +84 -0
  213. package/src/lib/manuscript-grade-v2.ts +298 -0
  214. package/src/lib/memory-governor.ts +173 -0
  215. package/src/lib/memory-stress-tests.ts +248 -0
  216. package/src/lib/model-intelligence.ts +48 -0
  217. package/src/lib/model-rotator.ts +75 -0
  218. package/src/lib/models.orig.ts +513 -0
  219. package/src/lib/models.ts +151 -0
  220. package/src/lib/n-deep.base.ts +592 -0
  221. package/src/lib/n-deep.ts +51 -0
  222. package/src/lib/nih-simulator.ts +56 -0
  223. package/src/lib/nih-vulnerability-fixes.ts +460 -0
  224. package/src/lib/omega-templates.base.ts +294 -0
  225. package/src/lib/omega-templates.ts +225 -0
  226. package/src/lib/omni-nexus.ts +64 -0
  227. package/src/lib/oracle-adapters.ts +67 -0
  228. package/src/lib/oracle-registry.ts +163 -0
  229. package/src/lib/orchestrator.ts +357 -0
  230. package/src/lib/output-boundary.ts +235 -0
  231. package/src/lib/overrides/vite-native-chaos-harness.ts +154 -0
  232. package/src/lib/overrides/vite-native-contract-plane.ts +245 -0
  233. package/src/lib/overrides/vite-native-doctor-plane.ts +208 -0
  234. package/src/lib/overrides/vite-native-knowledge-store.ts +267 -0
  235. package/src/lib/overrides/vite-native-policy-plane.ts +190 -0
  236. package/src/lib/overrides/vite-native-replay-plane.ts +197 -0
  237. package/src/lib/overrides/vite-native-runtime-plane.ts +106 -0
  238. package/src/lib/overrides/vite-native-scraper.ts +848 -0
  239. package/src/lib/overrides/vite-native-selftest-functional.ts +217 -0
  240. package/src/lib/overrides/vite-native-selftest.ts +183 -0
  241. package/src/lib/overrides/vite-native-snapshot-plane.ts +123 -0
  242. package/src/lib/philosophy-toolkit.ts +75 -0
  243. package/src/lib/pipeline.orig.ts +659 -0
  244. package/src/lib/pipeline.ts +42 -0
  245. package/src/lib/precache.ts +128 -0
  246. package/src/lib/py-sandbox.ts +205 -0
  247. package/src/lib/quality-score.ts +96 -0
  248. package/src/lib/quant-engine.ts +231 -0
  249. package/src/lib/quant-lib.ts +80 -0
  250. package/src/lib/reportos.ts +175 -0
  251. package/src/lib/research-os.ts +306 -0
  252. package/src/lib/resource-estimator.ts +114 -0
  253. package/src/lib/rpm-governor.ts +93 -0
  254. package/src/lib/runtime-estimator.ts +135 -0
  255. package/src/lib/sage/empty-space-v6.ts +560 -0
  256. package/src/lib/sage/empty-space-v7.ts +222 -0
  257. package/src/lib/sage/empty-space.ts +733 -0
  258. package/src/lib/sage/engine.ts +786 -0
  259. package/src/lib/sage/index.ts +17 -0
  260. package/src/lib/sage/judge.ts +246 -0
  261. package/src/lib/sage/prng.ts +53 -0
  262. package/src/lib/sage/reality-grounding.ts +380 -0
  263. package/src/lib/sage/repetition.ts +151 -0
  264. package/src/lib/sage/saga-os-v2-engines.ts +1870 -0
  265. package/src/lib/sage/saga-os-v2.ts +274 -0
  266. package/src/lib/sage/saga-os-v3-core.ts +1491 -0
  267. package/src/lib/sage/saga-os-v3-release.ts +739 -0
  268. package/src/lib/sage/saga-os-v4-aureate.ts +1161 -0
  269. package/src/lib/sage/saga-os-v4.ts +212 -0
  270. package/src/lib/sage/saga-os-v5.ts +205 -0
  271. package/src/lib/sage/saga-os-v6.ts +346 -0
  272. package/src/lib/sage/saga-os-v7.ts +314 -0
  273. package/src/lib/sage/saga-os-v8.ts +505 -0
  274. package/src/lib/sage/saga-os.ts +329 -0
  275. package/src/lib/sage/taxonomy.ts +403 -0
  276. package/src/lib/sage/unified-roll.ts +524 -0
  277. package/src/lib/sage/worked-sketches.ts +44 -0
  278. package/src/lib/sage.ts +166 -0
  279. package/src/lib/scraper-debug-bus.ts +53 -0
  280. package/src/lib/scraper-enhanced.orig.ts +153 -0
  281. package/src/lib/scraper-enhanced.ts +10 -0
  282. package/src/lib/scraper-hardener.base.orig.ts +155 -0
  283. package/src/lib/scraper-hardener.base.ts +317 -0
  284. package/src/lib/scraper-hardener.ts +1 -0
  285. package/src/lib/scraper-palisade/palisade-adjudicator.orig.ts +419 -0
  286. package/src/lib/scraper-palisade/palisade-adjudicator.ts +13 -0
  287. package/src/lib/scraper-vnext/arbiter-omega.orig.ts +688 -0
  288. package/src/lib/scraper-vnext/arbiter-omega.ts +13 -0
  289. package/src/lib/scraper-vnext/canonical-portfolio-augments.ts +479 -0
  290. package/src/lib/scraper-vnext/canonical-portfolio-orchestrator.orig.ts +1529 -0
  291. package/src/lib/scraper-vnext/canonical-portfolio-orchestrator.ts +13 -0
  292. package/src/lib/scraper-vnext/conclave-omega.ts +1830 -0
  293. package/src/lib/scraper-vnext/content-extractor-v2.ts +226 -0
  294. package/src/lib/scraper-vnext/diagnostics-suite.ts +36 -0
  295. package/src/lib/scraper-vnext/epistemic-packer.ts +97 -0
  296. package/src/lib/scraper-vnext/fusion-v2.ts +65 -0
  297. package/src/lib/scraper-vnext/hydra-reader.orig.ts +1216 -0
  298. package/src/lib/scraper-vnext/hydra-reader.ts +13 -0
  299. package/src/lib/scraper-vnext/native-scraper-browser-vnext.orig.ts +91 -0
  300. package/src/lib/scraper-vnext/native-scraper-browser-vnext.ts +13 -0
  301. package/src/lib/scraper-vnext/nexus-consensus.orig.ts +905 -0
  302. package/src/lib/scraper-vnext/nexus-consensus.ts +13 -0
  303. package/src/lib/scraper-vnext/portfolio-consensus-adjudicator.ts +804 -0
  304. package/src/lib/scraper-vnext/portfolio-consensus-memory.ts +377 -0
  305. package/src/lib/scraper-vnext/portfolio-terminal-governor.ts +548 -0
  306. package/src/lib/scraper-vnext/query-strategist.orig.ts +224 -0
  307. package/src/lib/scraper-vnext/query-strategist.ts +423 -0
  308. package/src/lib/scraper-vnext/retrieval-accelerator.ts +708 -0
  309. package/src/lib/scraper-vnext/retrieval-audit-augments.ts +477 -0
  310. package/src/lib/scraper-vnext/retrieval-control-plane.ts +904 -0
  311. package/src/lib/scraper-vnext/retrieval-policy-augments.ts +358 -0
  312. package/src/lib/scraper-vnext/safe-fetch-v2.ts +157 -0
  313. package/src/lib/scraper-vnext/sentinel-omega.ts +56 -0
  314. package/src/lib/scraper-vnext/sentinel-orchestrator.ts +69 -0
  315. package/src/lib/scraper-vnext/sibyl-oracle.orig.ts +413 -0
  316. package/src/lib/scraper-vnext/sibyl-oracle.ts +13 -0
  317. package/src/lib/scraper-vnext/smart-read-v2.ts +55 -0
  318. package/src/lib/scraper-vnext/spa-rescue-bridge.ts +608 -0
  319. package/src/lib/scraper-vnext/strata-engine.orig.ts +2666 -0
  320. package/src/lib/scraper-vnext/strata-engine.ts +13 -0
  321. package/src/lib/scraper-vnext/structured-source-adapter.orig.ts +369 -0
  322. package/src/lib/scraper-vnext/structured-source-adapter.ts +34 -0
  323. package/src/lib/scraper-vnext/terminal-complete.ts +224 -0
  324. package/src/lib/scraper-vnext/terminal-final.ts +277 -0
  325. package/src/lib/scraper-vnext/terminal-saturation.ts +345 -0
  326. package/src/lib/scraper-vnext/terminal-wire.ts +425 -0
  327. package/src/lib/scraper-vnext/vanguard-titanium.orig.ts +467 -0
  328. package/src/lib/scraper-vnext/vanguard-titanium.ts +17 -0
  329. package/src/lib/search-cache.ts +138 -0
  330. package/src/lib/sloop-runner.base.ts +223 -0
  331. package/src/lib/sloop-runner.ts +104 -0
  332. package/src/lib/sloop.ts +114 -0
  333. package/src/lib/small-model-adapters.ts +82 -0
  334. package/src/lib/sscp.ts +86 -0
  335. package/src/lib/system-map.ts +54 -0
  336. package/src/lib/uni-node.ts +86 -0
  337. package/src/lib/universal-rigor-guard.ts +433 -0
  338. package/src/lib/v15-gate-testbed.ts +274 -0
  339. package/src/lib/v15-gemini.base.ts +89 -0
  340. package/src/lib/v15-gemini.ts +1 -0
  341. package/src/lib/v15-grounding.orig.ts +241 -0
  342. package/src/lib/v15-grounding.ts +79 -0
  343. package/src/lib/v15-pipeline.base.ts +769 -0
  344. package/src/lib/v15-pipeline.orig.ts +1154 -0
  345. package/src/lib/v15-pipeline.ts +674 -0
  346. package/src/lib/v15-questions.ts +64 -0
  347. package/src/lib/v15-rate-limiter.orig.ts +91 -0
  348. package/src/lib/v15-rate-limiter.ts +48 -0
  349. package/src/lib/v15-state.base.ts +87 -0
  350. package/src/lib/v15-state.ts +28 -0
  351. package/src/lib/visual-table-generator.tsx +207 -0
  352. package/src/lib/wasm-runtime.ts +133 -0
  353. package/src/lib/williams-persona-guide.ts +265 -0
  354. package/src/lib/williams-style.base.ts +464 -0
  355. package/src/lib/williams-style.ts +59 -0
  356. package/src/lib/worker-pool.ts +96 -0
  357. package/src/lib/writing-tiers.ts +125 -0
  358. package/src/main.orig.tsx +10 -0
  359. package/src/main.tsx +10 -0
  360. package/src/utils/cn.orig.ts +6 -0
  361. package/src/utils/cn.ts +6 -0
  362. package/src/vite-env.d.ts +1 -0
  363. package/tsconfig.json +31 -0
  364. package/unify.mjs +537 -0
  365. package/vite.config.ts +18 -0
@@ -0,0 +1,904 @@
1
+ /**
2
+ * retrieval-control-plane.ts
3
+ * ============================================================================
4
+ * Additive control plane over canonical retrieval-accelerator.ts.
5
+ *
6
+ * Adds policy-aware cache/coalescing, reference-counted cancellation, optional
7
+ * Web Locks, BroadcastChannel congestion notices, lane-origin accounting,
8
+ * delayed hedging, runtime budgets, document discovery, and bounded crawling.
9
+ */
10
+
11
+ import {
12
+ RetrievalScheduler,
13
+ cachedValue,
14
+ warmupHosts,
15
+ type SchedulerOptions,
16
+ } from "./retrieval-accelerator";
17
+
18
+ export type RobotsMode = "off" | "advisory" | "strict";
19
+ export type CacheMode = "off" | "performance";
20
+ export type CrawlScope = "seed-origins" | "same-origin";
21
+
22
+ export interface RetrievalPolicy {
23
+ policyVersion: string;
24
+ extractionVersion: string;
25
+ maxBytes: number;
26
+ freshnessMs: number;
27
+ requiredLaneClasses: number;
28
+ cacheMode: CacheMode;
29
+ robotsMode: RobotsMode;
30
+ scope: CrawlScope;
31
+ allowHostedRenderer: boolean;
32
+ allowPublicRelays: boolean;
33
+ allowArchive: boolean;
34
+ }
35
+
36
+ export const DEFAULT_RETRIEVAL_POLICY: Readonly<RetrievalPolicy> = {
37
+ policyVersion: "rcp-v1",
38
+ extractionVersion: "unknown",
39
+ maxBytes: 2_000_000,
40
+ freshnessMs: 0,
41
+ requiredLaneClasses: 1,
42
+ cacheMode: "off",
43
+ robotsMode: "off",
44
+ scope: "seed-origins",
45
+ allowHostedRenderer: true,
46
+ allowPublicRelays: true,
47
+ allowArchive: true,
48
+ };
49
+
50
+ export type RetrievalFailureKind =
51
+ | "congestion"
52
+ | "access-denied"
53
+ | "challenge"
54
+ | "transient"
55
+ | "permanent"
56
+ | "abort";
57
+
58
+ export class RetrievalControlError extends Error {
59
+ readonly kind: RetrievalFailureKind;
60
+ readonly retryAfterMs?: number;
61
+ readonly scope: "origin" | "lane";
62
+
63
+ constructor(
64
+ kind: RetrievalFailureKind,
65
+ message: string,
66
+ options?: { retryAfterMs?: number; scope?: "origin" | "lane" },
67
+ ) {
68
+ super(message);
69
+ this.name = "RetrievalControlError";
70
+ this.kind = kind;
71
+ this.retryAfterMs = options?.retryAfterMs;
72
+ this.scope = options?.scope ?? "lane";
73
+ }
74
+ }
75
+
76
+ export interface ScheduleContext {
77
+ attempt: number;
78
+ host: string;
79
+ hostLimit: number;
80
+ signal?: AbortSignal;
81
+ }
82
+
83
+ export interface ControlledExecution<T> {
84
+ value: T;
85
+ source: "network" | "cache";
86
+ policyKey: string;
87
+ needsReattestation: boolean;
88
+ }
89
+
90
+ export interface LaneCandidate<T> {
91
+ id: string;
92
+ laneClass: string;
93
+ admissionUrl: string;
94
+ priority?: number;
95
+ run: (signal: AbortSignal) => Promise<T>;
96
+ }
97
+
98
+ export interface LaneSuccess<T> {
99
+ laneId: string;
100
+ laneClass: string;
101
+ value: T;
102
+ latencyMs: number;
103
+ }
104
+
105
+ export interface LaneFailure {
106
+ laneId: string;
107
+ laneClass: string;
108
+ kind: RetrievalFailureKind;
109
+ message: string;
110
+ }
111
+
112
+ export interface HedgedQuorumResult<T> {
113
+ quorumMet: boolean;
114
+ requiredClasses: number;
115
+ successes: LaneSuccess<T>[];
116
+ failures: LaneFailure[];
117
+ startedLaneIds: string[];
118
+ blockedByOriginPolicy: boolean;
119
+ elapsedMs: number;
120
+ }
121
+
122
+ export interface HedgedQuorumOptions<T> {
123
+ signal?: AbortSignal;
124
+ requiredClasses?: number;
125
+ initialFanout?: number;
126
+ maxStarted?: number;
127
+ defaultHedgeDelayMs?: number;
128
+ minHedgeDelayMs?: number;
129
+ maxHedgeDelayMs?: number;
130
+ quality?: (value: T, lane: LaneCandidate<T>) => boolean;
131
+ onDebug?: (message: string) => void;
132
+ }
133
+
134
+ export type DiscoveredKind = "page" | "sitemap" | "feed";
135
+
136
+ export interface DiscoveredUrl {
137
+ url: string;
138
+ kind: DiscoveredKind;
139
+ source: "html-link" | "html-feed" | "sitemap" | "sitemap-index" | "rss" | "atom";
140
+ priority: number;
141
+ }
142
+
143
+ export interface RuntimeSignals {
144
+ saveData?: boolean;
145
+ effectiveType?: string;
146
+ deviceMemoryGiB?: number;
147
+ hidden?: boolean;
148
+ }
149
+
150
+ export interface RuntimeBudget {
151
+ maxParallelPages: number;
152
+ enableHedging: boolean;
153
+ maxBytesScale: number;
154
+ pauseBackground: boolean;
155
+ }
156
+
157
+ export interface CrawlPayload<T> {
158
+ value: T;
159
+ text: string;
160
+ contentType?: string;
161
+ canonicalUrl?: string;
162
+ bytesRead?: number;
163
+ }
164
+
165
+ export interface CrawlPage<T> {
166
+ url: string;
167
+ canonicalUrl: string;
168
+ depth: number;
169
+ parentUrl?: string;
170
+ payload: T;
171
+ cacheSource: "network" | "cache";
172
+ needsReattestation: boolean;
173
+ }
174
+
175
+ export interface CrawlFailure {
176
+ url: string;
177
+ depth: number;
178
+ error: string;
179
+ }
180
+
181
+ export interface CrawlOptions {
182
+ signal?: AbortSignal;
183
+ maxPages?: number;
184
+ maxDepth?: number;
185
+ maxTotalBytes?: number;
186
+ maxParallelPages?: number;
187
+ maxDiscoveredPerPage?: number;
188
+ sameOriginOnly?: boolean;
189
+ allowBackground?: boolean;
190
+ query?: string;
191
+ policy?: Partial<RetrievalPolicy>;
192
+ robotsDecision?: (url: string, signal?: AbortSignal) => Promise<"allow" | "deny" | "unknown">;
193
+ onProgress?: (completed: number, maximum: number, url: string) => void;
194
+ onDebug?: (message: string) => void;
195
+ }
196
+
197
+ export interface CrawlResult<T> {
198
+ pages: CrawlPage<T>[];
199
+ failures: CrawlFailure[];
200
+ skippedByRobots: string[];
201
+ discovered: number;
202
+ bytesRead: number;
203
+ runtimeBudget: RuntimeBudget;
204
+ }
205
+
206
+ const TRACKING_PARAMETERS = new Set([
207
+ "fbclid",
208
+ "gclid",
209
+ "dclid",
210
+ "msclkid",
211
+ "mc_cid",
212
+ "mc_eid",
213
+ "ref_src",
214
+ "ref_url",
215
+ "igshid",
216
+ ]);
217
+
218
+ function clamp(value: number, minimum: number, maximum: number): number {
219
+ if (!Number.isFinite(value)) return minimum;
220
+ return Math.max(minimum, Math.min(maximum, value));
221
+ }
222
+
223
+ function nowMs(): number {
224
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
225
+ }
226
+
227
+ function abortError(): Error {
228
+ const error = new Error("ABORTED");
229
+ error.name = "AbortError";
230
+ return error;
231
+ }
232
+
233
+ function isAbort(error: unknown): boolean {
234
+ return error instanceof Error && (error.name === "AbortError" || error.message === "ABORTED");
235
+ }
236
+
237
+ function delay(milliseconds: number, signal?: AbortSignal): Promise<void> {
238
+ return new Promise((resolve, reject) => {
239
+ if (milliseconds <= 0) {
240
+ resolve();
241
+ return;
242
+ }
243
+ if (signal?.aborted) {
244
+ reject(abortError());
245
+ return;
246
+ }
247
+ const timer = setTimeout(() => {
248
+ cleanup();
249
+ resolve();
250
+ }, milliseconds);
251
+ const onAbort = () => {
252
+ cleanup();
253
+ reject(abortError());
254
+ };
255
+ const cleanup = () => {
256
+ clearTimeout(timer);
257
+ signal?.removeEventListener("abort", onAbort);
258
+ };
259
+ signal?.addEventListener("abort", onAbort, { once: true });
260
+ });
261
+ }
262
+
263
+ function fnv32(text: string, seed: number): number {
264
+ let hash = seed >>> 0;
265
+ for (let index = 0; index < text.length; index += 1) {
266
+ hash ^= text.charCodeAt(index);
267
+ hash = Math.imul(hash, 0x01000193);
268
+ }
269
+ return hash >>> 0;
270
+ }
271
+
272
+ function fnv128(text: string): string {
273
+ return [fnv32(text, 0x811c9dc5), fnv32(text, 0x9e3779b9), fnv32(text, 0x85ebca6b), fnv32(text, 0xc2b2ae35)]
274
+ .map((part) => part.toString(16).padStart(8, "0"))
275
+ .join("");
276
+ }
277
+
278
+ function canonicalUrl(rawUrl: string, baseUrl?: string): string {
279
+ try {
280
+ const url = baseUrl ? new URL(rawUrl, baseUrl) : new URL(rawUrl);
281
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "";
282
+ if (url.username || url.password) return "";
283
+ url.hash = "";
284
+ url.hostname = url.hostname.toLowerCase();
285
+ for (const key of Array.from(url.searchParams.keys())) {
286
+ const lower = key.toLowerCase();
287
+ if (lower.startsWith("utm_") || TRACKING_PARAMETERS.has(lower)) url.searchParams.delete(key);
288
+ }
289
+ url.searchParams.sort();
290
+ return url.toString();
291
+ } catch {
292
+ return "";
293
+ }
294
+ }
295
+
296
+ function originKey(url: string): string {
297
+ try {
298
+ return fnv128(new URL(url).origin);
299
+ } catch {
300
+ return fnv128(url);
301
+ }
302
+ }
303
+
304
+ function stableSerialize(value: unknown): string {
305
+ if (value === null) return "null";
306
+ switch (typeof value) {
307
+ case "string":
308
+ return JSON.stringify(value);
309
+ case "number":
310
+ return Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
311
+ case "boolean":
312
+ return value ? "true" : "false";
313
+ case "undefined":
314
+ return '"[undefined]"';
315
+ case "object": {
316
+ if (Array.isArray(value)) return `[${value.map(stableSerialize).join(",")}]`;
317
+ const object = value as Record<string, unknown>;
318
+ return `{${Object.keys(object)
319
+ .sort()
320
+ .map((key) => `${JSON.stringify(key)}:${stableSerialize(object[key])}`)
321
+ .join(",")}}`;
322
+ }
323
+ default:
324
+ return JSON.stringify(String(value));
325
+ }
326
+ }
327
+
328
+ function mergePolicy(policy?: Partial<RetrievalPolicy>): RetrievalPolicy {
329
+ const merged: RetrievalPolicy = { ...DEFAULT_RETRIEVAL_POLICY, ...policy };
330
+ merged.maxBytes = Math.max(1, Math.floor(merged.maxBytes));
331
+ merged.freshnessMs = Math.max(0, Math.floor(merged.freshnessMs));
332
+ merged.requiredLaneClasses = Math.max(1, Math.floor(merged.requiredLaneClasses));
333
+ return merged;
334
+ }
335
+
336
+ export function retrievalPolicyKey(rawUrl: string, policy?: Partial<RetrievalPolicy>): string {
337
+ const normalized = canonicalUrl(rawUrl) || rawUrl.trim();
338
+ return `rcp\u0000${normalized}\u0000${stableSerialize(mergePolicy(policy))}`;
339
+ }
340
+
341
+ function classifyFailure(error: unknown): {
342
+ kind: RetrievalFailureKind;
343
+ retryAfterMs?: number;
344
+ scope: "origin" | "lane";
345
+ } {
346
+ if (error instanceof RetrievalControlError) return { kind: error.kind, retryAfterMs: error.retryAfterMs, scope: error.scope };
347
+ if (isAbort(error)) return { kind: "abort", scope: "lane" };
348
+ const message = error instanceof Error ? error.message : String(error);
349
+ if (/\b(?:429|502|503|504)\b/.test(message) || /rate.?limit|too many requests|timeout|timed out/i.test(message)) return { kind: "congestion", scope: "lane" };
350
+ if (/challenge|captcha/i.test(message)) return { kind: "challenge", scope: "lane" };
351
+ return { kind: "permanent", scope: "lane" };
352
+ }
353
+
354
+ export function readRuntimeSignals(): RuntimeSignals {
355
+ const nav = typeof navigator !== "undefined" ? (navigator as Navigator & { connection?: { saveData?: boolean; effectiveType?: string }; deviceMemory?: number }) : undefined;
356
+ return {
357
+ saveData: nav?.connection?.saveData,
358
+ effectiveType: nav?.connection?.effectiveType,
359
+ deviceMemoryGiB: nav?.deviceMemory,
360
+ hidden: typeof document !== "undefined" ? document.hidden : undefined,
361
+ };
362
+ }
363
+
364
+ export function deriveRuntimeBudget(
365
+ requestedParallelPages: number,
366
+ signals: RuntimeSignals = readRuntimeSignals(),
367
+ allowBackground = false,
368
+ ): RuntimeBudget {
369
+ let parallel = clamp(Math.floor(requestedParallelPages), 1, 32);
370
+ let enableHedging = true;
371
+ let maxBytesScale = 1;
372
+ if (signals.saveData) {
373
+ parallel = 1;
374
+ enableHedging = false;
375
+ maxBytesScale = 0.5;
376
+ }
377
+ if (signals.effectiveType === "slow-2g" || signals.effectiveType === "2g") {
378
+ parallel = 1;
379
+ enableHedging = false;
380
+ maxBytesScale = Math.min(maxBytesScale, 0.4);
381
+ } else if (signals.effectiveType === "3g") {
382
+ parallel = Math.min(parallel, 3);
383
+ maxBytesScale = Math.min(maxBytesScale, 0.75);
384
+ }
385
+ if (signals.deviceMemoryGiB !== undefined && signals.deviceMemoryGiB <= 2) parallel = Math.min(parallel, 2);
386
+ const pauseBackground = signals.hidden === true && !allowBackground;
387
+ if (pauseBackground) {
388
+ parallel = 1;
389
+ enableHedging = false;
390
+ }
391
+ return { maxParallelPages: parallel, enableHedging, maxBytesScale, pauseBackground };
392
+ }
393
+
394
+ async function yieldToBrowser(): Promise<void> {
395
+ const schedulerApi = (globalThis as { scheduler?: { yield?: () => Promise<void> } }).scheduler;
396
+ if (typeof schedulerApi?.yield === "function") {
397
+ await schedulerApi.yield();
398
+ return;
399
+ }
400
+ await delay(0);
401
+ }
402
+
403
+ interface PressureMessage {
404
+ version: 1;
405
+ type: "pressure";
406
+ originKey: string;
407
+ until: number;
408
+ }
409
+
410
+ class SharedPressureBus {
411
+ private readonly backoffUntil = new Map<string, number>();
412
+ private channel: BroadcastChannel | null = null;
413
+
414
+ constructor(channelName = "retrieval-control-plane-v1") {
415
+ try {
416
+ if (typeof BroadcastChannel !== "undefined") {
417
+ this.channel = new BroadcastChannel(channelName);
418
+ this.channel.onmessage = (event: MessageEvent<PressureMessage>) => {
419
+ const message = event.data;
420
+ if (!message || message.version !== 1 || message.type !== "pressure" || typeof message.originKey !== "string" || !Number.isFinite(message.until)) return;
421
+ const now = Date.now();
422
+ const until = Math.min(message.until, now + 60_000);
423
+ if (until <= now) return;
424
+ this.backoffUntil.set(message.originKey, Math.max(this.backoffUntil.get(message.originKey) ?? 0, until));
425
+ };
426
+ }
427
+ } catch {
428
+ this.channel = null;
429
+ }
430
+ }
431
+
432
+ async wait(admissionUrl: string, signal?: AbortSignal): Promise<void> {
433
+ const until = this.backoffUntil.get(originKey(admissionUrl)) ?? 0;
434
+ const wait = until - Date.now();
435
+ if (wait > 0) await delay(wait, signal);
436
+ }
437
+
438
+ publish(admissionUrl: string, waitMs: number): void {
439
+ const bounded = clamp(Math.floor(waitMs), 100, 60_000);
440
+ const message: PressureMessage = { version: 1, type: "pressure", originKey: originKey(admissionUrl), until: Date.now() + bounded };
441
+ this.backoffUntil.set(message.originKey, message.until);
442
+ try {
443
+ this.channel?.postMessage(message);
444
+ } catch {
445
+ /* same-tab state remains active */
446
+ }
447
+ }
448
+
449
+ close(): void {
450
+ try {
451
+ this.channel?.close();
452
+ } catch {
453
+ /* ignore */
454
+ }
455
+ }
456
+ }
457
+
458
+ interface SharedFlight<T> {
459
+ promise: Promise<T>;
460
+ controller: AbortController;
461
+ consumers: number;
462
+ settled: boolean;
463
+ }
464
+
465
+ class SharedFlights {
466
+ private flights = new Map<string, SharedFlight<unknown>>();
467
+
468
+ run<T>(key: string, callerSignal: AbortSignal | undefined, factory: (signal: AbortSignal) => Promise<T>): Promise<T> {
469
+ let flight = this.flights.get(key) as SharedFlight<T> | undefined;
470
+ if (!flight) {
471
+ const controller = new AbortController();
472
+ const current: SharedFlight<T> = { controller, consumers: 0, settled: false, promise: Promise.resolve(undefined as T) };
473
+ current.promise = factory(controller.signal).finally(() => {
474
+ current.settled = true;
475
+ if (this.flights.get(key) === current) this.flights.delete(key);
476
+ });
477
+ flight = current;
478
+ this.flights.set(key, current as SharedFlight<unknown>);
479
+ }
480
+ flight.consumers += 1;
481
+ return new Promise<T>((resolve, reject) => {
482
+ let completed = false;
483
+ const release = () => {
484
+ if (completed) return;
485
+ completed = true;
486
+ callerSignal?.removeEventListener("abort", onAbort);
487
+ flight!.consumers = Math.max(0, flight!.consumers - 1);
488
+ if (flight!.consumers === 0 && !flight!.settled && !flight!.controller.signal.aborted) flight!.controller.abort();
489
+ };
490
+ const onAbort = () => {
491
+ release();
492
+ reject(abortError());
493
+ };
494
+ if (callerSignal?.aborted) {
495
+ onAbort();
496
+ return;
497
+ }
498
+ callerSignal?.addEventListener("abort", onAbort, { once: true });
499
+ flight!.promise.then(
500
+ (value) => {
501
+ if (completed) return;
502
+ release();
503
+ resolve(value);
504
+ },
505
+ (error) => {
506
+ if (completed) return;
507
+ release();
508
+ reject(error);
509
+ },
510
+ );
511
+ });
512
+ }
513
+ }
514
+
515
+ class LaneLatencyHistory {
516
+ private samples = new Map<string, number[]>();
517
+
518
+ record(laneClass: string, latencyMs: number): void {
519
+ const list = this.samples.get(laneClass) ?? [];
520
+ list.push(Math.max(0, latencyMs));
521
+ if (list.length > 64) list.splice(0, list.length - 64);
522
+ this.samples.set(laneClass, list);
523
+ }
524
+
525
+ percentile(laneClass: string, percentile: number): number | null {
526
+ const list = this.samples.get(laneClass);
527
+ if (!list || list.length < 4) return null;
528
+ const sorted = list.slice().sort((a, b) => a - b);
529
+ const index = Math.min(sorted.length - 1, Math.floor(clamp(percentile, 0, 1) * sorted.length));
530
+ return sorted[index];
531
+ }
532
+
533
+ hedgeDelay(activeClasses: string[], defaultDelayMs: number, minimum: number, maximum: number): number {
534
+ const estimates = activeClasses.map((laneClass) => this.percentile(laneClass, 0.9)).filter((value): value is number => value !== null);
535
+ const estimate = estimates.length > 0 ? Math.min(...estimates) : defaultDelayMs;
536
+ return clamp(Math.round(estimate), minimum, maximum);
537
+ }
538
+ }
539
+
540
+ function addDiscovered(
541
+ output: Map<string, DiscoveredUrl>,
542
+ rawUrl: string | null,
543
+ baseUrl: string,
544
+ kind: DiscoveredKind,
545
+ source: DiscoveredUrl["source"],
546
+ priority: number,
547
+ ): void {
548
+ if (!rawUrl) return;
549
+ const normalized = canonicalUrl(rawUrl.trim(), baseUrl);
550
+ if (!normalized) return;
551
+ const prior = output.get(normalized);
552
+ if (!prior || priority > prior.priority) output.set(normalized, { url: normalized, kind, source, priority });
553
+ }
554
+
555
+ function elementText(parent: Element, localName: string): string {
556
+ const element = parent.getElementsByTagNameNS("*", localName)[0];
557
+ return element?.textContent?.trim() ?? "";
558
+ }
559
+
560
+ export function discoverDocumentUrls(text: string, contentType: string | undefined, baseUrl: string, maximum = 100): DiscoveredUrl[] {
561
+ if (typeof DOMParser === "undefined" || !text.trim()) return [];
562
+ const output = new Map<string, DiscoveredUrl>();
563
+ const xmlLike = /(?:xml|rss|atom)/i.test(contentType ?? "") || /^\s*(?:<\?xml|<urlset|<sitemapindex|<rss|<feed)/i.test(text);
564
+ if (xmlLike) {
565
+ try {
566
+ const documentXml = new DOMParser().parseFromString(text, "application/xml");
567
+ if (documentXml.getElementsByTagName("parsererror").length === 0) {
568
+ const root = documentXml.documentElement.localName.toLowerCase();
569
+ if (root === "urlset") {
570
+ const urls = Array.from(documentXml.getElementsByTagNameNS("*", "url"));
571
+ for (const entry of urls) addDiscovered(output, elementText(entry, "loc"), baseUrl, "page", "sitemap", 40);
572
+ } else if (root === "sitemapindex") {
573
+ const maps = Array.from(documentXml.getElementsByTagNameNS("*", "sitemap"));
574
+ for (const entry of maps) addDiscovered(output, elementText(entry, "loc"), baseUrl, "sitemap", "sitemap-index", 50);
575
+ } else if (root === "rss") {
576
+ const items = Array.from(documentXml.getElementsByTagName("item"));
577
+ for (const item of items) addDiscovered(output, elementText(item, "link"), baseUrl, "page", "rss", 35);
578
+ } else if (root === "feed") {
579
+ const entries = Array.from(documentXml.getElementsByTagNameNS("*", "entry"));
580
+ for (const entry of entries) {
581
+ const links = Array.from(entry.getElementsByTagNameNS("*", "link"));
582
+ const alternate = links.find((link) => ((link.getAttribute("rel") ?? "alternate").toLowerCase()) === "alternate") ?? links[0];
583
+ addDiscovered(output, alternate?.getAttribute("href") ?? null, baseUrl, "page", "atom", 35);
584
+ }
585
+ }
586
+ }
587
+ } catch {
588
+ /* HTML fallback below */
589
+ }
590
+ }
591
+ if (output.size < maximum) {
592
+ try {
593
+ const documentHtml = new DOMParser().parseFromString(text, "text/html");
594
+ for (const anchor of Array.from(documentHtml.querySelectorAll("a[href]"))) {
595
+ addDiscovered(output, anchor.getAttribute("href"), baseUrl, "page", "html-link", 10);
596
+ if (output.size >= maximum) break;
597
+ }
598
+ if (output.size < maximum) {
599
+ for (const link of Array.from(documentHtml.querySelectorAll('link[rel~="alternate"][href]'))) {
600
+ const type = (link.getAttribute("type") ?? "").toLowerCase();
601
+ if (type.includes("rss") || type.includes("atom")) addDiscovered(output, link.getAttribute("href"), baseUrl, "feed", "html-feed", 45);
602
+ if (output.size >= maximum) break;
603
+ }
604
+ }
605
+ } catch {
606
+ /* no discovery */
607
+ }
608
+ }
609
+ return Array.from(output.values())
610
+ .sort((left, right) => right.priority - left.priority || left.url.localeCompare(right.url))
611
+ .slice(0, maximum);
612
+ }
613
+
614
+ export interface RetrievalControlPlaneOptions {
615
+ scheduler?: SchedulerOptions;
616
+ pressureChannelName?: string;
617
+ crossTabLeases?: boolean;
618
+ }
619
+
620
+ export class RetrievalControlPlane {
621
+ readonly scheduler: RetrievalScheduler;
622
+ private readonly pressure: SharedPressureBus;
623
+ private readonly flights = new SharedFlights();
624
+ private readonly latency = new LaneLatencyHistory();
625
+ private readonly crossTabLeases: boolean;
626
+
627
+ constructor(options?: RetrievalControlPlaneOptions) {
628
+ this.scheduler = new RetrievalScheduler(options?.scheduler);
629
+ this.pressure = new SharedPressureBus(options?.pressureChannelName);
630
+ this.crossTabLeases = options?.crossTabLeases !== false;
631
+ }
632
+
633
+ close(): void {
634
+ this.pressure.close();
635
+ }
636
+
637
+ private async withCrossTabLease<T>(key: string, signal: AbortSignal | undefined, operation: () => Promise<T>): Promise<T> {
638
+ if (!this.crossTabLeases || typeof navigator === "undefined") return operation();
639
+ const locks = (navigator as Navigator & {
640
+ locks?: { request: (name: string, options: Record<string, unknown>, callback: () => Promise<T>) => Promise<T> };
641
+ }).locks;
642
+ if (typeof locks?.request !== "function") return operation();
643
+ const options: Record<string, unknown> = { mode: "exclusive" };
644
+ if (signal) options.signal = signal;
645
+ return locks.request(`retrieval:${fnv128(key)}`, options, operation);
646
+ }
647
+
648
+ executePage<T>(
649
+ rawUrl: string,
650
+ worker: (context: ScheduleContext, policy: RetrievalPolicy) => Promise<T>,
651
+ options?: { admissionUrl?: string; signal?: AbortSignal; policy?: Partial<RetrievalPolicy> },
652
+ ): Promise<ControlledExecution<T>> {
653
+ const policy = mergePolicy(options?.policy);
654
+ const key = retrievalPolicyKey(rawUrl, policy);
655
+ const admissionUrl = options?.admissionUrl ?? rawUrl;
656
+ return this.flights.run(key, options?.signal, (sharedSignal) =>
657
+ this.withCrossTabLease(key, sharedSignal, async () => {
658
+ const executeNetwork = async () => {
659
+ await this.pressure.wait(admissionUrl, sharedSignal);
660
+ try {
661
+ return await this.scheduler.schedule(admissionUrl, (context) => worker(context, policy), sharedSignal);
662
+ } catch (error) {
663
+ const classified = classifyFailure(error);
664
+ if (classified.kind === "congestion") this.pressure.publish(admissionUrl, classified.retryAfterMs ?? 1_000);
665
+ throw error;
666
+ }
667
+ };
668
+ if (policy.cacheMode === "off" || policy.freshnessMs <= 0) {
669
+ return { value: await executeNetwork(), source: "network" as const, policyKey: key, needsReattestation: false };
670
+ }
671
+ const cached = await cachedValue(key, policy.freshnessMs, executeNetwork);
672
+ return { value: cached.value, source: cached.cached ? "cache" as const : "network" as const, policyKey: key, needsReattestation: cached.cached };
673
+ }),
674
+ );
675
+ }
676
+
677
+ async hedgedQuorum<T>(lanes: LaneCandidate<T>[], options?: HedgedQuorumOptions<T>): Promise<HedgedQuorumResult<T>> {
678
+ const startedAt = nowMs();
679
+ const failures: LaneFailure[] = [];
680
+ const successes: LaneSuccess<T>[] = [];
681
+ const startedLaneIds: string[] = [];
682
+ const distinctClasses = new Set(lanes.map((lane) => lane.laneClass)).size;
683
+ const requiredClasses = clamp(Math.floor(options?.requiredClasses ?? 1), 1, Math.max(1, distinctClasses));
684
+ const runtime = deriveRuntimeBudget(8);
685
+ const ordered = lanes.slice().sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0) || left.id.localeCompare(right.id));
686
+ const maximumStarted = clamp(Math.floor(options?.maxStarted ?? Math.min(ordered.length, requiredClasses + 2)), requiredClasses, ordered.length);
687
+ const initialFanout = runtime.enableHedging ? clamp(Math.floor(options?.initialFanout ?? requiredClasses), 1, maximumStarted) : 1;
688
+ if (ordered.length === 0) {
689
+ return { quorumMet: false, requiredClasses, successes, failures, startedLaneIds, blockedByOriginPolicy: false, elapsedMs: 0 };
690
+ }
691
+ warmupHosts(ordered.slice(0, maximumStarted).map((lane) => lane.admissionUrl), Math.min(8, maximumStarted));
692
+ const master = new AbortController();
693
+ const onParentAbort = () => master.abort();
694
+ options?.signal?.addEventListener("abort", onParentAbort, { once: true });
695
+ const successfulClasses = new Set<string>();
696
+ let nextLane = 0;
697
+ let active = 0;
698
+ let finished = false;
699
+ let blockedByOriginPolicy = false;
700
+ let hedgeTimer: ReturnType<typeof setTimeout> | undefined;
701
+ return new Promise((resolve) => {
702
+ const finish = () => {
703
+ if (finished) return;
704
+ finished = true;
705
+ if (hedgeTimer !== undefined) clearTimeout(hedgeTimer);
706
+ master.abort();
707
+ options?.signal?.removeEventListener("abort", onParentAbort);
708
+ resolve({ quorumMet: successfulClasses.size >= requiredClasses, requiredClasses, successes, failures, startedLaneIds, blockedByOriginPolicy, elapsedMs: Math.round(nowMs() - startedAt) });
709
+ };
710
+ const allAvailableExhausted = () => nextLane >= maximumStarted && active === 0;
711
+ const maybeFinish = () => {
712
+ if (successfulClasses.size >= requiredClasses || allAvailableExhausted()) finish();
713
+ };
714
+ const scheduleHedge = () => {
715
+ if (finished || !runtime.enableHedging || nextLane >= maximumStarted) return;
716
+ if (hedgeTimer !== undefined) clearTimeout(hedgeTimer);
717
+ const delayMs = this.latency.hedgeDelay(
718
+ ordered.slice(0, nextLane).map((lane) => lane.laneClass),
719
+ options?.defaultHedgeDelayMs ?? 400,
720
+ options?.minHedgeDelayMs ?? 100,
721
+ options?.maxHedgeDelayMs ?? 2_000,
722
+ );
723
+ hedgeTimer = setTimeout(() => {
724
+ hedgeTimer = undefined;
725
+ startNext();
726
+ scheduleHedge();
727
+ }, delayMs);
728
+ };
729
+ const startNext = () => {
730
+ if (finished || nextLane >= maximumStarted) {
731
+ maybeFinish();
732
+ return;
733
+ }
734
+ const lane = ordered[nextLane];
735
+ nextLane += 1;
736
+ active += 1;
737
+ startedLaneIds.push(lane.id);
738
+ const laneStarted = nowMs();
739
+ void (async () => {
740
+ try {
741
+ await this.pressure.wait(lane.admissionUrl, master.signal);
742
+ const value = await this.scheduler.schedule(lane.admissionUrl, (context) => lane.run(context.signal ?? master.signal), master.signal);
743
+ const latencyMs = Math.round(nowMs() - laneStarted);
744
+ this.latency.record(lane.laneClass, latencyMs);
745
+ const acceptable = options?.quality ? options.quality(value, lane) : true;
746
+ if (!acceptable) {
747
+ failures.push({ laneId: lane.id, laneClass: lane.laneClass, kind: "permanent", message: "QUALITY_REJECTED" });
748
+ return;
749
+ }
750
+ if (!successfulClasses.has(lane.laneClass)) {
751
+ successfulClasses.add(lane.laneClass);
752
+ successes.push({ laneId: lane.id, laneClass: lane.laneClass, value, latencyMs });
753
+ }
754
+ } catch (error) {
755
+ const classified = classifyFailure(error);
756
+ failures.push({ laneId: lane.id, laneClass: lane.laneClass, kind: classified.kind, message: error instanceof Error ? error.message : String(error) });
757
+ if (classified.kind === "congestion") this.pressure.publish(lane.admissionUrl, classified.retryAfterMs ?? 1_000);
758
+ if (classified.kind === "access-denied" && classified.scope === "origin") {
759
+ blockedByOriginPolicy = true;
760
+ finish();
761
+ }
762
+ } finally {
763
+ active = Math.max(0, active - 1);
764
+ if (!finished && successfulClasses.size < requiredClasses && active < requiredClasses - successfulClasses.size && nextLane < maximumStarted) startNext();
765
+ maybeFinish();
766
+ }
767
+ })();
768
+ };
769
+ for (let index = 0; index < initialFanout; index += 1) startNext();
770
+ scheduleHedge();
771
+ });
772
+ }
773
+
774
+ async crawl<T>(
775
+ seeds: string[],
776
+ reader: (url: string, context: ScheduleContext, policy: RetrievalPolicy) => Promise<CrawlPayload<T>>,
777
+ options?: CrawlOptions,
778
+ ): Promise<CrawlResult<T>> {
779
+ const maximumPages = clamp(Math.floor(options?.maxPages ?? 20), 1, 1_000);
780
+ const maximumDepth = clamp(Math.floor(options?.maxDepth ?? 2), 0, 10);
781
+ const maximumBytes = Math.max(1, Math.floor(options?.maxTotalBytes ?? 20_000_000));
782
+ const maxDiscoveredPerPage = clamp(Math.floor(options?.maxDiscoveredPerPage ?? 100), 0, 10_000);
783
+ const requestedParallel = clamp(Math.floor(options?.maxParallelPages ?? 8), 1, 32);
784
+ const runtimeBudget = deriveRuntimeBudget(requestedParallel, readRuntimeSignals(), options?.allowBackground === true);
785
+ const basePolicy = mergePolicy(options?.policy);
786
+ basePolicy.maxBytes = Math.max(1, Math.floor(basePolicy.maxBytes * runtimeBudget.maxBytesScale));
787
+ interface QueueItem {
788
+ url: string;
789
+ depth: number;
790
+ priority: number;
791
+ enqueuedAt: number;
792
+ parentUrl?: string;
793
+ kind: DiscoveredKind;
794
+ }
795
+ const queue: QueueItem[] = [];
796
+ const seen = new Set<string>();
797
+ const seedOrigins = new Set<string>();
798
+ const pages: CrawlPage<T>[] = [];
799
+ const failures: CrawlFailure[] = [];
800
+ const skippedByRobots: string[] = [];
801
+ let discoveredCount = 0;
802
+ let bytesRead = 0;
803
+ let reservedBytes = 0;
804
+ let scheduledPages = 0;
805
+ const enqueue = (rawUrl: string, depth: number, priority: number, kind: DiscoveredKind, parentUrl?: string) => {
806
+ const normalized = canonicalUrl(rawUrl);
807
+ if (!normalized || seen.has(normalized)) return;
808
+ if (options?.sameOriginOnly !== false && seedOrigins.size > 0) {
809
+ let origin = "";
810
+ try {
811
+ origin = new URL(normalized).origin;
812
+ } catch {
813
+ return;
814
+ }
815
+ if (!seedOrigins.has(origin)) return;
816
+ }
817
+ seen.add(normalized);
818
+ queue.push({ url: normalized, depth, priority, enqueuedAt: Date.now(), parentUrl, kind });
819
+ discoveredCount += 1;
820
+ };
821
+ for (const rawSeed of seeds) {
822
+ const normalized = canonicalUrl(rawSeed);
823
+ if (!normalized) continue;
824
+ try {
825
+ seedOrigins.add(new URL(normalized).origin);
826
+ } catch {
827
+ continue;
828
+ }
829
+ }
830
+ for (const rawSeed of seeds) enqueue(rawSeed, 0, 100, "page");
831
+ const popBest = (): QueueItem | undefined => {
832
+ if (queue.length === 0) return undefined;
833
+ const now = Date.now();
834
+ queue.sort((left, right) => {
835
+ const leftAging = Math.min(20, (now - left.enqueuedAt) / 1_000);
836
+ const rightAging = Math.min(20, (now - right.enqueuedAt) / 1_000);
837
+ return right.priority + rightAging - (left.priority + leftAging) || left.url.localeCompare(right.url);
838
+ });
839
+ return queue.shift();
840
+ };
841
+ const active = new Set<Promise<void>>();
842
+ const launch = (item: QueueItem, reservation: number) => {
843
+ scheduledPages += 1;
844
+ reservedBytes += reservation;
845
+ let promise!: Promise<void>;
846
+ promise = (async () => {
847
+ try {
848
+ if (runtimeBudget.pauseBackground && typeof document !== "undefined") {
849
+ while (document.hidden && !options?.signal?.aborted) await delay(250, options?.signal);
850
+ }
851
+ if (basePolicy.robotsMode !== "off" && options?.robotsDecision) {
852
+ const decision = await options.robotsDecision(item.url, options.signal);
853
+ if (decision === "deny" || (decision === "unknown" && basePolicy.robotsMode === "strict")) {
854
+ skippedByRobots.push(item.url);
855
+ return;
856
+ }
857
+ }
858
+ const pagePolicy: RetrievalPolicy = { ...basePolicy, maxBytes: reservation };
859
+ const execution = await this.executePage(item.url, (context, policy) => reader(item.url, context, policy), { signal: options?.signal, policy: pagePolicy });
860
+ const payload = execution.value;
861
+ const canonical = canonicalUrl(payload.canonicalUrl ?? item.url) || item.url;
862
+ const measuredBytes = payload.bytesRead ?? new TextEncoder().encode(payload.text).byteLength;
863
+ bytesRead += Math.min(measuredBytes, reservation);
864
+ pages.push({ url: item.url, canonicalUrl: canonical, depth: item.depth, parentUrl: item.parentUrl, payload: payload.value, cacheSource: execution.source, needsReattestation: execution.needsReattestation });
865
+ options?.onProgress?.(pages.length + failures.length, maximumPages, item.url);
866
+ if (item.depth < maximumDepth && maxDiscoveredPerPage > 0) {
867
+ const discovered = discoverDocumentUrls(payload.text, payload.contentType, canonical, maxDiscoveredPerPage);
868
+ for (const candidate of discovered) enqueue(candidate.url, item.depth + 1, candidate.priority, candidate.kind, canonical);
869
+ }
870
+ await yieldToBrowser();
871
+ } catch (error) {
872
+ failures.push({ url: item.url, depth: item.depth, error: error instanceof Error ? error.message : String(error) });
873
+ options?.onDebug?.(`control-plane crawl failure: ${item.url}: ${error instanceof Error ? error.message : "unknown"}`);
874
+ } finally {
875
+ reservedBytes = Math.max(0, reservedBytes - reservation);
876
+ active.delete(promise);
877
+ }
878
+ })();
879
+ active.add(promise);
880
+ };
881
+ while ((queue.length > 0 || active.size > 0) && scheduledPages < maximumPages) {
882
+ while (active.size < runtimeBudget.maxParallelPages && queue.length > 0 && scheduledPages < maximumPages) {
883
+ const remaining = maximumBytes - bytesRead - reservedBytes;
884
+ if (remaining <= 0) break;
885
+ const item = popBest();
886
+ if (!item) break;
887
+ const reservation = Math.min(basePolicy.maxBytes, remaining);
888
+ if (reservation <= 0) break;
889
+ launch(item, reservation);
890
+ }
891
+ if (active.size === 0) break;
892
+ await Promise.race(active);
893
+ }
894
+ await Promise.all(active);
895
+ return { pages, failures, skippedByRobots, discovered: discoveredCount, bytesRead, runtimeBudget };
896
+ }
897
+ }
898
+
899
+ let defaultControlPlane: RetrievalControlPlane | null = null;
900
+
901
+ export function getRetrievalControlPlane(options?: RetrievalControlPlaneOptions): RetrievalControlPlane {
902
+ if (!defaultControlPlane) defaultControlPlane = new RetrievalControlPlane(options);
903
+ return defaultControlPlane;
904
+ }