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,848 @@
1
+ /**
2
+ * vite-native-scraper.ts — L6: Main Vite Plugin, Routing, Metasearch
3
+ * Zero-dependency: node:* only. No axios, cheerio, express, cors, sqlite3.
4
+ *
5
+ * SSRF Defense: Iron Ring implementation with DNS pinning.
6
+ * SimHash/RRF+MMR: Real deduplication and re-ranking.
7
+ * Token bucket rate limiting with agent loop detection.
8
+ */
9
+ import type { Plugin, ViteDevServer } from 'vite';
10
+ import { createHash } from 'node:crypto';
11
+ import { watch } from 'node:fs';
12
+ import { lookup as dnsLookup } from 'node:dns/promises';
13
+ import * as http from 'node:http';
14
+ import * as https from 'node:https';
15
+ import * as zlib from 'node:zlib';
16
+ import { join, dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ // ── Config ────────────────────────────────────────────────────────────────
23
+ const MAX_PARSE_CHARS = 300_000;
24
+ const MAX_RESPONSE_BYTES = 5 * 1024 * 1024; // 5MB hard cap
25
+ const MAX_DECOMP_BYTES = 750_000;
26
+ const REQUEST_TIMEOUT_MS = 8_000;
27
+ const MAX_RESULTS_PER_ENGINE = 5;
28
+
29
+ // ── SSRF: IPv4 range blockers ─────────────────────────────────────────────
30
+ // Blocks: 0/8, 10/8, 127/8, 100.64/10, 169.254/16, 172.16/12, 192.168/16, 224/4
31
+ function blocked4(ip: string): boolean {
32
+ const parts = ip.split('.').map(Number);
33
+ if (parts.length !== 4 || parts.some(p => isNaN(p) || p < 0 || p > 255)) return false;
34
+ const [a, b] = parts;
35
+ return (
36
+ a === 0 || // 0.0.0.0/8
37
+ a === 10 || // 10.0.0.0/8
38
+ a === 127 || // 127.0.0.0/8
39
+ (a === 100 && b >= 64 && b <= 127) || // 100.64.0.0/10
40
+ (a === 169 && b === 254) || // 169.254.0.0/16 (link-local)
41
+ (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12
42
+ (a === 192 && b === 168) || // 192.168.0.0/16
43
+ a >= 224 // 224.0.0.0/4 (multicast+)
44
+ );
45
+ }
46
+
47
+ // Blocks: ::1, fc00::/7, fe80::/10, ff00::/8, and IPv4-mapped IPv6
48
+ function blocked6(ip: string): boolean {
49
+ const lower = ip.toLowerCase().replace(/[\[\]]/g, '');
50
+ if (lower === '::1') return true;
51
+
52
+ // IPv4-mapped IPv6: ::ffff:x.x.x.x or ::ffff:0:x.x.x.x
53
+ // Hand-trace: ::ffff:127.0.0.1
54
+ // Match /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i
55
+ // Extract 127.0.0.1 → pass to blocked4 → returns true
56
+ const v4mapped = lower.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
57
+ if (v4mapped) return blocked4(v4mapped[1]);
58
+
59
+ // Compressed mapping like ::ffff:7f00:1
60
+ const hexMapped = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
61
+ if (hexMapped) {
62
+ const high = parseInt(hexMapped[1], 16);
63
+ const low = parseInt(hexMapped[2], 16);
64
+ const a = (high >> 8) & 0xff, b = high & 0xff, c = (low >> 8) & 0xff, d = low & 0xff;
65
+ return blocked4(`${a}.${b}.${c}.${d}`);
66
+ }
67
+
68
+ // fc00::/7 — private range (also covers fd00::/8)
69
+ if (lower.startsWith('fc') || lower.startsWith('fd')) return true;
70
+ // fe80::/10 — link-local
71
+ if (lower.startsWith('fe8') || lower.startsWith('fe9') || lower.startsWith('fea') || lower.startsWith('feb')) return true;
72
+ // ff00::/8 — multicast
73
+ if (lower.startsWith('ff')) return true;
74
+
75
+ return false;
76
+ }
77
+
78
+ // ── SSRF: Iron Ring prepTarget ────────────────────────────────────────────
79
+ // Hand-trace 1: http://127.0.0.1%00.evil.com
80
+ // Step 1: raw.includes('\0') = false BUT /%00/i.test(raw) = TRUE (percent-encoded NUL)
81
+ // => throws 'NUL byte detected'
82
+ // Hand-trace 2: DNS Pinning
83
+ // We validate IPs in dnsLookup result, then set lookup override in https.get
84
+ // The override (h, o, cb) => cb(null, ch.address, ch.family) intercepts Node's DNS
85
+ // so it uses the pre-validated IP directly without a second lookup
86
+ interface PrepResult {
87
+ parsedUrl: URL;
88
+ rh: string;
89
+ addrs: Array<{ address: string; family: number }>;
90
+ }
91
+
92
+ async function prepTarget(raw: string): Promise<PrepResult> {
93
+ // Step 1: NUL byte check — catches http://127.0.0.1%00.evil.com
94
+ if (raw.includes('\0') || /%00/i.test(raw)) {
95
+ throw new Error('NUL byte detected in URL');
96
+ }
97
+
98
+ // Step 2: Parse
99
+ let p: URL;
100
+ try {
101
+ p = new URL(raw.trim());
102
+ } catch {
103
+ throw new Error(`Invalid URL: ${raw.slice(0, 100)}`);
104
+ }
105
+
106
+ // Step 3: Protocol check
107
+ if (!['http:', 'https:'].includes(p.protocol)) {
108
+ throw new Error(`Blocked protocol: ${p.protocol}`);
109
+ }
110
+
111
+ // Step 4: Credential check
112
+ if (p.username || p.password) throw new Error('Credentialed URLs blocked');
113
+
114
+ // Step 5: Host check
115
+ const rh = p.hostname.toLowerCase().replace(/^\[/, '').replace(/\]$/, '');
116
+ if (rh === 'localhost' || rh.endsWith('.localhost') || rh === 'metadata.google.internal') {
117
+ throw new Error(`Blocked host: ${rh}`);
118
+ }
119
+
120
+ // Step 6: DNS resolution — all:true to get every address
121
+ let addrs: Array<{ address: string; family: number }>;
122
+ try {
123
+ addrs = await dnsLookup(rh, { all: true, verbatim: true }) as Array<{ address: string; family: number }>;
124
+ } catch (e: any) {
125
+ throw new Error(`DNS resolution failed for ${rh}: ${e?.message}`);
126
+ }
127
+
128
+ if (!addrs || addrs.length === 0) throw new Error(`No DNS records for ${rh}`);
129
+
130
+ // Step 7: IP validation — ALL resolved addresses must be safe
131
+ for (const addr of addrs) {
132
+ if (addr.family === 4 && blocked4(addr.address)) {
133
+ throw new Error(`Blocked IPv4 address: ${addr.address} (resolved from ${rh})`);
134
+ }
135
+ if (addr.family === 6 && blocked6(addr.address)) {
136
+ throw new Error(`Blocked IPv6 address: ${addr.address} (resolved from ${rh})`);
137
+ }
138
+ }
139
+
140
+ return { parsedUrl: p, rh, addrs };
141
+ }
142
+
143
+ // ── HTTP fetcher with SSRF protection and DNS pinning ─────────────────────
144
+ interface FetchResult { body: Buffer; statusCode: number; headers: Record<string, string> }
145
+
146
+ async function safeFetch(url: string, timeoutMs = REQUEST_TIMEOUT_MS): Promise<FetchResult> {
147
+ const prep = await prepTarget(url);
148
+ // Use first validated address for DNS pinning
149
+ const ch = prep.addrs[0];
150
+
151
+ return new Promise((resolve, reject) => {
152
+ const parsedUrl = prep.parsedUrl;
153
+ const isHttps = parsedUrl.protocol === 'https:';
154
+ const lib = isHttps ? https : http;
155
+ const port = parsedUrl.port ? parseInt(parsedUrl.port) : (isHttps ? 443 : 80);
156
+
157
+ const reqOptions: http.RequestOptions = {
158
+ hostname: parsedUrl.hostname,
159
+ port,
160
+ path: parsedUrl.pathname + parsedUrl.search,
161
+ method: 'GET',
162
+ headers: {
163
+ 'User-Agent': 'Mozilla/5.0 (compatible; ViteNativeGateway/1.0)',
164
+ 'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8',
165
+ 'Accept-Encoding': 'gzip, deflate, br',
166
+ 'Accept-Language': 'en-US,en;q=0.9',
167
+ },
168
+ timeout: timeoutMs,
169
+ // DNS PINNING: Force Node.js to use the pre-validated IP
170
+ // This prevents DNS rebinding attacks between validation and fetch
171
+ // Hand-trace: lookup(_h, _o, cb) => cb(null, ch.address, ch.family)
172
+ // ch.address = '93.184.216.34' (example.com, after validation)
173
+ // ch.family = 4
174
+ // Node uses this IP directly, no second DNS lookup
175
+ lookup: ((_h: string, _o: unknown, cb: any) => cb(null, ch.address, ch.family)) as any,
176
+ };
177
+
178
+ const req = lib.request(reqOptions, (res) => {
179
+ const chunks: Buffer[] = [];
180
+ let total = 0;
181
+
182
+ res.on('data', (chunk: Buffer) => {
183
+ total += chunk.length;
184
+ if (total > MAX_RESPONSE_BYTES) {
185
+ req.destroy(new Error(`Payload too large: ${total} bytes`));
186
+ return;
187
+ }
188
+ chunks.push(chunk);
189
+ });
190
+
191
+ res.on('end', () => {
192
+ const rawBody = Buffer.concat(chunks);
193
+ const encoding = (res.headers['content-encoding'] || '').toLowerCase();
194
+ const headers: Record<string, string> = {};
195
+ for (const [k, v] of Object.entries(res.headers)) {
196
+ if (typeof v === 'string') headers[k] = v;
197
+ }
198
+
199
+ let body: Buffer;
200
+ try {
201
+ if (encoding === 'gzip') {
202
+ body = zlib.gunzipSync(rawBody, { maxOutputLength: MAX_DECOMP_BYTES });
203
+ } else if (encoding === 'deflate') {
204
+ body = zlib.inflateSync(rawBody, { maxOutputLength: MAX_DECOMP_BYTES });
205
+ } else if (encoding === 'br') {
206
+ body = zlib.brotliDecompressSync(rawBody, { params: { [zlib.constants.BROTLI_PARAM_SIZE_HINT]: MAX_DECOMP_BYTES } });
207
+ } else {
208
+ body = rawBody;
209
+ }
210
+ } catch (e: any) {
211
+ reject(new Error(`Decompression failed: ${e?.message}`));
212
+ return;
213
+ }
214
+
215
+ resolve({ body, statusCode: res.statusCode || 0, headers });
216
+ });
217
+
218
+ res.on('error', reject);
219
+ });
220
+
221
+ req.on('error', reject);
222
+ req.on('timeout', () => req.destroy(new Error(`Request timeout after ${timeoutMs}ms`)));
223
+ req.end();
224
+ });
225
+ }
226
+
227
+ // ── HTML text extractor (no DOM parsing library) ──────────────────────────
228
+ function extractText(html: string, maxChars = MAX_PARSE_CHARS): string {
229
+ const capped = html.slice(0, maxChars);
230
+ return capped
231
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ' ')
232
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ' ')
233
+ .replace(/<[^>]+>/g, ' ')
234
+ .replace(/&nbsp;/g, ' ')
235
+ .replace(/&amp;/g, '&')
236
+ .replace(/&lt;/g, '<')
237
+ .replace(/&gt;/g, '>')
238
+ .replace(/&quot;/g, '"')
239
+ .replace(/&#039;/g, "'")
240
+ .replace(/\s+/g, ' ')
241
+ .trim()
242
+ .slice(0, 5000);
243
+ }
244
+
245
+ // ── Search engine parsers (bounded negation — anti-ReDoS) ─────────────────
246
+ export interface SearchResult {
247
+ url: string;
248
+ title: string;
249
+ snippet: string;
250
+ engine: string;
251
+ }
252
+
253
+ function searchDDG(html: string): SearchResult[] {
254
+ const results: SearchResult[] = [];
255
+ const seen = new Set<string>();
256
+ const capped = html.slice(0, MAX_PARSE_CHARS);
257
+
258
+ // Match href with bounded negation [^"']+ — NOT (.*)
259
+ const linkRe = /href=["']([^"']*https?:\/\/[^"']+)["'][^>]*class=["'][^"']*result__url[^"']*["']/gi;
260
+ let m: RegExpExecArray | null;
261
+ while ((m = linkRe.exec(capped)) !== null && results.length < MAX_RESULTS_PER_ENGINE) {
262
+ try {
263
+ const url = decodeURIComponent(m[1]).split('?')[0];
264
+ if (url.includes('duckduckgo.com') || seen.has(url)) continue;
265
+ seen.add(url);
266
+ results.push({ url, title: '', snippet: '', engine: 'ddg' });
267
+ } catch {}
268
+ }
269
+
270
+ // Fallback: uddg parameter extraction
271
+ if (results.length === 0) {
272
+ const uddgRe = /uddg=(https?[^&"'\s]+)/gi;
273
+ while ((m = uddgRe.exec(capped)) !== null && results.length < MAX_RESULTS_PER_ENGINE) {
274
+ try {
275
+ const url = decodeURIComponent(m[1]);
276
+ if (!seen.has(url) && url.startsWith('http')) {
277
+ seen.add(url);
278
+ results.push({ url, title: '', snippet: '', engine: 'ddg' });
279
+ }
280
+ } catch {}
281
+ }
282
+ }
283
+
284
+ return results;
285
+ }
286
+
287
+ function searchBing(html: string): SearchResult[] {
288
+ const results: SearchResult[] = [];
289
+ const seen = new Set<string>();
290
+ const capped = html.slice(0, MAX_PARSE_CHARS);
291
+
292
+ // Parse .b_algo list items
293
+ const algoBlocks = capped.split(/<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>/gi).slice(1);
294
+ for (const block of algoBlocks.slice(0, MAX_RESULTS_PER_ENGINE)) {
295
+ // href must use bounded [^"'] — captures group 1
296
+ const hrefRe = /<h2[^>]*><a[^>]+href=["']([^"']+)["'][^>]*>([^<]*)<\/a>/i;
297
+ const hrefMatch = block.match(hrefRe);
298
+ if (!hrefMatch) continue;
299
+ const url = hrefMatch[1];
300
+ if (!url.startsWith('http') || url.includes('bing.com') || seen.has(url)) continue;
301
+ const title = hrefMatch[2] || '';
302
+ const snippetMatch = block.match(/<p[^>]*>([^<]{10,500})<\/p>/i);
303
+ const snippet = snippetMatch ? snippetMatch[1].trim() : '';
304
+ seen.add(url);
305
+ results.push({ url, title, snippet, engine: 'bing' });
306
+ }
307
+
308
+ return results;
309
+ }
310
+
311
+ function searchYahoo(html: string): SearchResult[] {
312
+ const results: SearchResult[] = [];
313
+ const seen = new Set<string>();
314
+ const capped = html.slice(0, MAX_PARSE_CHARS);
315
+
316
+ // Yahoo result links — bounded negation
317
+ const re = /href=["'](https?:\/\/[^"']+)["'][^>]*(?:class=["'][^"']*(?:td-hu|d-ib)[^"']*["']|target=["']_blank["'])/gi;
318
+ let m: RegExpExecArray | null;
319
+ while ((m = re.exec(capped)) !== null && results.length < MAX_RESULTS_PER_ENGINE) {
320
+ const url = m[1];
321
+ if (!url || url.includes('yahoo.com') || url.includes('yimg.com') || seen.has(url)) continue;
322
+ seen.add(url);
323
+ results.push({ url, title: '', snippet: '', engine: 'yahoo' });
324
+ }
325
+
326
+ return results;
327
+ }
328
+
329
+ function searchMojeek(html: string): SearchResult[] {
330
+ const results: SearchResult[] = [];
331
+ const seen = new Set<string>();
332
+ const capped = html.slice(0, MAX_PARSE_CHARS);
333
+
334
+ // Mojeek uses class="ob" for result links — bounded negation
335
+ const re = /class=["']ob["'][^>]*href=["']([^"']+)["']|href=["']([^"']+)["'][^>]*class=["']ob["']/gi;
336
+ let m: RegExpExecArray | null;
337
+ while ((m = re.exec(capped)) !== null && results.length < MAX_RESULTS_PER_ENGINE) {
338
+ const url = m[1] || m[2];
339
+ if (!url || !url.startsWith('http') || seen.has(url)) continue;
340
+ seen.add(url);
341
+ const snippetMatch = capped.slice(m.index, m.index + 800).match(/<p[^>]*class=["'][^"']*s[^"']*["'][^>]*>([^<]{10,400})<\/p>/i);
342
+ results.push({ url, title: '', snippet: snippetMatch ? snippetMatch[1].trim() : '', engine: 'mojeek' });
343
+ }
344
+
345
+ return results;
346
+ }
347
+
348
+ // ── Circuit Breaker ───────────────────────────────────────────────────────
349
+ const circuitBreaker = new Map<string, { failures: number; openUntil: number }>();
350
+
351
+ function isCircuitOpen(engine: string): boolean {
352
+ const state = circuitBreaker.get(engine);
353
+ if (!state) return false;
354
+ if (state.openUntil > Date.now()) return true;
355
+ if (state.openUntil > 0 && state.openUntil <= Date.now()) {
356
+ circuitBreaker.delete(engine);
357
+ }
358
+ return false;
359
+ }
360
+
361
+ function recordFailure(engine: string): void {
362
+ const state = circuitBreaker.get(engine) || { failures: 0, openUntil: 0 };
363
+ state.failures++;
364
+ if (state.failures >= 3) {
365
+ state.openUntil = Date.now() + 60_000; // Open for 60 seconds
366
+ console.warn(`[native-scraper] Circuit breaker OPEN for ${engine} — 3 consecutive failures`);
367
+ }
368
+ circuitBreaker.set(engine, state);
369
+ }
370
+
371
+ function recordSuccess(engine: string): void {
372
+ circuitBreaker.delete(engine);
373
+ }
374
+
375
+ // ── Engine search function ────────────────────────────────────────────────
376
+ async function runEngine(engine: string, query: string, parser: (html: string) => SearchResult[]): Promise<SearchResult[]> {
377
+ if (isCircuitOpen(engine)) return [];
378
+
379
+ const engineUrls: Record<string, string> = {
380
+ ddg: `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
381
+ bing: `https://www.bing.com/search?q=${encodeURIComponent(query)}`,
382
+ yahoo: `https://search.yahoo.com/search?p=${encodeURIComponent(query)}`,
383
+ mojeek: `https://www.mojeek.com/search?q=${encodeURIComponent(query)}`,
384
+ };
385
+
386
+ const url = engineUrls[engine];
387
+ if (!url) return [];
388
+
389
+ try {
390
+ const { body } = await safeFetch(url);
391
+ const html = body.toString('utf8');
392
+ const results = parser(html);
393
+ if (results.length > 0) recordSuccess(engine);
394
+ return results;
395
+ } catch (e: any) {
396
+ recordFailure(engine);
397
+ console.warn(`[native-scraper] ${engine} search failed:`, e?.message?.slice(0, 100));
398
+ return [];
399
+ }
400
+ }
401
+
402
+ // ── RRF (Reciprocal Rank Fusion) ─────────────────────────────────────────
403
+ const ENGINE_WEIGHTS: Record<string, number> = {
404
+ ddg: 1.0, bing: 1.0, yahoo: 0.9, mojeek: 0.8,
405
+ };
406
+
407
+ function computeRRF(engineResults: Map<string, SearchResult[]>): Map<string, number> {
408
+ const scores = new Map<string, number>();
409
+ for (const [engine, results] of engineResults) {
410
+ const weight = ENGINE_WEIGHTS[engine] ?? 0.7;
411
+ results.forEach((r, rank) => {
412
+ // RRF formula: score = (1 / (60 + rank)) * engineWeight
413
+ const rrf = (1 / (60 + rank)) * weight;
414
+ scores.set(r.url, (scores.get(r.url) ?? 0) + rrf);
415
+ });
416
+ }
417
+ return scores;
418
+ }
419
+
420
+ // ── SimHash similarity for MMR ────────────────────────────────────────────
421
+ function simHashSimilarity(a: string, b: string): number {
422
+ if (a === b) return 1.0;
423
+ const aBig = BigInt(`0x${a.padStart(16, '0')}`);
424
+ const bBig = BigInt(`0x${b.padStart(16, '0')}`);
425
+ let diff = aBig ^ bBig;
426
+ let hamming = 0;
427
+ while (diff > 0n) { hamming += Number(diff & 1n); diff >>= 1n; }
428
+ return 1 - (hamming / 64);
429
+ }
430
+
431
+ // ── MMR (Maximal Marginal Relevance) ─────────────────────────────────────
432
+ // Hand-trace for MMR score:
433
+ // Result A: RRF=0.015, not yet selected → MMR = 0.7*0.015 - 0.3*0 = 0.0105
434
+ // Result B: RRF=0.014, SimHash distance=0 from A (identical content)
435
+ // similarity = 1 - (0/64) = 1.0
436
+ // MMR = 0.7*0.014 - 0.3*1.0 = 0.0098 - 0.3 = -0.2902
437
+ // Result B's final MMR score = -0.2902 (penalized for being a duplicate)
438
+ function applyMMR(
439
+ candidates: SearchResult[],
440
+ rrfScores: Map<string, number>,
441
+ computeHash: (text: string) => string,
442
+ count = 10,
443
+ ): SearchResult[] {
444
+ if (candidates.length === 0) return [];
445
+
446
+ const selected: SearchResult[] = [];
447
+ const remaining = candidates.map(r => ({ ...r, simHash: computeHash(r.url + ' ' + r.title) }));
448
+
449
+ while (selected.length < count && remaining.length > 0) {
450
+ let bestIdx = -1;
451
+ let bestScore = -Infinity;
452
+
453
+ for (let i = 0; i < remaining.length; i++) {
454
+ const r = remaining[i];
455
+ const rrfScore = rrfScores.get(r.url) ?? 0;
456
+
457
+ // Max similarity to already-selected items
458
+ let maxSim = 0;
459
+ for (const s of selected) {
460
+ const sim = simHashSimilarity(r.simHash, (s as any).simHash ?? '0000000000000000');
461
+ if (sim > maxSim) maxSim = sim;
462
+ }
463
+
464
+ // MMR formula: 0.7*RRF - 0.3*MaxSimilarity
465
+ const mmrScore = (0.7 * rrfScore) - (0.3 * maxSim);
466
+ if (mmrScore > bestScore) { bestScore = mmrScore; bestIdx = i; }
467
+ }
468
+
469
+ if (bestIdx === -1) break;
470
+ selected.push(remaining[bestIdx]);
471
+ remaining.splice(bestIdx, 1);
472
+ }
473
+
474
+ return selected;
475
+ }
476
+
477
+ // ── Rate Limiting (Token Bucket) ──────────────────────────────────────────
478
+ const rateBuckets = new Map<string, { tokens: number; lastRefill: number }>();
479
+ const BUCKET_CAPACITY = 20;
480
+ const REFILL_RATE = 5; // tokens per second
481
+
482
+ function checkRateLimit(clientId: string): boolean {
483
+ const now = Date.now();
484
+ let bucket = rateBuckets.get(clientId);
485
+ if (!bucket) {
486
+ bucket = { tokens: BUCKET_CAPACITY, lastRefill: now };
487
+ rateBuckets.set(clientId, bucket);
488
+ }
489
+ const elapsed = (now - bucket.lastRefill) / 1000;
490
+ bucket.tokens = Math.min(BUCKET_CAPACITY, bucket.tokens + elapsed * REFILL_RATE);
491
+ bucket.lastRefill = now;
492
+ if (bucket.tokens < 1) return false; // Rate limited
493
+ bucket.tokens--;
494
+ return true;
495
+ }
496
+
497
+ // ── Agent Loop Detection ──────────────────────────────────────────────────
498
+ const agentLoopTracker = new Map<string, number[]>();
499
+ const LOOP_WINDOW_MS = 120_000;
500
+ const LOOP_MAX_REQUESTS = 5;
501
+
502
+ function checkAgentLoop(url: string): boolean {
503
+ const now = Date.now();
504
+ const key = createHash('sha256').update(url).digest('hex').slice(0, 16);
505
+ const timestamps = (agentLoopTracker.get(key) ?? []).filter(t => now - t < LOOP_WINDOW_MS);
506
+ timestamps.push(now);
507
+ agentLoopTracker.set(key, timestamps);
508
+ return timestamps.length > LOOP_MAX_REQUESTS;
509
+ }
510
+
511
+ // ── Cross-Origin Protection ───────────────────────────────────────────────
512
+ function rejectCrossOrigin(req: http.IncomingMessage): boolean {
513
+ const fetchSite = req.headers['sec-fetch-site'];
514
+ if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') return true;
515
+ const origin = req.headers['origin'];
516
+ if (origin && !origin.startsWith('http://localhost') && !origin.startsWith('http://127.0.0.1')) return true;
517
+ const referer = req.headers['referer'];
518
+ if (referer && !referer.startsWith('http://localhost') && !referer.startsWith('http://127.0.0.1')) return true;
519
+ return false;
520
+ }
521
+
522
+ // ── Main Metasearch ───────────────────────────────────────────────────────
523
+ async function runMetasearch(query: string, policy: { useDdg?: boolean; useBing?: boolean; useYahoo?: boolean; useMojeek?: boolean } = {}, count = 10) {
524
+ const { useDdg = true, useBing = true, useYahoo = true, useMojeek = true } = policy;
525
+
526
+ // Resilience: .catch(() => []) prevents one failing engine from crashing all
527
+ const [ddgResults, bingResults, yahooResults, mojeekResults] = await Promise.all([
528
+ useDdg ? runEngine('ddg', query, searchDDG).catch(() => []) : Promise.resolve([]),
529
+ useBing ? runEngine('bing', query, searchBing).catch(() => []) : Promise.resolve([]),
530
+ useYahoo ? runEngine('yahoo', query, searchYahoo).catch(() => []) : Promise.resolve([]),
531
+ useMojeek ? runEngine('mojeek', query, searchMojeek).catch(() => []) : Promise.resolve([]),
532
+ ]);
533
+
534
+ const engineResults = new Map<string, SearchResult[]>([
535
+ ['ddg', ddgResults], ['bing', bingResults], ['yahoo', yahooResults], ['mojeek', mojeekResults],
536
+ ]);
537
+
538
+ // All unique candidates
539
+ const all = [...ddgResults, ...bingResults, ...yahooResults, ...mojeekResults];
540
+ const urlToResult = new Map<string, SearchResult>();
541
+ for (const r of all) if (!urlToResult.has(r.url)) urlToResult.set(r.url, r);
542
+ const candidates = [...urlToResult.values()];
543
+
544
+ // RRF scoring
545
+ const rrfScores = computeRRF(engineResults);
546
+
547
+ // Sort by RRF first
548
+ candidates.sort((a, b) => (rrfScores.get(b.url) ?? 0) - (rrfScores.get(a.url) ?? 0));
549
+
550
+ // Apply MMR for deduplication
551
+ const { computeSimHash64 } = await import('./vite-native-knowledge-store.js');
552
+ const ranked = applyMMR(candidates, rrfScores, computeSimHash64, count);
553
+
554
+ return {
555
+ query,
556
+ results: ranked.map((r, i) => ({
557
+ rank: i + 1,
558
+ url: r.url,
559
+ title: r.title,
560
+ snippet: r.snippet,
561
+ engine: r.engine,
562
+ rrfScore: rrfScores.get(r.url) ?? 0,
563
+ })),
564
+ totalCandidates: candidates.length,
565
+ enginesQueried: [useDdg && 'ddg', useBing && 'bing', useYahoo && 'yahoo', useMojeek && 'mojeek'].filter(Boolean),
566
+ circuitBreakers: Object.fromEntries([...circuitBreaker.entries()].map(([k, v]) => [k, v.openUntil > Date.now() ? 'open' : 'closed'])),
567
+ };
568
+ }
569
+
570
+ // ── JSON helpers ──────────────────────────────────────────────────────────
571
+ function jsonResponse(res: http.ServerResponse, data: unknown, status = 200): void {
572
+ const body = JSON.stringify(data);
573
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://localhost:5173' });
574
+ res.end(body);
575
+ }
576
+
577
+ function problemResponse(res: http.ServerResponse, type: string, status: number, detail: string): void {
578
+ jsonResponse(res, { type, status, detail }, status);
579
+ }
580
+
581
+ function parseBody(req: http.IncomingMessage): Promise<string> {
582
+ return new Promise((resolve, reject) => {
583
+ let body = '';
584
+ req.on('data', (chunk: any) => { body += chunk.toString(); });
585
+ req.on('end', () => resolve(body));
586
+ req.on('error', reject);
587
+ });
588
+ }
589
+
590
+ // ── SSE heartbeat ─────────────────────────────────────────────────────────
591
+ function setupSSEHeartbeat(res: http.ServerResponse): ReturnType<typeof setInterval> {
592
+ const timer = setInterval(() => {
593
+ try { res.write(':heartbeat\n\n'); } catch { clearInterval(timer); }
594
+ }, 10_000);
595
+ (timer as any).unref?.();
596
+ return timer;
597
+ }
598
+
599
+ // ── Route handler ─────────────────────────────────────────────────────────
600
+ async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse, next: () => void): Promise<void> {
601
+ const url = req.url || '';
602
+ const urlObj = new URL(url, 'http://localhost');
603
+ const path = urlObj.pathname;
604
+
605
+ // Only handle /api/native-* routes
606
+ if (!path.startsWith('/api/native-') && !path.startsWith('/api/audit/')) {
607
+ return next();
608
+ }
609
+
610
+ // Cross-origin protection
611
+ if (rejectCrossOrigin(req)) {
612
+ return problemResponse(res, 'cross-origin', 403, 'Cross-origin requests rejected');
613
+ }
614
+
615
+ // CORS preflight
616
+ if (req.method === 'OPTIONS') {
617
+ res.writeHead(204, { 'Access-Control-Allow-Origin': 'http://localhost:5173', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' });
618
+ res.end();
619
+ return;
620
+ }
621
+
622
+ const clientIp = String(req.socket?.remoteAddress || 'unknown');
623
+
624
+ try {
625
+ // ── GET /api/native-search ───────────────────────────────────────────
626
+ if (path === '/api/native-search' && req.method === 'GET') {
627
+ const q = urlObj.searchParams.get('q')?.trim();
628
+ if (!q) return problemResponse(res, 'missing_query', 400, "The 'q' query parameter is required.");
629
+ if (!checkRateLimit(clientIp)) return problemResponse(res, 'rate_limited', 429, 'Rate limit exceeded.');
630
+ if (checkAgentLoop(q)) return problemResponse(res, 'agent_loop', 429, 'Agent loop detected for this query.');
631
+ const count = Math.max(1, Math.min(20, parseInt(urlObj.searchParams.get('count') || '5')));
632
+ const results = await runMetasearch(q, {}, count);
633
+ return jsonResponse(res, results);
634
+ }
635
+
636
+ // ── GET /api/native-search/stream ────────────────────────────────────
637
+ if (path === '/api/native-search/stream' && req.method === 'GET') {
638
+ const q = urlObj.searchParams.get('q')?.trim();
639
+ if (!q) return problemResponse(res, 'missing_query', 400, "The 'q' query parameter is required.");
640
+ if (!checkRateLimit(clientIp)) return problemResponse(res, 'rate_limited', 429, 'Rate limit exceeded.');
641
+
642
+ res.writeHead(200, {
643
+ 'Content-Type': 'text/event-stream',
644
+ 'Cache-Control': 'no-cache',
645
+ 'Connection': 'keep-alive',
646
+ 'Access-Control-Allow-Origin': 'http://localhost:5173',
647
+ });
648
+
649
+ const heartbeat = setupSSEHeartbeat(res);
650
+ try {
651
+ res.write('data: {"status":"searching"}\n\n');
652
+ const results = await runMetasearch(q, {}, 10);
653
+ for (const r of results.results) {
654
+ res.write(`data: ${JSON.stringify(r)}\n\n`);
655
+ }
656
+ res.write('data: {"status":"done"}\n\n');
657
+ } finally {
658
+ clearInterval(heartbeat);
659
+ res.end();
660
+ }
661
+ return;
662
+ }
663
+
664
+ // ── GET /api/native-read ─────────────────────────────────────────────
665
+ if (path === '/api/native-read' && req.method === 'GET') {
666
+ const rawUrl = urlObj.searchParams.get('url');
667
+ if (!rawUrl) return problemResponse(res, 'missing_query', 400, 'url parameter required.');
668
+ if (!checkRateLimit(clientIp)) return problemResponse(res, 'rate_limited', 429, 'Rate limit exceeded.');
669
+ try {
670
+ const { body, statusCode, headers } = await safeFetch(rawUrl);
671
+ const text = extractText(body.toString('utf8'));
672
+ return jsonResponse(res, { url: rawUrl, statusCode, contentType: headers['content-type'], text });
673
+ } catch (e: any) {
674
+ return problemResponse(res, 'ssrf_blocked', 400, e?.message ?? 'URL blocked');
675
+ }
676
+ }
677
+
678
+ // ── POST /api/native-ingest ──────────────────────────────────────────
679
+ if (path === '/api/native-ingest' && req.method === 'POST') {
680
+ const body = await parseBody(req);
681
+ let data: any;
682
+ try { data = JSON.parse(body); } catch { return problemResponse(res, 'internal_error', 400, 'Invalid JSON body'); }
683
+ const { kind, ref, summary } = data;
684
+ if (!kind || !ref || !summary) return problemResponse(res, 'missing_query', 400, 'kind, ref, summary required');
685
+ const { recordKnowledge } = await import('./vite-native-knowledge-store.js');
686
+ const result = recordKnowledge(kind, ref, summary);
687
+ return jsonResponse(res, { ok: !!result, id: result?.id });
688
+ }
689
+
690
+ // ── GET /api/native-knowledge ────────────────────────────────────────
691
+ if (path === '/api/native-knowledge' && req.method === 'GET') {
692
+ const q = urlObj.searchParams.get('q');
693
+ if (!q) return problemResponse(res, 'missing_query', 400, 'q required');
694
+ const limit = Math.max(1, Math.min(100, parseInt(urlObj.searchParams.get('limit') || '10')));
695
+ const { searchKnowledge } = await import('./vite-native-knowledge-store.js');
696
+ return jsonResponse(res, { results: searchKnowledge(q, limit) });
697
+ }
698
+
699
+ // ── GET /api/native-knowledge/stats ─────────────────────────────────
700
+ if (path === '/api/native-knowledge/stats') {
701
+ const { getKnowledgeStats } = await import('./vite-native-knowledge-store.js');
702
+ return jsonResponse(res, getKnowledgeStats());
703
+ }
704
+
705
+ // ── POST /api/native-knowledge/repair-index ──────────────────────────
706
+ if (path === '/api/native-knowledge/repair-index' && req.method === 'POST') {
707
+ const { repairKnowledgeFts } = await import('./vite-native-knowledge-store.js');
708
+ return jsonResponse(res, { ok: repairKnowledgeFts() });
709
+ }
710
+
711
+ // ── GET /api/native-runtime ──────────────────────────────────────────
712
+ if (path === '/api/native-runtime') {
713
+ const { getRuntimeStats, getRecentEvents } = await import('./vite-native-runtime-plane.js');
714
+ const limit = parseInt(urlObj.searchParams.get('events') || '50');
715
+ return jsonResponse(res, { ...getRuntimeStats(), recentEvents: getRecentEvents(limit) });
716
+ }
717
+
718
+ // ── GET/POST /api/native-policy ──────────────────────────────────────
719
+ if (path === '/api/native-policy') {
720
+ const { getAllPolicyFlags, setPolicyOverride } = await import('./vite-native-policy-plane.js');
721
+ if (req.method === 'GET') {
722
+ return jsonResponse(res, { flags: getAllPolicyFlags() });
723
+ }
724
+ if (req.method === 'POST') {
725
+ const body = await parseBody(req);
726
+ let data: any;
727
+ try { data = JSON.parse(body); } catch { return problemResponse(res, 'internal_error', 400, 'Invalid JSON'); }
728
+ const { name, value } = data;
729
+ if (!name) return problemResponse(res, 'missing_query', 400, 'name required');
730
+ return jsonResponse(res, { ok: setPolicyOverride(name, value) });
731
+ }
732
+ }
733
+
734
+ // ── GET /api/native-doctor ───────────────────────────────────────────
735
+ if (path === '/api/native-doctor') {
736
+ const deep = urlObj.searchParams.get('deep') === 'true';
737
+ const { buildDoctorReport } = await import('./vite-native-doctor-plane.js');
738
+ return jsonResponse(res, await buildDoctorReport({ deep }));
739
+ }
740
+
741
+ // ── GET /api/native-selftest ─────────────────────────────────────────
742
+ if (path === '/api/native-selftest') {
743
+ const { runStaticSelfTest } = await import('./vite-native-selftest.js');
744
+ return jsonResponse(res, runStaticSelfTest());
745
+ }
746
+
747
+ // ── GET /api/native-selftest/functional ─────────────────────────────
748
+ if (path === '/api/native-selftest/functional') {
749
+ const { runFunctionalTests } = await import('./vite-native-selftest-functional.js');
750
+ return jsonResponse(res, runFunctionalTests());
751
+ }
752
+
753
+ // ── POST /api/native-snapshot ────────────────────────────────────────
754
+ if (path === '/api/native-snapshot' && req.method === 'POST') {
755
+ const { createHotBackup } = await import('./vite-native-snapshot-plane.js');
756
+ const label = urlObj.searchParams.get('label') || undefined;
757
+ return jsonResponse(res, await createHotBackup(label));
758
+ }
759
+
760
+ // ── GET /api/native-snapshot ─────────────────────────────────────────
761
+ if (path === '/api/native-snapshot' && req.method === 'GET') {
762
+ const { getSnapshotManifest } = await import('./vite-native-snapshot-plane.js');
763
+ return jsonResponse(res, await getSnapshotManifest());
764
+ }
765
+
766
+ // ── POST /api/native-maintenance/run ────────────────────────────────
767
+ if (path === '/api/native-maintenance/run' && req.method === 'POST') {
768
+ const { getDb, purgeGhostFiles, repairKnowledgeFts } = await import('./vite-native-knowledge-store.js');
769
+ const db = getDb();
770
+ let vacuumOk = false;
771
+ try { if (db) { db.exec('VACUUM;'); vacuumOk = true; } } catch {}
772
+ const ghostsPurged = purgeGhostFiles();
773
+ const ftsRepaired = repairKnowledgeFts();
774
+ return jsonResponse(res, { vacuum: vacuumOk, ghostsPurged, ftsRepaired });
775
+ }
776
+
777
+ // ── GET /api/audit/verify ────────────────────────────────────────────
778
+ if (path === '/api/audit/verify') {
779
+ const { getKnowledgeStats } = await import('./vite-native-knowledge-store.js');
780
+ const stats = getKnowledgeStats();
781
+ return jsonResponse(res, { ok: stats.available, dbPath: stats.dbPath, totalRecords: stats.totalRecords });
782
+ }
783
+
784
+ // ── OpenAPI spec ─────────────────────────────────────────────────────
785
+ if (path === '/api/native-openapi.json') {
786
+ const { getNativeOpenApiDocument } = await import('./vite-native-contract-plane.js');
787
+ const base = `http://${req.headers.host || 'localhost:5173'}`;
788
+ return jsonResponse(res, getNativeOpenApiDocument(base));
789
+ }
790
+
791
+ // No matching route
792
+ next();
793
+ } catch (e: any) {
794
+ console.error('[native-scraper] Unhandled error:', e?.message);
795
+ problemResponse(res, 'internal_error', 500, e?.message ?? 'Internal error');
796
+ }
797
+ }
798
+
799
+ // ── Vite Plugin ───────────────────────────────────────────────────────────
800
+ export function nativeScraperPlugin(): Plugin {
801
+ return {
802
+ name: 'vite-native-scraper',
803
+
804
+ configureServer(server: ViteDevServer) {
805
+ server.middlewares.use(async (req: any, res: any, next: any) => {
806
+ await handleRequest(req, res, next);
807
+ });
808
+
809
+ // Boot doctor and self-test asynchronously — MUST use .unref() to avoid zombie process
810
+ const bootTimer = setTimeout(async () => {
811
+ try {
812
+ const { runStaticSelfTest } = await import('./vite-native-selftest.js');
813
+ const stResult = runStaticSelfTest();
814
+ if (!stResult.passed) {
815
+ const { applySelfTestRemediation } = await import('./vite-native-policy-plane.js');
816
+ applySelfTestRemediation(stResult);
817
+ }
818
+ console.log(`[native-scraper] Boot self-test: ${stResult.passed ? '✓ passed' : `✗ ${stResult.failures.length} failure(s)`}`);
819
+ } catch {}
820
+ }, 2000);
821
+ (bootTimer as any).unref?.();
822
+
823
+ // Auto-RAG file watcher — watch src directory for changes
824
+ let watcher: ReturnType<typeof watch> | null = null;
825
+ try {
826
+ const srcDir = join(__dirname, '..', '..', '..', 'src');
827
+ watcher = watch(srcDir, { recursive: true }, async (_event: string, filename: string | null) => {
828
+ if (!filename || !/\.(ts|tsx|md|txt)$/.test(filename)) return;
829
+ const { shouldDeferBackgroundWork } = await import('./vite-native-runtime-plane.js').catch(() => ({ shouldDeferBackgroundWork: () => true }));
830
+ if (shouldDeferBackgroundWork('rag')) return;
831
+ // Auto-ingest changed file summaries
832
+ // (lightweight: just records the file path, not full content)
833
+ });
834
+ } catch {}
835
+
836
+ // Close watcher on server shutdown
837
+ server.httpServer?.on('close', () => {
838
+ try { watcher?.close(); } catch {}
839
+ });
840
+ },
841
+
842
+ configurePreviewServer(server: any) {
843
+ server.middlewares.use(async (req: any, res: any, next: any) => {
844
+ await handleRequest(req, res, next);
845
+ });
846
+ },
847
+ };
848
+ }