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,382 @@
1
+ import { extractTextFromHtml, fetchRobust } from "./scraper-hardener";
2
+ import { searchAcademicSources } from "./academic-sources";
3
+
4
+ export interface BrowserScraperSearchResult {
5
+ title: string;
6
+ url: string;
7
+ description: string;
8
+ content: string;
9
+ }
10
+
11
+ function stripHtml(input: string): string {
12
+ return input
13
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
14
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
15
+ .replace(/<[^>]+>/g, " ")
16
+ .replace(/&amp;/g, "&")
17
+ .replace(/&quot;/g, '"')
18
+ .replace(/&#x27;|&#39;/g, "'")
19
+ .replace(/&lt;/g, "<")
20
+ .replace(/&gt;/g, ">")
21
+ .replace(/&nbsp;/g, " ")
22
+ .replace(/\s+/g, " ")
23
+ .trim();
24
+ }
25
+
26
+ function decodeRedirect(href: string, base: string): string {
27
+ try {
28
+ const parsed = new URL(href, base);
29
+ // DuckDuckGo wraps targets in ?uddg=
30
+ const uddg = parsed.searchParams.get("uddg");
31
+ if (uddg) return decodeURIComponent(uddg);
32
+ // Bing wraps in ?u= (sometimes base64); fall back to raw
33
+ const u = parsed.searchParams.get("u");
34
+ if (u) {
35
+ if (/^https?:/i.test(u)) return u;
36
+ // Bing often stores URL-safe base64 in u=a1<base64>. Decode if possible.
37
+ const raw = u.startsWith("a1") ? u.slice(2) : u;
38
+ try {
39
+ const padded = raw.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((raw.length + 3) % 4);
40
+ const decoded = atob(padded);
41
+ if (/^https?:/i.test(decoded)) return decoded;
42
+ } catch { /* ignore */ }
43
+ }
44
+ return parsed.toString();
45
+ } catch {
46
+ return href;
47
+ }
48
+ }
49
+
50
+ type EngineParser = (html: string, count: number) => BrowserScraperSearchResult[];
51
+
52
+ interface SearchEngine {
53
+ name: string;
54
+ url: (q: string) => string;
55
+ parse: EngineParser;
56
+ }
57
+
58
+ function normalizeQuery(query: string): string {
59
+ let q = query
60
+ .replace(/^\s*(please|can you|could you|would you|i need you to|help me|find me|please find me)\b/gi, "")
61
+ .replace(/\b(please|thanks|thank you)\b/gi, "")
62
+ .replace(/["“”]/g, "")
63
+ .replace(/\s+/g, " ")
64
+ .trim();
65
+
66
+ // Domain-specific rewrite for the recurring NIH/social-welfare task. This
67
+ // avoids engines spending the query budget on generic words like "please".
68
+ if (/\bNIH\b|National Institutes of Health|grant|funded|funding/i.test(query) && /social welfare|social determinants|SDOH|health disparities/i.test(query)) {
69
+ q = "NIH funding opportunities social determinants of health social welfare health disparities NIMHD NIMH NIA NOSI NOFO";
70
+ }
71
+ return q || query;
72
+ }
73
+
74
+ function isNihSocialWelfareQuery(query: string): boolean {
75
+ return (/\bNIH\b|National Institutes of Health|grant|funded|funding/i.test(query) &&
76
+ /social welfare|social determinants|SDOH|health disparities|housing|food insecurity|public health|community/i.test(query));
77
+ }
78
+
79
+ function curatedNihSeeds(query: string): BrowserScraperSearchResult[] {
80
+ if (!isNihSocialWelfareQuery(query)) return [];
81
+ return [
82
+ {
83
+ title: "NIH Guide for Grants and Contracts",
84
+ url: "https://grants.nih.gov/funding/searchguide/index.html#/",
85
+ description: "Official NIH Guide search for active NOFOs, NOSIs, notices, and funding announcements.",
86
+ content: "Official NIH funding announcement search. Use to identify active NOFOs and NOSIs; applications must route through an awarding Institute or Center, not coordinating offices.",
87
+ },
88
+ {
89
+ title: "NIH RePORTER Matchmaker and Project Search",
90
+ url: "https://reporter.nih.gov/",
91
+ description: "Official database of funded NIH projects useful for recent award pattern analysis.",
92
+ content: "NIH RePORTER is the official project database for NIH-funded grants and can be used to inspect recently awarded projects, awarding ICs, mechanisms, and topic clusters.",
93
+ },
94
+ {
95
+ title: "NIMHD Research Interest Areas",
96
+ url: "https://www.nimhd.nih.gov/programs/extramural/research-interest-areas/",
97
+ description: "NIMHD priority areas for minority health and health disparities research.",
98
+ content: "NIMHD supports research on minority health, health disparities, social determinants of health, community interventions, and multi-level influences on health outcomes.",
99
+ },
100
+ {
101
+ title: "NIMH Strategic Priorities",
102
+ url: "https://www.nimh.nih.gov/about/strategic-planning-reports",
103
+ description: "NIMH strategic planning and funding priorities for mental health research.",
104
+ content: "NIMH priorities can support mental health-focused work on social determinants, digital behavioral health, services research, implementation, and community intervention models.",
105
+ },
106
+ {
107
+ title: "NIH Office of Behavioral and Social Sciences Research (OBSSR)",
108
+ url: "https://obssr.od.nih.gov/",
109
+ description: "NIH coordinating office for behavioral and social sciences research.",
110
+ content: "OBSSR coordinates and co-funds behavioral and social sciences research but is not itself the awarding IC for R-series applications; proposals must identify an awarding Institute or Center.",
111
+ },
112
+ {
113
+ title: "Grants.gov Search Grants",
114
+ url: "https://www.grants.gov/search-grants",
115
+ description: "Government-wide funding opportunity search portal.",
116
+ content: "Grants.gov lists federal funding opportunities and can be used to confirm agency, eligibility, and opportunity status, but NIH Guide remains the canonical NIH opportunity source.",
117
+ },
118
+ ];
119
+ }
120
+
121
+ function relevanceScore(result: BrowserScraperSearchResult, query: string): number {
122
+ const hay = `${result.title} ${result.description} ${result.content} ${result.url}`.toLowerCase();
123
+ const q = query.toLowerCase();
124
+ let score = 0;
125
+ const terms = ["nih", "nimhd", "nimh", "nia", "grant", "funding", "nofo", "nosi", "social determinants", "health disparities", "sdoh", "research", "reporter", "grants.gov" ];
126
+ for (const t of terms) if (hay.includes(t)) score += 2;
127
+ for (const t of q.split(/\s+/).filter(w => w.length > 4)) if (hay.includes(t)) score += 0.5;
128
+ if (/dictionary|definition|fashion|collins|merriam|cambridge|grammar|synonym/i.test(hay)) score -= 20;
129
+ if (/bing\.com\/ck\//i.test(result.url)) score -= 15;
130
+ return score;
131
+ }
132
+
133
+ function filterAndRank(results: BrowserScraperSearchResult[], query: string, count: number): BrowserScraperSearchResult[] {
134
+ return results
135
+ .map(r => ({ r, score: relevanceScore(r, query) }))
136
+ .filter(x => x.score > -5)
137
+ .sort((a, b) => b.score - a.score)
138
+ .map(x => x.r)
139
+ .slice(0, count);
140
+ }
141
+
142
+ function genericAnchorParse(host: string): EngineParser {
143
+ return (html, count) => {
144
+ const results: BrowserScraperSearchResult[] = [];
145
+ const seen = new Set<string>();
146
+ const anchorRe = /<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
147
+ let m: RegExpExecArray | null;
148
+ while ((m = anchorRe.exec(html)) !== null && results.length < count) {
149
+ let url = decodeRedirect(m[1], `https://${host}`);
150
+ if (!/^https?:\/\//i.test(url)) continue;
151
+ if (url.includes(host)) continue; // skip internal engine links
152
+ const title = stripHtml(m[2]);
153
+ if (!title || title.length < 12) continue;
154
+ if (seen.has(url)) continue;
155
+ seen.add(url);
156
+ results.push({ title, url, description: "", content: "" });
157
+ }
158
+ return results;
159
+ };
160
+ }
161
+
162
+ interface SearchEngineExt extends SearchEngine {
163
+ /** Force a specific proxy that is confirmed to work for this engine. */
164
+ preferProxy?: string;
165
+ /** JSON engines bypass HTML parsing entirely and are CORS-safe. */
166
+ jsonFetch?: (q: string, count: number, onDebug?: (m: string) => void) => Promise<BrowserScraperSearchResult[]>;
167
+ }
168
+
169
+ // ── Wikipedia OpenSearch + REST summary — fully CORS-safe, no proxy needed ──
170
+ async function wikipediaSearch(query: string, count: number): Promise<BrowserScraperSearchResult[]> {
171
+ const url = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&origin=*&srlimit=${count}`;
172
+ const res = await fetch(url);
173
+ if (!res.ok) return [];
174
+ const data = await res.json();
175
+ const hits = data?.query?.search ?? [];
176
+ return hits.map((h: any) => ({
177
+ title: h.title,
178
+ url: `https://en.wikipedia.org/wiki/${encodeURIComponent(String(h.title).replace(/\s/g, "_"))}`,
179
+ description: stripHtml(h.snippet || ""),
180
+ content: stripHtml(h.snippet || ""),
181
+ }));
182
+ }
183
+
184
+ const ENGINES: SearchEngineExt[] = [
185
+ // 1. Wikipedia API — CORS-safe JSON, always works, no proxy. Highest priority
186
+ // for reliability; provides authoritative encyclopedic grounding.
187
+ {
188
+ name: "wikipedia-api",
189
+ url: () => "",
190
+ parse: () => [],
191
+ jsonFetch: (q, count) => wikipediaSearch(q, count),
192
+ },
193
+ // 2. Bing via codetabs — CONFIRMED working combination. Pinned proxy.
194
+ {
195
+ name: "bing(codetabs)",
196
+ url: (q) => `https://www.bing.com/search?q=${encodeURIComponent(q)}`,
197
+ preferProxy: "codetabs",
198
+ parse: (html, count) => {
199
+ const results: BrowserScraperSearchResult[] = [];
200
+ const blocks = html.split(/<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>/i).slice(1);
201
+ for (const block of blocks) {
202
+ const link = block.match(/<h2[^>]*>\s*<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/i);
203
+ if (!link || !/^https?:\/\//i.test(link[1])) continue;
204
+ const snip = block.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
205
+ results.push({
206
+ title: stripHtml(link[2]) || link[1],
207
+ url: link[1],
208
+ description: stripHtml(snip?.[1] || ""),
209
+ content: stripHtml(snip?.[1] || ""),
210
+ });
211
+ if (results.length >= count) break;
212
+ }
213
+ return results;
214
+ },
215
+ },
216
+ // 3. DuckDuckGo Lite — the lite endpoint renders server-side and is the most
217
+ // proxy-friendly DDG surface (the /html/ endpoint is JS-gated and never worked).
218
+ {
219
+ name: "duckduckgo-lite",
220
+ url: (q) => `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(q)}`,
221
+ parse: (html, count) => {
222
+ const results: BrowserScraperSearchResult[] = [];
223
+ const seen = new Set<string>();
224
+ const anchorRe = /<a[^>]+(?:class=["'][^"']*result-link[^"']*["'][^>]*)?href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
225
+ let m: RegExpExecArray | null;
226
+ while ((m = anchorRe.exec(html)) !== null && results.length < count) {
227
+ const url = decodeRedirect(m[1], "https://duckduckgo.com");
228
+ if (!/^https?:\/\//i.test(url)) continue;
229
+ if (/duckduckgo\.com/i.test(url)) continue;
230
+ const title = stripHtml(m[2]);
231
+ if (!title || title.length < 10) continue;
232
+ if (seen.has(url)) continue;
233
+ seen.add(url);
234
+ results.push({ title, url, description: "", content: "" });
235
+ }
236
+ return results;
237
+ },
238
+ },
239
+ // 4. Bing default (any proxy) — backup if codetabs is rate-limited.
240
+ {
241
+ name: "bing",
242
+ url: (q) => `https://www.bing.com/search?q=${encodeURIComponent(q)}`,
243
+ parse: (html, count) => {
244
+ const results: BrowserScraperSearchResult[] = [];
245
+ const blocks = html.split(/<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>/i).slice(1);
246
+ for (const block of blocks) {
247
+ const link = block.match(/<h2[^>]*>\s*<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/i);
248
+ if (!link || !/^https?:\/\//i.test(link[1])) continue;
249
+ const snip = block.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
250
+ results.push({
251
+ title: stripHtml(link[2]) || link[1],
252
+ url: link[1],
253
+ description: stripHtml(snip?.[1] || ""),
254
+ content: stripHtml(snip?.[1] || ""),
255
+ });
256
+ if (results.length >= count) break;
257
+ }
258
+ return results;
259
+ },
260
+ },
261
+ // 5. Mojeek — independent index, proxy-friendly.
262
+ {
263
+ name: "mojeek",
264
+ url: (q) => `https://www.mojeek.com/search?q=${encodeURIComponent(q)}`,
265
+ parse: (html, count) => {
266
+ const results: BrowserScraperSearchResult[] = [];
267
+ const re = /<a[^>]+class=["'][^"']*ob[^"']*["'][^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
268
+ let m: RegExpExecArray | null;
269
+ while ((m = re.exec(html)) !== null && results.length < count) {
270
+ if (!/^https?:\/\//i.test(m[1])) continue;
271
+ results.push({ title: stripHtml(m[2]) || m[1], url: m[1], description: "", content: "" });
272
+ }
273
+ return results.length ? results : genericAnchorParse("mojeek.com")(html, count);
274
+ },
275
+ },
276
+ ];
277
+
278
+ async function scrapeResultContent(result: BrowserScraperSearchResult, signal?: AbortSignal): Promise<BrowserScraperSearchResult> {
279
+ if (result.content && result.content.length > 240) return result;
280
+ // Hard 8s timeout per page: prevents slow downloads from holding heap
281
+ const ac = new AbortController();
282
+ const timer = setTimeout(() => ac.abort(), 8_000);
283
+ if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
284
+ try {
285
+ const fetched = await fetchRobust(result.url, ac.signal);
286
+ clearTimeout(timer);
287
+ // Cap extracted text immediately (1200 chars) — never hold full page body
288
+ const extracted = String(extractTextFromHtml(fetched.text).slice(0, 1_200));
289
+ if (extracted.length > result.content.length) {
290
+ return { ...result, content: extracted, description: result.description || String(extracted.slice(0, 300)) };
291
+ }
292
+ } catch {
293
+ clearTimeout(timer);
294
+ /* keep snippet */
295
+ }
296
+ return result;
297
+ }
298
+
299
+ /**
300
+ * Jina-free web search: tries each engine through the CORS-resilient fetcher
301
+ * until one yields results, then enriches the top hits with scraped page text.
302
+ */
303
+ export async function browserScraperSearch(
304
+ query: string,
305
+ opts?: { count?: number; signal?: AbortSignal; onDebug?: (msg: string) => void; enrich?: boolean },
306
+ ): Promise<BrowserScraperSearchResult[]> {
307
+ const count = Math.max(1, Math.min(20, opts?.count ?? 8));
308
+ const enrich = opts?.enrich ?? true;
309
+ const normalized = normalizeQuery(query);
310
+ if (normalized !== query) opts?.onDebug?.(`OG scraper normalized query: "${query.slice(0, 80)}" → "${normalized}"`);
311
+ const curated = curatedNihSeeds(query);
312
+
313
+ // Fire the academic/government APIs in parallel immediately. They are CORS-safe
314
+ // and frequently return before the proxy-backed search engines.
315
+ const academicPromise = searchAcademicSources(normalized, {
316
+ count: Math.min(count, 6),
317
+ onDebug: opts?.onDebug,
318
+ }).catch(() => []);
319
+
320
+ for (const engine of ENGINES) {
321
+ if (opts?.signal?.aborted) break;
322
+ try {
323
+ let hits: BrowserScraperSearchResult[];
324
+ if (engine.jsonFetch) {
325
+ // CORS-safe JSON engine — no proxy, no HTML parsing.
326
+ const raw = await engine.jsonFetch(normalized, count * 2, opts?.onDebug);
327
+ hits = filterAndRank(raw, normalized, count);
328
+ } else {
329
+ const page = await fetchRobust(engine.url(normalized), opts?.signal, engine.preferProxy);
330
+ hits = filterAndRank(engine.parse(page.text, count * 2), normalized, count);
331
+ opts?.onDebug?.(`OG scraper engine "${engine.name}": parsed via ${page.source}${page.proxy ? ` (${page.proxy})` : ""}`);
332
+ }
333
+ if (hits.length > 0) {
334
+ opts?.onDebug?.(`OG scraper engine "${engine.name}": ${hits.length} usable hits`);
335
+ if (!enrich) return filterAndRank([...hits, ...curated], normalized, count);
336
+ const queue = [...hits];
337
+ const out: BrowserScraperSearchResult[] = [];
338
+ // One enrichment worker per search keeps capability but prevents
339
+ // cluster waves from multiplying into dozens of page downloads.
340
+ const workers = Array.from({ length: Math.min(1, queue.length) }, async () => {
341
+ while (queue.length) {
342
+ const next = queue.shift();
343
+ if (!next) break;
344
+ out.push(await scrapeResultContent(next, opts?.signal));
345
+ }
346
+ });
347
+ await Promise.all(workers);
348
+ const academic = await academicPromise;
349
+ const academicAsBrowserResults: BrowserScraperSearchResult[] = academic.map(a => ({
350
+ title: a.title, url: a.url, description: a.description, content: a.content,
351
+ }));
352
+ const merged = filterAndRank([...out, ...curated, ...academicAsBrowserResults], normalized, count * 2);
353
+ return merged.slice(0, count);
354
+ }
355
+ opts?.onDebug?.(`OG scraper engine "${engine.name}": 0 hits — trying next engine`);
356
+ } catch (e: any) {
357
+ opts?.onDebug?.(`OG scraper engine "${engine.name}" failed (${e?.message?.slice(0, 80) ?? "err"}) — trying next engine`);
358
+ }
359
+ }
360
+
361
+ const academic = await academicPromise;
362
+ const academicAsBrowserResults: BrowserScraperSearchResult[] = academic.map(a => ({
363
+ title: a.title, url: a.url, description: a.description, content: a.content,
364
+ }));
365
+ const fallback = filterAndRank([...curated, ...academicAsBrowserResults], normalized, count * 2);
366
+ if (fallback.length > 0) {
367
+ opts?.onDebug?.(`OG scraper engines exhausted; academic/NIH fallback yielded ${fallback.length} source(s)`);
368
+ return fallback.slice(0, count);
369
+ }
370
+ opts?.onDebug?.(`OG scraper exhausted all ${ENGINES.length} engines and academic APIs with 0 results`);
371
+ return [];
372
+ }
373
+
374
+ export async function browserScraperRead(
375
+ url: string,
376
+ opts?: { signal?: AbortSignal; onDebug?: (msg: string) => void },
377
+ ): Promise<string> {
378
+ const fetched = await fetchRobust(url, opts?.signal);
379
+ const text = extractTextFromHtml(fetched.text);
380
+ opts?.onDebug?.(`OG scraper read: ${text.length} chars via ${fetched.source}${fetched.proxy ? ` (${fetched.proxy})` : ""}`);
381
+ return text;
382
+ }
@@ -0,0 +1,90 @@
1
+ import { browserScraperSearch as baseSearch, browserScraperRead } from "./browser-search-scraper.base";
2
+ import { searchAcademicSources } from "./academic-sources";
3
+
4
+ export interface BrowserScraperSearchResult { title: string; url: string; description: string; content: string }
5
+ export { browserScraperRead };
6
+
7
+ function normalize(q: string) {
8
+ return q.replace(/^\s*(please|find me|can you|could you|help me)\b/gi, "").replace(/\s+/g, " ").trim() || q;
9
+ }
10
+
11
+ function curated(query: string): BrowserScraperSearchResult[] {
12
+ const q = query.toLowerCase();
13
+ if (!/(cannabis|marijuana|hemp|thc|cbd)/.test(q)) return [];
14
+ return [
15
+ { title: "PubMed cannabis research", url: "https://pubmed.ncbi.nlm.nih.gov/?term=cannabis", description: "PubMed search entry point for cannabis literature.", content: "Use PubMed for peer-reviewed biomedical cannabis studies and reviews." },
16
+ { title: "ClinicalTrials.gov cannabis studies", url: "https://clinicaltrials.gov/search?term=cannabis", description: "ClinicalTrials.gov search for cannabis trials.", content: "ClinicalTrials.gov indexes active and completed cannabis-related interventional and observational studies." },
17
+ { title: "Reddit search: cannabis product unmet needs", url: "https://www.reddit.com/search/?q=cannabis%20product%20unmet%20needs", description: "Forum discovery seed.", content: "Use forum posts only as qualitative demand signals; validate claims with primary literature and legal caveats." },
18
+ ];
19
+ }
20
+
21
+ async function wikipedia(query: string, count: number): Promise<BrowserScraperSearchResult[]> {
22
+ const res = await fetch(`https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&origin=*&srlimit=${count}`);
23
+ if (!res.ok) return [];
24
+ const data = await res.json();
25
+ return (data.query?.search ?? []).map((h: any) => ({ title: h.title, url: `https://en.wikipedia.org/wiki/${encodeURIComponent(String(h.title).replace(/\s/g, "_"))}`, description: String(h.snippet || "").replace(/<[^>]+>/g, " "), content: String(h.snippet || "").replace(/<[^>]+>/g, " ") }));
26
+ }
27
+
28
+ // HackerNews Algolia — CORS-safe JSON, strong for tech/product-demand signals.
29
+ async function hnAlgolia(query: string, count: number): Promise<BrowserScraperSearchResult[]> {
30
+ const res = await fetch(`https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(query)}&tags=story&hitsPerPage=${count}`);
31
+ if (!res.ok) return [];
32
+ const data = await res.json();
33
+ return (data.hits ?? []).filter((h: any) => h.url || h.objectID).map((h: any) => ({
34
+ title: String(h.title || h.story_title || "HN discussion"),
35
+ url: String(h.url || `https://news.ycombinator.com/item?id=${h.objectID}`),
36
+ description: `HN · ${h.points ?? 0} pts · ${h.num_comments ?? 0} comments`,
37
+ content: `${h.title || h.story_title || ""}. ${(h.story_text || h._highlightResult?.title?.value || "").replace(/<[^>]+>/g, " ").slice(0, 400)}`,
38
+ }));
39
+ }
40
+
41
+ // DuckDuckGo Instant Answer — CORS-safe JSON topic summaries.
42
+ async function ddgInstant(query: string, count: number): Promise<BrowserScraperSearchResult[]> {
43
+ const res = await fetch(`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`);
44
+ if (!res.ok) return [];
45
+ const data = await res.json();
46
+ const out: BrowserScraperSearchResult[] = [];
47
+ if (data.AbstractText && data.AbstractURL) out.push({ title: String(data.Heading || query), url: String(data.AbstractURL), description: "DuckDuckGo abstract", content: String(data.AbstractText).slice(0, 500) });
48
+ for (const t of (data.RelatedTopics ?? [])) {
49
+ if (out.length >= count) break;
50
+ if (t.FirstURL && t.Text) out.push({ title: String(t.Text).slice(0, 120), url: String(t.FirstURL), description: "DuckDuckGo related", content: String(t.Text).slice(0, 400) });
51
+ }
52
+ return out;
53
+ }
54
+
55
+ export async function browserScraperSearch(query: string, opts?: { count?: number; signal?: AbortSignal; onDebug?: (msg: string) => void; enrich?: boolean }): Promise<BrowserScraperSearchResult[]> {
56
+ const count = Math.max(1, Math.min(20, opts?.count ?? 8));
57
+ // Truncate overly long queries for search engines (template-directed queries can be 200+ chars)
58
+ const q = normalize(query).slice(0, 100);
59
+
60
+ // PRIORITY ORDER FIX: CORS-safe JSON APIs FIRST (they actually work in 2026),
61
+ // proxy-based HTML scraping LAST (search engines changed their DOM structure,
62
+ // proxy scraping returns 0 hits consistently as of mid-2026).
63
+ const [academic, wiki, hn, ddg] = await Promise.all([
64
+ searchAcademicSources(q, { count, onDebug: opts?.onDebug }).catch(() => []),
65
+ wikipedia(q, count).catch(() => []),
66
+ hnAlgolia(q, Math.min(count, 5)).catch(() => []),
67
+ ddgInstant(q, Math.min(count, 4)).catch(() => []),
68
+ ]);
69
+ const academicHits = academic.map(a => ({ title: a.title, url: a.url, description: a.description, content: a.content }));
70
+
71
+ // Assemble CORS-safe results first
72
+ const corsResults = [...academicHits, ...wiki, ...hn, ...ddg, ...curated(q)];
73
+
74
+ // Only attempt proxy-based HTML scraping if CORS-safe APIs returned < 2 results
75
+ // (saves 30+ seconds of proxy timeout on most queries)
76
+ let base: BrowserScraperSearchResult[] = [];
77
+ if (corsResults.length < 2) {
78
+ opts?.onDebug?.(`[OG+] CORS-safe APIs returned ${corsResults.length} — trying proxy-based scraping as fallback`);
79
+ base = await baseSearch(q, opts).catch((e: any) => { opts?.onDebug?.(`[OG+] proxy scraper failed: ${e?.message ?? "error"}`); return []; });
80
+ } else {
81
+ opts?.onDebug?.(`[OG+] CORS-safe APIs returned ${corsResults.length} results — skipping slow proxy scraping`);
82
+ }
83
+
84
+ // Interleave: CORS results first (higher reliability), proxy results second
85
+ const all = [...corsResults, ...base];
86
+ const seen = new Set<string>();
87
+ const out = all.filter(r => r.url && !seen.has(r.url) && seen.add(r.url)).slice(0, count);
88
+ opts?.onDebug?.(`[OG+] merged ${out.length}/${all.length} unique — academic ${academicHits.length} · wiki ${wiki.length} · hn ${hn.length} · ddg ${ddg.length} · proxy ${base.length}`);
89
+ return out;
90
+ }
@@ -0,0 +1,10 @@
1
+ // SIDECAR SEAM — see flatten-guide.md.
2
+ // Alias-reachable retrieval lane: the packaged `v15-grounding.ts` imports this
3
+ // module via `@/lib/browser-search-scraper`, so this seam IS on the live path.
4
+ // Adds a provenance tap only; retrieval behaviour is byte-for-byte unchanged.
5
+ export * from "./browser-search-scraper.orig";
6
+
7
+ import { browserScraperSearch as packageBrowserScraperSearch } from "./browser-search-scraper.orig";
8
+ import { withLaneTap } from "@/lib/citation-lane-tap";
9
+
10
+ export const browserScraperSearch = withLaneTap("browser-scraper", packageBrowserScraperSearch);
@@ -0,0 +1,82 @@
1
+ /**
2
+ * CALC-REQUEST Interceptor
3
+ *
4
+ * Problem: LLMs sometimes emit plain-English "CALC REQUEST: ..." text instead
5
+ * of structured compute_requests, leaving the math undone in the final output.
6
+ *
7
+ * Solution: deterministic NL→compute mapping. We scan the draft for known
8
+ * calculation intents (cRCT power, attrition adequacy, ICC effective N, etc.),
9
+ * extract the numeric parameters with regex, run the REAL compute sandbox
10
+ * function, and return a verified-fact block to splice back into the answer.
11
+ *
12
+ * This never asks the model to "please confirm" — the app does the math.
13
+ */
14
+
15
+ import { runComputeCall, type ComputeRecord } from "./compute-sandbox";
16
+
17
+ function num(text: string, patterns: RegExp[], fallback: number): number {
18
+ for (const re of patterns) {
19
+ const m = text.match(re);
20
+ if (m && m[1] != null) {
21
+ const v = parseFloat(m[1]);
22
+ if (isFinite(v)) return v;
23
+ }
24
+ }
25
+ return fallback;
26
+ }
27
+
28
+ export interface InterceptedCalc {
29
+ record: ComputeRecord;
30
+ intent: string;
31
+ }
32
+
33
+ /** Detect whether the text contains an un-executed calculation request. */
34
+ export function hasCalcRequest(text: string): boolean {
35
+ return /\bCALC[\s_-]*REQUEST\b/i.test(text)
36
+ || /\bplease confirm the (required|necessary) (number|sample|clusters)\b/i.test(text)
37
+ || /\b(perform|run|compute|calculate) (a |the )?(cluster|power|sample[- ]size)\b/i.test(text);
38
+ }
39
+
40
+ /** Parse all calc requests in the text and execute them deterministically. */
41
+ export function interceptCalcRequests(text: string): InterceptedCalc[] {
42
+ const out: InterceptedCalc[] = [];
43
+ const lower = text.toLowerCase();
44
+
45
+ // ── cRCT power calculation ──────────────────────────────────────────
46
+ if (/cluster[\s-]*randomi[sz]ed|crct|clusters per arm|cluster size/i.test(lower) &&
47
+ /power|hba1c|mean difference|effect|sample size/i.test(lower)) {
48
+ const delta = num(text, [/mean difference of\s*([\d.]+)/i, /difference[^.\d]{0,12}([\d.]+)\s*%/i, /delta\s*[=:]\s*([\d.]+)/i], 0.5);
49
+ const sd = num(text, [/sd\s*[=:(]?\s*([\d.]+)/i, /standard deviation[^.\d]{0,8}([\d.]+)/i], 1.5);
50
+ const alpha = num(text, [/alpha\s*[=:]\s*([\d.]+)/i, /α\s*[=:]\s*([\d.]+)/i], 0.05);
51
+ const power = num(text, [/power\s*[=:]\s*([\d.]+)/i, /β\s*[=:]\s*([\d.]+)/i, /(\d?\.\d+)\s*power/i], 0.80);
52
+ const icc = num(text, [/icc\s*[=:]\s*([\d.]+)/i, /intra-?cluster correlation[^.\d]{0,12}([\d.]+)/i], 0.02);
53
+ const clusterSize = num(text, [/cluster size\s*[=:of]*\s*(\d+)/i, /(\d+)\s*participants per cluster/i], 14);
54
+ const attrition = num(text, [/attrition\s*(?:rate)?\s*(?:of)?\s*([\d.]+)\s*%/i, /([\d.]+)\s*%\s*attrition/i, /drop[- ]?out[^.\d]{0,12}([\d.]+)\s*%/i], 0) / (text.match(/%/) ? 100 : 1);
55
+ const rec = runComputeCall({ id: "crct_power", args: { delta, sd, alpha, power, icc, clusterSize, attrition: attrition > 1 ? attrition / 100 : attrition } });
56
+ out.push({ record: rec, intent: `cRCT power: Δ=${delta}, SD=${sd}, α=${alpha}, power=${power}, ICC=${icc}, m=${clusterSize}, attrition=${attrition}` });
57
+ }
58
+
59
+ // ── Attrition adequacy check ────────────────────────────────────────
60
+ if (/attrition|drop[- ]?out|evaluable|retention/i.test(lower) &&
61
+ /clusters? per arm|recruit|underpowered|preserve.*power/i.test(lower)) {
62
+ const clustersPerArm = num(text, [/(\d+)\s*clusters? per arm/i, /(\d+)\s*per arm/i], 15);
63
+ const recruitPerCluster = num(text, [/(\d+)\s*participants? per cluster/i, /recruit[^.\d]{0,12}(\d+)/i], 14);
64
+ const attritionPct = num(text, [/attrition\s*(?:rate)?\s*(?:of)?\s*([\d.]+)\s*%/i, /([\d.]+)\s*%\s*attrition/i], 20);
65
+ const requiredEvaluable = num(text, [/require\s*(\d+)\s*evaluable/i, /(\d+)\s*evaluable participants/i], 178);
66
+ const rec = runComputeCall({ id: "attrition_check", args: { clustersPerArm, recruitPerCluster, attrition: attritionPct / 100, requiredEvaluablePerArm: requiredEvaluable } });
67
+ out.push({ record: rec, intent: `Attrition adequacy: ${clustersPerArm} clusters/arm × ${recruitPerCluster} recruited, ${attritionPct}% attrition vs ${requiredEvaluable} required` });
68
+ }
69
+
70
+ return out;
71
+ }
72
+
73
+ /** Render intercepted calcs as a verified-fact block for the model to use. */
74
+ export function renderInterceptedCalcs(calcs: InterceptedCalc[]): string {
75
+ if (calcs.length === 0) return "";
76
+ const lines = calcs.map(c => {
77
+ if (!c.record.ok || !c.record.result) return ` • ${c.intent} → COMPUTE FAILED (${c.record.error ?? "unknown"})`;
78
+ const out = Object.entries(c.record.result).map(([k, v]) => `${k}=${v}`).join(", ");
79
+ return ` • ${c.intent}\n → ${out} (deterministic, verified)`;
80
+ });
81
+ return `DETERMINISTIC CALCULATION RESULTS (the application computed these — use these EXACT numbers, never emit "CALC REQUEST" or "please confirm"):\n${lines.join("\n")}`;
82
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * citation-lane-tap.ts — defensive provenance tap for retrieval lanes.
3
+ * ============================================================================
4
+ * The packaged `v15-grounding.ts` imports three lanes through the `@` ALIAS:
5
+ * `@/lib/academic-sources`, `@/lib/scraper-enhanced`, `@/lib/browser-search-scraper`
6
+ * so those seams ARE on the live retrieval path (unlike `@/lib/v15-grounding`,
7
+ * which the pipeline bypasses with a relative import).
8
+ *
9
+ * This tap normalises whatever shape a lane returns into the store's record
10
+ * shape. It is intentionally permissive and total: unknown shapes are skipped,
11
+ * never guessed. It performs no network access and mutates nothing.
12
+ * ============================================================================ */
13
+ import { recordCitationSources, type CitationStage } from "@/lib/citation-ledger-store";
14
+
15
+ interface LooseResult {
16
+ url?: unknown;
17
+ link?: unknown;
18
+ href?: unknown;
19
+ title?: unknown;
20
+ name?: unknown;
21
+ content?: unknown;
22
+ snippet?: unknown;
23
+ text?: unknown;
24
+ abstract?: unknown;
25
+ }
26
+
27
+ function str(value: unknown): string | undefined {
28
+ return typeof value === "string" && value.trim() ? value : undefined;
29
+ }
30
+
31
+ /** Extract {title,url,content} triples from an arbitrary lane return value. */
32
+ export function normaliseLaneResults(
33
+ value: unknown,
34
+ ): Array<{ title?: string; url?: string; content?: string }> {
35
+ const rows: unknown[] = Array.isArray(value)
36
+ ? value
37
+ : Array.isArray((value as { results?: unknown[] })?.results)
38
+ ? (value as { results: unknown[] }).results
39
+ : Array.isArray((value as { sources?: unknown[] })?.sources)
40
+ ? (value as { sources: unknown[] }).sources
41
+ : [];
42
+
43
+ const out: Array<{ title?: string; url?: string; content?: string }> = [];
44
+ for (const row of rows) {
45
+ if (!row || typeof row !== "object") continue;
46
+ const r = row as LooseResult;
47
+ const url = str(r.url) ?? str(r.link) ?? str(r.href);
48
+ if (!url) continue;
49
+ out.push({
50
+ url,
51
+ title: str(r.title) ?? str(r.name) ?? "Untitled",
52
+ content: str(r.content) ?? str(r.snippet) ?? str(r.text) ?? str(r.abstract) ?? "",
53
+ });
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /** Record a lane's output. Safe to call with any value; never throws. */
59
+ export function tapLane(lane: string, value: unknown, stage: CitationStage = "grounding"): void {
60
+ try {
61
+ const rows = normaliseLaneResults(value);
62
+ if (rows.length > 0) recordCitationSources(rows, { stage, lane });
63
+ } catch {
64
+ /* provenance capture must never break retrieval */
65
+ }
66
+ }
67
+
68
+ /** Wrap an async lane function so its results are tapped transparently. */
69
+ export function withLaneTap<TArgs extends unknown[], TResult>(
70
+ lane: string,
71
+ fn: (...args: TArgs) => Promise<TResult>,
72
+ ): (...args: TArgs) => Promise<TResult> {
73
+ return async (...args: TArgs): Promise<TResult> => {
74
+ const result = await fn(...args);
75
+ tapLane(lane, result);
76
+ return result;
77
+ };
78
+ }