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,1871 @@
1
+ /**
2
+ * V15 Calibration Dialog — Rigor Guard Calibration Live UI
3
+ *
4
+ * Layout matches the uploaded reference screenshots:
5
+ * - Title bar with V15 Pipeline Enabled / Single Judge / Continuous toggles
6
+ * - Two sub-tabs: "Live Compare" (single Q&A) and "Batch Bank" (multi-Q calibration)
7
+ * - Header stats row: API key, V15 mean, Baseline mean, Δ delta
8
+ * - Config profile bar (4-Stage, N-Deep(n), Cluster(n), SLOOP pages, Template, Style override)
9
+ * - Live Compare tab: prompt box, roster, 246-Defense Catalog Pack card,
10
+ * Long-Report Cohesion Pass card, Gate Testbed card
11
+ * - Batch Bank tab: number of questions (1-5), custom-question input, sequential
12
+ * row-by-row completion, 3-score display (baseline, V15 guard, third-party judge),
13
+ * full untruncated result display, and pipeline diagram
14
+ * - Divergence review log (guard vs judge disagreements → improvement suggestions)
15
+ */
16
+ import { useEffect, useMemo, useRef, useState } from "react";
17
+ import { getGeminiKey, getV15Enabled as readV15Enabled, setV15Enabled as writeV15Enabled, subscribeV15 } from "@/lib/v15-state";
18
+ import {
19
+ runV15OnQuestion, runBaselineOnQuestion, analyzeDivergence, runCohesionPass, runComparativeJudge,
20
+ getDivergenceLog, saveDivergenceEntry, clearDivergenceLog,
21
+ type V15RunOutcome, type V15Profile, type DivergenceEntry, type CohesionPassResult, type ComparativeJudgeResult,
22
+ } from "@/lib/v15-pipeline";
23
+ import { ARCHETYPES } from "@/lib/williams-style";
24
+ import { shuffleQuestions, type CalibQuestion } from "@/lib/v15-questions";
25
+ import { GEMINI_ELO_ROSTER, getModelEloInfo } from "@/lib/elo-registry";
26
+ import { getAdvancedGatesEnabled, listTestbedGates, setAdvancedGatesEnabled } from "@/lib/v15-gate-testbed";
27
+ import { snapshotAllUsage } from "@/lib/v15-rate-limiter";
28
+
29
+ interface Row {
30
+ q: CalibQuestion;
31
+ stage: "queued" | "baseline" | "v15" | "judging" | "done" | "error";
32
+ baseline?: V15RunOutcome;
33
+ v15?: V15RunOutcome;
34
+ comparative?: ComparativeJudgeResult;
35
+ }
36
+
37
+ interface Props { open: boolean; onClose: () => void; }
38
+
39
+ const TARGET_SCORE = 9.0;
40
+ const DEFAULT_BATCH_SIZE = 1;
41
+ const MAX_BATCH_SIZE = 5;
42
+ const MAX_CONTINUOUS_ROUNDS = 6;
43
+
44
+ const TEMPLATE_IDS = ["OMEGA-STRATEGY","OMEGA-DILIGENCE","OMEGA-DISCOVERY","OMEGA-COMPLIANCE","OMEGA-BUILD","OMEGA-SCIENCE","OMEGA-CRISIS","NIH-GRANT-SRF"];
45
+ const STYLE_OVERRIDES = [
46
+ "--mckinsey-classic","--mgi-research","--bcg-tmodel","--bcg-perspective","--bain-pe","--bain-strategy",
47
+ "--deloitte-engagement","--deloitte-regoutlook","--strategy&-cds","--pwc-ceosurvey","--ey-parthenon",
48
+ "--ey-sectoroutlook","--kpmg-advisory","--kpmg-ceooutlook","--olwyman-finserv","--rolandberger","--lek",
49
+ "--kearney","--alixpartners","--accenture-transform","--accenture-techvision","--ibm","--capgemini",
50
+ "--gao-audit","--gao-par","--federal-rfp","--govcon","--nih-r01","--nih-rppr","--nsf-pappg","--imrad",
51
+ "--prisma","--policybrief","--market-entry","--cdd","--digital-assess","--esg-csrd","--erm-coso","--qofe","--healthcare",
52
+ ];
53
+
54
+ type Tab = "live" | "batch" | "guide";
55
+
56
+ export function V15CalibrationDialog({ open, onClose }: Props) {
57
+ const [tab, setTab] = useState<Tab>("live");
58
+
59
+ // Global toggles. V15 enabled is SHARED with the floating overlay via v15-state.
60
+ const [v15Enabled, setV15EnabledState] = useState(() => readV15Enabled());
61
+ const [singleJudge, setSingleJudge] = useState(true);
62
+ const [continuousMode, setContinuousMode] = useState(false);
63
+
64
+ // Profile knobs — mirror the real pipeline exactly
65
+ const [fourStage, setFourStage] = useState(true);
66
+ const [nDeep, setNDeep] = useState(true);
67
+ const [nDeepPasses, setNDeepPasses] = useState(3);
68
+ const [cluster, setCluster] = useState(true);
69
+ const [clusterSize, setClusterSize] = useState(5);
70
+ const [sloop, setSloop] = useState(true);
71
+ const [sloopPages, setSloopPages] = useState(4);
72
+ const [templateId, setTemplateId] = useState<string>("OMEGA-STRATEGY");
73
+ const [styleOverride, setStyleOverride] = useState<string>("--bain-pe");
74
+ const [williamsPersona, setWilliamsPersona] = useState<string>("");
75
+ const [adversarial, setAdversarial] = useState(true);
76
+ const [webSearch, setWebSearch] = useState(false);
77
+ const [webOg, setWebOg] = useState(true);
78
+ const [webPrisma, setWebPrisma] = useState(false);
79
+ const [webJina, setWebJina] = useState(false);
80
+ const [webSearxng, setWebSearxng] = useState(true);
81
+ const [bestOfNModels, setBestOfNModels] = useState(2);
82
+ const [bestOfNHypotheses, setBestOfNHypotheses] = useState(5);
83
+ const [bestOfNPackHypotheses, setBestOfNPackHypotheses] = useState(false);
84
+ const [searxngUrl, setSearxngUrl] = useState(() => { try { return localStorage.getItem("veritas.v15.searxngUrl") || "http://localhost:8080"; } catch { return "http://localhost:8080"; } });
85
+ const [searxngApiKey, setSearxngApiKey] = useState(() => { try { return localStorage.getItem("veritas.v15.searxngApiKey") || ""; } catch { return ""; } });
86
+ const [searxngCategories, setSearxngCategories] = useState(() => { try { return localStorage.getItem("veritas.v15.searxngCategories") || "general"; } catch { return "general"; } });
87
+ const [searxngLanguage, setSearxngLanguage] = useState(() => { try { return localStorage.getItem("veritas.v15.searxngLanguage") || "en"; } catch { return "en"; } });
88
+ const [searxngSafe, setSearxngSafe] = useState(() => { try { return localStorage.getItem("veritas.v15.searxngSafe") || "0"; } catch { return "0"; } });
89
+ const [searxngTesting, setSearxngTesting] = useState(false);
90
+ const [searxngTestResult, setSearxngTestResult] = useState<{ ok: boolean; message: string } | null>(null);
91
+ async function testSearxngConnection() {
92
+ setSearxngTesting(true); setSearxngTestResult(null);
93
+ try {
94
+ const base = searxngUrl.replace(/\/+$/, "");
95
+ const params = new URLSearchParams({ q: "test", format: "json", categories: searxngCategories, language: searxngLanguage, safesearch: searxngSafe });
96
+ const headers: Record<string, string> = { Accept: "application/json" };
97
+ if (searxngApiKey) headers["Authorization"] = `Bearer ${searxngApiKey}`;
98
+ const res = await fetch(`${base}/search?${params.toString()}`, { headers });
99
+ if (!res.ok) { setSearxngTestResult({ ok: false, message: `HTTP ${res.status} — ensure format=json is enabled in SearXNG settings.yml.` }); return; }
100
+ const json = await res.json();
101
+ const count = Array.isArray(json.results) ? json.results.length : 0;
102
+ setSearxngTestResult({ ok: true, message: `Connected — ${count} result(s) returned for test query.` });
103
+ } catch (e: any) {
104
+ setSearxngTestResult({ ok: false, message: e?.message || "Connection failed (network/CORS)." });
105
+ } finally { setSearxngTesting(false); }
106
+ }
107
+ const [useDefensePack, setUseDefensePack] = useState(true);
108
+ const [advancedGates, setAdvancedGates] = useState(getAdvancedGatesEnabled());
109
+ const [advancedMode, setAdvancedMode] = useState(true);
110
+ const [liveStage, setLiveStage] = useState<string>("");
111
+ const [showAdvancedConfig, setShowAdvancedConfig] = useState(false);
112
+
113
+ // Batch bank state
114
+ const [batchSize, setBatchSize] = useState(DEFAULT_BATCH_SIZE);
115
+ const [customQuestion, setCustomQuestion] = useState("");
116
+ const [useCustom, setUseCustom] = useState(false);
117
+ const [rows, setRows] = useState<Row[]>([]);
118
+ const [running, setRunning] = useState(false);
119
+ const [round, setRound] = useState(0);
120
+ const [status, setStatus] = useState<string>("");
121
+ const [batchLog, setBatchLog] = useState<string[]>([]);
122
+ function pushBatchLog(line: string) {
123
+ const ts = new Date().toLocaleTimeString();
124
+ setBatchLog(prev => [...prev.slice(-500), `${ts} ${line}`]);
125
+ }
126
+ const [selected, setSelected] = useState<number | null>(null);
127
+
128
+ // Live Compare tab state
129
+ const [liveQuestion, setLiveQuestion] = useState("Calculate the expected cost of a 3-year project with 8% inflation.");
130
+ const [liveRow, setLiveRow] = useState<Row | null>(null);
131
+ const [liveRunning, setLiveRunning] = useState(false);
132
+ const [liveLog, setLiveLog] = useState<string[]>([]);
133
+ const [cohesionInput, setCohesionInput] = useState("");
134
+ const [cohesionQuestion, setCohesionQuestion] = useState("Write a 4-page report on the strategic implications of AI regulation in 2026.");
135
+ const [cohesionResult, setCohesionResult] = useState<CohesionPassResult | null>(null);
136
+ const [cohesionRunning, setCohesionRunning] = useState(false);
137
+
138
+ // Divergence log (auto-updates when new entries are saved)
139
+ const [divergenceLog, setDivergenceLog] = useState<DivergenceEntry[]>(getDivergenceLog());
140
+ const [showDivergenceLog, setShowDivergenceLog] = useState(false);
141
+
142
+ // Custom Presets system
143
+ const [presets, setPresets] = useState<Array<{ name: string; date: string; settings: any }>>(() => {
144
+ try {
145
+ const raw = localStorage.getItem("veritas.v15.savedPresets");
146
+ return raw ? JSON.parse(raw) : [];
147
+ } catch { return []; }
148
+ });
149
+ const [newPresetName, setNewPresetName] = useState("");
150
+
151
+ // Auxiliary API keys
152
+ const [auxKeysInput, setAuxKeysInput] = useState("");
153
+ const [auxKeys, setAuxKeys] = useState<string[]>(() => {
154
+ try {
155
+ const raw = localStorage.getItem("veritas.v15.rotationKeys");
156
+ return raw ? JSON.parse(raw) : [];
157
+ } catch { return []; }
158
+ });
159
+
160
+ // Allowed models
161
+ const [allowedModels, setAllowedModels] = useState<string[]>(() => {
162
+ try {
163
+ const raw = localStorage.getItem("veritas.v15.allowedModels");
164
+ return raw ? JSON.parse(raw) : [];
165
+ } catch { return []; }
166
+ });
167
+
168
+ // Rate-limit snapshot
169
+ const [rateUsage, setRateUsage] = useState(() => snapshotAllUsage());
170
+ useEffect(() => {
171
+ const t = setInterval(() => setRateUsage(snapshotAllUsage()), 1500);
172
+ return () => clearInterval(t);
173
+ }, []);
174
+
175
+ const abortRef = useRef<{ cancelled: boolean }>({ cancelled: false });
176
+ useEffect(() => {
177
+ if (!open) { abortRef.current.cancelled = true; setRunning(false); setLiveRunning(false); }
178
+ }, [open]);
179
+ useEffect(() => subscribeV15(setV15EnabledState), []);
180
+ function handleV15Enabled(on: boolean) {
181
+ setV15EnabledState(on);
182
+ writeV15Enabled(on);
183
+ }
184
+
185
+ const apiKey = getGeminiKey();
186
+
187
+ const profile: V15Profile = useMemo(() => ({
188
+ fourStage, nDeep, nDeepPasses,
189
+ cluster, clusterSize, sloop, sloopPages,
190
+ templateId: templateId || undefined,
191
+ styleOverride: styleOverride || undefined,
192
+ williamsPersona: williamsPersona || undefined,
193
+ adversarial,
194
+ webSearch,
195
+ webBackends: { ogScraper: webOg, prismafetch: webPrisma, jina: webJina, searxng: webSearxng },
196
+ useOriginalDefensePack: useDefensePack,
197
+ bestOfNModels,
198
+ bestOfNHypotheses,
199
+ bestOfNPackHypotheses,
200
+ }), [fourStage, nDeep, nDeepPasses, cluster, clusterSize, sloop, sloopPages, templateId, styleOverride, williamsPersona, adversarial, webSearch, webOg, webPrisma, webJina, useDefensePack, bestOfNModels, bestOfNHypotheses, bestOfNPackHypotheses]);
201
+
202
+ const effectiveDepth = nDeep ? Math.max(1, Math.min(8, nDeepPasses)) : 1;
203
+
204
+ // ── Preset Helpers ────────────────────────────────────────────────────────
205
+ function savePreset() {
206
+ if (!newPresetName.trim()) return;
207
+ const item = {
208
+ name: newPresetName.trim(),
209
+ date: new Date().toLocaleString(),
210
+ settings: {
211
+ fourStage, nDeep, nDeepPasses, cluster, clusterSize, sloop, sloopPages,
212
+ templateId, styleOverride, williamsPersona, adversarial, webSearch,
213
+ webOg, webPrisma, webJina, useDefensePack, advancedGates, singleJudge,
214
+ continuousMode, batchSize, useCustom,
215
+ }
216
+ };
217
+ const next = [...presets.filter(p => p.name !== item.name), item];
218
+ setPresets(next);
219
+ localStorage.setItem("veritas.v15.savedPresets", JSON.stringify(next));
220
+ setNewPresetName("");
221
+ }
222
+
223
+ function deletePreset(name: string) {
224
+ const next = presets.filter(p => p.name !== name);
225
+ setPresets(next);
226
+ localStorage.setItem("veritas.v15.savedPresets", JSON.stringify(next));
227
+ }
228
+
229
+ function loadPreset(settings: any) {
230
+ if (!settings) return;
231
+ if (settings.fourStage !== undefined) setFourStage(settings.fourStage);
232
+ if (settings.nDeep !== undefined) setNDeep(settings.nDeep);
233
+ if (settings.nDeepPasses !== undefined) setNDeepPasses(settings.nDeepPasses);
234
+ if (settings.cluster !== undefined) setCluster(settings.cluster);
235
+ if (settings.clusterSize !== undefined) setClusterSize(settings.clusterSize);
236
+ if (settings.sloop !== undefined) setSloop(settings.sloop);
237
+ if (settings.sloopPages !== undefined) setSloopPages(settings.sloopPages);
238
+ if (settings.templateId !== undefined) setTemplateId(settings.templateId);
239
+ if (settings.styleOverride !== undefined) setStyleOverride(settings.styleOverride);
240
+ if (settings.williamsPersona !== undefined) setWilliamsPersona(settings.williamsPersona);
241
+ if (settings.adversarial !== undefined) setAdversarial(settings.adversarial);
242
+ if (settings.webSearch !== undefined) setWebSearch(settings.webSearch);
243
+ if (settings.webOg !== undefined) setWebOg(settings.webOg);
244
+ if (settings.webPrisma !== undefined) setWebPrisma(settings.webPrisma);
245
+ if (settings.webJina !== undefined) setWebJina(settings.webJina);
246
+ if (settings.useDefensePack !== undefined) setUseDefensePack(settings.useDefensePack);
247
+ if (settings.advancedGates !== undefined) {
248
+ setAdvancedGates(settings.advancedGates);
249
+ setAdvancedGatesEnabled(settings.advancedGates);
250
+ }
251
+ if (settings.singleJudge !== undefined) setSingleJudge(settings.singleJudge);
252
+ if (settings.continuousMode !== undefined) setContinuousMode(settings.continuousMode);
253
+ if (settings.batchSize !== undefined) setBatchSize(settings.batchSize);
254
+ if (settings.useCustom !== undefined) setUseCustom(settings.useCustom);
255
+ }
256
+
257
+ // ── Auxiliary Keys Helpers ────────────────────────────────────────────────
258
+ function addAuxKey() {
259
+ if (!auxKeysInput.trim()) return;
260
+ const next = [...auxKeys.filter(k => k !== auxKeysInput.trim()), auxKeysInput.trim()];
261
+ setAuxKeys(next);
262
+ localStorage.setItem("veritas.v15.rotationKeys", JSON.stringify(next));
263
+ // Import helper is imported from v15-state and triggers registration automatically
264
+ import("../lib/v15-state").then(m => m.saveAuxiliaryGeminiKeys(next));
265
+ setAuxKeysInput("");
266
+ }
267
+
268
+ function deleteAuxKey(key: string) {
269
+ const next = auxKeys.filter(k => k !== key);
270
+ setAuxKeys(next);
271
+ localStorage.setItem("veritas.v15.rotationKeys", JSON.stringify(next));
272
+ import("../lib/v15-state").then(m => m.saveAuxiliaryGeminiKeys(next));
273
+ }
274
+
275
+ // ── Allowed Models Helpers ────────────────────────────────────────────────
276
+ function toggleAllowedModel(model: string) {
277
+ const next = allowedModels.includes(model)
278
+ ? allowedModels.filter(m => m !== model)
279
+ : [...allowedModels, model];
280
+ setAllowedModels(next);
281
+ import("../lib/v15-state").then(m => m.saveAllowedModels(next));
282
+ }
283
+
284
+ // ── Synthesis Settings Port Link ─────────────────────────────────────────
285
+ function downloadPortConfig() {
286
+ const config = {
287
+ veritas_v15_synthesis_port: {
288
+ timestamp: new Date().toISOString(),
289
+ profile,
290
+ rate_limits: rateUsage,
291
+ }
292
+ };
293
+ const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" });
294
+ const url = URL.createObjectURL(blob);
295
+ const a = document.createElement("a");
296
+ a.href = url;
297
+ a.download = "v15-synthesis-port.json";
298
+ a.click();
299
+ URL.revokeObjectURL(url);
300
+ }
301
+
302
+ function pushLog(line: string) {
303
+ const ts = new Date().toLocaleTimeString();
304
+ setLiveLog(prev => [...prev.slice(-200), `${ts} ${line}`]);
305
+ }
306
+
307
+ // ── Batch Bank runner (SEQUENTIAL — completes one Q fully, then next) ────
308
+ async function runBatch() {
309
+ if (!apiKey) { setStatus("❌ No Gemini API key found. Set one in Chat → Keys."); return; }
310
+ abortRef.current.cancelled = false;
311
+ setRunning(true);
312
+ setRound(0);
313
+ setSelected(null);
314
+ // ACCUMULATE: prior-run rows are preserved; new rows append below them.
315
+ let baseLen = rows.length;
316
+ const size = Math.max(1, Math.min(MAX_BATCH_SIZE, batchSize));
317
+ const maxRounds = continuousMode ? MAX_CONTINUOUS_ROUNDS : 1;
318
+
319
+ for (let r = 0; r < maxRounds; r++) {
320
+ if (abortRef.current.cancelled) break;
321
+ setRound(r + 1);
322
+ // Choose questions
323
+ let questions: CalibQuestion[] = [];
324
+ if (useCustom && customQuestion.trim().length > 0) {
325
+ const parts = customQuestion.split(/\n{2,}/).map(s => s.trim()).filter(Boolean).slice(0, size);
326
+ questions = parts.map((text, i) => ({ id: `custom-${r}-${i}`, domain: "custom", text }));
327
+ if (questions.length < size) {
328
+ const extras = shuffleQuestions(Date.now() + r * 991).slice(0, size - questions.length);
329
+ questions = [...questions, ...extras];
330
+ }
331
+ } else {
332
+ questions = shuffleQuestions(Date.now() + r * 991).slice(0, size);
333
+ }
334
+ const acc: Row[] = questions.map(q => ({ q, stage: "queued" }));
335
+ // Always append (never overwrite prior runs' outputs).
336
+ setRows(prev => [...prev, ...acc]);
337
+ const offset = baseLen;
338
+ baseLen += acc.length;
339
+
340
+ // SEQUENTIAL: complete each question fully (baseline+V15+judging+divergence-check) before moving on.
341
+ for (let i = 0; i < acc.length; i++) {
342
+ if (abortRef.current.cancelled) break;
343
+ const globalIdx = offset + i;
344
+ acc[i] = { ...acc[i], stage: "baseline" };
345
+ setRows(prev => { const c = [...prev]; c[globalIdx] = acc[i]; return c; });
346
+ setStatus(`R${r + 1} · Q${i + 1}/${acc.length} [${acc[i].q.domain}] · baseline…`);
347
+ pushBatchLog(`▶ R${r + 1} Q${i + 1} [${acc[i].q.domain}] — ${acc[i].q.text.slice(0, 80)}`);
348
+
349
+ const baselineRes = await runBaselineOnQuestion({
350
+ apiKey, question: acc[i].q.text, singleJudge,
351
+ onProgress: (s) => { setStatus(`R${r + 1} · Q${i + 1} baseline · ${s}`); pushBatchLog(`[baseline] ${s}`); },
352
+ });
353
+ acc[i] = { ...acc[i], baseline: baselineRes, stage: "v15" };
354
+ setRows(prev => { const c = [...prev]; c[globalIdx] = acc[i]; return c; });
355
+
356
+ if (abortRef.current.cancelled) break;
357
+
358
+ setStatus(`R${r + 1} · Q${i + 1} · V15 (depth ${effectiveDepth}, ${clusterSize}-cluster, ${sloop ? sloopPages + "pg" : "no"}-sloop)…`);
359
+ pushBatchLog(`[V15] starting depth ${effectiveDepth}, cluster×${clusterSize}${sloop ? `, SLOOP ${sloopPages}pg` : ""}`);
360
+ const v15Res = v15Enabled ? await runV15OnQuestion({
361
+ apiKey, question: acc[i].q.text,
362
+ maxDepth: effectiveDepth,
363
+ advancedGates, singleJudge, profile,
364
+ onProgress: (s) => { setStatus(`R${r + 1} · Q${i + 1} V15 · ${s}`); setLiveStage(s); pushBatchLog(`[V15] ${s}`); },
365
+ }) : baselineRes;
366
+
367
+ // Divergence analysis if guard vs judge disagree by ≥ 1.0
368
+ if (v15Res.judgeScore !== null) {
369
+ const delta = v15Res.judgeScore - v15Res.guardScore;
370
+ if (Math.abs(delta) >= 1.0) {
371
+ setStatus(`R${r + 1} · Q${i + 1} · analyzing guard↔judge divergence (Δ ${delta.toFixed(1)})…`);
372
+ pushBatchLog(`[divergence] guard/judge gap Δ${delta.toFixed(1)} — analyzing root cause`);
373
+ const analysis = await analyzeDivergence({
374
+ apiKey, question: acc[i].q.text, answer: v15Res.fixed,
375
+ guardScore: v15Res.guardScore, judgeScore: v15Res.judgeScore,
376
+ guardIssues: v15Res.issues, judgeNote: v15Res.judgeNote,
377
+ });
378
+ if (analysis) {
379
+ const scoredJudges = (v15Res.judgeRoster ?? []).filter(j => j.score !== undefined);
380
+ const entry: DivergenceEntry = {
381
+ timestamp: Date.now(),
382
+ question: acc[i].q.text,
383
+ guardScore: v15Res.guardScore,
384
+ judgeScore: v15Res.judgeScore,
385
+ delta,
386
+ suggestion: analysis,
387
+ authorityModel: v15Res.eloConsensus?.authorityModel ?? scoredJudges[0]?.model ?? "unknown",
388
+ judgePanel: scoredJudges.map(j => ({ model: j.model, score: j.score ?? 0, elo: j.elo })),
389
+ };
390
+ saveDivergenceEntry(entry);
391
+ setDivergenceLog(getDivergenceLog());
392
+ }
393
+ }
394
+ }
395
+
396
+ // Independent comparative judge (fresh context, sees BOTH answers).
397
+ if (!v15Res.error && !baselineRes.error) {
398
+ acc[i] = { ...acc[i], v15: v15Res, stage: "judging" };
399
+ setRows(prev => { const c = [...prev]; c[globalIdx] = acc[i]; return c; });
400
+ setStatus(`R${r + 1} · Q${i + 1} · independent comparative judge…`);
401
+ pushBatchLog(`[judge] independent comparative judge (fresh context)…`);
402
+ const comparative = await runComparativeJudge({
403
+ apiKey, question: acc[i].q.text,
404
+ baselineAnswer: baselineRes.draft, v15Answer: v15Res.fixed,
405
+ });
406
+ acc[i] = { ...acc[i], v15: v15Res, comparative, stage: "done" };
407
+ } else {
408
+ acc[i] = { ...acc[i], v15: v15Res, stage: v15Res.error ? "error" : "done" };
409
+ }
410
+ setRows(prev => { const c = [...prev]; c[globalIdx] = acc[i]; return c; });
411
+ }
412
+
413
+ // Round summary
414
+ const scores = acc.map(row => (row.v15?.judgeScore ?? row.v15?.guardScore ?? 0));
415
+ const mean = scores.reduce((a, b) => a + b, 0) / (scores.length || 1);
416
+ const allPass = scores.every(s => s >= TARGET_SCORE);
417
+ if (allPass && continuousMode) {
418
+ setStatus(`✓ Round ${r + 1} passed. Mean V15: ${mean.toFixed(2)}/10.`);
419
+ pushBatchLog(`✓ Round ${r + 1} complete — mean V15 ${mean.toFixed(2)}/10`);
420
+ break;
421
+ }
422
+ if (r === maxRounds - 1) {
423
+ setStatus(`■ Set complete (auto-stop). Mean V15: ${mean.toFixed(2)}/10.${continuousMode ? "" : " Enable Continuous Mode to auto-retry with fresh questions."}`);
424
+ pushBatchLog(`■ Set complete — mean V15 ${mean.toFixed(2)}/10`);
425
+ }
426
+ }
427
+ setRunning(false);
428
+ }
429
+
430
+ // ── Live Compare tab runner (single question, full pipeline emulation) ───
431
+ async function runLiveOnce() {
432
+ if (!apiKey) { setStatus("❌ No Gemini API key found."); return; }
433
+ if (!liveQuestion.trim()) return;
434
+ abortRef.current.cancelled = false;
435
+ setLiveRunning(true);
436
+ setLiveLog([]);
437
+ setLiveRow(null);
438
+ const q: CalibQuestion = { id: "live-1", domain: "live", text: liveQuestion.trim() };
439
+ pushLog("▶ Live LLM Pipeline Comparison started (Old vs V15)");
440
+ pushLog("[Old Path] Direct generation (no V15 processing)");
441
+
442
+ const baselineRes = await runBaselineOnQuestion({
443
+ apiKey, question: q.text, singleJudge,
444
+ onProgress: (s) => pushLog(`[Baseline] ${s}`),
445
+ });
446
+ pushLog(`[Baseline] complete · judge ${baselineRes.judgeScore ?? "—"}`);
447
+
448
+ pushLog(`[Universal Rigor] Drafting via 9-model Test & Rotate (random order)`);
449
+ const v15Res = v15Enabled ? await runV15OnQuestion({
450
+ apiKey, question: q.text,
451
+ maxDepth: effectiveDepth,
452
+ advancedGates, singleJudge, profile,
453
+ onProgress: (s) => pushLog(`[Universal Rigor] ${s}`),
454
+ }) : baselineRes;
455
+ pushLog(`[Universal Rigor] ✓ V15 pipeline complete · guard ${v15Res.guardScore} · judge ${v15Res.judgeScore ?? "—"}`);
456
+
457
+ let comparative: ComparativeJudgeResult | undefined;
458
+ if (!v15Res.error && !baselineRes.error) {
459
+ pushLog(`[Comparative Judge] evaluating baseline vs V15 in fresh context…`);
460
+ comparative = await runComparativeJudge({
461
+ apiKey,
462
+ question: q.text,
463
+ baselineAnswer: baselineRes.draft,
464
+ v15Answer: v15Res.fixed,
465
+ });
466
+ if (comparative.ok) {
467
+ pushLog(`[Comparative Judge] baseline ${comparative.baselineScore.toFixed(1)} vs V15 ${comparative.v15Score.toFixed(1)} · winner ${comparative.winner}`);
468
+ } else {
469
+ pushLog(`[Comparative Judge] unavailable: ${comparative.error ?? "unknown error"}`);
470
+ }
471
+ }
472
+ const row: Row = { q, stage: "done", baseline: baselineRes, v15: v15Res, comparative };
473
+ setLiveRow(row);
474
+
475
+ if (v15Res.judgeScore !== null && Math.abs(v15Res.judgeScore - v15Res.guardScore) >= 1.0) {
476
+ pushLog(`[Divergence] Δ ${(v15Res.judgeScore - v15Res.guardScore).toFixed(1)} — analyzing…`);
477
+ const analysis = await analyzeDivergence({
478
+ apiKey, question: q.text, answer: v15Res.fixed,
479
+ guardScore: v15Res.guardScore, judgeScore: v15Res.judgeScore,
480
+ guardIssues: v15Res.issues, judgeNote: v15Res.judgeNote,
481
+ });
482
+ if (analysis) {
483
+ saveDivergenceEntry({
484
+ timestamp: Date.now(), question: q.text,
485
+ guardScore: v15Res.guardScore, judgeScore: v15Res.judgeScore,
486
+ delta: v15Res.judgeScore - v15Res.guardScore, suggestion: analysis,
487
+ authorityModel: v15Res.eloConsensus?.authorityModel ?? "unknown",
488
+ });
489
+ setDivergenceLog(getDivergenceLog());
490
+ pushLog(`[Divergence] logged: ${analysis.reason.slice(0, 80)}…`);
491
+ }
492
+ }
493
+ setLiveRunning(false);
494
+ }
495
+
496
+ async function runCohesion() {
497
+ if (!apiKey) { setStatus("❌ No API key"); return; }
498
+ if (!cohesionInput.trim()) return;
499
+ setCohesionRunning(true);
500
+ setCohesionResult(null);
501
+ const res = await runCohesionPass({
502
+ apiKey,
503
+ question: cohesionQuestion,
504
+ report: cohesionInput,
505
+ onProgress: (s) => pushLog(`[Cohesion] ${s}`),
506
+ });
507
+ setCohesionResult(res);
508
+ setCohesionRunning(false);
509
+ }
510
+
511
+ function stop() {
512
+ abortRef.current.cancelled = true;
513
+ setRunning(false); setLiveRunning(false);
514
+ setStatus("⏹ Cancelled by engineer.");
515
+ }
516
+
517
+ if (!open) return null;
518
+
519
+ const meanV15 = rows.length ? (rows.reduce((a, r) => a + (r.v15?.judgeScore ?? r.v15?.guardScore ?? 0), 0) / rows.length) : 0;
520
+ const meanBase = rows.length ? (rows.reduce((a, r) => a + (r.baseline?.judgeScore ?? 0), 0) / rows.length) : 0;
521
+ const delta = meanV15 - meanBase;
522
+
523
+ return (
524
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
525
+ <div className="flex h-[94vh] max-h-[980px] w-full max-w-6xl flex-col overflow-hidden rounded-2xl bg-white shadow-2xl" onClick={e => e.stopPropagation()}>
526
+ {/* ── Title bar ─────────────────────────────────────────── */}
527
+ <div className="flex-none border-b border-zinc-200 px-5 py-3">
528
+ <div className="flex items-start justify-between">
529
+ <div>
530
+ <h2 className="text-lg font-bold text-zinc-900">Rigor Guard Calibration — Live</h2>
531
+ <p className="text-[11px] text-zinc-500">Real flaw-registry scans + deterministic remediation convergence + universal scoring</p>
532
+ </div>
533
+ <div className="flex items-center gap-2">
534
+ <label className={`flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs font-bold cursor-pointer ${v15Enabled ? "text-emerald-700" : "text-zinc-500"}`}>
535
+ <input type="checkbox" checked={v15Enabled} onChange={e => handleV15Enabled(e.target.checked)} className="h-3.5 w-3.5 accent-emerald-600" />
536
+ V15 Pipeline Enabled
537
+ </label>
538
+ <label className={`flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs font-bold cursor-pointer ${singleJudge ? "text-sky-700" : "text-zinc-500"}`}>
539
+ <input type="checkbox" checked={singleJudge} onChange={e => setSingleJudge(e.target.checked)} className="h-3.5 w-3.5 accent-sky-600" />
540
+ Single Judge
541
+ </label>
542
+ <label className={`flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs font-bold cursor-pointer ${continuousMode ? "text-amber-700" : "text-zinc-500"}`}>
543
+ <input type="checkbox" checked={continuousMode} onChange={e => setContinuousMode(e.target.checked)} className="h-3.5 w-3.5 accent-amber-600" />
544
+ Continuous
545
+ </label>
546
+ {tab === "batch" && !running && (
547
+ <button onClick={runBatch} disabled={!apiKey} className="rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-bold text-white hover:bg-indigo-700 disabled:opacity-40" title="Rotate models & restart calibration">
548
+ ▶ Rotate & Restart
549
+ </button>
550
+ )}
551
+ {running && <button onClick={stop} className="rounded-lg bg-rose-600 px-3 py-1.5 text-xs font-bold text-white">⏹ Stop</button>}
552
+ <button onClick={onClose} className="rounded-lg border border-zinc-300 px-3 py-1.5 text-xs font-bold text-zinc-700 hover:bg-zinc-100">Close</button>
553
+ </div>
554
+ </div>
555
+
556
+ {/* Sub-tabs */}
557
+ <div className="mt-3 flex items-center gap-1.5">
558
+ <button onClick={() => setTab("live")} className={`rounded-lg px-3 py-1.5 text-xs font-bold ${tab === "live" ? "bg-zinc-900 text-white" : "border border-zinc-300 bg-white text-zinc-700 hover:bg-zinc-100"}`}>Live Compare</button>
559
+ <button onClick={() => setTab("batch")} className={`rounded-lg px-3 py-1.5 text-xs font-bold ${tab === "batch" ? "bg-zinc-900 text-white" : "border border-zinc-300 bg-white text-zinc-700 hover:bg-zinc-100"}`}>Batch Bank</button>
560
+ <button onClick={() => setTab("guide")} className={`rounded-lg px-3 py-1.5 text-xs font-bold flex items-center gap-1 ${tab === "guide" ? "bg-sky-600 text-white" : "border border-sky-300 bg-sky-50 text-sky-700 hover:bg-sky-100"}`}>🔗 Web Grounding Guide</button>
561
+ <button onClick={() => setShowAdvancedConfig(v => !v)} className={`ml-auto rounded-lg border px-3 py-1.5 text-xs font-bold ${showAdvancedConfig ? "border-indigo-500 bg-indigo-50 text-indigo-800" : "border-zinc-300 text-zinc-700 hover:bg-zinc-100"}`}>
562
+ ⚙️ Advanced Config
563
+ </button>
564
+ <button onClick={() => setShowDivergenceLog(v => !v)} className={`rounded-lg border px-3 py-1.5 text-xs font-bold ${showDivergenceLog ? "border-fuchsia-400 bg-fuchsia-50 text-fuchsia-800" : "border-zinc-300 text-zinc-700 hover:bg-zinc-100"}`}>
565
+ 📋 Divergence Log ({divergenceLog.length})
566
+ </button>
567
+ </div>
568
+
569
+ {/* Stats row */}
570
+ <div className="mt-2 flex flex-wrap items-center gap-2 text-[11px]">
571
+ {!apiKey && <span className="rounded-full bg-rose-100 px-2.5 py-0.5 font-bold text-rose-800">⚠ No Gemini API key — set in Chat → Keys</span>}
572
+ {apiKey && <span className="rounded-full bg-emerald-100 px-2 py-0.5 font-mono text-emerald-800">key: {apiKey.slice(0, 6)}…{apiKey.slice(-4)}</span>}
573
+ {running && <span className="rounded-full bg-amber-100 px-2 py-0.5 font-bold text-amber-800">▶ round {round}</span>}
574
+ {rows.length > 0 && (
575
+ <>
576
+ <span className="rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-zinc-800">V15 mean: <b className={meanV15 >= TARGET_SCORE ? "text-emerald-700" : "text-rose-700"}>{meanV15.toFixed(2)}</b></span>
577
+ <span className="rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-zinc-800">Baseline mean: <b>{meanBase.toFixed(2)}</b></span>
578
+ <span className={`rounded-full px-2 py-0.5 font-mono font-bold ${delta >= 0 ? "bg-emerald-100 text-emerald-800" : "bg-rose-100 text-rose-800"}`}>Δ {delta >= 0 ? "+" : ""}{delta.toFixed(2)}</span>
579
+ </>
580
+ )}
581
+ </div>
582
+ {status && <div className="mt-1.5 font-mono text-[11px] text-zinc-500">{status}</div>}
583
+ </div>
584
+
585
+ {/* ── Profile config bar ────── */}
586
+ <ProfileBar
587
+ fourStage={fourStage} setFourStage={setFourStage}
588
+ nDeep={nDeep} setNDeep={setNDeep} nDeepPasses={nDeepPasses} setNDeepPasses={setNDeepPasses}
589
+ cluster={cluster} setCluster={setCluster} clusterSize={clusterSize} setClusterSize={setClusterSize}
590
+ sloop={sloop} setSloop={setSloop} sloopPages={sloopPages} setSloopPages={setSloopPages}
591
+ templateId={templateId} setTemplateId={setTemplateId}
592
+ styleOverride={styleOverride} setStyleOverride={setStyleOverride}
593
+ williamsPersona={williamsPersona} setWilliamsPersona={setWilliamsPersona}
594
+ adversarial={adversarial} setAdversarial={setAdversarial}
595
+ webSearch={webSearch} setWebSearch={setWebSearch}
596
+ webOg={webOg} setWebOg={setWebOg}
597
+ webPrisma={webPrisma} setWebPrisma={setWebPrisma}
598
+ webJina={webJina} setWebJina={setWebJina}
599
+ webSearxng={webSearxng} setWebSearxng={setWebSearxng}
600
+ searxngUrl={searxngUrl} setSearxngUrl={setSearxngUrl}
601
+ searxngApiKey={searxngApiKey} setSearxngApiKey={setSearxngApiKey}
602
+ searxngCategories={searxngCategories} setSearxngCategories={setSearxngCategories}
603
+ searxngLanguage={searxngLanguage} setSearxngLanguage={setSearxngLanguage}
604
+ searxngSafe={searxngSafe} setSearxngSafe={setSearxngSafe}
605
+ searxngTesting={searxngTesting} searxngTestResult={searxngTestResult}
606
+ testSearxngConnection={testSearxngConnection}
607
+ bestOfNModels={bestOfNModels} setBestOfNModels={setBestOfNModels}
608
+ bestOfNHypotheses={bestOfNHypotheses} setBestOfNHypotheses={setBestOfNHypotheses}
609
+ bestOfNPackHypotheses={bestOfNPackHypotheses} setBestOfNPackHypotheses={setBestOfNPackHypotheses}
610
+ />
611
+
612
+ {/* ── Advanced configuration panel ── */}
613
+ {showAdvancedConfig && (
614
+ <AdvancedConfigPanel
615
+ presets={presets}
616
+ newPresetName={newPresetName}
617
+ setNewPresetName={setNewPresetName}
618
+ savePreset={savePreset}
619
+ deletePreset={deletePreset}
620
+ loadPreset={loadPreset}
621
+ auxKeys={auxKeys}
622
+ auxKeysInput={auxKeysInput}
623
+ setAuxKeysInput={setAuxKeysInput}
624
+ addAuxKey={addAuxKey}
625
+ deleteAuxKey={deleteAuxKey}
626
+ allowedModels={allowedModels}
627
+ toggleAllowedModel={toggleAllowedModel}
628
+ downloadPortConfig={downloadPortConfig}
629
+ onClose={() => setShowAdvancedConfig(false)}
630
+ />
631
+ )}
632
+
633
+ {/* ── Body ───────────────────────────────────────────────── */}
634
+ <div className="flex-1 overflow-hidden">
635
+ {showDivergenceLog && (
636
+ <DivergenceLogPanel
637
+ log={divergenceLog}
638
+ onClear={() => { clearDivergenceLog(); setDivergenceLog([]); }}
639
+ onClose={() => setShowDivergenceLog(false)}
640
+ />
641
+ )}
642
+ {!showDivergenceLog && tab === "live" && (
643
+ <LiveCompareTab
644
+ question={liveQuestion} setQuestion={setLiveQuestion}
645
+ onRun={runLiveOnce} running={liveRunning}
646
+ log={liveLog}
647
+ row={liveRow}
648
+ apiKey={!!apiKey}
649
+ rateUsage={rateUsage}
650
+ useDefensePack={useDefensePack} setUseDefensePack={setUseDefensePack}
651
+ advancedGates={advancedGates} setAdvancedGates={(v) => { setAdvancedGates(v); setAdvancedGatesEnabled(v); }}
652
+ cohesionInput={cohesionInput} setCohesionInput={setCohesionInput}
653
+ cohesionQuestion={cohesionQuestion} setCohesionQuestion={setCohesionQuestion}
654
+ cohesionResult={cohesionResult} cohesionRunning={cohesionRunning}
655
+ onCohesion={runCohesion}
656
+ />
657
+ )}
658
+ {!showDivergenceLog && tab === "batch" && (
659
+ <BatchBankTab
660
+ rows={rows} selected={selected} setSelected={setSelected}
661
+ batchSize={batchSize} setBatchSize={setBatchSize}
662
+ useCustom={useCustom} setUseCustom={setUseCustom}
663
+ customQuestion={customQuestion} setCustomQuestion={setCustomQuestion}
664
+ profile={profile} effectiveDepth={effectiveDepth}
665
+ apiKey={!!apiKey}
666
+ onRun={runBatch}
667
+ running={running}
668
+ advancedMode={advancedMode} setAdvancedMode={setAdvancedMode}
669
+ liveStage={liveStage}
670
+ useDefensePack={useDefensePack} setUseDefensePack={setUseDefensePack}
671
+ advancedGates={advancedGates} setAdvancedGates={(v) => { setAdvancedGates(v); setAdvancedGatesEnabled(v); }}
672
+ rateUsage={rateUsage}
673
+ batchLog={batchLog}
674
+ />
675
+ )}
676
+ {!showDivergenceLog && tab === "guide" && <GroundingGuidePanel />}
677
+ </div>
678
+ </div>
679
+ </div>
680
+ );
681
+ }
682
+
683
+ // ────────────────────────────── SUB-COMPONENTS ─────────────────────────────
684
+
685
+ function ProfileBar(p: {
686
+ fourStage: boolean; setFourStage: (v: boolean) => void;
687
+ nDeep: boolean; setNDeep: (v: boolean) => void; nDeepPasses: number; setNDeepPasses: (n: number) => void;
688
+ cluster: boolean; setCluster: (v: boolean) => void; clusterSize: number; setClusterSize: (n: number) => void;
689
+ sloop: boolean; setSloop: (v: boolean) => void; sloopPages: number; setSloopPages: (n: number) => void;
690
+ templateId: string; setTemplateId: (v: string) => void;
691
+ styleOverride: string; setStyleOverride: (v: string) => void;
692
+ williamsPersona: string; setWilliamsPersona: (v: string) => void;
693
+ adversarial: boolean; setAdversarial: (v: boolean) => void;
694
+ webSearch: boolean; setWebSearch: (v: boolean) => void;
695
+ webOg: boolean; setWebOg: (v: boolean) => void;
696
+ webPrisma: boolean; setWebPrisma: (v: boolean) => void;
697
+ webJina: boolean; setWebJina: (v: boolean) => void;
698
+ webSearxng: boolean; setWebSearxng: (v: boolean) => void;
699
+ searxngUrl: string; setSearxngUrl: (v: string) => void;
700
+ searxngApiKey: string; setSearxngApiKey: (v: string) => void;
701
+ searxngCategories: string; setSearxngCategories: (v: string) => void;
702
+ searxngLanguage: string; setSearxngLanguage: (v: string) => void;
703
+ searxngSafe: string; setSearxngSafe: (v: string) => void;
704
+ searxngTesting: boolean; searxngTestResult: { ok: boolean; message: string } | null;
705
+ testSearxngConnection: () => void;
706
+ bestOfNModels: number; setBestOfNModels: (v: number) => void;
707
+ bestOfNHypotheses: number; setBestOfNHypotheses: (v: number) => void;
708
+ bestOfNPackHypotheses: boolean; setBestOfNPackHypotheses: (v: boolean) => void;
709
+ }) {
710
+ return (
711
+ <div className="flex-none border-b border-indigo-100 bg-indigo-50/30 px-5 py-2.5">
712
+ <div className="flex flex-wrap items-center gap-3 text-[11px]">
713
+ <label className="flex items-center gap-1.5">
714
+ <input type="checkbox" checked={p.fourStage} onChange={e => p.setFourStage(e.target.checked)} className="accent-indigo-600" />
715
+ <span className="font-bold text-zinc-800">4-Stage</span>
716
+ </label>
717
+ <span className="flex items-center gap-1.5">
718
+ <label className="flex items-center gap-1">
719
+ <input type="checkbox" checked={p.nDeep} onChange={e => p.setNDeep(e.target.checked)} className="accent-indigo-600" />
720
+ <span className="font-bold text-zinc-800">N-Deep</span>
721
+ </label>
722
+ <input type="number" min={1} max={8} value={p.nDeepPasses} disabled={!p.nDeep} onChange={e => p.setNDeepPasses(Math.max(1, Math.min(8, Number(e.target.value))))} className="w-14 rounded border border-zinc-300 px-1.5 py-0.5 text-center font-mono disabled:opacity-40" />
723
+ </span>
724
+ <span className="flex items-center gap-1.5">
725
+ <label className="flex items-center gap-1">
726
+ <input type="checkbox" checked={p.cluster} onChange={e => p.setCluster(e.target.checked)} className="accent-indigo-600" />
727
+ <span className="font-bold text-zinc-800">Cluster</span>
728
+ </label>
729
+ <input type="number" min={1} max={16} value={p.clusterSize} disabled={!p.cluster} onChange={e => p.setClusterSize(Math.max(1, Math.min(16, Number(e.target.value))))} className="w-14 rounded border border-zinc-300 px-1.5 py-0.5 text-center font-mono disabled:opacity-40" />
730
+ </span>
731
+ <span className="flex items-center gap-1.5">
732
+ <label className="flex items-center gap-1">
733
+ <input type="checkbox" checked={p.sloop} onChange={e => p.setSloop(e.target.checked)} className="accent-indigo-600" />
734
+ <span className="font-bold text-zinc-800">SLOOP pages</span>
735
+ </label>
736
+ <input type="number" min={1} max={32} value={p.sloopPages} disabled={!p.sloop} onChange={e => p.setSloopPages(Math.max(1, Math.min(32, Number(e.target.value))))} className="w-14 rounded border border-zinc-300 px-1.5 py-0.5 text-center font-mono disabled:opacity-40" />
737
+ </span>
738
+ <span className="flex items-center gap-1.5">
739
+ <span className="font-bold text-zinc-800">Template</span>
740
+ <select value={p.templateId} onChange={e => p.setTemplateId(e.target.value)} className="rounded border border-zinc-300 px-2 py-0.5 font-mono text-[11px]">
741
+ <option value="">— none —</option>
742
+ {TEMPLATE_IDS.map(t => <option key={t} value={t}>{t}</option>)}
743
+ </select>
744
+ </span>
745
+ <span className="flex items-center gap-1.5">
746
+ <span className="font-bold text-zinc-800">Style override</span>
747
+ <select value={p.styleOverride} onChange={e => p.setStyleOverride(e.target.value)} className="rounded border border-zinc-300 px-2 py-0.5 font-mono text-[11px]">
748
+ <option value="">— none —</option>
749
+ {STYLE_OVERRIDES.map(s => <option key={s} value={s}>{s}</option>)}
750
+ </select>
751
+ </span>
752
+ </div>
753
+ <div className="mt-2 flex flex-wrap items-center gap-3 text-[11px] border-t border-indigo-100 pt-2">
754
+ <span className="flex items-center gap-1">
755
+ <span className="font-bold text-zinc-800">Williams persona</span>
756
+ <select value={p.williamsPersona} onChange={e => p.setWilliamsPersona(e.target.value)} className="rounded border border-zinc-300 px-2 py-0.5 font-mono text-[11px]">
757
+ <option value="">— none —</option>
758
+ {ARCHETYPES.map(a => <option key={a.name} value={a.name}>{a.name}</option>)}
759
+ </select>
760
+ </span>
761
+ <label className="flex items-center gap-1.5">
762
+ <input type="checkbox" checked={p.adversarial} onChange={e => p.setAdversarial(e.target.checked)} className="accent-rose-600" />
763
+ <span className="font-bold text-rose-700">Adversarial engine</span>
764
+ </label>
765
+ <label className="flex items-center gap-1.5">
766
+ <input type="checkbox" checked={p.webSearch} onChange={e => p.setWebSearch(e.target.checked)} className="accent-sky-600" />
767
+ <span className="font-bold text-sky-700">Web grounding</span>
768
+ </label>
769
+ {p.webSearch && (
770
+ <span className="flex items-center gap-2 rounded bg-sky-50 px-2 py-0.5">
771
+ <label className="flex items-center gap-1"><input type="checkbox" checked={p.webOg} onChange={e => p.setWebOg(e.target.checked)} className="accent-sky-600" /><span className="text-sky-800">OG scraper</span></label>
772
+ <label className="flex items-center gap-1"><input type="checkbox" checked={p.webPrisma} onChange={e => p.setWebPrisma(e.target.checked)} className="accent-sky-600" /><span className="text-sky-800">PrismaFetch</span></label>
773
+ <label className="flex items-center gap-1"><input type="checkbox" checked={p.webJina} onChange={e => p.setWebJina(e.target.checked)} className="accent-sky-600" /><span className="text-sky-800">Jina</span></label>
774
+ <label className="flex items-center gap-1"><input type="checkbox" checked={p.webSearxng} onChange={e => p.setWebSearxng(e.target.checked)} className="accent-sky-600" /><span className="text-sky-800">SearXNG</span></label>
775
+ </span>
776
+ )}
777
+ </div>
778
+ {p.webSearch && p.webSearxng && (
779
+ <div className="mt-2 rounded-xl border border-violet-200 bg-violet-50/40 p-3">
780
+ <div className="mb-2 text-[10px] font-bold uppercase tracking-wider text-violet-800">SearXNG Configuration</div>
781
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-6 text-[10px]">
782
+ <label className="flex flex-col gap-0.5">
783
+ <span className="font-bold text-zinc-600">Base URL</span>
784
+ <input type="text" value={p.searxngUrl} onChange={e => { p.setSearxngUrl(e.target.value); try { localStorage.setItem("veritas.v15.searxngUrl", e.target.value); } catch {} }} className="rounded border border-zinc-300 px-1.5 py-1 font-mono" placeholder="http://localhost:8080" />
785
+ </label>
786
+ <label className="flex flex-col gap-0.5">
787
+ <span className="font-bold text-zinc-600">API Key</span>
788
+ <input type="password" value={p.searxngApiKey} onChange={e => { p.setSearxngApiKey(e.target.value); try { localStorage.setItem("veritas.v15.searxngApiKey", e.target.value); } catch {} }} className="rounded border border-zinc-300 px-1.5 py-1 font-mono" placeholder="optional" />
789
+ </label>
790
+ <label className="flex flex-col gap-0.5">
791
+ <span className="font-bold text-zinc-600">Categories</span>
792
+ <select value={p.searxngCategories} onChange={e => { p.setSearxngCategories(e.target.value); try { localStorage.setItem("veritas.v15.searxngCategories", e.target.value); } catch {} }} className="rounded border border-zinc-300 px-1.5 py-1 font-mono">
793
+ <option value="general">general</option>
794
+ <option value="science">science</option>
795
+ <option value="it">it</option>
796
+ <option value="news">news</option>
797
+ <option value="academic">academic</option>
798
+ <option value="files">files</option>
799
+ </select>
800
+ </label>
801
+ <label className="flex flex-col gap-0.5">
802
+ <span className="font-bold text-zinc-600">Language</span>
803
+ <select value={p.searxngLanguage} onChange={e => { p.setSearxngLanguage(e.target.value); try { localStorage.setItem("veritas.v15.searxngLanguage", e.target.value); } catch {} }} className="rounded border border-zinc-300 px-1.5 py-1 font-mono">
804
+ <option value="en">en</option>
805
+ <option value="es">es</option>
806
+ <option value="fr">fr</option>
807
+ <option value="de">de</option>
808
+ <option value="all">all</option>
809
+ </select>
810
+ </label>
811
+ <label className="flex flex-col gap-0.5">
812
+ <span className="font-bold text-zinc-600">Safe</span>
813
+ <select value={p.searxngSafe} onChange={e => { p.setSearxngSafe(e.target.value); try { localStorage.setItem("veritas.v15.searxngSafe", e.target.value); } catch {} }} className="rounded border border-zinc-300 px-1.5 py-1 font-mono">
814
+ <option value="0">Off</option>
815
+ <option value="1">Moderate</option>
816
+ <option value="2">Strict</option>
817
+ </select>
818
+ </label>
819
+ <div className="flex items-end">
820
+ <button onClick={p.testSearxngConnection} disabled={p.searxngTesting} type="button" className="w-full rounded-lg bg-violet-600 px-2 py-1.5 text-[10px] font-bold text-white hover:bg-violet-700 disabled:opacity-50">
821
+ {p.searxngTesting ? "Testing…" : "🔌 Test Connection"}
822
+ </button>
823
+ </div>
824
+ </div>
825
+ {p.searxngTestResult && (
826
+ <div className={`mt-2 rounded px-2 py-1 text-[10px] font-mono ${p.searxngTestResult.ok ? "bg-emerald-100 text-emerald-800" : "bg-rose-100 text-rose-800"}`}>
827
+ {p.searxngTestResult.ok ? "✓" : "✗"} {p.searxngTestResult.message}
828
+ </div>
829
+ )}
830
+ <div className="mt-1.5 text-[9px] text-violet-700">Tip: enable format=json in your SearXNG settings.yml under search: formats: [json]. All settings auto-saved to localStorage.</div>
831
+ </div>
832
+ )}
833
+ {p.cluster && (
834
+ <div className="mt-2 rounded-xl border border-indigo-200 bg-indigo-50/40 p-3">
835
+ <div className="mb-2 text-[10px] font-bold uppercase tracking-wider text-indigo-800">Best-of-N Outline-First Drafting (dense skeletons, expand winner only)</div>
836
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4 text-[10px]">
837
+ <label className="flex flex-col gap-0.5">
838
+ <span className="font-bold text-zinc-600">Models (distinct LLMs)</span>
839
+ <input type="number" min={1} max={5} value={p.bestOfNModels} onChange={e => p.setBestOfNModels(Math.max(1, Math.min(5, Number(e.target.value) || 1)))} className="rounded border border-zinc-300 px-2 py-1 font-mono" />
840
+ </label>
841
+ <label className="flex flex-col gap-0.5">
842
+ <span className="font-bold text-zinc-600">Hypotheses (outlines)</span>
843
+ <input type="number" min={2} max={8} value={p.bestOfNHypotheses} onChange={e => p.setBestOfNHypotheses(Math.max(2, Math.min(8, Number(e.target.value) || 2)))} className="rounded border border-zinc-300 px-2 py-1 font-mono" />
844
+ </label>
845
+ <label className="col-span-2 flex items-end gap-1.5">
846
+ <input type="checkbox" checked={p.bestOfNPackHypotheses} onChange={e => p.setBestOfNPackHypotheses(e.target.checked)} className="accent-indigo-600" />
847
+ <span className="font-bold text-indigo-800">Pack multiple outlines per model call (RPM-saving mode)</span>
848
+ </label>
849
+ </div>
850
+ <div className="mt-1.5 text-[9px] text-indigo-700">Each hypothesis is a DENSE outline (~250 words), not a full draft — only the highest-scoring outline is expanded into the full answer. This saves (N-1)/N of the draft-stage tokens versus generating N full drafts.</div>
851
+ </div>
852
+ )}
853
+ <div className="mt-1 text-[10px] text-zinc-500">Mix freely: any combination or all together, with variable quantities — mirrors the real pipeline knobs. Williams persona · adversarial engine · live web grounding all route through the same stack as the live app.</div>
854
+ </div>
855
+ );
856
+ }
857
+
858
+ // ── Live Compare Tab ─────────────────────────────────────────────────────
859
+ function LiveCompareTab(p: {
860
+ question: string; setQuestion: (v: string) => void;
861
+ onRun: () => void; running: boolean;
862
+ log: string[]; row: Row | null;
863
+ apiKey: boolean;
864
+ rateUsage: ReturnType<typeof snapshotAllUsage>;
865
+ useDefensePack: boolean; setUseDefensePack: (v: boolean) => void;
866
+ advancedGates: boolean; setAdvancedGates: (v: boolean) => void;
867
+ cohesionInput: string; setCohesionInput: (v: string) => void;
868
+ cohesionQuestion: string; setCohesionQuestion: (v: string) => void;
869
+ cohesionResult: CohesionPassResult | null; cohesionRunning: boolean;
870
+ onCohesion: () => void;
871
+ }) {
872
+ return (
873
+ <div className="h-full overflow-y-auto p-4 space-y-3">
874
+ {/* Prompt card */}
875
+ <div className="rounded-2xl border border-zinc-200 bg-white p-3 shadow-sm">
876
+ <div className="mb-2 flex items-center justify-between">
877
+ <div>
878
+ <div className="text-sm font-bold text-zinc-900">Live LLM Pipeline Comparison (Old vs V15)</div>
879
+ <div className="text-[10px] text-zinc-500">Requires a configured provider API key in the Control panel. Without one, this is honestly reported as unavailable — never fabricated. When Test & Rotate is on, the judge ensemble spreads calls across every keyed, non-rate-limited model (Elo-sorted) instead of one fixed model.</div>
880
+ </div>
881
+ <span className="rounded bg-sky-100 px-2 py-0.5 text-[10px] font-bold text-sky-800 whitespace-nowrap">🔀 Test & Rotate (9 available)</span>
882
+ </div>
883
+ <textarea
884
+ value={p.question}
885
+ onChange={e => p.setQuestion(e.target.value)}
886
+ rows={3}
887
+ className="w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm font-sans"
888
+ placeholder="Enter a question…"
889
+ />
890
+ <button onClick={p.onRun} disabled={p.running || !p.apiKey} className="mt-2 rounded-lg bg-zinc-900 px-4 py-2 text-xs font-bold text-white disabled:opacity-40">
891
+ {p.running ? "Running…" : "Run Old vs V15 Live"}
892
+ </button>
893
+ </div>
894
+
895
+ {/* Live log (terminal-style) */}
896
+ {p.log.length > 0 && (
897
+ <div className="rounded-2xl border border-zinc-900 bg-zinc-950 p-3 font-mono text-[11px] text-emerald-200">
898
+ <div className="max-h-56 overflow-y-auto space-y-0.5">
899
+ {p.log.map((l, i) => <div key={i}>{l}</div>)}
900
+ </div>
901
+ </div>
902
+ )}
903
+
904
+ {/* Side-by-side comparison */}
905
+ {p.row && (
906
+ <div className="space-y-3">
907
+ {p.row.v15?.runSettings && (
908
+ <div className="rounded-xl border border-zinc-200 bg-zinc-50 p-2 text-[10px] font-mono text-zinc-700">
909
+ <span className="font-bold text-zinc-500 uppercase mr-1">Run settings:</span>
910
+ depth {p.row.v15.runSettings.depth} · model {p.row.v15.modelUsed}
911
+ {p.row.v15.runSettings.fourStage ? " · 4-Stage" : ""}
912
+ {p.row.v15.runSettings.cluster ? ` · cluster×${p.row.v15.runSettings.clusterSize}` : ""}
913
+ {p.row.v15.runSettings.sloop ? ` · SLOOP ${p.row.v15.runSettings.sloopPages}pg` : ""}
914
+ {p.row.v15.runSettings.templateId ? ` · ${p.row.v15.runSettings.templateId}` : ""}
915
+ {p.row.v15.runSettings.styleOverride ? ` · ${p.row.v15.runSettings.styleOverride}` : ""}
916
+ {p.row.v15.runSettings.williamsPersona ? ` · persona: ${p.row.v15.runSettings.williamsPersona}` : ""}
917
+ {p.row.v15.runSettings.adversarial ? " · adversarial" : ""}
918
+ {p.row.v15.runSettings.webSearch ? ` · web(${p.row.v15.groundingProvider ?? "?"}, ${p.row.v15.groundingCount ?? 0}src)` : ""}
919
+ {p.row.v15.runSettings.defensePack ? " · 246-pack" : ""}
920
+ {p.row.v15.runSettings.advancedGates ? " · testbed" : ""}
921
+ {p.row.v15.runSettings.singleJudge ? " · single-judge" : " · panel-judge"}
922
+ </div>
923
+ )}
924
+ {p.row.v15 && (p.row.v15 as any).citationAudit && <CitationAuditPanel audit={(p.row.v15 as any).citationAudit} />}
925
+ {p.row.v15 && (p.row.v15 as any).passHistory && (p.row.v15 as any).passHistory.length > 0 && <DraftStatsPanel out={p.row.v15} />}
926
+ {p.row.comparative && (
927
+ <div className="rounded-xl border-2 border-amber-300 bg-amber-50/50 p-3 text-[11px]">
928
+ <div className="mb-1 flex items-center justify-between">
929
+ <div className="font-bold text-amber-900">⚖️ Independent Comparative Judge (fresh context) — {p.row.comparative.judgeModel}</div>
930
+ <div className="font-mono">baseline <b>{p.row.comparative.baselineScore.toFixed(1)}</b> vs V15 <b className={p.row.comparative.v15Score >= 9.5 ? "text-emerald-700" : ""}>{p.row.comparative.v15Score.toFixed(1)}</b> · gap <b className={p.row.comparative.gap >= 0 ? "text-emerald-700" : "text-rose-700"}>{p.row.comparative.gap >= 0 ? "+" : ""}{p.row.comparative.gap.toFixed(1)}</b> · winner: {p.row.comparative.winner}</div>
931
+ </div>
932
+ {p.row.comparative.rationale && <div className="italic text-amber-800 mb-1">{p.row.comparative.rationale}</div>}
933
+ </div>
934
+ )}
935
+ <div className="grid gap-3 md:grid-cols-2">
936
+ <div className="rounded-2xl border border-rose-200 bg-white p-3">
937
+ <div className="mb-1 text-[10px] font-bold uppercase tracking-wider text-rose-600">OLD PATH (direct generation)</div>
938
+ <pre className="max-h-96 overflow-auto whitespace-pre-wrap font-sans text-[12px] leading-relaxed text-zinc-800">{p.row.baseline?.draft ?? "…"}</pre>
939
+ {p.row.baseline?.judgeScore !== null && p.row.baseline?.judgeScore !== undefined && (
940
+ <div className="mt-2 border-t pt-2 text-[11px]"><b>Judge (baseline):</b> {p.row.baseline.judgeScore.toFixed(2)}/10</div>
941
+ )}
942
+ </div>
943
+ <div className="rounded-2xl border-2 border-emerald-400 bg-white p-3">
944
+ <div className="mb-1 text-[10px] font-bold uppercase tracking-wider text-emerald-600">V15 PIPELINE (LIVE)</div>
945
+ <pre className="max-h-96 overflow-auto whitespace-pre-wrap font-sans text-[12px] leading-relaxed text-zinc-800">{p.row.v15?.fixed ?? "…"}</pre>
946
+ {p.row.v15 && (
947
+ <div className="mt-2 border-t pt-2 space-y-1 text-[11px]">
948
+ <div className="font-mono text-emerald-700">Guard: {p.row.v15.guardScore.toFixed(2)}/10 · Issues: {p.row.v15.issues.length} · Judge median: {p.row.v15.judgeScore?.toFixed(2) ?? "n/a"}</div>
949
+ {p.row.v15.eloConsensus?.tieBreakApplied && (
950
+ <div className="rounded bg-indigo-50 p-1.5 text-[10px] italic text-indigo-800">🏅 Elo Tie-Break Applied: {p.row.v15.eloConsensus.rationale}</div>
951
+ )}
952
+ </div>
953
+ )}
954
+ </div>
955
+ </div>
956
+ </div>
957
+ )}
958
+
959
+ {/* Roster */}
960
+ <ModelRoster rateUsage={p.rateUsage} judgeRoster={p.row?.v15?.judgeRoster ?? []} />
961
+
962
+ {/* 246-Defense Catalog Pack card */}
963
+ <div className="rounded-2xl border border-amber-200 bg-amber-50/40 p-3">
964
+ <div className="flex items-start justify-between">
965
+ <div>
966
+ <div className="text-sm font-bold text-amber-900">🛡️ 246-Defense Catalog Pack (additive)</div>
967
+ <div className="mt-1 text-[11px] text-amber-800">Adds ~24 signature-based detectors from the original 246-defense catalog (A2, A5, C5, D1, E3, F2, F4, F5, G2, G6, H4, H7, I6, J2, K4, L2, N1, O2, Q4, R3, U1, V1, W1, X2) that complement V15 builtins without duplication. OFF by default — turning it on adds coverage; leaving it off preserves current V15 scoring exactly. Receipts include catalog cross-refs regardless.</div>
968
+ </div>
969
+ <label className="flex items-center gap-1.5 text-[11px] font-bold text-amber-900">
970
+ <input type="checkbox" checked={p.useDefensePack} onChange={e => p.setUseDefensePack(e.target.checked)} className="accent-amber-600" />
971
+ Enable catalog-derived pack
972
+ </label>
973
+ </div>
974
+ </div>
975
+
976
+ {/* Long-Report Cohesion Pass card */}
977
+ <div className="rounded-2xl border border-violet-200 bg-violet-50/40 p-3 space-y-2">
978
+ <div>
979
+ <div className="text-sm font-bold text-violet-900">📑 Long-Report Cohesion Pass (post-processor for 4-stage / N-Deep / cluster / SLOOP)</div>
980
+ <div className="text-[11px] text-violet-800">Additive to the original app: paste a long report → V15 runs deterministic cohesion audit → inserts thesis if missing → rewrites ONLY defective sections (O(defective) not O(total) LLM calls) → preserves everything else verbatim.</div>
981
+ </div>
982
+ <input value={p.cohesionQuestion} onChange={e => p.setCohesionQuestion(e.target.value)} className="w-full rounded border border-violet-300 bg-white px-2 py-1.5 text-[12px]" placeholder="Question the report was written for…" />
983
+ <textarea value={p.cohesionInput} onChange={e => p.setCohesionInput(e.target.value)} rows={5} className="w-full rounded border border-violet-300 bg-white px-2 py-1.5 text-[12px] font-mono" placeholder="Paste the long report output here (from 4-Stage / N-Deep / SLOOP / cluster pipeline)…" />
984
+ <button onClick={p.onCohesion} disabled={p.cohesionRunning || !p.apiKey || !p.cohesionInput.trim()} className="rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-bold text-white disabled:opacity-40">
985
+ {p.cohesionRunning ? "Running…" : "▶ Run Cohesion Pass"}
986
+ </button>
987
+ {p.cohesionResult && (
988
+ <div className="rounded bg-white p-2 text-[11px]">
989
+ <div className="font-bold text-violet-900">Cohesion pass complete · rewrote {p.cohesionResult.sectionsRewritten} section(s)</div>
990
+ {p.cohesionResult.cohesionIssues.length > 0 && (
991
+ <ul className="mt-1 text-zinc-700">{p.cohesionResult.cohesionIssues.map((i, k) => <li key={k}>• {i}</li>)}</ul>
992
+ )}
993
+ <details className="mt-2">
994
+ <summary className="cursor-pointer font-bold text-violet-800">View improved report ↓</summary>
995
+ <pre className="mt-1 max-h-96 overflow-auto whitespace-pre-wrap font-sans text-[11px] text-zinc-800">{p.cohesionResult.improved}</pre>
996
+ </details>
997
+ </div>
998
+ )}
999
+ </div>
1000
+
1001
+ {/* Gate Testbed card */}
1002
+ <div className="rounded-2xl border border-fuchsia-200 bg-fuchsia-50/40 p-3">
1003
+ <div className="flex items-start justify-between">
1004
+ <div>
1005
+ <div className="text-sm font-bold text-fuchsia-900">🧪 Gate Testbed (Advanced — auto-discovered gates)</div>
1006
+ <div className="mt-1 text-[11px] text-fuchsia-800">When enabled, an LLM analyzes each run for failure patterns NOT covered by the current gate registry and proposes new deterministic gates here. Toggle individual gates into live scanning.</div>
1007
+ </div>
1008
+ <label className="flex items-center gap-1.5 text-[11px] font-bold text-fuchsia-900">
1009
+ <input type="checkbox" checked={p.advancedGates} onChange={e => p.setAdvancedGates(e.target.checked)} className="accent-fuchsia-600" />
1010
+ Enable cutting-edge testbed gates
1011
+ </label>
1012
+ </div>
1013
+ <div className="mt-2 text-[11px] text-fuchsia-700 italic">
1014
+ {listTestbedGates().length === 0
1015
+ ? "No gates discovered yet — run a live comparison with the testbed enabled."
1016
+ : `${listTestbedGates().length} testbed gate(s) discovered: ${listTestbedGates().map(g => g.code).join(", ")}`}
1017
+ </div>
1018
+ </div>
1019
+ </div>
1020
+ );
1021
+ }
1022
+
1023
+ // ── Batch Bank Tab ────────────────────────────────────────────────────────
1024
+ function BatchBankTab(p: {
1025
+ rows: Row[]; selected: number | null; setSelected: (i: number | null) => void;
1026
+ batchSize: number; setBatchSize: (n: number) => void;
1027
+ useCustom: boolean; setUseCustom: (v: boolean) => void;
1028
+ customQuestion: string; setCustomQuestion: (v: string) => void;
1029
+ profile: V15Profile; effectiveDepth: number;
1030
+ apiKey: boolean;
1031
+ onRun: () => void; running: boolean;
1032
+ advancedMode: boolean; setAdvancedMode: (v: boolean) => void;
1033
+ liveStage: string;
1034
+ useDefensePack: boolean; setUseDefensePack: (v: boolean) => void;
1035
+ advancedGates: boolean; setAdvancedGates: (v: boolean) => void;
1036
+ rateUsage: ReturnType<typeof snapshotAllUsage>;
1037
+ batchLog?: string[];
1038
+ }) {
1039
+ const selectedRow = p.selected !== null ? p.rows[p.selected] : null;
1040
+ return (
1041
+ <div className="flex h-full overflow-hidden">
1042
+ {/* Left: batch inputs + row list */}
1043
+ <div className="w-1/2 overflow-y-auto border-r border-zinc-200">
1044
+ <div className="border-b border-zinc-200 bg-white p-3 space-y-2">
1045
+ <div className="text-[10px] font-bold uppercase tracking-wider text-zinc-500">Batch calibration</div>
1046
+ <label className="flex items-center gap-2 text-[11px]">
1047
+ <span className="font-bold text-zinc-700">Questions (1-{MAX_BATCH_SIZE}):</span>
1048
+ <input type="number" min={1} max={MAX_BATCH_SIZE} value={p.batchSize} onChange={e => p.setBatchSize(Math.max(1, Math.min(MAX_BATCH_SIZE, Number(e.target.value))))} className="w-16 rounded border border-zinc-300 px-2 py-1 font-mono text-center" />
1049
+ </label>
1050
+ <label className="flex items-center gap-2 text-[11px]">
1051
+ <input type="checkbox" checked={p.useCustom} onChange={e => p.setUseCustom(e.target.checked)} className="accent-indigo-600" />
1052
+ <span className="font-bold text-zinc-700">Use custom question(s) instead of random bank</span>
1053
+ </label>
1054
+ {p.useCustom && (
1055
+ <textarea value={p.customQuestion} onChange={e => p.setCustomQuestion(e.target.value)} rows={4} className="w-full rounded border border-zinc-300 px-2 py-1.5 text-[12px]" placeholder="One question per double-line-break. Extra slots are filled from the random bank." />
1056
+ )}
1057
+ <div className="flex items-center justify-between">
1058
+ <label className="flex items-center gap-1.5 text-[11px]">
1059
+ <input type="checkbox" checked={p.advancedMode} onChange={e => p.setAdvancedMode(e.target.checked)} className="accent-fuchsia-600" />
1060
+ <span className="font-bold text-fuchsia-800">Advanced diagram (per-pass live status)</span>
1061
+ </label>
1062
+ <span className="text-[9px] text-zinc-400">outputs accumulate across runs</span>
1063
+ </div>
1064
+ <button onClick={p.onRun} disabled={p.running || !p.apiKey} className="w-full rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-bold text-white disabled:opacity-40">
1065
+ {p.running ? "Running…" : "▶ Run Batch"}
1066
+ </button>
1067
+ </div>
1068
+
1069
+ {/* Pipeline diagram (live status) */}
1070
+ <PipelineDiagram profile={p.profile} effectiveDepth={p.effectiveDepth} advancedMode={p.advancedMode} liveStage={p.liveStage} running={p.running} />
1071
+
1072
+ {/* Live Batch Log (terminal-style, scrollable, persistent) */}
1073
+ {p.batchLog && p.batchLog.length > 0 && (
1074
+ <div className="border-b border-zinc-200 bg-zinc-950 p-2">
1075
+ <div className="mb-1 flex items-center justify-between px-1 text-[9px] font-bold uppercase tracking-wider text-emerald-400">
1076
+ <span>▸ LIVE BATCH LOG</span>
1077
+ <span className="text-zinc-500">{p.batchLog.length} LINES</span>
1078
+ </div>
1079
+ <div className="max-h-56 overflow-y-auto rounded bg-zinc-950 p-1.5 font-mono text-[10px] leading-relaxed text-emerald-200">
1080
+ {p.batchLog.map((l, i) => <div key={i} className="whitespace-pre-wrap break-all">{l}</div>)}
1081
+ </div>
1082
+ </div>
1083
+ )}
1084
+
1085
+ {/* Row list */}
1086
+ {p.rows.length === 0 ? (
1087
+ <div className="p-6 text-center text-sm text-zinc-500">Batch has not been run yet.</div>
1088
+ ) : (
1089
+ p.rows.map((r, i) => (
1090
+ <BatchRow key={i} row={r} idx={i} selected={p.selected === i} onSelect={() => p.setSelected(i)} />
1091
+ ))
1092
+ )}
1093
+ </div>
1094
+
1095
+ {/* Right: prompt + baseline/V15 detail */}
1096
+ <div className="flex-1 overflow-y-auto bg-zinc-50/50 p-4">
1097
+ {!selectedRow ? (
1098
+ <div className="flex h-full items-center justify-center text-sm text-zinc-400">Select a batch row to view full untruncated results and telemetry.</div>
1099
+ ) : (
1100
+ <RowDetailPane row={selectedRow} />
1101
+ )}
1102
+ </div>
1103
+ </div>
1104
+ );
1105
+ }
1106
+
1107
+ function BatchRow({ row, idx, selected, onSelect }: { row: Row; idx: number; selected: boolean; onSelect: () => void }) {
1108
+ const baseJudge = row.baseline?.judgeScore ?? null;
1109
+ const guardScore = row.v15?.guardScore ?? null;
1110
+ const v15Judge = row.v15?.judgeScore ?? null;
1111
+ const stage = row.stage;
1112
+ return (
1113
+ <button onClick={onSelect} className={`block w-full border-b border-zinc-100 px-3 py-2.5 text-left text-xs transition-colors hover:bg-indigo-50/50 ${selected ? "bg-indigo-50 border-l-4 border-l-indigo-600" : ""}`}>
1114
+ <div className="flex items-center justify-between gap-2">
1115
+ <div className="flex items-center gap-1.5">
1116
+ <span className="rounded bg-zinc-200 px-1.5 py-0.5 font-mono text-[9px] font-bold text-zinc-700">{row.q.domain}</span>
1117
+ <span className={`text-[10px] font-bold ${stage === "done" ? "text-emerald-600" : stage === "error" ? "text-rose-600" : stage === "queued" ? "text-zinc-400" : "text-amber-600"}`}>
1118
+ {stage === "queued" ? "…" : stage === "baseline" ? "▶ baseline" : stage === "v15" ? "▶ V15" : stage === "judging" ? "▶ judging" : stage === "done" ? "✓" : "✗"}
1119
+ </span>
1120
+ </div>
1121
+ <div className="flex items-center gap-2 font-mono text-[10px]">
1122
+ <span className="text-zinc-500">base: <b className={baseJudge !== null && baseJudge >= TARGET_SCORE ? "text-emerald-700" : "text-zinc-700"}>{baseJudge?.toFixed(1) ?? "—"}</b></span>
1123
+ <span className="text-zinc-500">guard: <b className={guardScore !== null && guardScore >= TARGET_SCORE ? "text-emerald-700" : "text-zinc-700"}>{guardScore?.toFixed(1) ?? "—"}</b></span>
1124
+ <span className="text-zinc-500">judge: <b className={v15Judge !== null && v15Judge >= TARGET_SCORE ? "text-emerald-700" : "text-zinc-700"}>{v15Judge?.toFixed(1) ?? "—"}</b></span>
1125
+ </div>
1126
+ </div>
1127
+ <div className="mt-1 line-clamp-2 text-zinc-800 font-medium">Q{idx + 1}. {row.q.text}</div>
1128
+ {row.v15 && (
1129
+ <div className="mt-1 flex items-center gap-2 text-[9px] text-zinc-500">
1130
+ <span>depth: {row.v15.passes}</span>
1131
+ <span>model: {row.v15.modelUsed}</span>
1132
+ </div>
1133
+ )}
1134
+ </button>
1135
+ );
1136
+ }
1137
+
1138
+ function RowDetailPane({ row }: { row: Row }) {
1139
+ return (
1140
+ <div className="space-y-3 text-xs">
1141
+ <div className="rounded-xl border border-zinc-200 bg-white p-3 shadow-sm">
1142
+ <div className="text-[10px] font-bold uppercase tracking-wider text-indigo-600">PROMPT ({row.q.domain})</div>
1143
+ <div className="mt-1 font-medium text-zinc-900 leading-relaxed">{row.q.text}</div>
1144
+ </div>
1145
+
1146
+ {row.v15?.runSettings && (
1147
+ <div className="rounded-xl border border-zinc-200 bg-zinc-50 p-2 text-[10px] font-mono text-zinc-700">
1148
+ <span className="font-bold text-zinc-500 uppercase mr-1">Run settings:</span>
1149
+ depth {row.v15.runSettings.depth} · model {row.v15.modelUsed}
1150
+ {row.v15.runSettings.fourStage ? " · 4-Stage" : ""}
1151
+ {row.v15.runSettings.cluster ? ` · cluster×${row.v15.runSettings.clusterSize}` : ""}
1152
+ {row.v15.runSettings.sloop ? ` · SLOOP ${row.v15.runSettings.sloopPages}pg` : ""}
1153
+ {row.v15.runSettings.templateId ? ` · ${row.v15.runSettings.templateId}` : ""}
1154
+ {row.v15.runSettings.styleOverride ? ` · ${row.v15.runSettings.styleOverride}` : ""}
1155
+ {row.v15.runSettings.williamsPersona ? ` · persona: ${row.v15.runSettings.williamsPersona}` : ""}
1156
+ {row.v15.runSettings.adversarial ? " · adversarial" : ""}
1157
+ {row.v15.runSettings.webSearch ? ` · web(${row.v15.groundingProvider ?? "?"}, ${row.v15.groundingCount ?? 0}src)` : ""}
1158
+ {row.v15.runSettings.defensePack ? " · 246-pack" : ""}
1159
+ {row.v15.runSettings.advancedGates ? " · testbed" : ""}
1160
+ {row.v15.runSettings.singleJudge ? " · single-judge" : " · panel-judge"}
1161
+ </div>
1162
+ )}
1163
+
1164
+ {row.comparative && (
1165
+ <div className="rounded-xl border-2 border-amber-300 bg-amber-50/50 p-3 text-[11px]">
1166
+ <div className="mb-1 flex items-center justify-between">
1167
+ <div className="font-bold text-amber-900">⚖️ Independent Comparative Judge (fresh context) — {row.comparative.judgeModel}</div>
1168
+ <div className="font-mono">baseline <b>{row.comparative.baselineScore.toFixed(1)}</b> vs V15 <b className={row.comparative.v15Score >= 9.5 ? "text-emerald-700" : ""}>{row.comparative.v15Score.toFixed(1)}</b> · gap <b className={row.comparative.gap >= 0 ? "text-emerald-700" : "text-rose-700"}>{row.comparative.gap >= 0 ? "+" : ""}{row.comparative.gap.toFixed(1)}</b> · winner: {row.comparative.winner}</div>
1169
+ </div>
1170
+ {row.comparative.rationale && <div className="italic text-amber-800 mb-1">{row.comparative.rationale}</div>}
1171
+ <div className="grid grid-cols-2 gap-2">
1172
+ <div>
1173
+ <div className="text-[9px] font-bold uppercase text-zinc-500">Baseline → 9.9 needs:</div>
1174
+ <ul className="mt-0.5 list-disc pl-4 text-zinc-700">{row.comparative.baselineImprovements.map((s, k) => <li key={k}>{s}</li>)}</ul>
1175
+ </div>
1176
+ <div>
1177
+ <div className="text-[9px] font-bold uppercase text-zinc-500">V15 → 9.9 needs:</div>
1178
+ <ul className="mt-0.5 list-disc pl-4 text-zinc-700">{row.comparative.v15Improvements.map((s, k) => <li key={k}>{s}</li>)}</ul>
1179
+ </div>
1180
+ </div>
1181
+ </div>
1182
+ )}
1183
+
1184
+ <div className="grid grid-cols-2 gap-3">
1185
+ <div className="rounded-xl border border-zinc-200 bg-white p-3">
1186
+ <div className="mb-1 flex items-center justify-between">
1187
+ <div className="text-[10px] font-bold uppercase tracking-wider text-zinc-500">BASELINE (SINGLE PASS)</div>
1188
+ {row.baseline?.judgeScore !== null && row.baseline?.judgeScore !== undefined && (
1189
+ <div className="font-mono text-xs">judge: <b>{row.baseline.judgeScore.toFixed(2)}</b></div>
1190
+ )}
1191
+ </div>
1192
+ {row.baseline?.error && <div className="mb-2 rounded bg-rose-50 p-2 font-mono text-rose-800">✗ {row.baseline.error}</div>}
1193
+ <pre className="max-h-[500px] overflow-auto whitespace-pre-wrap font-sans text-[11px] leading-relaxed text-zinc-800">{row.baseline?.draft ?? "…"}</pre>
1194
+ {row.baseline?.judgeNote && <div className="mt-2 border-t pt-1.5 text-[10px] italic text-zinc-500">"{row.baseline.judgeNote}"</div>}
1195
+ </div>
1196
+
1197
+ <div className={`rounded-xl border bg-white p-3 ${(row.v15?.judgeScore ?? row.v15?.guardScore ?? 0) >= 9 ? "border-emerald-400" : "border-indigo-200"}`}>
1198
+ <div className="mb-1 flex items-center justify-between">
1199
+ <div>
1200
+ <span className="text-[10px] font-bold uppercase tracking-wider text-indigo-700">V15 RIGOR GUARD</span>
1201
+ {row.v15 && <span className="ml-1.5 rounded bg-indigo-50 px-1.5 py-0.5 text-[9px] font-mono text-indigo-700">Depth {row.v15.passes}</span>}
1202
+ </div>
1203
+ {row.v15 && (
1204
+ <div className="font-mono text-xs">
1205
+ guard: <b>{row.v15.guardScore.toFixed(2)}</b> · judge: <b className={(row.v15.judgeScore ?? 0) >= 9 ? "text-emerald-700" : "text-zinc-700"}>{row.v15.judgeScore?.toFixed(2) ?? "—"}</b>
1206
+ </div>
1207
+ )}
1208
+ </div>
1209
+ {row.v15?.error && <div className="mb-2 rounded bg-rose-50 p-2 font-mono text-rose-800">✗ {row.v15.error}</div>}
1210
+ <pre className="max-h-[500px] overflow-auto whitespace-pre-wrap font-sans text-[11px] leading-relaxed text-zinc-800">{row.v15?.fixed ?? "…"}</pre>
1211
+ {row.v15 && (
1212
+ <div className="mt-2 border-t pt-2 space-y-1.5">
1213
+ {row.v15.eloConsensus && (
1214
+ <div className="rounded bg-indigo-50/70 p-2 text-[10px]">
1215
+ <div className="font-bold text-indigo-900">Elo Consensus ({row.v15.eloConsensus.authorityModel} @ {row.v15.eloConsensus.authorityElo} Elo)</div>
1216
+ <div className="text-indigo-800 italic mt-0.5">"{row.v15.eloConsensus.rationale}"</div>
1217
+ </div>
1218
+ )}
1219
+ {row.v15.issues.length > 0 && (
1220
+ <div>
1221
+ <div className="text-[10px] font-bold uppercase tracking-wider text-rose-600">REMAINING ISSUES ({row.v15.issues.length})</div>
1222
+ <ul className="mt-1 space-y-1">
1223
+ {row.v15.issues.map((i, k) => (
1224
+ <li key={k} className="font-mono text-[9px] text-zinc-700 leading-tight">
1225
+ <span className={`mr-1 rounded px-1 font-bold ${i.severity === "critical" ? "bg-rose-200 text-rose-900" : i.severity === "major" ? "bg-orange-100 text-orange-900" : i.severity === "warning" ? "bg-amber-100 text-amber-900" : "bg-zinc-100 text-zinc-700"}`}>{i.severity}</span>
1226
+ <b>{i.code}</b>: {i.message}
1227
+ </li>
1228
+ ))}
1229
+ </ul>
1230
+ </div>
1231
+ )}
1232
+ {row.v15.autoFixesApplied.length > 0 && (
1233
+ <div className="text-[10px] text-emerald-700 font-bold">✓ auto-fixes: {row.v15.autoFixesApplied.join(", ")}</div>
1234
+ )}
1235
+ {(row.v15.testbedGatesProposed?.length ?? 0) > 0 && (
1236
+ <div className="rounded bg-violet-50 p-2 text-[10px]">
1237
+ <div className="font-bold text-violet-900">Advanced Gate Testbed proposals</div>
1238
+ <ul className="mt-1">{row.v15.testbedGatesProposed!.map(g => <li key={g.id} className="font-mono">• <b>{g.code}</b> ({g.severity}) · /{g.regex}/{g.flags || "i"}</li>)}</ul>
1239
+ </div>
1240
+ )}
1241
+ </div>
1242
+ )}
1243
+ </div>
1244
+ </div>
1245
+ </div>
1246
+ );
1247
+ }
1248
+
1249
+ function PipelineDiagram({ profile, effectiveDepth, advancedMode, liveStage, running }: { profile: V15Profile; effectiveDepth: number; advancedMode: boolean; liveStage: string; running: boolean }) {
1250
+ // Parse the live progress string to know which stage is currently active.
1251
+ const stageKey = (s: string): string => {
1252
+ const t = (s || "").toLowerCase();
1253
+ if (t.includes("draft")) return "draft";
1254
+ if (t.includes("depth") && t.includes("scan")) { const m = t.match(/depth (\d+)/); return `scan${m?.[1] ?? "1"}`; }
1255
+ if (t.includes("depth") && t.includes("auto")) { const m = t.match(/depth (\d+)/); return `fix${m?.[1] ?? "1"}`; }
1256
+ if (t.includes("depth") && t.includes("refin")) { const m = t.match(/depth (\d+)/); return `refine${m?.[1] ?? "1"}`; }
1257
+ if (t.includes("gate mining")) return "mine";
1258
+ if (t.includes("judg")) return "judge";
1259
+ if (t.includes("done")) return "done";
1260
+ return "";
1261
+ };
1262
+ const cur = running ? stageKey(liveStage) : "";
1263
+
1264
+ if (!advancedMode) {
1265
+ const stages: { name: string; active: boolean; detail?: string; key?: string }[] = [
1266
+ { name: "Draft (rotated)", active: true, detail: profile.templateId ? `template: ${profile.templateId}` : undefined, key: "draft" },
1267
+ { name: "4-Stage framing", active: !!profile.fourStage },
1268
+ { name: "Cluster synthesis", active: !!profile.cluster, detail: profile.cluster ? `${profile.clusterSize ?? 8} parallel` : undefined },
1269
+ { name: `Deterministic scan → auto-fix`, active: true, key: "fix1" },
1270
+ { name: `N-Deep refine loop`, active: !!profile.nDeep, detail: `depth ${effectiveDepth}`, key: "refine1" },
1271
+ { name: "SLOOP long-form", active: !!profile.sloop, detail: profile.sloop ? `${profile.sloopPages ?? 4} pages` : undefined },
1272
+ { name: "246-Defense catalog", active: !!profile.useOriginalDefensePack },
1273
+ { name: "Elo-consensus judge", active: true, key: "judge" },
1274
+ { name: "Divergence review", active: true },
1275
+ ];
1276
+ return (
1277
+ <div className="border-b border-zinc-200 bg-gradient-to-b from-indigo-50/40 to-white p-3">
1278
+ <div className="text-[10px] font-bold uppercase tracking-wider text-indigo-700 mb-2">Pipeline diagram (from current settings){running && <span className="ml-2 text-emerald-600">● live</span>}</div>
1279
+ <div className="flex flex-wrap items-center gap-1">
1280
+ {stages.map((s, i) => {
1281
+ const isCur = s.key && cur === s.key;
1282
+ return (
1283
+ <div key={i} className="flex items-center gap-1">
1284
+ <div className={`rounded-md border px-2 py-1 text-[10px] font-mono ${isCur ? "border-emerald-500 bg-emerald-100 text-emerald-900 animate-pulse" : s.active ? "border-indigo-400 bg-indigo-50 text-indigo-900" : "border-zinc-200 bg-zinc-50 text-zinc-400 line-through"}`}>
1285
+ {s.name}{s.detail && <span className="ml-1 text-[9px] text-indigo-600">({s.detail})</span>}
1286
+ </div>
1287
+ {i < stages.length - 1 && <span className="text-zinc-400">→</span>}
1288
+ </div>
1289
+ );
1290
+ })}
1291
+ </div>
1292
+ </div>
1293
+ );
1294
+ }
1295
+
1296
+ // Advanced mode: per-pass buttons. Each N-Deep pass gets a Draft/Refine + a
1297
+ // 246-gate scan node, so N-Deep=4 → 8 nodes for that section.
1298
+ const nodes: { label: string; key: string; kind: "draft" | "scan" | "refine" | "gate" | "cluster" | "judge" | "sloop" | "divergence" }[] = [];
1299
+ nodes.push({ label: `Draft${profile.templateId ? ` · ${profile.templateId}` : ""}`, key: "draft", kind: "draft" });
1300
+ if (profile.fourStage) nodes.push({ label: "4-Stage", key: "fourstage", kind: "cluster" });
1301
+ if (profile.cluster) nodes.push({ label: `Cluster ×${profile.clusterSize ?? 8}`, key: "cluster", kind: "cluster" });
1302
+ for (let d = 1; d <= effectiveDepth; d++) {
1303
+ nodes.push({ label: `Scan #${d}`, key: `scan${d}`, kind: "scan" });
1304
+ nodes.push({ label: `246-Gate #${d}`, key: `fix${d}`, kind: "gate" });
1305
+ if (d < effectiveDepth) nodes.push({ label: `Refine #${d}`, key: `refine${d}`, kind: "refine" });
1306
+ }
1307
+ if (profile.sloop) nodes.push({ label: `SLOOP ${profile.sloopPages ?? 4}pg`, key: "sloop", kind: "sloop" });
1308
+ nodes.push({ label: "Elo Judge", key: "judge", kind: "judge" });
1309
+ nodes.push({ label: "Divergence", key: "done", kind: "divergence" });
1310
+
1311
+ const kindColor: Record<string, string> = {
1312
+ draft: "border-indigo-400 bg-indigo-50 text-indigo-900",
1313
+ scan: "border-sky-400 bg-sky-50 text-sky-900",
1314
+ gate: "border-amber-400 bg-amber-50 text-amber-900",
1315
+ refine: "border-violet-400 bg-violet-50 text-violet-900",
1316
+ cluster: "border-teal-400 bg-teal-50 text-teal-900",
1317
+ sloop: "border-fuchsia-400 bg-fuchsia-50 text-fuchsia-900",
1318
+ judge: "border-emerald-400 bg-emerald-50 text-emerald-900",
1319
+ divergence: "border-rose-400 bg-rose-50 text-rose-900",
1320
+ };
1321
+
1322
+ return (
1323
+ <div className="border-b border-zinc-200 bg-gradient-to-b from-fuchsia-50/30 to-white p-3">
1324
+ <div className="text-[10px] font-bold uppercase tracking-wider text-fuchsia-700 mb-2">
1325
+ ADVANCED PIPELINE — {nodes.length} NODES{running && <span className="ml-2 text-emerald-600">● live</span>}
1326
+ </div>
1327
+ <div className="flex flex-wrap items-center gap-1">
1328
+ {nodes.map((n, i) => {
1329
+ const isCur = running && cur === n.key;
1330
+ return (
1331
+ <div key={i} className="flex items-center gap-1">
1332
+ <div className={`rounded-md border px-2 py-1 text-[9px] font-mono ${isCur ? "border-emerald-600 bg-emerald-200 text-emerald-950 animate-pulse ring-2 ring-emerald-300" : kindColor[n.kind]}`}>
1333
+ {isCur ? "▶ " : ""}{n.label}
1334
+ </div>
1335
+ {i < nodes.length - 1 && <span className="text-zinc-300">→</span>}
1336
+ </div>
1337
+ );
1338
+ })}
1339
+ </div>
1340
+ </div>
1341
+ );
1342
+ }
1343
+
1344
+ function ModelRoster({ rateUsage, judgeRoster }: { rateUsage: ReturnType<typeof snapshotAllUsage>; judgeRoster: NonNullable<V15RunOutcome["judgeRoster"]> }) {
1345
+ const judgeMap = new Map(judgeRoster.map(j => [j.model, j]));
1346
+ const usageBy = new Map(rateUsage.map(u => [u.model, u]));
1347
+ return (
1348
+ <div className="rounded-2xl border border-zinc-900/70 bg-white p-3 font-mono text-[11px]">
1349
+ <div className="mb-1 flex items-center justify-between">
1350
+ <div className="font-sans text-sm font-bold text-zinc-900">🔀 Test-&-Rotate LLM Pool (Elo-ranked)</div>
1351
+ <div className="text-[9px] uppercase tracking-wider text-zinc-400">snapshot</div>
1352
+ </div>
1353
+ <div className="space-y-0.5">
1354
+ {GEMINI_ELO_ROSTER.map((model, idx) => {
1355
+ const info = getModelEloInfo(model);
1356
+ const usage = usageBy.get(model);
1357
+ const j = judgeMap.get(model);
1358
+ const throttled = usage?.throttled ?? false;
1359
+ const dot = throttled ? "bg-amber-500" : j ? (j.ok ? "bg-emerald-500" : "bg-rose-500") : idx === 0 ? "bg-emerald-500" : "bg-sky-500";
1360
+ return (
1361
+ <div key={model} className="flex items-center justify-between gap-2 whitespace-nowrap">
1362
+ <span className="flex min-w-0 items-center gap-1.5">
1363
+ <span className={`h-1.5 w-1.5 shrink-0 rounded-full ${dot}`} />
1364
+ <span className="truncate text-zinc-900">{info.name}</span>
1365
+ </span>
1366
+ <span className="shrink-0 text-zinc-600 text-[10px]">
1367
+ {info.elo} · {info.tier}{info.tieBreakAuthority ? " · tie-break authority" : ""}
1368
+ {j ? ` · ${j.latencyMs}ms` : ""}
1369
+ {usage ? ` · ${usage.rpmUsed}/${usage.rpmMax}rpm ${usage.rpdUsed}/${usage.rpdMax}rpd` : ""}
1370
+ </span>
1371
+ </div>
1372
+ );
1373
+ })}
1374
+ </div>
1375
+ </div>
1376
+ );
1377
+ }
1378
+
1379
+ function DivergenceLogPanel({ log, onClear, onClose }: { log: DivergenceEntry[]; onClear: () => void; onClose: () => void }) {
1380
+ return (
1381
+ <div className="h-full overflow-y-auto p-4 space-y-3">
1382
+ <div className="flex items-center justify-between rounded-xl border border-amber-300 bg-amber-50/50 p-3">
1383
+ <div>
1384
+ <div className="text-sm font-bold text-amber-900">🔎 Improvement Ledger — guard-vs-judge divergences ({log.length}) · engineer decides integration next turn</div>
1385
+ <div className="text-[11px] text-amber-800">When guard score and judge score differ by ≥ 1.0, the highest-Elo reviewer model analyzes WHY and proposes concrete improvements (single or multi-option). Nothing is auto-applied — each entry is pending your decision.</div>
1386
+ </div>
1387
+ <div className="flex gap-2">
1388
+ <button onClick={onClear} className="rounded-lg border border-rose-300 bg-white px-2 py-1 text-xs font-bold text-rose-700 hover:bg-rose-50">Clear</button>
1389
+ <button onClick={onClose} className="rounded-lg border border-zinc-300 bg-white px-2 py-1 text-xs font-bold text-zinc-700 hover:bg-zinc-100">Back</button>
1390
+ </div>
1391
+ </div>
1392
+ {log.length === 0 && <div className="rounded border border-zinc-200 bg-white p-6 text-center text-sm text-zinc-500">No divergences logged yet. Run some calibrations first.</div>}
1393
+ {log.slice().reverse().map((e, i) => (
1394
+ <div key={i} className="rounded-xl border border-zinc-200 bg-white p-3 text-xs space-y-1.5">
1395
+ <div className="flex items-center justify-between">
1396
+ <div className="text-[10px] text-zinc-500">
1397
+ {new Date(e.timestamp).toLocaleString()} · guard {e.guardScore} vs judge {e.judgeScore} (gap {Math.abs(e.delta).toFixed(2)}) · reviewed by {e.authorityModel} · <span className="font-bold text-amber-700">{e.decision ?? "pending-decision"}</span>
1398
+ </div>
1399
+ <div className="font-mono">Δ <b className={e.delta >= 0 ? "text-emerald-700" : "text-rose-700"}>{e.delta >= 0 ? "+" : ""}{e.delta.toFixed(1)}</b></div>
1400
+ </div>
1401
+ {e.judgePanel && e.judgePanel.length > 0 && (
1402
+ <div className="flex flex-wrap gap-1 text-[9px]">
1403
+ {e.judgePanel.map((j, k) => (
1404
+ <span key={k} className="rounded bg-sky-50 px-1.5 py-0.5 font-mono text-sky-800">
1405
+ {k === 0 ? "judge-1" : `judge-${k + 1}`}: {j.model} (Elo {j.elo}) → {j.score}
1406
+ </span>
1407
+ ))}
1408
+ </div>
1409
+ )}
1410
+ <div className="rounded bg-zinc-50 p-2 text-[11px] italic text-zinc-700">{e.question.slice(0, 320)}{e.question.length > 320 ? "…" : ""}</div>
1411
+ <div className="rounded bg-indigo-50 p-2 text-[11px] text-indigo-900">{e.suggestion.reason}</div>
1412
+ <div>
1413
+ {e.suggestion.suggestions.map((s, k) => (
1414
+ <div key={k} className="text-[11px] mb-0.5">
1415
+ <b className="text-blue-700">{s.approach}:</b> <span className="text-zinc-800">{s.description}</span>
1416
+ {s.tradeoffs && <span className="text-[10px] italic text-zinc-500"> — trade-off: {s.tradeoffs}</span>}
1417
+ </div>
1418
+ ))}
1419
+ </div>
1420
+ </div>
1421
+ ))}
1422
+ </div>
1423
+ );
1424
+ }
1425
+
1426
+ interface AdvancedConfigProps {
1427
+ presets: Array<{ name: string; date: string; settings: any }>;
1428
+ newPresetName: string;
1429
+ setNewPresetName: (v: string) => void;
1430
+ savePreset: () => void;
1431
+ deletePreset: (name: string) => void;
1432
+ loadPreset: (settings: any) => void;
1433
+ auxKeys: string[];
1434
+ auxKeysInput: string;
1435
+ setAuxKeysInput: (v: string) => void;
1436
+ addAuxKey: () => void;
1437
+ deleteAuxKey: (key: string) => void;
1438
+ allowedModels: string[];
1439
+ toggleAllowedModel: (model: string) => void;
1440
+ downloadPortConfig: () => void;
1441
+ onClose: () => void;
1442
+ }
1443
+
1444
+ function AdvancedConfigPanel(p: AdvancedConfigProps) {
1445
+ // Domain 9.9 index roster definitions
1446
+ const SCORE_INDEX_ROSTER = [
1447
+ { dom: "General", quant: "worked examples, detailed step traces", qual: "calibrated confidence, clear structured definitions" },
1448
+ { dom: "Numeric", quant: "exact worked equations, concrete units on every quantity", qual: "stated constraints, explicit variables defined first" },
1449
+ { dom: "Medical", quant: "enrollment target estimates, target dosage mg/kg ceilings", qual: "SABV sex-disaggregated secondary plans, clinic safety crisis triage" },
1450
+ { dom: "Legal", quant: "exact Bluebook reporter volume numbers, statute sections", qual: "jurisdiction limit disclosure, explicit attorney advisory disclaimers" },
1451
+ { dom: "Finance", quant: "worked CAGR, EBITDA Bridges, NPV/IRR discount rate math", qual: "registered adviser disclaimers, forward-looking past-perf disclaimers" },
1452
+ { dom: "Statistics", quant: "exact confidence intervals, effect sizes, p-value caps", qual: "multiplicity correction, causal vs correlation limitations" },
1453
+ { dom: "Software", quant: "worked complexity analyses, exact code line validations", qual: "CORS wildcard disclaimers, unhandled exception coverage, strict types" },
1454
+ { dom: "Science", quant: "Worked reactants weight balance, descriptive statistics", qual: "IMRAD pure structure, citation DOI lookups, retraction audits" },
1455
+ ];
1456
+
1457
+ return (
1458
+ <div className="flex-none border-b border-indigo-200 bg-white p-5 space-y-4 text-xs">
1459
+ <div className="flex items-center justify-between border-b pb-2">
1460
+ <div className="text-sm font-bold text-zinc-900">⚙️ Advanced Configuration Panel (V15 Calibration)</div>
1461
+ <button onClick={p.onClose} className="text-zinc-400 hover:text-zinc-600">✕</button>
1462
+ </div>
1463
+
1464
+ <div className="grid gap-4 md:grid-cols-2 max-h-[65vh] overflow-y-auto pr-2">
1465
+ {/* Left: Presets & Rotating Keys */}
1466
+ <div className="space-y-4">
1467
+ {/* Saved Presets */}
1468
+ <div className="rounded-xl border border-zinc-200 p-3 space-y-2 bg-zinc-50/50">
1469
+ <div className="font-bold text-zinc-800">💾 Calibration Presets Manager</div>
1470
+ <div className="flex gap-2">
1471
+ <input
1472
+ type="text"
1473
+ value={p.newPresetName}
1474
+ onChange={e => p.setNewPresetName(e.target.value)}
1475
+ placeholder="New preset name..."
1476
+ className="flex-1 rounded border border-zinc-300 px-2 py-1"
1477
+ />
1478
+ <button onClick={p.savePreset} className="rounded bg-indigo-600 px-3 py-1 font-bold text-white hover:bg-indigo-700">Save</button>
1479
+ </div>
1480
+ {p.presets.length > 0 ? (
1481
+ <div className="max-h-36 overflow-y-auto space-y-1">
1482
+ {p.presets.map(item => (
1483
+ <div key={item.name} className="flex items-center justify-between rounded border bg-white p-1.5">
1484
+ <div>
1485
+ <div className="font-bold text-zinc-900">{item.name}</div>
1486
+ <div className="text-[10px] text-zinc-400">saved {item.date}</div>
1487
+ </div>
1488
+ <div className="flex gap-1">
1489
+ <button onClick={() => p.loadPreset(item.settings)} className="rounded bg-zinc-100 px-2 py-0.5 text-zinc-700 hover:bg-zinc-200">Load</button>
1490
+ <button onClick={() => p.deletePreset(item.name)} className="rounded bg-rose-50 px-2 py-0.5 text-rose-700 hover:bg-rose-100">Delete</button>
1491
+ </div>
1492
+ </div>
1493
+ ))}
1494
+ </div>
1495
+ ) : (
1496
+ <div className="text-[11px] text-zinc-400 italic">No saved presets yet. Name and save your custom calibration settings above.</div>
1497
+ )}
1498
+ <button onClick={p.downloadPortConfig} className="w-full rounded border border-indigo-200 bg-white py-1.5 font-bold text-indigo-700 hover:bg-indigo-50">
1499
+ 📥 Download V15 Synthesis Port Configuration (.json)
1500
+ </button>
1501
+ </div>
1502
+
1503
+ {/* Auxiliary Rotating API Keys */}
1504
+ <div className="rounded-xl border border-zinc-200 p-3 space-y-2 bg-zinc-50/50">
1505
+ <div className="font-bold text-zinc-800">🔑 Rotating API Keys (Vendor: Gemini)</div>
1506
+ <div className="text-[10px] text-zinc-500">Provide multiple keys to rotate randomly, multiplying your available requests/minute and day quotas.</div>
1507
+ <div className="flex gap-2">
1508
+ <input
1509
+ type="password"
1510
+ value={p.auxKeysInput}
1511
+ onChange={e => p.setAuxKeysInput(e.target.value)}
1512
+ placeholder="Enter extra Gemini key..."
1513
+ className="flex-1 rounded border border-zinc-300 px-2 py-1"
1514
+ />
1515
+ <button onClick={p.addAuxKey} className="rounded bg-indigo-600 px-3 py-1 font-bold text-white hover:bg-indigo-700">Add Key</button>
1516
+ </div>
1517
+ {p.auxKeys.length > 0 ? (
1518
+ <div className="max-h-24 overflow-y-auto space-y-1">
1519
+ {p.auxKeys.map((key, idx) => (
1520
+ <div key={idx} className="flex items-center justify-between rounded border bg-white p-1.5 font-mono">
1521
+ <span>Key #{idx + 1}: {key.slice(0, 6)}…{key.slice(-4)}</span>
1522
+ <button onClick={() => p.deleteAuxKey(key)} className="rounded bg-rose-50 px-2 py-0.5 text-rose-700 hover:bg-rose-100">Remove</button>
1523
+ </div>
1524
+ ))}
1525
+ </div>
1526
+ ) : (
1527
+ <div className="text-[11px] text-zinc-400 italic">No auxiliary keys added yet. Live runs will fallback strictly to the primary key.</div>
1528
+ )}
1529
+ </div>
1530
+ </div>
1531
+
1532
+ {/* Right: Allowed Models & 9.9 Index */}
1533
+ <div className="space-y-4">
1534
+ {/* Model Allowlist Checklist */}
1535
+ <div className="rounded-xl border border-zinc-200 p-3 bg-zinc-50/50">
1536
+ <div className="font-bold text-zinc-800 mb-1.5">🎯 Model Access List (Allow / Disallow)</div>
1537
+ <div className="text-[10px] text-zinc-500 mb-2">Check the models below that the calibration pipeline is allowed to query randomly per run. Unchecked models are fully disabled. Empty allowlist = all active.</div>
1538
+ <div className="grid gap-x-4 gap-y-1.5 sm:grid-cols-2">
1539
+ {GEMINI_ELO_ROSTER.map(model => {
1540
+ const info = getModelEloInfo(model);
1541
+ const active = p.allowedModels.length === 0 || p.allowedModels.includes(model);
1542
+ return (
1543
+ <label key={model} className="flex items-center gap-2 cursor-pointer select-none">
1544
+ <input
1545
+ type="checkbox"
1546
+ checked={active}
1547
+ onChange={() => p.toggleAllowedModel(model)}
1548
+ className="h-3.5 w-3.5 accent-indigo-600"
1549
+ />
1550
+ <div className="min-w-0">
1551
+ <div className="font-bold text-zinc-900 truncate">{info.name.replace("gemini:", "")}</div>
1552
+ <div className="text-[10px] text-zinc-500">{info.elo} · {info.tier}</div>
1553
+ </div>
1554
+ </label>
1555
+ );
1556
+ })}
1557
+ </div>
1558
+ </div>
1559
+
1560
+ {/* 9.9 Score Calibration Index */}
1561
+ <div className="rounded-xl border border-zinc-200 p-3 bg-zinc-50/50">
1562
+ <div className="font-bold text-zinc-800 mb-1">📐 9.9 Score Calibration Index</div>
1563
+ <div className="text-[10px] text-zinc-500 mb-2">The exact balance of quantitative and qualitative data required for a perfect 9.9 score (independent of any specific OMEGA template structure):</div>
1564
+ <div className="max-h-36 overflow-y-auto space-y-1 text-[11px] leading-relaxed">
1565
+ {SCORE_INDEX_ROSTER.map(item => (
1566
+ <div key={item.dom} className="border-b pb-1">
1567
+ <span className="font-bold text-zinc-900">{item.dom}:</span>
1568
+ <span className="text-zinc-600 ml-1">Requires <b>{item.quant}</b> (quantitative) paired with <b>{item.qual}</b> (qualitative).</span>
1569
+ </div>
1570
+ ))}
1571
+ </div>
1572
+ </div>
1573
+ </div>
1574
+ </div>
1575
+ </div>
1576
+ );
1577
+ }
1578
+
1579
+
1580
+ // ── Draft Stats Panel (deterministic, no LLM call — from V15 Engine) ────────
1581
+ function DraftStatsPanel({ out }: { out: any }) {
1582
+ const passes = out.passHistory || [];
1583
+ if (passes.length === 0) return null;
1584
+ const bestIdx = out.bestPassIndex ?? -1;
1585
+ const isMonotonic = passes.every((p: any, i: number) => i === 0 || p.guardScore >= passes[i-1].guardScore);
1586
+
1587
+ return (
1588
+ <div className="space-y-3 mb-4">
1589
+ <div className="rounded-xl border border-sky-200 bg-sky-50/30 overflow-hidden text-[10px]">
1590
+ <div className="flex items-center justify-between px-3 py-2 border-b border-sky-100 bg-sky-50">
1591
+ <div className="font-bold text-sky-900 flex items-center gap-1.5">
1592
+ <span className="text-sm">📊</span> Draft Stats (deterministic, no LLM call)
1593
+ </div>
1594
+ <div className="text-sky-700">{passes.length} pass(es) - best: pass {bestIdx + 1}</div>
1595
+ </div>
1596
+ <div className="overflow-x-auto">
1597
+ <table className="w-full text-left">
1598
+ <thead className="text-sky-800 font-bold border-b border-sky-100 bg-sky-50/50">
1599
+ <tr>
1600
+ <th className="px-3 py-1.5">Pass</th>
1601
+ <th className="px-2 py-1.5">Guard</th>
1602
+ <th className="px-2 py-1.5">Model</th>
1603
+ <th className="px-2 py-1.5">Chars</th>
1604
+ <th className="px-2 py-1.5">Words</th>
1605
+ <th className="px-2 py-1.5">Sent.</th>
1606
+ <th className="px-2 py-1.5">Avg/sent</th>
1607
+ <th className="px-2 py-1.5">Cites</th>
1608
+ <th className="px-2 py-1.5">Code</th>
1609
+ <th className="px-2 py-1.5">H#</th>
1610
+ <th className="px-2 py-1.5">Tbl rows</th>
1611
+ <th className="px-2 py-1.5">Crit</th>
1612
+ <th className="px-2 py-1.5">Major</th>
1613
+ <th className="px-2 py-1.5">Warn</th>
1614
+ <th className="px-2 py-1.5">Canon gates</th>
1615
+ <th className="px-2 py-1.5">Testbed gates</th>
1616
+ </tr>
1617
+ </thead>
1618
+ <tbody className="divide-y divide-sky-50 font-mono text-zinc-700">
1619
+ {passes.map((p: any, i: number) => {
1620
+ const isBest = i === bestIdx;
1621
+ return (
1622
+ <tr key={i} className={isBest ? "bg-emerald-50/60" : "hover:bg-white/50"}>
1623
+ <td className="px-3 py-1.5 font-bold flex items-center gap-1">{i + 1} {isBest && <span className="text-emerald-600">★</span>}</td>
1624
+ <td className={"px-2 py-1.5 font-bold " + (p.guardScore >= 9 ? "text-emerald-700" : "text-amber-700")}>{p.guardScore.toFixed(2)}</td>
1625
+ <td className="px-2 py-1.5 whitespace-nowrap">{p.modelUsed.replace("gemini-", "").slice(0, 12)}</td>
1626
+ <td className="px-2 py-1.5">{p.charCount}</td>
1627
+ <td className="px-2 py-1.5 font-bold">{p.wordCount}</td>
1628
+ <td className="px-2 py-1.5">{p.sentenceCount}</td>
1629
+ <td className="px-2 py-1.5">{p.avgSentenceLen.toFixed(1)}</td>
1630
+ <td className="px-2 py-1.5">{p.citationCount}</td>
1631
+ <td className="px-2 py-1.5">{p.codeBlockCount}</td>
1632
+ <td className="px-2 py-1.5">{p.headingCount}</td>
1633
+ <td className="px-2 py-1.5">{p.tableRowCount}</td>
1634
+ <td className={"px-2 py-1.5 font-bold " + (p.criticalCount > 0 ? "text-rose-600" : "text-zinc-400")}>{p.criticalCount}</td>
1635
+ <td className={"px-2 py-1.5 font-bold " + (p.majorCount > 0 ? "text-amber-600" : "text-zinc-400")}>{p.majorCount}</td>
1636
+ <td className={"px-2 py-1.5 " + (p.warningCount > 0 ? "text-zinc-700" : "text-zinc-400")}>{p.warningCount}</td>
1637
+ <td className="px-2 py-1.5">{p.canonicalGateHits?.length || 0}</td>
1638
+ <td className="px-2 py-1.5">{p.testbedGateHits?.length || 0}</td>
1639
+ </tr>
1640
+ );
1641
+ })}
1642
+ </tbody>
1643
+ </table>
1644
+ </div>
1645
+ <div className="px-3 py-2 border-t border-sky-100 text-sky-800 flex items-center gap-1.5">
1646
+ {isMonotonic ? (
1647
+ <><span className="text-emerald-600">✅</span> Score was monotonically non-decreasing across all passes.</>
1648
+ ) : (
1649
+ <><span className="text-amber-600">⚠️</span> {passes.filter((p: any, i: number) => i > 0 && p.guardScore < passes[i-1].guardScore).length} pass(es) scored lower than the previous pass — engine kept pass {bestIdx + 1} as final.</>
1650
+ )}
1651
+ </div>
1652
+ </div>
1653
+
1654
+ {out.bestOfNCandidates && out.bestOfNCandidates.length > 0 && (
1655
+ <div className="rounded-xl border border-indigo-200 bg-indigo-50/30 p-3 text-[10px] mt-3">
1656
+ <div className="font-bold text-indigo-900 mb-2 flex items-center gap-1.5 justify-between">
1657
+ <span><span className="text-sm">🎯</span> Best-of-N Outline-First Candidates</span>
1658
+ <span className="text-indigo-700 font-normal">outline-first: only the winning skeleton was expanded to full length</span>
1659
+ </div>
1660
+ <div className="overflow-x-auto">
1661
+ <table className="w-full text-left font-mono">
1662
+ <thead className="border-b border-indigo-100 text-indigo-800">
1663
+ <tr><th className="px-2 py-1">#</th><th className="px-2 py-1">Model</th><th className="px-2 py-1">Stage</th><th className="px-2 py-1">Chars</th><th className="px-2 py-1">Score</th><th className="px-2 py-1">Status</th><th className="px-2 py-1">Snippet</th></tr>
1664
+ </thead>
1665
+ <tbody>
1666
+ {out.bestOfNCandidates.map((c: any, i: number) => (
1667
+ <tr key={i} className={c.chosen ? "bg-emerald-50/60 align-top" : "align-top"}>
1668
+ <td className="px-2 py-1">{c.index + 1}</td>
1669
+ <td className="px-2 py-1">{c.model}</td>
1670
+ <td className="px-2 py-1">
1671
+ <span className={"rounded px-1.5 py-0.5 text-[9px] font-bold " + (c.stage === "expanded" ? "bg-emerald-200 text-emerald-900" : "bg-zinc-200 text-zinc-700")}>
1672
+ {c.stage === "expanded" ? "EXPANDED" : c.stage === "outline" ? "OUTLINE ONLY" : "—"}
1673
+ </span>
1674
+ </td>
1675
+ <td className="px-2 py-1">{c.charCount}</td>
1676
+ <td className="px-2 py-1 font-bold">{typeof c.guardScore === "number" ? c.guardScore.toFixed(2) : "—"}</td>
1677
+ <td className="px-2 py-1">{c.chosen ? "★ CHOSEN" : "discarded (outline)"}</td>
1678
+ <td className="px-2 py-1 max-w-xs whitespace-pre-wrap text-zinc-600 text-[9px]">{c.snippet ? c.snippet + (c.snippet.length >= 160 ? "…" : "") : "—"}</td>
1679
+ </tr>
1680
+ ))}
1681
+ </tbody>
1682
+ </table>
1683
+ </div>
1684
+ </div>
1685
+ )}
1686
+
1687
+ {out.coveReport && out.coveReport.questions && out.coveReport.questions.length > 0 && (
1688
+ <div className="rounded-xl border border-teal-200 bg-teal-50/30 p-3 text-[10px] mt-3">
1689
+ <div className="font-bold text-teal-900 mb-2 flex items-center gap-1.5 justify-between">
1690
+ <span><span className="text-sm">🔗</span> Chain-of-Verification (CoVe)</span>
1691
+ <span className={"px-2 py-0.5 rounded text-white font-bold " + (out.coveReport.inconsistencies === 0 ? "bg-emerald-500" : "bg-amber-500")}>{out.coveReport.inconsistencies}/{out.coveReport.questions.length} mismatch(es)</span>
1692
+ </div>
1693
+ <div className="overflow-x-auto">
1694
+ <table className="w-full text-left">
1695
+ <thead className="border-b border-teal-100 text-teal-800 font-bold">
1696
+ <tr><th className="px-2 py-1">Question</th><th className="px-2 py-1">Draft implied</th><th className="px-2 py-1">Verified</th><th className="px-2 py-1">OK</th></tr>
1697
+ </thead>
1698
+ <tbody className="divide-y divide-teal-50 font-mono text-zinc-700">
1699
+ {out.coveReport.questions.map((q: any, i: number) => (
1700
+ <tr key={i} className={q.consistent ? "" : "bg-amber-50/50"}>
1701
+ <td className="px-2 py-1 align-top">{q.question}</td>
1702
+ <td className="px-2 py-1 align-top">{q.expectedAnswer}</td>
1703
+ <td className="px-2 py-1 align-top">{q.verifiedAnswer}</td>
1704
+ <td className="px-2 py-1 align-top font-bold">{q.consistent ? "✓" : "✗"}</td>
1705
+ </tr>
1706
+ ))}
1707
+ </tbody>
1708
+ </table>
1709
+ </div>
1710
+ </div>
1711
+ )}
1712
+
1713
+ {out.polishApplied && (
1714
+ <div className="rounded-xl border border-fuchsia-200 bg-fuchsia-50/30 p-3 text-[10px] mt-3">
1715
+ <div className="font-bold text-fuchsia-900 flex items-center gap-1.5"><span className="text-sm">✨</span> Polish pass applied</div>
1716
+ <div className="mt-1 text-fuchsia-800">The final pass repaired structural issues (e.g. References section, scaffolding leaks, punctuation, or unclosed blocks) without changing the substantive content.</div>
1717
+ </div>
1718
+ )}
1719
+
1720
+ {out.judgeExcluded && out.judgeExcluded.length > 0 && (
1721
+ <div className="rounded-xl border border-amber-200 bg-amber-50/30 p-3 text-[10px]">
1722
+ <div className="font-bold text-amber-900 mb-1.5 flex items-center gap-1.5">
1723
+ <span className="text-sm">⚠️</span> Judges excluded this run (never fabricated as a fallback score)
1724
+ </div>
1725
+ <ul className="list-disc pl-4 space-y-1 font-mono text-amber-800">
1726
+ {out.judgeExcluded.map((e: any, i: number) => (
1727
+ <li key={i}><b>{e.model}</b>: {e.reason}</li>
1728
+ ))}
1729
+ </ul>
1730
+ </div>
1731
+ )}
1732
+
1733
+ {out.adversarialPreview && (
1734
+ <div className="rounded-xl border border-rose-200 bg-rose-50/30 p-3 text-[10px] font-mono mt-3">
1735
+ <div className="font-bold text-rose-900 mb-1.5 flex items-center gap-1.5 justify-between">
1736
+ <span><span className="text-sm">⚔️</span> Adversarial Engine Preview</span>
1737
+ <span className={"px-2 py-0.5 rounded text-white font-bold " + (out.adversarialPreview.verdict === "pass" ? "bg-emerald-500" : "bg-rose-500")}>{out.adversarialPreview.verdict.toUpperCase()}</span>
1738
+ </div>
1739
+ <div className="text-rose-800 mb-2 whitespace-pre-wrap max-h-48 overflow-y-auto bg-rose-100/50 p-2 rounded">
1740
+ {out.adversarialPreview.rawCritique || "(No raw critique text returned)"}
1741
+ </div>
1742
+ {out.adversarialPreview.defectCount > 0 && (
1743
+ <div className="text-rose-900 font-bold">
1744
+ Detected Categories: {out.adversarialPreview.categories.join(", ")}
1745
+ </div>
1746
+ )}
1747
+ </div>
1748
+ )}
1749
+ </div>
1750
+ );
1751
+ }
1752
+
1753
+ // ── Citation Audit Panel ──────────────────────────────────────────────────
1754
+ function CitationAuditPanel({ audit }: { audit: any }) {
1755
+ if (!audit || audit.totalCitations === 0) return null;
1756
+ const isOk = audit.missingCount === 0 && audit.untrustedCount === 0;
1757
+
1758
+ return (
1759
+ <div className="rounded-xl border border-indigo-200 bg-indigo-50/30 p-3 text-[10px] mt-3">
1760
+ <div className="font-bold text-indigo-900 mb-2 flex items-center gap-1.5 justify-between">
1761
+ <span><span className="text-sm">📚</span> Citation Provenance Audit</span>
1762
+ <span className={"px-2 py-0.5 rounded text-white font-bold " + (isOk ? "bg-emerald-500" : "bg-amber-500")}>
1763
+ {audit.trustedCount}/{audit.totalCitations} TRUSTED
1764
+ </span>
1765
+ </div>
1766
+
1767
+ <div className="flex gap-4 mb-2 text-indigo-800">
1768
+ <div><b>Total [S#] tags:</b> {audit.totalCitations}</div>
1769
+ <div className={audit.untrustedCount > 0 ? "text-amber-700 font-bold" : ""}><b>Untrusted:</b> {audit.untrustedCount}</div>
1770
+ <div className={audit.missingCount > 0 ? "text-rose-700 font-bold" : ""}><b>Missing Source:</b> {audit.missingCount}</div>
1771
+ </div>
1772
+
1773
+ <div className="overflow-x-auto">
1774
+ <table className="w-full text-left font-mono bg-white rounded border border-indigo-100">
1775
+ <thead className="bg-indigo-50/50 text-indigo-900 border-b border-indigo-100">
1776
+ <tr>
1777
+ <th className="px-2 py-1.5">Tag</th>
1778
+ <th className="px-2 py-1.5">Status</th>
1779
+ <th className="px-2 py-1.5">Overlap/Entail</th>
1780
+ <th className="px-2 py-1.5">Claim Context (from draft)</th>
1781
+ <th className="px-2 py-1.5">Source Snippet (verbatim)</th>
1782
+ <th className="px-2 py-1.5">URL</th>
1783
+ </tr>
1784
+ </thead>
1785
+ <tbody className="divide-y divide-indigo-50">
1786
+ {audit.auditResults.map((r: any, i: number) => (
1787
+ <tr key={i} className={!r.found ? "bg-rose-50" : !r.trusted ? "bg-amber-50" : ""}>
1788
+ <td className="px-2 py-1.5 font-bold whitespace-nowrap">{r.tag}</td>
1789
+ <td className="px-2 py-1.5">
1790
+ <span className={"px-1.5 py-0.5 rounded " + (r.trusted ? "bg-emerald-100 text-emerald-800" : !r.found ? "bg-rose-100 text-rose-800" : "bg-amber-100 text-amber-800")}>
1791
+ {r.trusted ? "TRUSTED" : !r.found ? "MISSING" : "UNTRUSTED"}
1792
+ </span>
1793
+ </td>
1794
+ <td className="px-2 py-1.5">{r.found ? r.entailmentScore !== undefined ? `LLM:${r.entailmentScore.toFixed(2)}` : `Word:${r.snippetOverlap}` : "—"}</td>
1795
+ <td className="px-2 py-1.5 text-zinc-800 max-w-[250px] truncate" title={r.claimContext}>
1796
+ {r.claimContext.slice(0, 100)}...
1797
+ </td>
1798
+ <td className="px-2 py-1.5 text-zinc-600 max-w-[250px] truncate" title={r.entry?.snippet}>
1799
+ {r.entry ? r.entry.snippet.slice(0, 100) + "..." : "—"}
1800
+ </td>
1801
+ <td className="px-2 py-1.5">
1802
+ {r.entry?.url ? (
1803
+ <a href={r.entry.url} target="_blank" rel="noreferrer" className="text-blue-600 hover:underline flex items-center gap-1">
1804
+ {new URL(r.entry.url).hostname.replace('www.','')}
1805
+ </a>
1806
+ ) : "—"}
1807
+ </td>
1808
+ </tr>
1809
+ ))}
1810
+ </tbody>
1811
+ </table>
1812
+ </div>
1813
+ </div>
1814
+ );
1815
+ }
1816
+
1817
+ // ── Web Grounding Guide (SearXNG vs PrismaFetch comparison & setup) ─────────
1818
+ function GroundingGuidePanel() {
1819
+ return (
1820
+ <div className="h-full overflow-y-auto p-4 space-y-4">
1821
+ <div className="rounded-2xl border border-sky-200 bg-sky-50/40 p-4">
1822
+ <div className="text-sm font-bold text-sky-900 mb-1">🔗 Web Grounding Guide — SearXNG vs PrismaFetch</div>
1823
+ <div className="text-[11px] text-sky-800">Both backends are additive and can be combined freely. This guide compares their capabilities so you can choose the right combination for your calibration run.</div>
1824
+ </div>
1825
+
1826
+ <div className="overflow-x-auto rounded-xl border border-zinc-200">
1827
+ <table className="w-full text-left text-[11px]">
1828
+ <thead className="bg-zinc-100 font-bold text-zinc-700">
1829
+ <tr>
1830
+ <th className="px-3 py-2">Capability</th>
1831
+ <th className="px-3 py-2">SearXNG</th>
1832
+ <th className="px-3 py-2">PrismaFetch</th>
1833
+ </tr>
1834
+ </thead>
1835
+ <tbody className="divide-y divide-zinc-100">
1836
+ <tr><td className="px-3 py-2 font-bold">Type</td><td className="px-3 py-2">Self-hosted metasearch aggregator</td><td className="px-3 py-2">Local read/search proxy service</td></tr>
1837
+ <tr><td className="px-3 py-2 font-bold">Setup</td><td className="px-3 py-2">Docker one-liner, format=json required</td><td className="px-3 py-2">Local server on 127.0.0.1:8080</td></tr>
1838
+ <tr><td className="px-3 py-2 font-bold">API key</td><td className="px-3 py-2">Optional (Bearer token)</td><td className="px-3 py-2">None required</td></tr>
1839
+ <tr><td className="px-3 py-2 font-bold">Categories</td><td className="px-3 py-2">general / science / it / news / academic / files</td><td className="px-3 py-2">Single unified index</td></tr>
1840
+ <tr><td className="px-3 py-2 font-bold">Language/Safe filters</td><td className="px-3 py-2">Yes — language + safesearch params</td><td className="px-3 py-2">No</td></tr>
1841
+ <tr><td className="px-3 py-2 font-bold">Read/Extract full page</td><td className="px-3 py-2">No (search results only)</td><td className="px-3 py-2">Yes — /api/read with OCR modes</td></tr>
1842
+ <tr><td className="px-3 py-2 font-bold">Best for</td><td className="px-3 py-2">Broad, category-filtered discovery</td><td className="px-3 py-2">Deep single-page extraction</td></tr>
1843
+ </tbody>
1844
+ </table>
1845
+ </div>
1846
+
1847
+ <div className="rounded-2xl border border-violet-200 bg-violet-50/30 p-4">
1848
+ <div className="text-[11px] font-bold text-violet-900 mb-2">🔧 SearXNG Setup</div>
1849
+ <ol className="list-decimal pl-5 space-y-1 text-[11px] text-violet-900">
1850
+ <li>Run: <code className="rounded bg-violet-100 px-1 font-mono">docker run -d -p 8080:8080 searxng/searxng</code></li>
1851
+ <li>Edit <code className="rounded bg-violet-100 px-1 font-mono">settings.yml</code>: under <code className="rounded bg-violet-100 px-1 font-mono">search: formats:</code> add <code className="rounded bg-violet-100 px-1 font-mono">- json</code></li>
1852
+ <li>Restart the container, then set the Base URL in the profile bar above (default <code className="rounded bg-violet-100 px-1 font-mono">http://localhost:8080</code>).</li>
1853
+ <li>Click <b>Test Connection</b> to verify — a successful test returns a result count for a sample query.</li>
1854
+ </ol>
1855
+ </div>
1856
+
1857
+ <div className="rounded-2xl border border-emerald-200 bg-emerald-50/30 p-4">
1858
+ <div className="text-[11px] font-bold text-emerald-900 mb-2">🔧 PrismaFetch Setup</div>
1859
+ <ol className="list-decimal pl-5 space-y-1 text-[11px] text-emerald-900">
1860
+ <li>Run the PrismaFetch local service (default <code className="rounded bg-emerald-100 px-1 font-mono">http://127.0.0.1:8080</code>).</li>
1861
+ <li>Enable it under Chat → Control Plane → PrismaFetch, then confirm the healthz check passes.</li>
1862
+ <li>Toggle PrismaFetch in this dialog's Web grounding row to route calibration searches through it first.</li>
1863
+ </ol>
1864
+ </div>
1865
+
1866
+ <div className="rounded-2xl border border-zinc-200 bg-white p-4 text-[11px] text-zinc-600">
1867
+ <b>Recommendation:</b> enable both. The pipeline tries PrismaFetch → OG scraper → SearXNG → Jina in that order, falling back automatically whenever a backend is unreachable, so mixing backends only increases coverage — never risk.
1868
+ </div>
1869
+ </div>
1870
+ );
1871
+ }