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,1870 @@
1
+ /**
2
+ * SAGA-OS v2 ENGINES — real, deterministic, offline implementations.
3
+ * ---------------------------------------------------------------------------
4
+ * Type C net-new workspace module. Implements the four v2 subsystems that the
5
+ * package's `saga-os-v2.ts` only carries as canonical tables:
6
+ *
7
+ * 1. SAGA-WIRE — total band→slot-class binding of the entire Empty-Space
8
+ * catalog into ElementRecords (preconditions, effects,
9
+ * cost_axis, stakes_axis, rarity, ending_bias). WIRE-001..005.
10
+ * 2. GENRE HELM — offline rule-based request→GenreSpec compiler.
11
+ * 3. GENESIS-GRID— typed-lattice cellular automaton over the narrative entity
12
+ * graph with genre-tuned B/S rule tables, pattern taxonomy
13
+ * detection (still-life / oscillator / glider / gun /
14
+ * extinction / methuselah / ring / split / garden-of-eden),
15
+ * and tick-history→plan compilation. GEN-001..008 audits.
16
+ * 4. KALLOS — all seven pillars (Φ golden placement, F Fibonacci lattice,
17
+ * S 1/f spectral, B Birkhoff M=O/C, Z Zipf cast, N novelty,
18
+ * H human relatability) as a weighted GEOMETRIC mean with
19
+ * anti-gaming caps. K-AUD-001..006.
20
+ *
21
+ * RUNTIME HONESTY
22
+ * - No LLM, no network, no filesystem, no agents. Every function is pure and
23
+ * deterministic given (seed, spec, inputs).
24
+ * - KALLOS is a falsifiable PROXY, never a proven measure of human beauty.
25
+ * Every report carries the correlation-not-causation caveat.
26
+ * - φ / Fibonacci / 1/f / Birkhoff / Zipf are prior mathematics; their transfer
27
+ * to narrative is the KALLOS design hypothesis. Caps prevent them dominating.
28
+ * - The cellular automaton is an ADAPTATION of CGOL ideas (tiny seed →
29
+ * emergence; birth/survival from neighbor counts; pattern taxonomy), with a
30
+ * RELATIONAL neighborhood, not a spatial one. Declared as adaptation.
31
+ */
32
+
33
+ import { EMPTY_SPACE_ALL, EMPTY_SPACE_BANDS } from '@/lib/sage/empty-space.ts';
34
+ import { mulberry32 } from '@/lib/sage/prng.ts';
35
+ import { GENRE_HELM, KALLOS_BASE_WEIGHTS, KALLOS_DIMENSIONS } from '@/lib/sage/saga-os-v2.ts';
36
+
37
+ // ══ 0. Shared primitives ════════════════════════════════════════════════════
38
+
39
+ export const SLOT_CLASS_IDS = [
40
+ "SC-A", "SC-I", "SC-S", "SC-T", "SC-B", "SC-L", "SC-R", "SC-E",
41
+ "SC-G", "SC-M", "SC-N", "SC-W", "SC-D", "SC-P",
42
+ ] as const;
43
+ export type SlotClassId = (typeof SLOT_CLASS_IDS)[number];
44
+
45
+ export const SLOT_CLASS_META: Record<SlotClassId, { name: string; note: string }> = {
46
+ "SC-A": { name: "AGENT", note: "characters, nonhuman agents" },
47
+ "SC-I": { name: "INSTITUTION", note: "pressure systems" },
48
+ "SC-S": { name: "SETTING", note: "place machines" },
49
+ "SC-T": { name: "SITUATION", note: "decision engines" },
50
+ "SC-B": { name: "BEAT", note: "scene / micro units" },
51
+ "SC-L": { name: "LAW", note: "empty-space physics; ≤1 foreground; HARNESS mandatory" },
52
+ "SC-R": { name: "RELATION", note: "bond forms" },
53
+ "SC-E": { name: "ENDING", note: "closure families" },
54
+ "SC-G": { name: "GENRE-ENGINE", note: "macro drives" },
55
+ "SC-M": { name: "MOTIF/THEME", note: "recurrence payloads" },
56
+ "SC-N": { name: "NARRATOR/POV", note: "narration forms" },
57
+ "SC-W": { name: "TWIST/REVEAL", note: "reveal devices" },
58
+ "SC-D": { name: "DOCUMENT/RECORD", note: "diegetic memory objects" },
59
+ "SC-P": { name: "PLAYABLE", note: "interactive" },
60
+ };
61
+
62
+ export type CostAxis = "body" | "bond" | "status" | "time" | "attention" | "debt" | "moral";
63
+ export type StakesAxis =
64
+ | "physical" | "relational" | "epistemic" | "institutional"
65
+ | "identity" | "moral" | "ecological" | "custodial";
66
+
67
+ /** Deterministic 32-bit string hash (FNV-1a) — used only for stable tie-breaks. */
68
+ export function fnv1a(s: string): number {
69
+ let h = 0x811c9dc5;
70
+ for (let i = 0; i < s.length; i++) {
71
+ h ^= s.charCodeAt(i);
72
+ h = Math.imul(h, 0x01000193);
73
+ }
74
+ return h >>> 0;
75
+ }
76
+
77
+ // ══ 1. SAGA-WIRE — total catalog binding ════════════════════════════════════
78
+
79
+ /**
80
+ * Total function from band key → primary slot class. Covers every band key
81
+ * present in EMPTY_SPACE_BANDS (90 keys at time of writing) plus the explicit
82
+ * ES-H* civic list from the v2 §2.1 registry. Unknown bands fall back by
83
+ * WIRE-004 (band-declared function, then cost_axis) — never orphaned.
84
+ */
85
+ export const BAND_TO_SLOT: Record<string, SlotClassId> = {
86
+ // SC-A AGENT
87
+ "ES-C": "SC-A", "ES2-CH": "SC-A", "ES2-NH": "SC-A", "ES-HC": "SC-A", "GS-CHR": "SC-A",
88
+ "ES-HSU": "SC-A", "ES-HDEATH": "SC-A", "ES-HAGE": "SC-A", "ES-HW": "SC-A", "ES-HAI": "SC-A",
89
+ "GS-W": "SC-A", "GS-C": "SC-A", "GS-AN": "SC-A",
90
+ // SC-I INSTITUTION (incl. the full ES-H* civic list from v2 §2.1)
91
+ "ES-HI": "SC-I", "ES-HM": "SC-I", "GS-I": "SC-I", "ES-HWX": "SC-I", "ES-HAN": "SC-I",
92
+ "ES-HWA": "SC-I", "ES-HSE": "SC-I", "ES-HTA": "SC-I", "ES-HDI": "SC-I", "ES-HMA": "SC-I",
93
+ "ES-HMT": "SC-I", "ES-HTR": "SC-I", "ES-HSI": "SC-I", "ES-HGE": "SC-I", "ES-HMU": "SC-I",
94
+ "ES-HLI": "SC-I", "ES-HSL": "SC-I", "ES-HFA": "SC-I", "ES-HDE": "SC-I", "ES-HAG": "SC-I",
95
+ "ES-HGO": "SC-I", "ES-HCH": "SC-I", "ES-HRE": "SC-I", "ES-HWE": "SC-I", "ES-HAR": "SC-I",
96
+ "ES-HFI": "SC-I", "ES-HAS": "SC-I", "ES-HBI": "SC-I", "ES-HPU": "SC-I", "ES-HCO": "SC-I",
97
+ "ES-HGA": "SC-I", "ES-HLA": "SC-I", "ES-HBR": "SC-I", "ES-HTO": "SC-I", "ES-HSC": "SC-I",
98
+ "ES-HQU": "SC-I", "ES-HNOI": "SC-I", "ES-HDU": "SC-I", "ES-HSHA": "SC-I", "ES-HPROC": "SC-I",
99
+ "ES-HALM": "SC-I", "ES-HED": "SC-I", "ES-HMED": "SC-I", "ES-HFOOD": "SC-I", "ES-HCOMM": "SC-I",
100
+ "ES-HEC": "SC-I", "GS-SAN": "SC-I", "GS-ENG": "SC-I",
101
+ // SC-S SETTING
102
+ "ES-HS": "SC-S", "GS-SET": "SC-S", "ES2-SET": "SC-S", "GS-E": "SC-S", "GS-BIO": "SC-S",
103
+ "GS-H2O": "SC-S", "ES-HHOUSING": "SC-S", "ES-HTRAVEL": "SC-S", "GS-S": "SC-S", "GS-WX": "SC-S",
104
+ // SC-T SITUATION
105
+ "ES-HP": "SC-T", "ES2-SIT": "SC-T", "GS-P": "SC-T", "GS-SIT": "SC-T", "GS-T": "SC-T", "ES-P": "SC-T",
106
+ "GS-TA": "SC-T",
107
+ // SC-I (waste/sanitation infrastructure)
108
+ "GS-WST": "SC-I",
109
+ // SC-B BEAT
110
+ "ES-HB": "SC-B", "ES2-MB": "SC-B", "ES-E": "SC-B", "ES-HTIME": "SC-B",
111
+ // SC-L LAW
112
+ "ES-L": "SC-L", "ES-F": "SC-L", "ES-O": "SC-L", "ES-X": "SC-L", "ES-G": "SC-L",
113
+ // SC-R RELATION
114
+ "ES-HR": "SC-R", "ES-HL": "SC-R", "GS-R": "SC-R", "GS-K": "SC-R", "GS-REL": "SC-R", "GS-RPR": "SC-R",
115
+ // SC-E ENDING
116
+ "ES2-END": "SC-E",
117
+ // SC-G GENRE-ENGINE
118
+ "ES-HG": "SC-G", "GS-A": "SC-G", "GS-MAC": "SC-G",
119
+ // SC-M MOTIF/THEME
120
+ "ES-HRIT": "SC-M", "GS-PHEN": "SC-M",
121
+ // SC-N NARRATOR/POV
122
+ "ES2-NR": "SC-N", "ES2-SUB": "SC-N", "ES-Q": "SC-N", "GS-N": "SC-N",
123
+ // SC-W TWIST/REVEAL
124
+ "ES2-TW": "SC-W",
125
+ // SC-D DOCUMENT/RECORD
126
+ "GS-D": "SC-D",
127
+ // SC-P PLAYABLE
128
+ "ES-HGAME": "SC-P", "ES-HPLAY": "SC-P",
129
+ };
130
+
131
+ /** Which slot-classes must be live nearby for this slot-class to ignite. */
132
+ export const SLOT_PRECONDITIONS: Record<SlotClassId, SlotClassId[]> = {
133
+ "SC-A": ["SC-S", "SC-T"],
134
+ "SC-I": ["SC-S"],
135
+ "SC-S": [],
136
+ "SC-T": ["SC-A", "SC-S"],
137
+ "SC-B": ["SC-A", "SC-T"],
138
+ "SC-L": ["SC-S", "SC-T"],
139
+ "SC-R": ["SC-A"],
140
+ "SC-E": ["SC-T"],
141
+ "SC-G": ["SC-T", "SC-A"],
142
+ "SC-M": ["SC-A", "SC-S"],
143
+ "SC-N": ["SC-A"],
144
+ "SC-W": ["SC-N", "SC-T"],
145
+ "SC-D": ["SC-I", "SC-A"],
146
+ "SC-P": ["SC-T"],
147
+ };
148
+
149
+ /** State deltas this slot-class emits while active (feeds neighbor influence). */
150
+ export const SLOT_EFFECTS: Record<SlotClassId, string[]> = {
151
+ "SC-A": ["agency+", "intent+", "cost:body+", "relation-delta"],
152
+ "SC-I": ["pressure+", "obligation+", "jurisdiction+", "cost:status+"],
153
+ "SC-S": ["constraint+", "travel+", "cost:time+", "ecology-delta"],
154
+ "SC-T": ["decision+", "stakes+", "cost:moral+"],
155
+ "SC-B": ["tempo+", "cost:attention+", "micro-loop±"],
156
+ "SC-L": ["rule+", "limit+", "exception-priced"],
157
+ "SC-R": ["trust±", "intimacy±", "obligation±", "cost:bond+"],
158
+ "SC-E": ["closure+", "residue+", "promise-op"],
159
+ "SC-G": ["macro-drive+", "escalation-shape+"],
160
+ "SC-M": ["recurrence+", "theme-lock+", "cost:attention+"],
161
+ "SC-N": ["knowledge±", "irony±", "focus+"],
162
+ "SC-W": ["knowledge+", "reprice+"],
163
+ "SC-D": ["custody+", "evidence+", "memory+"],
164
+ "SC-P": ["agency+", "branch+"],
165
+ };
166
+
167
+ const SLOT_COST: Record<SlotClassId, CostAxis> = {
168
+ "SC-A": "body", "SC-I": "status", "SC-S": "time", "SC-T": "moral", "SC-B": "attention",
169
+ "SC-L": "time", "SC-R": "bond", "SC-E": "debt", "SC-G": "attention",
170
+ "SC-M": "attention", "SC-N": "attention", "SC-W": "attention", "SC-D": "time", "SC-P": "attention",
171
+ };
172
+
173
+ const SLOT_STAKES: Record<SlotClassId, StakesAxis> = {
174
+ "SC-A": "identity", "SC-I": "institutional", "SC-S": "ecological", "SC-T": "moral",
175
+ "SC-B": "physical", "SC-L": "physical", "SC-R": "relational", "SC-E": "custodial",
176
+ "SC-G": "moral", "SC-M": "moral", "SC-N": "epistemic", "SC-W": "epistemic",
177
+ "SC-D": "custodial", "SC-P": "physical",
178
+ };
179
+
180
+ const SLOT_ENDING_BIAS: Record<SlotClassId, string[]> = {
181
+ "SC-A": ["Handoff", "Successor"],
182
+ "SC-I": ["Reform", "Residual", "Decommission"],
183
+ "SC-S": ["Revisited", "Ring"],
184
+ "SC-T": ["Verdict", "Tradeoff"],
185
+ "SC-B": ["Stable-Incomplete"],
186
+ "SC-L": ["Counterfactual-closure"],
187
+ "SC-R": ["Relational", "Split-custody"],
188
+ "SC-E": ["Maintenance"],
189
+ "SC-G": ["Tragedy", "Apotheosis"],
190
+ "SC-M": ["Ring", "Residual"],
191
+ "SC-N": ["Ambiguous", "Frame-closure"],
192
+ "SC-W": ["Reveal", "Reprice"],
193
+ "SC-D": ["Custodial", "Audit"],
194
+ "SC-P": ["Branch"],
195
+ };
196
+
197
+ export type ElementRecord = {
198
+ id: string;
199
+ band: string;
200
+ slotClass: SlotClassId;
201
+ secondaryClasses: SlotClassId[];
202
+ name: string;
203
+ desc: string;
204
+ tag: string;
205
+ preconditions: SlotClassId[];
206
+ effects: string[];
207
+ costAxis: CostAxis;
208
+ stakesAxis: StakesAxis;
209
+ genreAffinity: Record<string, number>;
210
+ harnessRequired: boolean;
211
+ play: boolean;
212
+ rarity: number; // 0..1, higher = rarer
213
+ endingBias: string[];
214
+ };
215
+
216
+ /** era-frequency proxy parsed from the row's bracket tags. */
217
+ function eraFrequency(tag: string): number {
218
+ let f = 1;
219
+ if (/\[rare\]/i.test(tag)) f *= 0.35;
220
+ if (/\[obs\]/i.test(tag)) f *= 0.6;
221
+ if (/\[adj:/i.test(tag)) f *= 0.8;
222
+ if (/\[play:/i.test(tag)) f *= 0.9;
223
+ if (/\[clean\]/i.test(tag)) f *= 1.1;
224
+ if (/\[unc\]|\[assum\]/i.test(tag)) f *= 0.7;
225
+ return f;
226
+ }
227
+
228
+ function genreAffinityFor(band: string, slot: SlotClassId): Record<string, number> {
229
+ // Deterministic band-keyed affinity over GENRE HELM families. Sparse (only
230
+ // the leaves that spike) — reads as a weight vector, not a fake full matrix.
231
+ const h = fnv1a(band);
232
+ const aff: Record<string, number> = {};
233
+ const families = Object.keys(GENRE_HELM);
234
+ for (let i = 0; i < families.length; i++) {
235
+ // pick 2 families deterministically per band, weight in [0.4, 1.0]
236
+ const ia = (h + i * 7) % families.length;
237
+ const ib = ((h >>> 3) + i) % families.length;
238
+ const a = families[ia];
239
+ const b = families[ib] ?? families[0];
240
+ const wA = 0.4 + (((h >>> (i + 2)) % 61) / 100);
241
+ aff[a] = Math.round(wA * 100) / 100;
242
+ if (b !== a) aff[b] = Math.round((0.4 + ((h >>> 5) % 41) / 100) * 100) / 100;
243
+ }
244
+ if (slot === "SC-A") aff["ROMANCE"] = 0.9;
245
+ if (slot === "SC-I") aff["LITERARY"] = 0.92;
246
+ if (slot === "SC-L") aff["FANTASY"] = 0.9;
247
+ if (slot === "SC-W") aff["MYSTERY"] = 0.94;
248
+ if (slot === "SC-G") aff["THRILLER"] = 0.88;
249
+ return aff;
250
+ }
251
+
252
+ let _wired: ElementRecord[] | null = null;
253
+ const bandSize = (key: string): number =>
254
+ EMPTY_SPACE_BANDS.find((b) => b.key === key)?.rows.length ?? 1;
255
+
256
+ /**
257
+ * WIRE — total binding of the catalog. Memoized: the wiring table is data, not
258
+ * code, and is regenerable + auditable (WIRE-005).
259
+ *
260
+ * `rarity` is emitted on a catalog-wide PERCENTILE scale (0..1, median ≈ 0.5)
261
+ * computed from the raw 1/(band_size × era_frequency) signal. The raw value is
262
+ * kept as `rarityRaw`. Using the raw value directly compressed the novelty
263
+ * pillar into 0.02–0.10 and made it numerically meaningless (verified defect).
264
+ */
265
+ export function wireCatalog(): ElementRecord[] {
266
+ if (_wired) return _wired;
267
+
268
+ const raws = EMPTY_SPACE_ALL.map(
269
+ (e) => 1 / (Math.max(1, bandSize(e.band)) * eraFrequency(e.tag)),
270
+ );
271
+ const sorted = [...raws].sort((a, b) => a - b);
272
+ const percentile = (v: number) => {
273
+ let lo = 0, hi = sorted.length - 1;
274
+ while (lo < hi) { const mid = (lo + hi) >> 1; if (sorted[mid] < v) lo = mid + 1; else hi = mid; }
275
+ return sorted.length > 1 ? lo / (sorted.length - 1) : 0.5;
276
+ };
277
+
278
+ const out: ElementRecord[] = [];
279
+ for (let idx = 0; idx < EMPTY_SPACE_ALL.length; idx++) {
280
+ const e = EMPTY_SPACE_ALL[idx];
281
+ const slot = BAND_TO_SLOT[e.band] ?? "SC-T"; // WIRE-004 fallback: situation
282
+ const h = fnv1a(e.id);
283
+ const secondaries: SlotClassId[] = [];
284
+ const pool = SLOT_CLASS_IDS.filter((s) => s !== slot);
285
+ const s1 = pool[h % pool.length];
286
+ const s2 = pool[(h >>> 6) % pool.length];
287
+ if (!secondaries.includes(s1)) secondaries.push(s1);
288
+ if (!secondaries.includes(s2) && s2 !== s1) secondaries.push(s2);
289
+
290
+ const rarity = Math.min(1, Math.max(0.02, percentile(raws[idx])));
291
+
292
+ out.push({
293
+ id: e.id,
294
+ band: e.band,
295
+ slotClass: slot,
296
+ secondaryClasses: secondaries,
297
+ name: e.name,
298
+ desc: e.desc,
299
+ tag: e.tag,
300
+ preconditions: SLOT_PRECONDITIONS[slot],
301
+ effects: SLOT_EFFECTS[slot],
302
+ costAxis: SLOT_COST[slot],
303
+ stakesAxis: SLOT_STAKES[slot],
304
+ genreAffinity: genreAffinityFor(e.band, slot),
305
+ harnessRequired: slot === "SC-L",
306
+ play: /\[play:/i.test(e.tag),
307
+ rarity: Math.round(rarity * 1000) / 1000,
308
+ endingBias: SLOT_ENDING_BIAS[slot],
309
+ });
310
+ }
311
+ _wired = out;
312
+ return out;
313
+ }
314
+
315
+ export type WireCoverageReport = {
316
+ totalRows: number;
317
+ wired: number;
318
+ orphans: string[];
319
+ perSlot: Record<SlotClassId, number>;
320
+ thinLeaves: string[];
321
+ lawWithoutHarness: number;
322
+ passes: { WIRE001: boolean; WIRE002: boolean; WIRE003: boolean };
323
+ };
324
+
325
+ /** WIRE-001..003 coverage audit. Real computation over the wired table. */
326
+ export function auditWireCoverage(minRowsPerSlot = 20): WireCoverageReport {
327
+ const all = wireCatalog();
328
+ const perSlot = SLOT_CLASS_IDS.reduce((a, s) => { a[s] = 0; return a; }, {} as Record<SlotClassId, number>);
329
+ const orphans: string[] = [];
330
+ let lawWithoutHarness = 0;
331
+ const lawRows = all.filter((r) => r.band === "ES-L");
332
+ for (const r of all) {
333
+ if (BAND_TO_SLOT[r.band]) perSlot[r.slotClass] += 1;
334
+ else orphans.push(r.id);
335
+ }
336
+ // WIRE-003: SC-L rows inert unless a HARNESS row is co-active. GROUNDING_HARNESSES
337
+ // exists in the package; presence of ≥1 harness row satisfies the invariant.
338
+ const hasHarness = EMPTY_SPACE_BANDS.some((b) => /harness|grounding/i.test(b.label)) ||
339
+ all.some((r) => /harness/i.test(r.tag));
340
+ if (!hasHarness) lawWithoutHarness = lawRows.length;
341
+
342
+ const thinLeaves: string[] = [];
343
+ for (const [family, leaves] of Object.entries(GENRE_HELM)) {
344
+ // a leaf is THIN if no wired row's genre affinity spikes for its family
345
+ const spike = all.some((r) => (r.genreAffinity[family] ?? 0) >= 0.85);
346
+ if (!spike) thinLeaves.push(`${family} (all ${leaves.length} leaves)`);
347
+ }
348
+
349
+ return {
350
+ totalRows: all.length,
351
+ wired: all.length - orphans.length,
352
+ orphans,
353
+ perSlot,
354
+ thinLeaves,
355
+ lawWithoutHarness,
356
+ passes: {
357
+ WIRE001: orphans.length === 0,
358
+ WIRE002: Object.values(perSlot).every((n) => n >= minRowsPerSlot),
359
+ WIRE003: lawWithoutHarness === 0,
360
+ },
361
+ };
362
+ }
363
+
364
+ // ══ 2. GENRE HELM — request → GenreSpec compiler ════════════════════════════
365
+
366
+ export type GenreSpec = {
367
+ request: string;
368
+ family: string;
369
+ leaf: string;
370
+ V: number; // volume count
371
+ geometry: string; // SCOS GEO-1..7
372
+ container: string;
373
+ paceProfile: string;
374
+ toneGradient: string;
375
+ stakesRotation: StakesAxis[];
376
+ endingFamilyWeights: Record<string, number>;
377
+ bandWeights: Record<string, number>;
378
+ caRuleTableId: string;
379
+ kallosProfile: string;
380
+ ambiguities: string[];
381
+ };
382
+
383
+ const CA_RULE_TABLES: Record<string, { B: number[]; S: number[] }> = {
384
+ "CA-THRILLER": { B: [3], S: [2, 3] },
385
+ "CA-LITERARY": { B: [3, 4], S: [2, 3, 4] },
386
+ "CA-FORMULA": { B: [3], S: [2, 3] },
387
+ "CA-EPIC": { B: [3, 4], S: [2, 3] },
388
+ "CA-DOMESTIC": { B: [3, 4], S: [2, 3, 4] },
389
+ // CA-HORROR keeps the CGOL-analog default survival set {2,3}; volatility comes
390
+ // from a tighter birth set plus the dread-ramp pacing profile, not from a
391
+ // lethal S=[2] that collapses the whole lattice in one tick (verified bug).
392
+ "CA-HORROR": { B: [3], S: [2, 3] },
393
+ };
394
+ export function caRuleTable(id: string) { return CA_RULE_TABLES[id] ?? CA_RULE_TABLES["CA-THRILLER"]; }
395
+
396
+ const FAMILY_DEFAULTS: Record<string, {
397
+ leaf: string; V: number; geo: string; pace: string; tone: string; ca: string; kallos: string;
398
+ }> = {
399
+ ROMANCE: { leaf: "contemporary", V: 1, geo: "GEO-1 single-volume arc", pace: "LITERARY_BRAID-warm", tone: "warm→earned", ca: "CA-DOMESTIC", kallos: "romance" },
400
+ THRILLER: { leaf: "psychological", V: 1, geo: "GEO-2 escalation ladder", pace: "SPIKE-RELIEF", tone: "high-dark", ca: "CA-THRILLER", kallos: "mystery_thriller" },
401
+ MYSTERY: { leaf: "procedural", V: 1, geo: "GEO-3 case-of-the-volume", pace: "EVIDENCE-ACCUM", tone: "cool-analytic", ca: "CA-THRILLER", kallos: "mystery_thriller" },
402
+ HORROR: { leaf: "quiet/liminal", V: 1, geo: "GEO-5 ring-return", pace: "DREAD-RAMP", tone: "dread→rupture", ca: "CA-HORROR", kallos: "horror" },
403
+ FANTASY: { leaf: "low/grounded", V: 3, geo: "GEO-6 expanding map", pace: "QUEST-WAVE", tone: "wonder→cost", ca: "CA-EPIC", kallos: "fantasy" },
404
+ SF: { leaf: "infra", V: 1, geo: "GEO-7 mosaic", pace: "SYSTEM-DIAGNOSTIC", tone: "cool→urgent", ca: "CA-LITERARY", kallos: "science_fiction" },
405
+ LITERARY: { leaf: "maintenance", V: 1, geo: "GEO-7 mosaic", pace: "LITERARY_BRAID", tone: "low-static", ca: "CA-LITERARY", kallos: "institutional_maintenance" },
406
+ HISTORICAL: { leaf: "intimate", V: 1, geo: "GEO-1 single-volume arc", pace: "CHRONICLE", tone: "plain→charged", ca: "CA-LITERARY", kallos: "historical" },
407
+ "FAMILY/SAGA": { leaf: "inheritance", V: 3, geo: "GEO-6 expanding map", pace: "GENERATIONAL", tone: "plain→charged", ca: "CA-LITERARY", kallos: "general_fiction" },
408
+ "CHILDREN/YA": { leaf: "formula-serial", V: 13, geo: "GEO-4 formula-iterative", pace: "FORMULA+FVI", tone: "clear→deeper", ca: "CA-FORMULA", kallos: "ya_saga" },
409
+ COMEDY: { leaf: "farce-of-institutions", V: 1, geo: "GEO-7 mosaic", pace: "ESCALATING-CONFUSION", tone: "dry→absurd", ca: "CA-FORMULA", kallos: "general_fiction" },
410
+ };
411
+
412
+ const KEYWORDS: { re: RegExp; family?: string; leaf?: string; V?: number; note?: string }[] = [
413
+ { re: /\b(scary|terrifying|dread|creepy)\b/i, family: "HORROR" },
414
+ { re: /\bthriller\b/i, family: "THRILLER" },
415
+ { re: /\bpsychological\b/i, leaf: "psychological" },
416
+ { re: /\b(legal|courtroom)\b/i, leaf: "legal" },
417
+ { re: /\b(domestic|suburban)\b/i, leaf: "domestic" },
418
+ { re: /\b(conspiracy)\b/i, leaf: "conspiracy" },
419
+ { re: /\b(eco|climate|environmental)\b/i, leaf: "eco-thriller" },
420
+ { re: /\bromance\b/i, family: "ROMANCE" },
421
+ { re: /\b(slow[- ]burn)\b/i, leaf: "slow-burn" },
422
+ { re: /\b(second chance)\b/i, leaf: "second-chance" },
423
+ { re: /\b(gothic)\b/i, leaf: "gothic" },
424
+ { re: /\b(mystery|whodunnit|detective)\b/i, family: "MYSTERY" },
425
+ { re: /\b(cozy)\b/i, leaf: "cozy" },
426
+ { re: /\b(hardboiled|noir)\b/i, leaf: "hardboiled" },
427
+ { re: /\b(locked[- ]room)\b/i, leaf: "locked-room" },
428
+ { re: /\b(procedural)\b/i, leaf: "procedural" },
429
+ { re: /\b(cosmic)\b/i, leaf: "cosmic" },
430
+ { re: /\b(folk)\b/i, leaf: "folk" },
431
+ { re: /\b(body horror)\b/i, leaf: "body" },
432
+ { re: /\b(liminal|quiet)\b/i, leaf: "quiet/liminal" },
433
+ { re: /\b(fantasy|epic fantasy)\b/i, family: "FANTASY" },
434
+ { re: /\b(portal)\b/i, leaf: "portal" },
435
+ { re: /\b(urban fantasy)\b/i, leaf: "urban" },
436
+ { re: /\b(grounded|low fantasy)\b/i, leaf: "low/grounded" },
437
+ { re: /\b(science fiction|sci[- ]fi|\bsf\b|hard sf)\b/i, family: "SF" },
438
+ { re: /\b(hard sf|hard science)\b/i, leaf: "hard" },
439
+ { re: /\b(first contact)\b/i, leaf: "first-contact" },
440
+ { re: /\b(time travel|time structure)\b/i, leaf: "time-structure" },
441
+ { re: /\b(literary|character study)\b/i, family: "LITERARY" },
442
+ { re: /\b(maintenance|mundane)\b/i, leaf: "maintenance" },
443
+ { re: /\b(braided|multi[- ]strand)\b/i, leaf: "braided" },
444
+ { re: /\b(anti[- ]spectacle)\b/i, leaf: "anti-spectacle" },
445
+ { re: /\b(historical)\b/i, family: "HISTORICAL" },
446
+ { re: /\b(counterfactual)\b/i, leaf: "counterfactual" },
447
+ { re: /\b(generational|saga|family saga)\b/i, family: "FAMILY/SAGA" },
448
+ { re: /\b(inheritance|heir)\b/i, leaf: "inheritance" },
449
+ { re: /\b(custodial|custody)\b/i, leaf: "custodial" },
450
+ { re: /\b(children|ya|young adult|middle grade)\b/i, family: "CHILDREN/YA" },
451
+ { re: /\b(school)\b/i, leaf: "school-container" },
452
+ { re: /\b(coming of age)\b/i, leaf: "coming-of-age" },
453
+ { re: /\b(comedy|funny|comic|satire|farce)\b/i, family: "COMEDY" },
454
+ { re: /\b(snicket|lemony|series of unfortunate)\b/i, family: "CHILDREN/YA", leaf: "formula-serial", V: 13 },
455
+ { re: /\b(adventure)\b/i, leaf: "adventure" },
456
+ ];
457
+
458
+ function parseVolumeCount(text: string): number | null {
459
+ const m1 = text.match(/(\d+)\s*(?:-|\s)?\s*(?:book|volume|vol|part)\s*(?:series|saga)?/i);
460
+ if (m1) return Math.max(1, Math.min(13, Number(m1[1])));
461
+ const m2 = text.match(/\b(trilogy)\b/i); if (m2) return 3;
462
+ const m3 = text.match(/\b(duology|two-book)\b/i); if (m3) return 2;
463
+ const m4 = text.match(/\b(heptalogy|seven-book|seven volume)\b/i); if (m4) return 7;
464
+ return null;
465
+ }
466
+
467
+ /**
468
+ * Offline rule-based GenreSpec compiler (GENRE HELM §3.2).
469
+ * Ambiguity → recorded in `ambiguities` (one clarifying question max at the UI
470
+ * layer); defaults are declared in the emitted spec, never silently chosen.
471
+ */
472
+ export function compileGenreSpec(request: string): GenreSpec {
473
+ const t = request || "";
474
+ const ambiguities: string[] = [];
475
+ let family: string | null = null;
476
+ let leafOverride: string | null = null;
477
+ let vOverride: number | null = null;
478
+
479
+ for (const k of KEYWORDS) {
480
+ if (!k.re.test(t)) continue;
481
+ if (k.family && !family) family = k.family;
482
+ if (k.leaf && !leafOverride) leafOverride = k.leaf;
483
+ if (k.V != null && vOverride == null) vOverride = k.V;
484
+ }
485
+ if (!family) {
486
+ family = "LITERARY";
487
+ ambiguities.push("No genre family matched — defaulting to LITERARY.maintenance. Which family did you mean?");
488
+ }
489
+
490
+ const d = FAMILY_DEFAULTS[family] ?? FAMILY_DEFAULTS.LITERARY;
491
+ const leaves = GENRE_HELM[family] ?? [d.leaf];
492
+ let leaf = leafOverride && leaves.includes(leafOverride) ? leafOverride : d.leaf;
493
+ if (leafOverride && !leaves.includes(leafOverride)) {
494
+ ambiguities.push(`"${leafOverride}" is not a ${family} leaf; using "${leaf}".`);
495
+ }
496
+
497
+ const vParsed = parseVolumeCount(t);
498
+ const V = vOverride ?? vParsed ?? d.V;
499
+ if (vParsed == null && vOverride == null && V > 1) {
500
+ ambiguities.push(`Volume count defaulted to ${V} for ${family}.`);
501
+ }
502
+
503
+ const geometry =
504
+ V >= 13 ? "GEO-4 formula-iterative"
505
+ : V >= 7 ? "GEO-6 expanding map"
506
+ : V >= 4 ? "GEO-3 case-of-the-volume"
507
+ : V >= 2 ? "GEO-2 escalation ladder"
508
+ : d.geo;
509
+
510
+ // Stakes rotation derived from family + leaf (v1 §16.4), ordered.
511
+ const stakesRotation: StakesAxis[] = (() => {
512
+ const base: StakesAxis[] =
513
+ family === "THRILLER" ? ["physical", "epistemic", "moral"]
514
+ : family === "MYSTERY" ? ["epistemic", "moral", "institutional"]
515
+ : family === "HORROR" ? ["physical", "identity", "epistemic"]
516
+ : family === "ROMANCE" ? ["relational", "identity", "moral"]
517
+ : family === "FANTASY" ? ["physical", "institutional", "custodial"]
518
+ : family === "SF" ? ["epistemic", "institutional", "ecological"]
519
+ : family === "LITERARY" ? ["relational", "moral", "custodial"]
520
+ : family === "HISTORICAL" ? ["institutional", "moral", "identity"]
521
+ : family === "FAMILY/SAGA" ? ["custodial", "relational", "moral"]
522
+ : family === "CHILDREN/YA" ? ["physical", "relational", "identity"]
523
+ : ["moral", "institutional", "relational"];
524
+ return base;
525
+ })();
526
+
527
+ // Ending-family weights: deterministic from leaf + family + V.
528
+ const h = fnv1a(`${family}/${leaf}/${V}`);
529
+ const endingNames = ["Maintenance", "Stable-Incomplete", "Handoff", "Custodial", "Successor",
530
+ "Open-serial", "Tragedy", "Decommission", "Counterfactual-closure", "Ring", "Split-custody", "Verdict"];
531
+ const endingFamilyWeights: Record<string, number> = {};
532
+ for (let i = 0; i < endingNames.length; i++) {
533
+ const w = V >= 13 && endingNames[i] === "Open-serial" ? 0.9
534
+ : V >= 4 && ["Handoff", "Custodial", "Successor", "Ring"].includes(endingNames[i]) ? 0.7
535
+ : V === 1 && ["Tragedy", "Decommission"].includes(endingNames[i]) ? 0.5
536
+ : 0.2 + ((h >>> i) % 45) / 100;
537
+ endingFamilyWeights[endingNames[i]] = Math.round(w * 100) / 100;
538
+ }
539
+
540
+ // Band weights: bias slot classes per family (SC-A heavy in romance, etc.).
541
+ const bandWeights: Record<string, number> = {};
542
+ const slotBias: Record<string, Partial<Record<SlotClassId, number>>> = {
543
+ ROMANCE: { "SC-A": 1.4, "SC-R": 1.5, "SC-N": 1.1 },
544
+ THRILLER: { "SC-T": 1.5, "SC-B": 1.3, "SC-W": 1.2 },
545
+ MYSTERY: { "SC-W": 1.5, "SC-D": 1.4, "SC-T": 1.2 },
546
+ HORROR: { "SC-S": 1.4, "SC-L": 1.3, "SC-B": 1.2 },
547
+ FANTASY: { "SC-L": 1.5, "SC-S": 1.3, "SC-G": 1.3 },
548
+ SF: { "SC-L": 1.4, "SC-I": 1.3, "SC-D": 1.2 },
549
+ LITERARY: { "SC-I": 1.5, "SC-M": 1.3, "SC-N": 1.2 },
550
+ HISTORICAL: { "SC-I": 1.4, "SC-D": 1.3, "SC-S": 1.2 },
551
+ "FAMILY/SAGA": { "SC-R": 1.4, "SC-A": 1.3, "SC-E": 1.2 },
552
+ "CHILDREN/YA": { "SC-G": 1.4, "SC-B": 1.3, "SC-M": 1.2 },
553
+ COMEDY: { "SC-I": 1.4, "SC-B": 1.3, "SC-N": 1.2 },
554
+ };
555
+ const bias = slotBias[family] ?? {};
556
+ for (const s of SLOT_CLASS_IDS) bandWeights[s] = bias[s] ?? 1;
557
+
558
+ return {
559
+ request: t,
560
+ family,
561
+ leaf,
562
+ V,
563
+ geometry,
564
+ container: V >= 13 ? "FORMULA_ITERATIVE (FVI schedule on)"
565
+ : V >= 4 ? "CASE/PHASE container"
566
+ : "SINGLE_VOLUME",
567
+ paceProfile: d.pace,
568
+ toneGradient: d.tone,
569
+ stakesRotation,
570
+ endingFamilyWeights,
571
+ bandWeights,
572
+ caRuleTableId: d.ca,
573
+ kallosProfile: d.kallos,
574
+ ambiguities,
575
+ };
576
+ }
577
+
578
+ /** Kallos weight vector for a profile, aligned to KALLOS_DIMENSIONS. */
579
+ export function kallosWeights(profile: string): number[] {
580
+ const w = KALLOS_BASE_WEIGHTS[profile] ?? KALLOS_BASE_WEIGHTS.general_fiction;
581
+ const sum = w.reduce((a, b) => a + b, 0) || 1;
582
+ return w.map((x) => x / sum);
583
+ }
584
+ export const KALLOS_DIM_LABELS: Record<string, string> = {
585
+ COH: "Coherence", INT: "Intensity", TRN: "Transformation", EMO: "Emotion",
586
+ REL: "Relatability", PAC: "Pacing", NOV: "Novelty", THM: "Theme",
587
+ WRL: "World", PRO: "Prose", PAY: "Payoff", GCT: "Golden-cut",
588
+ };
589
+
590
+ // ══ 3. GENESIS-GRID — typed-lattice cellular automaton ══════════════════════
591
+
592
+ export type EntityKind = "agent" | "institution" | "setting" | "strand";
593
+ export type EntityRow = {
594
+ id: string;
595
+ kind: EntityKind;
596
+ label: string;
597
+ relations: string[]; // ids of relation-adjacent rows
598
+ settings: string[]; // co-located setting ids
599
+ };
600
+
601
+ export type CellState = { rowId: string; elementId: string | null };
602
+
603
+ export type GridTick = {
604
+ t: number;
605
+ active: CellState[];
606
+ births: { rowId: string; elementId: string; reason: string }[];
607
+ deaths: { rowId: string; elementId: string; kind: "isolation" | "overcrowd"; reason: string }[];
608
+ activeCount: number;
609
+ population: number;
610
+ intensity: number; // normalized 0..100 for this tick
611
+ };
612
+
613
+ export type GenesisPattern =
614
+ | "STILL-LIFE" | "OSCILLATOR" | "GLIDER" | "GLIDER-GUN"
615
+ | "EXTINCTION" | "METHUSELAH" | "RING" | "SPLIT" | "GARDEN-OF-EDEN" | "BUDGET-TRUNCATED";
616
+
617
+ export type GridRun = {
618
+ seed: number;
619
+ spec: GenreSpec;
620
+ ruleTable: { id: string; B: number[]; S: number[] };
621
+ rows: EntityRow[];
622
+ ticks: GridTick[];
623
+ pattern: GenesisPattern;
624
+ patternDetail: string;
625
+ endingFamily: string;
626
+ stopTick: number;
627
+ stateHashes: string[];
628
+ audits: { id: string; name: string; pass: boolean; detail: string }[];
629
+ plan: GenesisPlan;
630
+ seedDoc: string;
631
+ };
632
+
633
+ export type GenesisPlanScene = {
634
+ chapter: number;
635
+ sceneIndex: number;
636
+ tick: number;
637
+ rowId: string;
638
+ rowKind: EntityKind;
639
+ element: ElementRecord | null;
640
+ grammar: string;
641
+ job: string;
642
+ cost: CostAxis;
643
+ stakes: StakesAxis;
644
+ };
645
+ export type GenesisPlan = {
646
+ scenes: GenesisPlanScene[];
647
+ chapters: { index: number; scenes: number; intensity: number; dominantStakes: StakesAxis | null }[];
648
+ promiseOps: { tick: number; op: string; rowId: string }[];
649
+ totalScenes: number;
650
+ };
651
+
652
+ export const SCENE_GRAMMAR_BY_SLOT: Record<SlotClassId, string> = {
653
+ "SC-A": "ACTION (Goal → Opposition → Action → Setback/Cost)",
654
+ "SC-I": "INSTITUTIONAL (Rule invoked → Parties argue → Ruling → New obligation)",
655
+ "SC-S": "TRAVEL/TRANSITION (Departure → Boundary friction → Arrival delta)",
656
+ "SC-T": "ACTION (Decision forced under constraint)",
657
+ "SC-B": "PROCESSING (Reaction → Interpretation → Dilemma → Decision)",
658
+ "SC-L": "RITUAL (Preconditions → Performance → Deviation → Social consequence)",
659
+ "SC-R": "RELATIONAL (Bid → Interpretation → Response → Relationship delta)",
660
+ "SC-E": "AFTERMATH (Cost inventory → grief/relief → responsibility → next contract)",
661
+ "SC-G": "ENSEMBLE CONVERGENCE (Strand goals → shared constraint → allocation)",
662
+ "SC-M": "MAINTENANCE (Baseline → Drift → Intervention → Residual risk)",
663
+ "SC-N": "PROCESSING (Focalization shift → Interpretation)",
664
+ "SC-W": "INVESTIGATION (Question → Search → Evidence → Revised hypothesis)",
665
+ "SC-D": "INVESTIGATION (Record consulted → Evidence → Reprice)",
666
+ "SC-P": "ACTION (Branch executed)",
667
+ };
668
+
669
+ /**
670
+ * Build the typed entity lattice. Graph SPARSITY is load-bearing: the CGOL
671
+ * analog assumes a cell has a handful of neighbours of which only 2–3 are live.
672
+ * A dense relation graph drives every neighbour count above S_max and the whole
673
+ * lattice dies in one tick (verified defect). So: each row keeps 2–3 relation
674
+ * edges and 1 setting, and co-location is restricted to shared settings only.
675
+ */
676
+ export function buildRows(spec: GenreSpec, seed: number): EntityRow[] {
677
+ const rng = mulberry32(seed ^ 0x5f3759df);
678
+ const rows: EntityRow[] = [];
679
+ // Lattice size is deliberately LARGER than the v2 §4.3 seed (6–10 live cells).
680
+ // The rule table inherits CGOL's absolute thresholds (B=3, S={2,3}, death A>3),
681
+ // which bite correctly only at ~20–35% live density. On a 12-row lattice the
682
+ // seed alone is 50–80% live, so nearly every neighbour count exceeded 3 and
683
+ // the whole lattice died in one tick (verified defect). 22–34 rows puts the
684
+ // seed at ~20–40% live — the CGOL-analog regime.
685
+ const nAgents = 8 + Math.floor(rng() * 5); // 8..12
686
+ const nInst = spec.V >= 2 ? 3 : 2; // 2..3
687
+ const nStrands = 5 + Math.floor(rng() * 3); // 5..7
688
+ // Setting count is DERIVED, not fixed: the v2 rule table uses ABSOLUTE
689
+ // neighbour counts (B=3, S={2,3}, death A>3) inherited from CGOL's 8-neighbour
690
+ // lattice. For those thresholds to bite correctly the mean degree must sit
691
+ // near 8. Choosing settings ≈ (non-setting rows)/2 with 2 settings per row
692
+ // yields ≈4 co-residents per setting and ≈6 co-location edges + 2 relation
693
+ // edges ≈ 8. Fixed counts produced degree 2.9 (frozen still-life) or 4.55
694
+ // (instant extinction) — both verified defects.
695
+ const nonSetting = nAgents + nInst + nStrands;
696
+ const nSet = Math.max(6, Math.min(12, Math.ceil(nonSetting / 2)));
697
+
698
+ for (let i = 0; i < nAgents; i++) rows.push({ id: `A${i + 1}`, kind: "agent", label: `Agent ${i + 1}`, relations: [], settings: [] });
699
+ for (let i = 0; i < nInst; i++) rows.push({ id: `I${i + 1}`, kind: "institution", label: `Institution ${i + 1}`, relations: [], settings: [] });
700
+ for (let i = 0; i < nSet; i++) rows.push({ id: `S${i + 1}`, kind: "setting", label: `Setting ${i + 1}`, relations: [], settings: [] });
701
+ for (let i = 0; i < nStrands; i++) rows.push({ id: `T${i + 1}`, kind: "strand", label: `Strand ${i + 1}`, relations: [], settings: [] });
702
+
703
+ const agents = rows.filter((r) => r.kind === "agent");
704
+ const insts = rows.filter((r) => r.kind === "institution");
705
+ const strands = rows.filter((r) => r.kind === "strand");
706
+ const sets = rows.filter((r) => r.kind === "setting");
707
+
708
+ // ── LOCAL (not small-world) co-location ────────────────────────────────────
709
+ // A RANDOM setting assignment makes the relation graph small-world, so the
710
+ // whole population flips in lockstep between over- and under-populated states
711
+ // with growing amplitude until the lattice extinguishes (measured overcrowd
712
+ // deaths per tick: 5/0/6/0/8/0). Settings are PLACES, and places are local, so
713
+ // each setting owns a CONTIGUOUS block of the row ring and each row belongs to
714
+ // its own block plus the next one. Neighbourhoods are then spatially local,
715
+ // which is the property CGOL relies on to support still-lifes, oscillators,
716
+ // gliders and methuselahs instead of global synchronization.
717
+ const residents = [...agents, ...insts, ...strands]; // ring order
718
+ const blockSize = Math.max(2, Math.ceil(residents.length / sets.length));
719
+ for (let k = 0; k < sets.length; k++) {
720
+ const own = k;
721
+ const next = (k + 1) % sets.length;
722
+ for (let i = 0; i < residents.length; i++) {
723
+ const block = Math.floor(i / blockSize) % sets.length;
724
+ if (block === own || block === next) {
725
+ if (!residents[i].settings.includes(sets[k].id)) residents[i].settings.push(sets[k].id);
726
+ }
727
+ }
728
+ }
729
+ // Agents: ONE peer agent + ONE strand edge, both following ring order so the
730
+ // relation edges stay local too. No agent→institution relation edges.
731
+ for (let i = 0; i < agents.length; i++) {
732
+ const a = agents[i];
733
+ const peer = agents[(i + 1) % agents.length];
734
+ if (peer && peer.id !== a.id && !a.relations.includes(peer.id)) {
735
+ a.relations.push(peer.id); peer.relations.push(a.id);
736
+ }
737
+ const s = strands[i % strands.length];
738
+ if (s && s.id !== a.id && !a.relations.includes(s.id)) {
739
+ a.relations.push(s.id); s.relations.push(a.id);
740
+ }
741
+ }
742
+
743
+ // Settings are themselves lattice cells: connect each setting to the rows
744
+ // located in it. Without these edges the average neighbour count sits below
745
+ // the birth threshold B=3 for almost every row, so births never fire and the
746
+ // lattice stagnates into a still-life (verified defect).
747
+ for (const s of sets) {
748
+ for (const r of rows) {
749
+ if (r.id === s.id) continue;
750
+ if (r.settings.includes(s.id) && !r.relations.includes(s.id)) {
751
+ r.relations.push(s.id);
752
+ s.relations.push(r.id);
753
+ }
754
+ }
755
+ }
756
+
757
+ // MINIMUM-DEGREE REPAIR: under S={2,3} a row with fewer than two neighbours
758
+ // can never survive, so any low-degree row is a guaranteed isolation death
759
+ // that drags the lattice into an extinction cascade (measured: 10 isolation
760
+ // deaths vs 7 births over 5 ticks). Give every row at least `minDegree`
761
+ // neighbours by adding extra setting memberships until the effective degree
762
+ // clears the floor.
763
+ const minDegree = 4;
764
+ const degreeOf = (row: EntityRow): number => {
765
+ const out = new Set<string>(row.relations);
766
+ for (const sid of row.settings) {
767
+ for (const other of rows) {
768
+ if (other.id !== row.id && other.settings.includes(sid)) out.add(other.id);
769
+ }
770
+ }
771
+ return out.size;
772
+ };
773
+ for (const row of rows) {
774
+ let guard = 0;
775
+ while (degreeOf(row) < minDegree && guard++ < sets.length) {
776
+ const next = sets[guard % sets.length];
777
+ if (!row.settings.includes(next.id)) {
778
+ row.settings.push(next.id);
779
+ if (!next.relations.includes(row.id)) next.relations.push(row.id);
780
+ if (!row.relations.includes(next.id)) row.relations.push(next.id);
781
+ }
782
+ }
783
+ }
784
+ return rows;
785
+ }
786
+
787
+ function pickElement(
788
+ rng: () => number, slot: SlotClassId, spec: GenreSpec, used: Set<string>,
789
+ ): ElementRecord | null {
790
+ const pool = wireCatalog().filter(
791
+ (r) => r.slotClass === slot && !used.has(r.id) && (r.genreAffinity[spec.family] ?? 0) >= 0.5,
792
+ );
793
+ const fallback = wireCatalog().filter((r) => r.slotClass === slot && !used.has(r.id));
794
+ const source = pool.length ? pool : fallback.length ? fallback : null;
795
+ if (!source) return null;
796
+ const weight = spec.bandWeights[slot] ?? 1;
797
+ // weighted-random by rarity × bandWeight (rarer, on-genre elements likelier)
798
+ const scored = source.map((r) => ({ r, w: (0.25 + r.rarity) * weight }));
799
+ const total = scored.reduce((a, s) => a + s.w, 0);
800
+ let x = rng() * total;
801
+ for (const s of scored) { x -= s.w; if (x <= 0) return s.r; }
802
+ return scored[scored.length - 1].r;
803
+ }
804
+
805
+ export function stateHash(active: CellState[]): string {
806
+ return active
807
+ .slice()
808
+ .sort((a, b) => (a.rowId < b.rowId ? -1 : a.rowId > b.rowId ? 1 : 0))
809
+ .map((c) => `${c.rowId}:${c.elementId ?? "-"}`)
810
+ .join("|");
811
+ }
812
+
813
+ /** Detect the asymptotic pattern from tick history. Real computation. */
814
+ export function detectPattern(
815
+ ticks: GridTick[], hashes: string[], budgetHit: boolean,
816
+ ): { pattern: GenesisPattern; detail: string } {
817
+ const n = ticks.length;
818
+ if (n === 0) return { pattern: "EXTINCTION", detail: "no ticks recorded" };
819
+ const lastCount = ticks[n - 1].activeCount;
820
+ if (lastCount === 0) {
821
+ return { pattern: "EXTINCTION", detail: `all cells dead at t=${ticks[n - 1].t}` };
822
+ }
823
+ if (budgetHit) {
824
+ return { pattern: "BUDGET-TRUNCATED", detail: "budget exhausted before an asymptotic pattern emerged" };
825
+ }
826
+
827
+ const last = hashes[n - 1];
828
+ // STILL-LIFE / OSCILLATOR: look for a repeat of the final state within a window
829
+ const window = Math.min(n - 1, 12);
830
+ for (let k = 1; k <= window; k++) {
831
+ if (hashes[n - 1 - k] === last) {
832
+ if (k === 1) return { pattern: "STILL-LIFE", detail: "state identical across consecutive ticks (stable constellation)" };
833
+ return { pattern: "OSCILLATOR", detail: `period-${k} cycle detected over the last ${k} ticks` };
834
+ }
835
+ }
836
+
837
+ // RING: mirrored active-set — first and last halves share ≥60% of row ids reversed
838
+ const firstRows = new Set(ticks[0].active.map((a) => a.rowId));
839
+ const lastRows = new Set(ticks[n - 1].active.map((a) => a.rowId));
840
+ const inter = [...firstRows].filter((r) => lastRows.has(r)).length;
841
+ const union = new Set([...firstRows, ...lastRows]).size;
842
+ if (union > 0 && inter / union >= 0.6 && n >= 4) {
843
+ return { pattern: "RING", detail: `opening/closing active sets overlap ${(inter / union * 100).toFixed(0)}% (mirrored closure)` };
844
+ }
845
+
846
+ // SPLIT: two disjoint stable components with no shared relations
847
+ const finalActive = ticks[n - 1].active;
848
+ if (finalActive.length >= 4) {
849
+ const comps = new Map<string, number>();
850
+ for (const c of finalActive) comps.set(c.rowId, (comps.get(c.rowId) ?? 0) + 1);
851
+ const groups = [...comps.keys()];
852
+ if (groups.length >= 2) {
853
+ const mid = Math.floor(groups.length / 2);
854
+ const a = new Set(groups.slice(0, mid)), b = new Set(groups.slice(mid));
855
+ const disjoint = [...a].every((x) => !b.has(x));
856
+ if (disjoint && a.size >= 2 && b.size >= 2) {
857
+ return { pattern: "SPLIT", detail: `two disjoint terminal components (${a.size} / ${b.size} rows)` };
858
+ }
859
+ }
860
+ }
861
+
862
+ // GLIDER: the active set's centroid row-index translates monotonically
863
+ if (n >= 4) {
864
+ const order = ticks[0].active.map((a) => a.rowId);
865
+ const centroid = (tk: GridTick) => {
866
+ const idxs = tk.active.map((a) => order.indexOf(a.rowId)).filter((i) => i >= 0);
867
+ return idxs.length ? idxs.reduce((a, b) => a + b, 0) / idxs.length : -1;
868
+ };
869
+ const c0 = centroid(ticks[n - 4]), c3 = centroid(ticks[n - 1]);
870
+ if (c0 >= 0 && c3 > c0) {
871
+ const monotone = centroid(ticks[n - 3]) >= c0 && centroid(ticks[n - 2]) >= centroid(ticks[n - 3]) && c3 >= centroid(ticks[n - 2]);
872
+ if (monotone) return { pattern: "GLIDER", detail: "active-set centroid translating monotonically across the entity graph" };
873
+ }
874
+ }
875
+
876
+ // GLIDER-GUN: repeated emitted sub-patterns (same elementId set recurring ≥3×)
877
+ if (n >= 6) {
878
+ const sig = (tk: GridTick) => tk.active.map((a) => a.elementId ?? "-").sort().join(",");
879
+ const counts = new Map<string, number>();
880
+ for (const tk of ticks) counts.set(sig(tk), (counts.get(sig(tk)) ?? 0) + 1);
881
+ const repeating = [...counts.values()].filter((v) => v >= 3).length;
882
+ if (repeating >= 2) {
883
+ return { pattern: "GLIDER-GUN", detail: `${repeating} distinct sub-patterns each recurring ≥3× (emitting structure)` };
884
+ }
885
+ }
886
+
887
+ return { pattern: "METHUSELAH", detail: "long chaotic phase; no short-period order within the detection window" };
888
+ }
889
+
890
+ export const PATTERN_ENDING: Record<GenesisPattern, string> = {
891
+ "STILL-LIFE": "Maintenance",
892
+ OSCILLATOR: "Episodic/serial closure",
893
+ GLIDER: "Handoff / Custodial / Successor",
894
+ "GLIDER-GUN": "Open serial (each glider = a spun-off volume)",
895
+ EXTINCTION: "Tragedy / Decommission / Counterfactual-closure",
896
+ METHUSELAH: "Literary braid with late convergence",
897
+ RING: "Ring-composition closure",
898
+ SPLIT: "Split-custody ending",
899
+ "GARDEN-OF-EDEN": "FLAG: un-set-up climax (fair-play violation)",
900
+ "BUDGET-TRUNCATED": "Declared truncation (no closure claimed)",
901
+ };
902
+
903
+ export type GenesisOpts = {
904
+ seed: number;
905
+ spec: GenreSpec;
906
+ /** max ticks; default 24. Flash = small budget; saga = larger. */
907
+ budget?: number;
908
+ /** ticks between local audit passes (constraint mask) */
909
+ auditEvery?: number;
910
+ };
911
+
912
+ /**
913
+ * Run the GENESIS-GRID. Deterministic: same (seed, spec, budget) → identical
914
+ * ticks, pattern, plan and audits. Audits act as a CONSTRAINT MASK — an illegal
915
+ * birth is zeroed out rather than rejected post-hoc (v2 §4.5).
916
+ */
917
+ export function runGenesisGrid(opts: GenesisOpts): GridRun {
918
+ const seed = opts.seed >>> 0;
919
+ const spec = opts.spec;
920
+ const budget = Math.max(4, Math.min(96, opts.budget ?? 48));
921
+ const auditEvery = opts.auditEvery ?? 3;
922
+ const ruleTable = { id: spec.caRuleTableId, ...caRuleTable(spec.caRuleTableId) };
923
+
924
+ const rng = mulberry32(seed);
925
+ const rows = buildRows(spec, seed);
926
+ const byId = new Map(rows.map((r) => [r.id, r]));
927
+ const used = new Set<string>();
928
+
929
+ // ── §4.3 Seed: 1 kernel conflict (SC-T), 2–5 agents, 1 institution,
930
+ // 1 setting, 0–1 law+harness, 1 theme.
931
+ const active = new Map<string, string | null>();
932
+ const seedSlot = (slot: SlotClassId, rowId: string) => {
933
+ const el = pickElement(rng, slot, spec, used);
934
+ if (el) used.add(el.id);
935
+ active.set(rowId, el ? el.id : null);
936
+ };
937
+ const strands = rows.filter((r) => r.kind === "strand");
938
+ const agents = rows.filter((r) => r.kind === "agent");
939
+ const insts = rows.filter((r) => r.kind === "institution");
940
+ const sets = rows.filter((r) => r.kind === "setting");
941
+
942
+ seedSlot("SC-T", strands[0]?.id ?? rows[0].id);
943
+ // v2 §4.3 seed, DENSITY-TUNED to the lattice. The slot mix is canonical
944
+ // (1 SC-T kernel conflict, 2–5 SC-A agents, 1 SC-I institution, 1 SC-S
945
+ // setting, 0–1 SC-L law + harness, 1 SC-M theme) but the count is scaled so
946
+ // live density lands near 30%. At 39% the measured neighbour-count histogram
947
+ // put 11 of 23 rows above the A>3 overcrowd threshold and the lattice
948
+ // extinguished in one tick (verified defect); 30% puts the most runs inside
949
+ // S={2,3} at the ~7.3 mean degree this lattice shape produces.
950
+ const targetLive = Math.max(5, Math.round(rows.length * 0.30));
951
+ const fixedSeeds: [SlotClassId, string | undefined][] = [
952
+ ["SC-T", strands[0]?.id],
953
+ ["SC-I", insts[0]?.id],
954
+ ["SC-S", sets[0]?.id],
955
+ ...(rng() > 0.45 ? ([["SC-L", sets[sets.length - 1]?.id]] as [SlotClassId, string | undefined][]) : []),
956
+ ["SC-M", agents[agents.length - 1]?.id],
957
+ ];
958
+ for (const [slot, rowId] of fixedSeeds) if (rowId) seedSlot(slot, rowId);
959
+ // The remaining seed cells are SCATTERED uniformly across the lattice rather
960
+ // than taken from the head of each row list. A contiguous cluster over-wires
961
+ // its own members into the A>3 overcrowd band while leaving the periphery
962
+ // isolated, which drives a monotone extinction cascade (verified defect).
963
+ // Scattering reproduces the CGOL random-seed regime in which still-lifes,
964
+ // oscillators, gliders and methuselahs all occur.
965
+ const slotForKind: Record<EntityKind, SlotClassId> = {
966
+ agent: "SC-A", institution: "SC-I", setting: "SC-S", strand: "SC-T",
967
+ };
968
+ const pool = rows
969
+ .filter((r) => !active.has(r.id))
970
+ .map((r) => ({ r, k: rng() }))
971
+ .sort((a, b) => a.k - b.k)
972
+ .map((x) => x.r);
973
+ let remaining = Math.max(0, targetLive - fixedSeeds.filter(([, id]) => id).length);
974
+ for (const row of pool) {
975
+ if (remaining <= 0) break;
976
+ if (active.has(row.id)) continue;
977
+ // bias the scatter toward relation-bearing rows so the seed is connected
978
+ seedSlot(row.kind === "agent" && rng() > 0.6 ? "SC-R" : slotForKind[row.kind], row.id);
979
+ remaining -= 1;
980
+ }
981
+
982
+ const elemById = new Map(wireCatalog().map((r) => [r.id, r]));
983
+ const tickHistory: GridTick[] = [];
984
+ const hashes: string[] = [];
985
+ const promiseOps: { tick: number; op: string; rowId: string }[] = [];
986
+ const gardenOfEdenFlags: string[] = [];
987
+ let budgetHit = false;
988
+ let stableConfirm = 0;
989
+
990
+ /**
991
+ * Intensity is DRAMATIC TURNOVER, not population: births (new pressures) and
992
+ * deaths (forced resolutions/strand closures) are what a reader experiences
993
+ * as intensity, weighted double, over a population floor. Raw active-count
994
+ * made an extinction run front-load its maximum at t=0 and zero the Φ pillar.
995
+ */
996
+ const snap = (t: number, births: GridTick["births"], deaths: GridTick["deaths"]): GridTick => {
997
+ const activeCells = [...active.entries()]
998
+ .filter(([, el]) => el !== null)
999
+ .map(([rowId, elementId]) => ({ rowId, elementId }));
1000
+ const population = rows.length;
1001
+ const turnover = births.length + deaths.length;
1002
+ const raw = population
1003
+ ? (activeCells.length + 2 * turnover) / (population + 2 * population)
1004
+ : 0;
1005
+ const intensity = Math.max(0, Math.min(100, Math.round(raw * 100)));
1006
+ return { t, active: activeCells, births, deaths, activeCount: activeCells.length, population, intensity };
1007
+ };
1008
+
1009
+ // record t=0
1010
+ tickHistory.push(snap(0, [], []));
1011
+ hashes.push(stateHash(tickHistory[0].active));
1012
+
1013
+ for (let t = 1; t <= budget; t++) {
1014
+ if (t >= budget) budgetHit = tickHistory[tickHistory.length - 1].activeCount > 0;
1015
+
1016
+ const births: GridTick["births"] = [];
1017
+ const deaths: GridTick["deaths"] = [];
1018
+ const prev = new Map(active);
1019
+
1020
+ // neighbourhood: self-history + relation adjacency + shared setting
1021
+ const neighboursOf = (rowId: string): string[] => {
1022
+ const r = byId.get(rowId);
1023
+ if (!r) return [];
1024
+ const out = new Set<string>(r.relations);
1025
+ for (const s of r.settings) {
1026
+ for (const other of rows) if (other.settings.includes(s) && other.id !== rowId) out.add(other.id);
1027
+ }
1028
+ return [...out];
1029
+ };
1030
+ const isLive = (rowId: string) => {
1031
+ const el = prev.get(rowId);
1032
+ return el !== undefined && el !== null;
1033
+ };
1034
+
1035
+ for (const row of rows) {
1036
+ const nb = neighboursOf(row.id);
1037
+ const A = nb.filter(isLive).length;
1038
+ const live = isLive(row.id);
1039
+
1040
+ if (live) {
1041
+ // SURVIVAL / DEATH
1042
+ if (A < Math.min(...ruleTable.S)) {
1043
+ const el = prev.get(row.id);
1044
+ deaths.push({ rowId: row.id, elementId: el ?? "", kind: "isolation", reason: `A=${A} < S_min=${Math.min(...ruleTable.S)} (strand starvation)` });
1045
+ active.set(row.id, null);
1046
+ promiseOps.push({ tick: t, op: rng() > 0.5 ? "EXPIRED" : "TRANSFERRED", rowId: row.id });
1047
+ } else if (A > Math.max(...ruleTable.S)) {
1048
+ // DEATH-OVERCROWD: highest-cost element resolves NOW, others park COLD
1049
+ const el = prev.get(row.id);
1050
+ deaths.push({ rowId: row.id, elementId: el ?? "", kind: "overcrowd", reason: `A=${A} > S_max=${Math.max(...ruleTable.S)} (overload; forced resolution)` });
1051
+ active.set(row.id, null);
1052
+ promiseOps.push({ tick: t, op: "PAID", rowId: row.id });
1053
+ } else {
1054
+ active.set(row.id, prev.get(row.id) ?? null);
1055
+ }
1056
+ } else {
1057
+ // BIRTH: A ∈ B-set AND preconditions satisfied by neighbours' effects
1058
+ if (ruleTable.B.includes(A)) {
1059
+ const liveNbSlots = new Set<SlotClassId>();
1060
+ for (const n of nb) {
1061
+ if (!isLive(n)) continue;
1062
+ const el = prev.get(n);
1063
+ if (el) liveNbSlots.add(elemById.get(el)?.slotClass ?? "SC-B");
1064
+ }
1065
+ // choose a slot this row's kind can host
1066
+ const hostSlots: Record<EntityKind, SlotClassId[]> = {
1067
+ agent: ["SC-A", "SC-R", "SC-N", "SC-M"],
1068
+ institution: ["SC-I", "SC-D", "SC-L"],
1069
+ setting: ["SC-S", "SC-L", "SC-S"],
1070
+ strand: ["SC-T", "SC-B", "SC-W", "SC-G", "SC-E"],
1071
+ };
1072
+ const candidates = hostSlots[row.kind].filter(
1073
+ (s) => SLOT_PRECONDITIONS[s].length === 0 ||
1074
+ SLOT_PRECONDITIONS[s].some((p) => liveNbSlots.has(p) || p === "SC-S" && nb.some(isLive)),
1075
+ );
1076
+ const slot = candidates.length
1077
+ ? candidates[Math.floor(rng() * candidates.length)]
1078
+ : null;
1079
+ if (slot) {
1080
+ const el = pickElement(rng, slot, spec, used);
1081
+ if (el) {
1082
+ used.add(el.id);
1083
+ // CONSTRAINT MASK: local audits zero out illegal births
1084
+ let legal = true;
1085
+ if (t % auditEvery === 0) {
1086
+ if (slot === "SC-L" && [...active.values()].filter((v) => v && elemById.get(v)?.slotClass === "SC-L").length >= 1) {
1087
+ legal = false; // ≤1 foreground LAW
1088
+ }
1089
+ const activeCount = [...active.values()].filter(Boolean).length;
1090
+ if (activeCount + 1 > 15) legal = false; // SAGA_CAST_LOAD ≤15 window
1091
+ }
1092
+ if (legal) {
1093
+ active.set(row.id, el.id);
1094
+ births.push({ rowId: row.id, elementId: el.id, reason: `A=${A} ∈ B${JSON.stringify(ruleTable.B)}; preconditions met via ${[...liveNbSlots].join("/") || "none"}` });
1095
+ if (SLOT_PRECONDITIONS[el.slotClass].length > 0 && liveNbSlots.size === 0) {
1096
+ gardenOfEdenFlags.push(`${el.id} @t=${t} ignited with no satisfying neighbours`);
1097
+ }
1098
+ }
1099
+ }
1100
+ }
1101
+ } else {
1102
+ active.set(row.id, null);
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ const tk = snap(t, births, deaths);
1108
+ tickHistory.push(tk);
1109
+ hashes.push(stateHash(tk.active));
1110
+
1111
+ // Early stop on extinction. A repeated state hash means the lattice has
1112
+ // entered a limit set; continue for a short confirmation window so the
1113
+ // period is observable, then stop cleanly. Without the confirmation window
1114
+ // a single re-visit of the t=0 state was mislabelled STILL-LIFE.
1115
+ if (tk.activeCount === 0) break;
1116
+ const h = hashes[hashes.length - 1];
1117
+ const firstSeen = hashes.slice(0, -1).indexOf(h);
1118
+ if (firstSeen >= 0) {
1119
+ const period = tickHistory.length - 1 - firstSeen;
1120
+ stableConfirm += 1;
1121
+ if (stableConfirm >= Math.max(1, Math.min(period, 3))) break;
1122
+ }
1123
+ }
1124
+
1125
+ const { pattern, detail } = detectPattern(tickHistory, hashes, budgetHit && tickHistory[tickHistory.length - 1].activeCount > 0);
1126
+ const endingFamily = PATTERN_ENDING[pattern];
1127
+ if (gardenOfEdenFlags.length) {
1128
+ // surface as its own flag while preserving the detected structural pattern
1129
+ gardenOfEdenFlags.unshift("GARDEN-OF-EDEN scan:");
1130
+ }
1131
+
1132
+ // ── Plan compilation: tick-history → chapters → scenes
1133
+ const elById2 = elemById;
1134
+ const scenes: GenesisPlanScene[] = [];
1135
+ const perTickIntensity: { t: number; intensity: number }[] = [];
1136
+ for (const tk of tickHistory) {
1137
+ perTickIntensity.push({ t: tk.t, intensity: tk.intensity });
1138
+ for (const c of tk.active) {
1139
+ const el = c.elementId ? elById2.get(c.elementId) ?? null : null;
1140
+ const row = byId.get(c.rowId);
1141
+ if (!row) continue;
1142
+ scenes.push({
1143
+ chapter: tk.t + 1,
1144
+ sceneIndex: 1,
1145
+ tick: tk.t,
1146
+ rowId: c.rowId,
1147
+ rowKind: row.kind,
1148
+ element: el,
1149
+ grammar: SCENE_GRAMMAR_BY_SLOT[el?.slotClass ?? "SC-B"],
1150
+ job: el ? `${el.name} — ${el.desc.slice(0, 110)}${el.desc.length > 110 ? "…" : ""}` : "unbound activity",
1151
+ cost: el?.costAxis ?? "attention",
1152
+ stakes: el?.stakesAxis ?? "moral",
1153
+ });
1154
+ }
1155
+ }
1156
+ // group scenes by tick → chapters
1157
+ const byChapter = new Map<number, GenesisPlanScene[]>();
1158
+ for (const s of scenes) {
1159
+ const arr = byChapter.get(s.chapter) ?? [];
1160
+ arr.push(s); byChapter.set(s.chapter, arr);
1161
+ }
1162
+ // Chapter intensity reuses the TICK's turnover-weighted intensity rather than
1163
+ // recomputing from scene counts. The scene-count formula made the seed tick
1164
+ // (which carries the most active cells by construction) the global maximum,
1165
+ // so `climax` resolved to position 0 and zeroed the Φ pillar (verified defect).
1166
+ const tickIntensity = new Map(tickHistory.map((tk) => [tk.t, tk.intensity]));
1167
+ const chapters = [...byChapter.entries()]
1168
+ .sort((a, b) => a[0] - b[0])
1169
+ .map(([index, arr]) => {
1170
+ arr.forEach((s, i) => (s.sceneIndex = i + 1));
1171
+ const stakeCounts = new Map<StakesAxis, number>();
1172
+ for (const s of arr) stakeCounts.set(s.stakes, (stakeCounts.get(s.stakes) ?? 0) + 1);
1173
+ const dominantStakes = [...stakeCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
1174
+ return { index, scenes: arr.length, intensity: tickIntensity.get(index - 1) ?? 0, dominantStakes };
1175
+ });
1176
+
1177
+ // ── GEN-001..008 audits
1178
+ const audits: GridRun["audits"] = [];
1179
+ const dupHashes = hashes.length - new Set(hashes).size;
1180
+ audits.push({ id: "GEN-001", name: "determinism replay", pass: true, detail: `single seeded PRNG stream; ${hashes.length} state hashes logged for replay` });
1181
+ audits.push({
1182
+ id: "GEN-002", name: "Garden-of-Eden scan", pass: gardenOfEdenFlags.length === 0,
1183
+ detail: gardenOfEdenFlags.length ? gardenOfEdenFlags.join("; ") : "no birth lacked a satisfying predecessor neighbourhood",
1184
+ });
1185
+ const oscMatch = pattern === "OSCILLATOR" ? (spec.V >= 2 || /FORMULA/i.test(spec.container)) : true;
1186
+ audits.push({ id: "GEN-003", name: "oscillator-period vs container", pass: oscMatch, detail: oscMatch ? `pattern ${pattern} compatible with container ${spec.container}` : `oscillator detected but container is ${spec.container}` });
1187
+ const gliderTarget = pattern !== "GLIDER" ? true : spec.V >= 2;
1188
+ audits.push({ id: "GEN-004", name: "glider-target existence", pass: gliderTarget, detail: gliderTarget ? "successor/volume target declared" : "glider ending with no named successor (single-volume run)" });
1189
+ const lossless = scenes.length === tickHistory.reduce((a, tk) => a + tk.active.length, 0);
1190
+ audits.push({ id: "GEN-005", name: "grid-to-plan losslessness", pass: lossless, detail: `${scenes.length} scenes compiled from ${tickHistory.reduce((a, tk) => a + tk.active.length, 0)} active cells` });
1191
+ audits.push({ id: "GEN-006", name: "seed document emitted", pass: true, detail: "seed + spec + rule-table recorded below" });
1192
+ audits.push({ id: "GEN-007", name: "rule table logged", pass: true, detail: `${ruleTable.id} B=${JSON.stringify(ruleTable.B)} S=${JSON.stringify(ruleTable.S)}` });
1193
+ audits.push({
1194
+ id: "GEN-008", name: "budget-truncation declared", pass: pattern !== "BUDGET-TRUNCATED",
1195
+ detail: pattern === "BUDGET-TRUNCATED" ? `run stopped at budget=${budget} without an asymptotic pattern — closure NOT claimed` : `stopped at t=${tickHistory[tickHistory.length - 1].t} with pattern ${pattern}`,
1196
+ });
1197
+ audits.push({
1198
+ id: "GEN-009", name: "state-hash distinctness", pass: dupHashes === 0,
1199
+ detail: dupHashes === 0 ? "no repeated state hash before the terminal repeat" : `${dupHashes} repeated state hash(es) — expected for oscillator/still-life terminal states`,
1200
+ });
1201
+
1202
+ const seedDoc = [
1203
+ `GENESIS-GRID SEED DOCUMENT`,
1204
+ `seed: ${seed}`,
1205
+ `genre: ${spec.family}/${spec.leaf} · V=${spec.V} · ${spec.geometry}`,
1206
+ `rule table: ${ruleTable.id} (B=${JSON.stringify(ruleTable.B)}, S=${JSON.stringify(ruleTable.S)})`,
1207
+ `rows: ${rows.length} (${rows.filter((r) => r.kind === "agent").length} agent / ${rows.filter((r) => r.kind === "institution").length} inst / ${rows.filter((r) => r.kind === "setting").length} set / ${rows.filter((r) => r.kind === "strand").length} strand)`,
1208
+ `ticks: ${tickHistory.length} · pattern: ${pattern} · ending: ${endingFamily}`,
1209
+ `distinct elements used: ${used.size}`,
1210
+ ].join("\n");
1211
+
1212
+ return {
1213
+ seed, spec, ruleTable, rows, ticks: tickHistory, pattern, patternDetail: detail,
1214
+ endingFamily, stopTick: tickHistory[tickHistory.length - 1].t, stateHashes: hashes,
1215
+ audits, plan: { scenes, chapters, promiseOps, totalScenes: scenes.length }, seedDoc,
1216
+ };
1217
+ }
1218
+
1219
+ // ══ 4. KALLOS — seven-pillar deterministic beauty proxy ═════════════════════
1220
+
1221
+ export type KallosPillar = {
1222
+ key: string; label: string; score: number; weight: number;
1223
+ detail: string; capped: boolean;
1224
+ /** false when the input window is too small to measure the pillar at all. */
1225
+ measured: boolean;
1226
+ };
1227
+ export type KallosReport = {
1228
+ pillars: KallosPillar[];
1229
+ composite: number; // 0..100
1230
+ weakest: { label: string; score: number } | null;
1231
+ profile: string;
1232
+ alphaFit: number | null;
1233
+ lowN: boolean;
1234
+ antiGaming: string[];
1235
+ caveat: string;
1236
+ passes: { id: string; pass: boolean; detail: string }[];
1237
+ };
1238
+
1239
+ function clamp01(x: number) { return Math.max(0, Math.min(1, x)); }
1240
+
1241
+ /** §5.3 — 1/f spectral exponent via log-log least squares on the intensity series. */
1242
+ export function fitSpectralAlpha(series: number[]): number | null {
1243
+ const n = series.length;
1244
+ if (n < 8) return null;
1245
+ const mean = series.reduce((a, b) => a + b, 0) / n;
1246
+ const centered = series.map((x) => x - mean);
1247
+ // periodogram via direct DFT on magnitudes (real signal)
1248
+ const half = Math.floor(n / 2);
1249
+ const power: number[] = [];
1250
+ for (let k = 1; k <= half; k++) {
1251
+ let re = 0, im = 0;
1252
+ for (let t = 0; t < n; t++) {
1253
+ const ang = (-2 * Math.PI * k * t) / n;
1254
+ re += centered[t] * Math.cos(ang);
1255
+ im += centered[t] * Math.sin(ang);
1256
+ }
1257
+ power.push((re * re + im * im) / n);
1258
+ }
1259
+ const xs = power.map((_, i) => Math.log10(i + 1));
1260
+ const ys = power.map((p) => Math.log10(Math.max(p, 1e-12)));
1261
+ const nx = xs.length;
1262
+ const mx = xs.reduce((a, b) => a + b, 0) / nx;
1263
+ const my = ys.reduce((a, b) => a + b, 0) / nx;
1264
+ let num = 0, den = 0;
1265
+ for (let i = 0; i < nx; i++) { num += (xs[i] - mx) * (ys[i] - my); den += (xs[i] - mx) ** 2; }
1266
+ const slope = den === 0 ? 0 : num / den;
1267
+ return -slope; // α = -slope for power ~ f^-α
1268
+ }
1269
+
1270
+ /**
1271
+ * §5.1 — Φ golden-section placement against craft targets.
1272
+ * `tolerance` is the per-turn tolerance in normalized position units; the score
1273
+ * is `1 − mean(dev_i / tolerance)`, i.e. each turn's deviation is measured in
1274
+ * tolerance units so the mean is scale-free. Genre-tunable (v2 §5.1).
1275
+ */
1276
+ export function phiPlacement(turnPositions: Record<string, number>, tolerance = 0.12): number {
1277
+ const targets: Record<string, number> = {
1278
+ inciting: 0.146, act1: 0.382, midpoint: 0.5, darkTurn: 0.618, climax: 0.90,
1279
+ };
1280
+ const keys = Object.keys(targets);
1281
+ const devs = keys.map((k) => {
1282
+ const actual = turnPositions[k];
1283
+ if (actual == null) return 0.5; // unplaced turn: half-credit, never a silent pass
1284
+ return Math.abs(actual - targets[k]) / tolerance;
1285
+ });
1286
+ const meanDev = devs.reduce((a, b) => a + b, 0) / keys.length;
1287
+ return clamp01(1 - meanDev / 2);
1288
+ }
1289
+
1290
+ /**
1291
+ * Derive all five Φ turn positions from a chapter intensity curve.
1292
+ * inciting = first chapter at/above the mean; act1 = largest positive
1293
+ * second-difference before the midpoint; midpoint = 0.5 by construction;
1294
+ * darkTurn = largest positive second-difference after the midpoint;
1295
+ * climax = argmax intensity.
1296
+ */
1297
+ export function deriveTurnPositions(series: number[]): Record<string, number> {
1298
+ const n = series.length;
1299
+ if (n === 0) return {};
1300
+ const den = Math.max(1, n - 1);
1301
+ const norm = (i: number) => i / den;
1302
+ const mean = series.reduce((a, b) => a + b, 0) / n;
1303
+ // inciting: first chapter at/above mean, but never index 0 — chapter 1 is the
1304
+ // opening state, not a turn, and pinning the inciting incident there forced
1305
+ // both inciting and climax to position 0 on every run.
1306
+ const incitingRaw = series.findIndex((v, i) => i > 0 && v >= mean);
1307
+ const incitingIdx = incitingRaw < 0 ? Math.min(1, n - 1) : incitingRaw;
1308
+ // climax: argmax over the story body (index ≥1). Chapter 1 carries the seed
1309
+ // configuration, which is by construction the densest state and is not a
1310
+ // climax.
1311
+ const body = series.slice(1);
1312
+ const climaxIdx = body.length ? 1 + body.indexOf(Math.max(...body)) : 0;
1313
+
1314
+ const peak2nd = (from: number, to: number) => {
1315
+ let best = -1, bestV = -Infinity;
1316
+ for (let i = from; i < to; i++) {
1317
+ const d2 = (series[i + 1] ?? 0) - 2 * series[i] + (series[i - 1] ?? 0);
1318
+ if (d2 > bestV) { bestV = d2; best = i; }
1319
+ }
1320
+ return best;
1321
+ };
1322
+ const mid = Math.floor(n / 2);
1323
+ const act1 = peak2nd(1, Math.max(2, mid));
1324
+ const dark = peak2nd(Math.max(2, mid), Math.max(3, n - 1));
1325
+
1326
+ return {
1327
+ inciting: norm(incitingIdx < 0 ? 0 : incitingIdx),
1328
+ act1: act1 >= 0 ? norm(act1) : undefined as unknown as number,
1329
+ midpoint: 0.5,
1330
+ darkTurn: dark >= 0 ? norm(dark) : undefined as unknown as number,
1331
+ climax: norm(climaxIdx),
1332
+ };
1333
+ }
1334
+
1335
+ /** §5.2 — Fibonacci lattice closeness. */
1336
+ export function fibonacciLattice(lengths: number[]): number {
1337
+ if (lengths.length < 3) return 0;
1338
+ const ratios: number[] = [];
1339
+ for (let i = 1; i < lengths.length; i++) {
1340
+ const a = lengths[i - 1], b = lengths[i];
1341
+ if (a > 0 && b > 0) ratios.push(Math.max(a, b) / Math.min(a, b));
1342
+ }
1343
+ if (!ratios.length) return 0;
1344
+ const phiDev = ratios.reduce((s, r) => s + Math.abs(Math.log(r / 1.618)), 0) / ratios.length;
1345
+ const fibTerms = [3, 5, 8, 13];
1346
+ const groupDev = lengths.reduce((s, L) => {
1347
+ const nearest = fibTerms.reduce((p, c) => (Math.abs(c - L) < Math.abs(p - L) ? c : p), fibTerms[0]);
1348
+ return s + Math.abs(L - nearest) / nearest;
1349
+ }, 0) / lengths.length;
1350
+ return clamp01(0.5 * (1 - phiDev) + 0.5 * (1 - Math.min(1, groupDev)));
1351
+ }
1352
+
1353
+ /** §5.4 — Birkhoff M = O/C over plan-derived order/complexity terms. */
1354
+ export function birkhoffMeasure(input: {
1355
+ motifRecurrenceRate: number; ringSymmetry: number; promisePayoffClosure: number; themeLockCoherence: number;
1356
+ activeCastLoad: number; openLoopCount: number; povSwitchRate: number; newNounRate: number;
1357
+ }): number {
1358
+ const O = 0.3 * input.motifRecurrenceRate + 0.25 * input.ringSymmetry +
1359
+ 0.25 * input.promisePayoffClosure + 0.2 * input.themeLockCoherence;
1360
+ const C = 0.3 * input.activeCastLoad + 0.25 * input.openLoopCount +
1361
+ 0.2 * input.povSwitchRate + 0.25 * input.newNounRate;
1362
+ if (C <= 1e-9) return 0;
1363
+ const M = O / C;
1364
+ return clamp01(M);
1365
+ }
1366
+
1367
+ /** §5.5 — Zipf cast harmony: ranked share vs 1/rank fit. */
1368
+ export function zipfCastHarmony(shares: number[]): number {
1369
+ const s = shares.filter((x) => x > 0).sort((a, b) => b - a);
1370
+ if (s.length < 3) return 0;
1371
+ const total = s.reduce((a, b) => a + b, 0) || 1;
1372
+ const ideal = s.map((_, i) => (1 / (i + 1)) / s.reduce((a, _, j) => a + 1 / (j + 1), 0));
1373
+ const actual = s.map((x) => x / total);
1374
+ const dev = actual.reduce((a, x, i) => a + Math.abs(x - ideal[i]), 0) / actual.length;
1375
+ return clamp01(1 - dev * 2.5);
1376
+ }
1377
+
1378
+ /** §5.6 — Novelty: mean rarity × (1 − trope-collision penalty). */
1379
+ export function noveltyScore(elements: ElementRecord[], topBandShare = 0.35): number {
1380
+ if (!elements.length) return 0;
1381
+ const meanRarity = elements.reduce((a, e) => a + e.rarity, 0) / elements.length;
1382
+ // collision penalty: over-representation of the single most common band
1383
+ const bandCounts = new Map<string, number>();
1384
+ for (const e of elements) bandCounts.set(e.band, (bandCounts.get(e.band) ?? 0) + 1);
1385
+ const maxShare = Math.max(...[...bandCounts.values()]) / elements.length;
1386
+ const penalty = clamp01((maxShare - topBandShare) / (1 - topBandShare));
1387
+ return clamp01(meanRarity * (1 - 0.5 * penalty));
1388
+ }
1389
+
1390
+ /** §5.7 — Human relatability: anchors, emotion coverage, speech acts. */
1391
+ export function relatabilityScore(input: {
1392
+ empathyAnchorDensity: number; emotionsPresent: string[]; speechActDensity: number;
1393
+ }): number {
1394
+ const UNIVERSAL = ["grief", "fear", "joy", "anger", "tenderness", "awe", "shame", "relief"];
1395
+ const coverage = UNIVERSAL.filter((e) => input.emotionsPresent.includes(e)).length;
1396
+ const covScore = clamp01(coverage / 5); // ≥5 of 8 required
1397
+ const anchor = clamp01(input.empathyAnchorDensity / 12); // 12 per 10k words = saturated
1398
+ const speech = clamp01(input.speechActDensity / 8);
1399
+ return clamp01(0.4 * covScore + 0.35 * anchor + 0.25 * speech);
1400
+ }
1401
+
1402
+ export type KallosInput = {
1403
+ profile: string;
1404
+ run: GridRun;
1405
+ /** normalized turn positions 0..1 if known; derived from the grid otherwise */
1406
+ turnPositions?: Record<string, number>;
1407
+ /** cast activity shares 0..1 per rowId */
1408
+ castShares?: Record<string, number>;
1409
+ emotionsPresent?: string[];
1410
+ empathyAnchorDensity?: number;
1411
+ speechActDensity?: number;
1412
+ };
1413
+
1414
+ /**
1415
+ * KALLOS composite — weighted GEOMETRIC mean. A zero in any pillar zeroes
1416
+ * beauty: no compensating a broken pillar (v2 §5.8).
1417
+ */
1418
+ export function computeKallos(input: KallosInput): KallosReport {
1419
+ const { run } = input;
1420
+ const profile = input.profile || run.spec.kallosProfile;
1421
+ const series = run.plan.chapters.map((c) => c.intensity);
1422
+ const nCh = run.plan.chapters.length;
1423
+ const lowN = nCh < 16;
1424
+
1425
+ // ── Φ placement: all five turns derived from the intensity curve when absent
1426
+ const derived = deriveTurnPositions(series);
1427
+ const tp: Record<string, number> = {};
1428
+ for (const [k, v] of Object.entries(derived)) {
1429
+ const user = input.turnPositions?.[k];
1430
+ tp[k] = user ?? v;
1431
+ }
1432
+ const phiScore = phiPlacement(tp);
1433
+
1434
+ // ── F Fibonacci
1435
+ const chapterLengths = run.plan.chapters.map((c) => c.scenes);
1436
+ let fibScore = fibonacciLattice(chapterLengths);
1437
+ let fibCapped = false;
1438
+ // ANTI-GAMING: perfectly equal chapters must not max O_fib
1439
+ const allEqual = chapterLengths.length > 1 && chapterLengths.every((L) => L === chapterLengths[0]);
1440
+ if (allEqual) { fibScore = Math.min(fibScore, 0.45); fibCapped = true; }
1441
+
1442
+ // ── S 1/f spectral
1443
+ const alpha = fitSpectralAlpha(series);
1444
+ let pinkScore = 0;
1445
+ if (alpha != null) {
1446
+ const sigma = 0.45;
1447
+ pinkScore = clamp01(Math.exp(-((alpha - 1) ** 2) / (2 * sigma * sigma)));
1448
+ }
1449
+
1450
+ // ── B Birkhoff
1451
+ const elements = run.plan.scenes.map((s) => s.element).filter((e): e is ElementRecord => !!e);
1452
+ const elIds = elements.map((e) => e.id);
1453
+ const distinctEl = new Set(elIds).size;
1454
+ const motifRecurrenceRate = clamp01(elIds.length ? 1 - distinctEl / elIds.length : 0);
1455
+ const firstRows = new Set(run.ticks[0]?.active.map((a) => a.rowId) ?? []);
1456
+ const lastRows = new Set(run.ticks[run.ticks.length - 1]?.active.map((a) => a.rowId) ?? []);
1457
+ const inter = [...firstRows].filter((r) => lastRows.has(r)).length;
1458
+ const union = new Set([...firstRows, ...lastRows]).size;
1459
+ const ringSymmetry = union ? clamp01(inter / union) : 0;
1460
+ const closures = run.plan.promiseOps.length;
1461
+ const promisePayoffClosure = clamp01(closures / Math.max(4, run.plan.totalScenes / 4));
1462
+ const themeSlots = new Set(elements.filter((e) => e.slotClass === "SC-M" || e.slotClass === "SC-G").map((e) => e.id));
1463
+ const themeLockCoherence = clamp01(themeSlots.size === 0 ? 0.3 : themeSlots.size <= 3 ? 0.9 : 0.6);
1464
+ const activeCastLoad = clamp01(run.rows.filter((r) => r.kind === "agent").length / 8);
1465
+ const unresolved = run.plan.promiseOps.filter((p) => p.op === "TRANSFERRED").length;
1466
+ const openLoopCount = clamp01(unresolved / Math.max(1, closures || 1));
1467
+ const povRows = new Set(run.plan.scenes.map((s) => s.rowId));
1468
+ const povSwitchRate = clamp01(povRows.size / Math.max(1, run.rows.length));
1469
+ const newNounRate = clamp01(distinctEl / Math.max(1, elIds.length));
1470
+ const birkhoff = birkhoffMeasure({
1471
+ motifRecurrenceRate, ringSymmetry, promisePayoffClosure, themeLockCoherence,
1472
+ activeCastLoad, openLoopCount, povSwitchRate, newNounRate,
1473
+ });
1474
+
1475
+ // ── Z Zipf cast harmony
1476
+ const sharesFromGrid: Record<string, number> = {};
1477
+ for (const s of run.plan.scenes) sharesFromGrid[s.rowId] = (sharesFromGrid[s.rowId] ?? 0) + 1;
1478
+ const castIds = run.rows.filter((r) => r.kind === "agent" || r.kind === "institution").map((r) => r.id);
1479
+ const shares = castIds.map((id) => (input.castShares?.[id] ?? sharesFromGrid[id] ?? 0));
1480
+ let zipfScore = zipfCastHarmony(shares);
1481
+ let zipfCapped = false;
1482
+ // ANTI-GAMING: deleting the minor cast must not improve the score
1483
+ const shareTotal = shares.reduce((a, b) => a + b, 0);
1484
+ if (shareTotal > 0 && shares.filter((x) => x > 0).length < 3) { zipfScore = Math.min(zipfScore, 0.4); zipfCapped = true; }
1485
+
1486
+ // ── N Novelty
1487
+ const novelty = noveltyScore(elements);
1488
+
1489
+ // ── H Relatability
1490
+ const relatability = relatabilityScore({
1491
+ empathyAnchorDensity: input.empathyAnchorDensity ?? elements.filter((e) => e.costAxis === "body" || e.costAxis === "bond").length * 1.5,
1492
+ emotionsPresent: input.emotionsPresent ?? ["grief", "fear", "anger", "tenderness", "relief"],
1493
+ speechActDensity: input.speechActDensity ?? elements.filter((e) => e.slotClass === "SC-R" || e.slotClass === "SC-T").length,
1494
+ });
1495
+
1496
+ // ── weights: genre profile projected onto the 7 computable pillars
1497
+ const w = kallosWeights(profile);
1498
+ const dimIndex: Record<string, number> = {};
1499
+ KALLOS_DIMENSIONS.forEach((d, i) => (dimIndex[d] = i));
1500
+ const pillarDefs: { key: string; label: string; score: number; dims: string[] }[] = [
1501
+ { key: "PHI", label: "Φ golden placement", score: phiScore, dims: ["GCT", "PAC"] },
1502
+ { key: "FIB", label: "Fibonacci lattice", score: fibScore, dims: ["PAC", "TRN"] },
1503
+ { key: "PINK", label: "1/f spectral pacing", score: pinkScore, dims: ["PAC", "INT"] },
1504
+ { key: "BIRK", label: "Birkhoff M = O/C", score: birkhoff, dims: ["COH", "THM"] },
1505
+ { key: "ZIPF", label: "Zipf cast harmony", score: zipfScore, dims: ["COH", "REL"] },
1506
+ { key: "NOV", label: "Novelty (rarity × anti-collision)", score: novelty, dims: ["NOV", "WRL"] },
1507
+ { key: "REL", label: "Human relatability", score: relatability, dims: ["REL", "EMO"] },
1508
+ ];
1509
+ // Unmeasured pillars (input window too small) are EXCLUDED from the composite
1510
+ // and reported as LOW-N, not scored zero: "insufficient data" is not evidence
1511
+ // of zero beauty, and conflating them zeroed every short-work composite
1512
+ // through the geometric mean (verified defect).
1513
+ const unmeasured = (key: string): boolean =>
1514
+ (key === "PINK" && alpha == null) ||
1515
+ (key === "FIB" && nCh < 3) ||
1516
+ // Φ needs enough resolution to place five distinct turns.
1517
+ (key === "PHI" && nCh < 6) ||
1518
+ (key === "ZIPF" && shares.filter((x) => x > 0).length < 3);
1519
+
1520
+ const pillars: KallosPillar[] = pillarDefs.map((p) => {
1521
+ const weight = p.dims.reduce((a, d) => a + (w[dimIndex[d]] ?? 0), 0) / p.dims.length;
1522
+ const capped = (p.key === "FIB" && fibCapped) || (p.key === "ZIPF" && zipfCapped);
1523
+ return {
1524
+ key: p.key, label: p.label, score: Math.round(p.score * 1000) / 1000,
1525
+ weight: Math.round(weight * 1000) / 1000,
1526
+ detail: detailFor(p.key, { phiScore, fibScore, pinkScore, birkhoff, zipfScore, novelty, relatability, alpha, tp, lowN }),
1527
+ capped,
1528
+ measured: !unmeasured(p.key),
1529
+ };
1530
+ });
1531
+
1532
+ const measuredPillars = pillars.filter((p) => p.measured);
1533
+ const wSum = measuredPillars.reduce((a, p) => a + p.weight, 0) || 1;
1534
+ // weighted geometric mean over MEASURED pillars: exp( Σ w_i · ln(score_i) / Σ w_i )
1535
+ const logSum = measuredPillars.reduce((a, p) => a + p.weight * Math.log(Math.max(p.score, 1e-6)), 0);
1536
+ const composite = measuredPillars.length
1537
+ ? Math.round(Math.exp(logSum / wSum) * 1000) / 10
1538
+ : 0;
1539
+ const weakest = measuredPillars.length
1540
+ ? measuredPillars.reduce((a, p) => (p.score < a.score ? p : a), measuredPillars[0])
1541
+ : null;
1542
+
1543
+ const antiGaming: string[] = [];
1544
+ if (fibCapped) antiGaming.push("O_fib capped at 0.45 — equal-length chapters cannot max the Fibonacci pillar.");
1545
+ if (zipfCapped) antiGaming.push("O_zipf capped at 0.40 — deleting the minor cast cannot raise Zipf harmony.");
1546
+ if (lowN) antiGaming.push(`α-fit window is ${nCh} chapters (<16) — pillar S marked LOW-N.`);
1547
+ const zeroPillars = measuredPillars.filter((p) => p.score <= 1e-6).map((p) => p.label);
1548
+ if (zeroPillars.length) antiGaming.push(`Geometric mean zeroed by: ${zeroPillars.join(", ")} (no compensating a broken pillar).`);
1549
+ const unmeasuredList = pillars.filter((p) => !p.measured).map((p) => p.label);
1550
+ if (unmeasuredList.length) antiGaming.push(`Excluded as unmeasured (LOW-N, weights renormalized): ${unmeasuredList.join(", ")}.`);
1551
+
1552
+ const passes = [
1553
+ { id: "K-AUD-001", pass: true, detail: "deterministic: all inputs derive from (seed, spec, plan)" },
1554
+ { id: "K-AUD-002", pass: antiGaming.filter((s) => /capped/.test(s)).length === 0, detail: antiGaming.filter((s) => /capped/.test(s)).join(" ") || "no anti-gaming cap engaged" },
1555
+ { id: "K-AUD-003", pass: true, detail: `genre weight table logged: ${profile} (${KALLOS_DIMENSIONS.join(",")})` },
1556
+ { id: "K-AUD-004", pass: !lowN, detail: lowN ? `α-fit window ${nCh} < 16 → LOW-N` : `α-fit window ${nCh} ≥ 16` },
1557
+ { id: "K-AUD-005", pass: true, detail: "geometric mean prevents pillar-masking" },
1558
+ { id: "K-AUD-006", pass: true, detail: "human-panel calibration hook available (offline); no panel run" },
1559
+ ];
1560
+
1561
+ return {
1562
+ pillars, composite, weakest: weakest ? { label: weakest.label, score: weakest.score } : null,
1563
+ profile, alphaFit: alpha, lowN, antiGaming,
1564
+ caveat:
1565
+ "KALLOS is a deterministic, falsifiable PROXY for aesthetic structure — not a proven measure of beauty. " +
1566
+ "φ/Fibonacci/1/f/Birkhoff/Zipf are prior mathematics; their transfer to narrative is the KALLOS design " +
1567
+ "hypothesis. Correlation ≠ causation. Human-panel calibration is available offline and has NOT been run here.",
1568
+ passes,
1569
+ };
1570
+ }
1571
+
1572
+ function detailFor(
1573
+ key: string,
1574
+ d: {
1575
+ phiScore: number; fibScore: number; pinkScore: number; birkhoff: number;
1576
+ zipfScore: number; novelty: number; relatability: number; alpha: number | null;
1577
+ tp: Record<string, number>; lowN: boolean;
1578
+ },
1579
+ ): string {
1580
+ switch (key) {
1581
+ case "PHI":
1582
+ return `targets inciting ${fmt3(d.tp.inciting)}/0.146 · act1 ${fmt3(d.tp.act1)}/0.382 · midpoint ${fmt3(d.tp.midpoint)}/0.5 · dark ${fmt3(d.tp.darkTurn)}/0.618 · climax ${fmt3(d.tp.climax)}/0.90 (±0.04)`;
1583
+ case "FIB":
1584
+ return `consecutive chapter scene-count ratios vs φ≈1.618 and groupings vs {3,5,8,13}`;
1585
+ case "PINK":
1586
+ return d.alpha == null
1587
+ ? "series too short for a spectral fit (needs ≥8 chapters)"
1588
+ : `spectral exponent α=${d.alpha.toFixed(3)} (1=f pink optimum; 0=white, 2=brown)${d.lowN ? " · LOW-N" : ""}`;
1589
+ case "BIRK":
1590
+ return `O = motif·ring·promise·theme over C = cast·loops·pov·nouns → M=${d.birkhoff.toFixed(3)}`;
1591
+ case "ZIPF":
1592
+ return `ranked cast/institution activity share fitted to 1/rank`;
1593
+ case "NOV":
1594
+ return `mean ElementRecord rarity × (1 − single-band collision penalty)`;
1595
+ case "REL":
1596
+ return `emotion coverage (≥5 of 8) + empathy-anchor density + speech-act density`;
1597
+ default:
1598
+ return "";
1599
+ }
1600
+ }
1601
+ function fmt3(x: number | undefined): string {
1602
+ return x == null ? "—" : x.toFixed(3);
1603
+ }
1604
+
1605
+ // ══ 5. MODAL-SHORTCUT TEST (v2 §7) ══════════════════════════════════════════
1606
+
1607
+ /**
1608
+ * Negative-space detector (v2 §7 modal-shortcut test). Strips every label,
1609
+ * name, band and genre term from a run, then asks whether the residual
1610
+ * structure reduces to (template + dice). Emergence requires that grid history
1611
+ * be NON-recoverable from GenreSpec alone.
1612
+ */
1613
+ export type ModalShortcutReport = {
1614
+ strippedGraph: {
1615
+ agents: number; intentions: number; worldStates: number; events: number;
1616
+ knowledgeStates: number; relationships: number; institutions: number;
1617
+ costs: number; promises: number; volumeContracts: number;
1618
+ };
1619
+ collapsesToTemplatePlusDice: boolean;
1620
+ reasons: string[];
1621
+ verdict: string;
1622
+ };
1623
+
1624
+ export function modalShortcutTest(run: GridRun): ModalShortcutReport {
1625
+ const agents = run.rows.filter((r) => r.kind === "agent").length;
1626
+ const institutions = run.rows.filter((r) => r.kind === "institution").length;
1627
+ const events = run.plan.totalScenes;
1628
+ const promises = run.plan.promiseOps.length;
1629
+ const relationships = run.rows.reduce((a, r) => a + r.relations.length, 0) / 2;
1630
+ const distinctElements = new Set(run.plan.scenes.map((s) => s.element?.id).filter(Boolean)).size;
1631
+ const distinctSlots = new Set(run.plan.scenes.map((s) => s.element?.slotClass).filter(Boolean)).size;
1632
+ const stateTransitions = new Set(run.stateHashes).size;
1633
+ const costs = new Set(run.plan.scenes.map((s) => s.cost)).size;
1634
+ const stakesDims = new Set(run.plan.scenes.map((s) => s.stakes)).size;
1635
+
1636
+ const strippedGraph = {
1637
+ agents, intentions: agents, worldStates: stateTransitions, events,
1638
+ knowledgeStates: distinctSlots, relationships, institutions,
1639
+ costs, promises, volumeContracts: run.spec.V,
1640
+ };
1641
+
1642
+ const reasons: string[] = [];
1643
+ let collapses = true;
1644
+
1645
+ if (distinctElements >= 6) { collapses = false; reasons.push(`${distinctElements} distinct catalog elements active — not a fixed template.`); }
1646
+ if (distinctSlots >= 4) { collapses = false; reasons.push(`${distinctSlots}/14 slot classes exercised — structural variety exceeds a single mould.`); }
1647
+ if (stakesDims >= 3) { collapses = false; reasons.push(`${stakesDims} stakes dimensions rotated — not monodimensional escalation.`); }
1648
+ if (relationships >= agents) { collapses = false; reasons.push(`${relationships} relation edges over ${agents} agents — relational graph is non-trivial.`); }
1649
+ if (stateTransitions >= 4) { collapses = false; reasons.push(`${stateTransitions} distinct world states — history is not recoverable from the spec alone.`); }
1650
+ if (promises >= 2) { collapses = false; reasons.push(`${promises} promise operations logged (PAID/EXPIRED/TRANSFERRED) — loop discipline present.`); }
1651
+ if (collapses) {
1652
+ reasons.unshift("Residual structure reduces to: hero wants X → obstacles increase → stronger obstacle defeated → larger obstacle teased.");
1653
+ }
1654
+
1655
+ return {
1656
+ strippedGraph,
1657
+ collapsesToTemplatePlusDice: collapses,
1658
+ reasons,
1659
+ verdict: collapses
1660
+ ? "REJECT — not SAGA-OS compliant (modal shortcut detected)."
1661
+ : "PASS — grid history is non-recoverable from GenreSpec alone; emergence present.",
1662
+ };
1663
+ }
1664
+
1665
+ // ══ 6. v1 integration — directive block contribution ════════════════════════
1666
+
1667
+ /**
1668
+ * The v2 block appended to the SAGE chapter prompt so v2 actually contributes
1669
+ * to the final output in the main output box (wired by the workspace engine
1670
+ * wrapper, not by editing the package).
1671
+ */
1672
+ export function buildSagaOsV2DirectiveBlock(run: GridRun, kallos: KallosReport): string {
1673
+ const stakeLine = run.plan.chapters
1674
+ .slice(0, 12)
1675
+ .map((c) => `ch${c.index}:${c.dominantStakes ?? "—"}`)
1676
+ .join(" · ");
1677
+ const activeElements = [...new Set(run.plan.scenes.map((s) => s.element?.name).filter(Boolean))].slice(0, 14);
1678
+ return [
1679
+ "SAGA-OS v2 COMPOSITION DISCIPLINE (apply silently; never name these rules):",
1680
+ `- Genre spec: ${run.spec.family}/${run.spec.leaf}; V=${run.spec.V}; geometry ${run.spec.geometry}; container ${run.spec.container}; tone gradient ${run.spec.toneGradient}.`,
1681
+ `- Genesis pattern: ${run.pattern} → ending family "${run.endingFamily}". ${run.patternDetail}.`,
1682
+ `- Cell rule ${run.ruleTable.id}: birth at neighbour-count B=${JSON.stringify(run.ruleTable.B)}, survival S=${JSON.stringify(run.ruleTable.S)} over the RELATION graph (not space). Underload (A<${Math.min(...run.ruleTable.S)}) closes a strand as EXPIRED or TRANSFERRED; overload (A>${Math.max(...run.ruleTable.S)}) forces the highest-cost element to resolve NOW.`,
1683
+ `- Stakes rotation — do not let one dimension dominate consecutive chapters: ${stakeLine}${run.plan.chapters.length > 12 ? " …" : ""}.`,
1684
+ `- Load-bearing catalog mechanisms to dramatize in-scene (not to name): ${activeElements.join("; ")}.`,
1685
+ `- Cost routing: every apparent success must charge a visible ${[...new Set(run.plan.scenes.map((s) => s.cost))].join("/")} cost to a named person, institution, or future.`,
1686
+ `- Promise discipline this run: ${run.plan.promiseOps.length} closures logged — ${run.plan.promiseOps.slice(0, 8).map((p) => `${p.op}@t${p.tick}`).join(", ")}${run.plan.promiseOps.length > 8 ? " …" : ""}. No orphan setups, no unset payoffs.`,
1687
+ `- KALLOS weakest pillar: ${kallos.weakest ? `${kallos.weakest.label} = ${kallos.weakest.score}` : "n/a"}. Write to strengthen it specifically${kallos.lowN ? " (spectral pillar marked LOW-N — insufficient chapters for a fit)" : ""}.`,
1688
+ `- ${run.spec.stakesRotation.length} stakes axes in rotation: ${run.spec.stakesRotation.join(" → ")}. Rotate; never escalate one axis monotonically.`,
1689
+ ].join("\n");
1690
+ }
1691
+
1692
+ // ══ 7. ONOMASTRATA — deterministic name generator ═══════════════════════════
1693
+
1694
+ /**
1695
+ * Deterministic seeded name synthesis per ONOMASTRATA class. No corpus, no
1696
+ * network: syllable inventories are closed lists and the selection is driven by
1697
+ * the seeded PRNG, so (seed, class, index) always yields the same name.
1698
+ * Confusability is guarded by `auditCastNameConfusion` from the package.
1699
+ */
1700
+ const ONOMA_INVENTORY: Record<string, { pre: string[]; mid: string[]; post: string[]; joiner: string[]; phrase?: boolean }> = {
1701
+ PERSON: {
1702
+ pre: ["Mar", "Kel", "Sor", "Av", "Ther", "Bel", "Cor", "Del", "Esh", "Fen", "Hal", "Ivor", "Jun", "Lir", "Mora", "Nev", "Or", "Pra", "Rha", "Sel", "Tor", "Ul", "Ver", "Wren", "Yar", "Zel"],
1703
+ mid: ["a", "e", "i", "o", "u", "ae", "ia", "ei", "ou", "y"],
1704
+ post: ["n", "th", "s", "r", "l", "nd", "ss", "rn", "ck", "ne", "ra", "va", "wen", "dric", "holt", "mere", "wick", "field"],
1705
+ joiner: [" ", "-", "'", ""],
1706
+ },
1707
+ DYNASTY: {
1708
+ pre: ["Ash", "Cold", "Green", "High", "Iron", "Long", "North", "Pale", "Red", "Stone", "Storm", "Sun", "White", "Wind", "Black", "Grey"],
1709
+ mid: ["", "", "", "a", "e", "o"],
1710
+ post: ["born", "ward", "haven", "reach", "fell", "mere", "crest", "hold", "watch", "gate", "ford", "bridge", "hall", "keep"],
1711
+ joiner: ["", "", "", "-"],
1712
+ },
1713
+ PLACE: {
1714
+ pre: ["Ash", "Bram", "Cald", "Dun", "Elm", "Fair", "Glass", "Hollow", "Kettle", "Lark", "Mill", "Nettle", "Ox", "Pine", "Quarry", "Reed", "Salt", "Thorn", "Under", "Vale", "Whet", "Yew"],
1715
+ mid: ["", "", "a", "e", "i", "o"],
1716
+ post: ["ford", "stead", "bury", "combe", "thwaite", "garth", "march", "wick", "holt", "bridge", "water", "field", "gate", "moor", "fen", "haven", "row", "end"],
1717
+ joiner: ["", "", "", "-"],
1718
+ },
1719
+ INSTITUTION: {
1720
+ pre: ["The Office of", "The Bureau of", "The Registry of", "The Commis\u00adsion of", "The Wardens of", "The Company of", "The Court of", "The Ledger of", "The Trust of", "The Board of"],
1721
+ mid: ["", "", "", ""],
1722
+ post: ["Water", "Waste", "Land", "Time", "Names", "Debts", "Roads", "Grain", "Ash", "Signals", "Custody", "Succession", "Repair", "Weather", "Bones"],
1723
+ joiner: [" "],
1724
+ phrase: true,
1725
+ },
1726
+ OFFICE: {
1727
+ pre: ["Chief", "First", "Second", "Senior", "Standing", "Acting", "Deputy", "Sole"],
1728
+ mid: ["", "", ""],
1729
+ post: ["Cartographer", "Custodian", "Adjudicator", "Assessor", "Interpreter", "Maintainer", "Witness", "Recorder", "Surveyor", "Auditor", "Predecessor", "Successor"],
1730
+ joiner: [" "],
1731
+ phrase: true,
1732
+ },
1733
+ ARTIFACT: {
1734
+ pre: ["The", "The", "The"],
1735
+ mid: ["", ""],
1736
+ post: ["Ledger", "Compass", "Register", "Almanac", "Almanac of Tides", "Chain of Weights", "Standard Measure", "Field Book", "Handbook", "Inventory", "Clock", "Key", "Seal", "Tally"],
1737
+ joiner: [" "],
1738
+ phrase: true,
1739
+ },
1740
+ DOCUMENT: {
1741
+ pre: ["The", "A", "The"],
1742
+ mid: ["", ""],
1743
+ post: ["Schedule", "Bond", "Warrant", "Census", "Deed", "Receipt", "Affidavit", "Survey", "Ledger Page", "Amendment", "Notice", "Summons", "Permit"],
1744
+ joiner: [" "],
1745
+ phrase: true,
1746
+ },
1747
+ LAW: {
1748
+ pre: ["The", "The", "The"],
1749
+ mid: ["", ""],
1750
+ post: ["Dry-Day Rule", "Succession Act", "Watershed Compact", "Counting Ordinance", "Custody Provision", "Maintenance Statute", "Priority Clause", "Residual Measure"],
1751
+ joiner: [" "],
1752
+ phrase: true,
1753
+ },
1754
+ DEFAULT: {
1755
+ pre: ["Aa", "Ba", "Ca", "Da", "Ea", "Fa", "Ga", "Ha"],
1756
+ mid: ["a", "e", "i", "o"],
1757
+ post: ["ra", "na", "sa", "ta", "va", "la"],
1758
+ joiner: [""],
1759
+ },
1760
+ };
1761
+
1762
+ function titleCase(s: string): string {
1763
+ return s.replace(/\b([a-z])/g, (m) => m.toUpperCase()).replace(/\u00ad/g, "");
1764
+ }
1765
+
1766
+ export type OnomaName = { name: string; nameClass: string; index: number };
1767
+
1768
+ /** Deterministic ONOMASTRATA name generation. */
1769
+ export function generateOnoma(
1770
+ seed: number, nameClass: string, count: number,
1771
+ ): OnomaName[] {
1772
+ const inv = ONOMA_INVENTORY[nameClass] ?? ONOMA_INVENTORY.DEFAULT;
1773
+ const rng = mulberry32((seed ^ fnv1a(nameClass)) >>> 0);
1774
+ const out: OnomaName[] = [];
1775
+ const used = new Set<string>();
1776
+ let guard = 0;
1777
+ while (out.length < count && guard++ < count * 40) {
1778
+ const pre = inv.pre[Math.floor(rng() * inv.pre.length)];
1779
+ const mid = rng() > 0.55 ? inv.mid[Math.floor(rng() * inv.mid.length)] : "";
1780
+ let post = inv.post[Math.floor(rng() * inv.post.length)];
1781
+ // A bare single-letter tail ("Bel R") is not a usable name — redraw long.
1782
+ if (post.length < 3) {
1783
+ const longs = inv.post.filter((x) => x.length >= 3);
1784
+ if (longs.length) post = longs[Math.floor(rng() * longs.length)];
1785
+ }
1786
+ // PHRASE classes (INSTITUTION / OFFICE / ARTIFACT / DOCUMENT / LAW) carry a
1787
+ // pre-formed, already-cased phrase and MUST keep their joiner and their
1788
+ // original casing. Concatenating them and title-casing produced
1789
+ // "The Company OfAsh" / "The CommisSion OfNames" (verified defect).
1790
+ const isPhrase = inv.phrase === true;
1791
+ let name: string;
1792
+ if (isPhrase) {
1793
+ name = `${pre}${inv.joiner[0] ?? " "}${post}`.replace(/\u00ad/g, "").replace(/\s+/g, " ").trim();
1794
+ } else {
1795
+ const joined = mid ? pre + mid + post : pre + post;
1796
+ const raw = joined.replace(/([a-z])\1{2,}/g, "$1$1");
1797
+ name = titleCase(raw).replace(/\s+/g, " ").trim();
1798
+ }
1799
+ if (!name || used.has(name) || name.length < 3) continue;
1800
+ used.add(name);
1801
+ out.push({ name, nameClass, index: out.length });
1802
+ }
1803
+ return out;
1804
+ }
1805
+
1806
+ /** All ONOMASTRATA classes the generator supports (superset checkable in UI). */
1807
+ export const ONOMA_GENERATABLE_CLASSES = Object.keys(ONOMA_INVENTORY).filter((k) => k !== "DEFAULT");
1808
+
1809
+ // ══ 8. World map + relationship graph extracts (for UI) ═════════════════════
1810
+
1811
+ export type WorldMapPlace = {
1812
+ settingId: string;
1813
+ label: string;
1814
+ residents: { id: string; kind: EntityKind; label: string; live: boolean }[];
1815
+ adjacentSettings: string[];
1816
+ };
1817
+
1818
+ export type RelationEdge = { a: string; b: string; kind: "peer" | "strand" | "co-located" };
1819
+
1820
+ /** Deterministic place names for the run's settings (world-map view). */
1821
+ export function buildWorldMap(run: GridRun): WorldMapPlace[] {
1822
+ const live = new Set(run.ticks[run.ticks.length - 1].active.map((a) => a.rowId));
1823
+ const onoma = generateOnoma(run.seed, "PLACE", run.rows.filter((r) => r.kind === "setting").length);
1824
+ return run.rows
1825
+ .filter((r) => r.kind === "setting")
1826
+ .map((s, i) => {
1827
+ const residents = run.rows
1828
+ .filter((r) => r.kind !== "setting" && r.settings.includes(s.id))
1829
+ .map((r) => ({ id: r.id, kind: r.kind, label: r.label, live: live.has(r.id) }));
1830
+ const adjacentSettings = run.rows
1831
+ .filter((o) => o.kind === "setting" && o.id !== s.id)
1832
+ .filter((o) => residents.some((r) => run.rows.find((x) => x.id === r.id)?.settings.includes(o.id)))
1833
+ .map((o) => o.id);
1834
+ return { settingId: s.id, label: onoma[i]?.name ?? s.label, residents, adjacentSettings };
1835
+ });
1836
+ }
1837
+
1838
+ /** Deterministic person names + the relation graph (relationship-system view). */
1839
+ export function buildCastGraph(run: GridRun): {
1840
+ cast: { id: string; label: string; name: string; kind: EntityKind; live: boolean; degree: number }[];
1841
+ edges: RelationEdge[];
1842
+ } {
1843
+ const live = new Set(run.ticks[run.ticks.length - 1].active.map((a) => a.rowId));
1844
+ const people = run.rows.filter((r) => r.kind === "agent" || r.kind === "institution");
1845
+ const names = generateOnoma(run.seed, "PERSON", run.rows.filter((r) => r.kind === "agent").length);
1846
+ const instNames = generateOnoma(run.seed, "INSTITUTION", run.rows.filter((r) => r.kind === "institution").length);
1847
+ let ai = 0, ii = 0;
1848
+ const cast = people.map((r) => {
1849
+ const name = r.kind === "agent" ? names[ai++]?.name ?? r.label : instNames[ii++]?.name ?? r.label;
1850
+ return { id: r.id, label: r.label, name, kind: r.kind, live: live.has(r.id), degree: r.relations.length };
1851
+ });
1852
+ const byId = new Map(run.rows.map((r) => [r.id, r]));
1853
+ const edges: RelationEdge[] = [];
1854
+ const seen = new Set<string>();
1855
+ for (const r of run.rows) {
1856
+ for (const other of r.relations) {
1857
+ const key = [r.id, other].sort().join("~");
1858
+ if (seen.has(key)) continue;
1859
+ seen.add(key);
1860
+ const o = byId.get(other);
1861
+ if (!o) continue;
1862
+ const bothPeople = r.kind !== "setting" && o.kind !== "setting";
1863
+ edges.push({
1864
+ a: r.id, b: other,
1865
+ kind: r.kind === "strand" || o.kind === "strand" ? "strand" : bothPeople ? "peer" : "co-located",
1866
+ });
1867
+ }
1868
+ }
1869
+ return { cast, edges };
1870
+ }