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,1491 @@
1
+ /**
2
+ * SAGA-OS v3 CORE — SYSTEM v14 (fable.canon)
3
+ * ---------------------------------------------------------------------------
4
+ * Type C net-new. Real, deterministic, offline implementations of the v3
5
+ * subsystems that the package (`saga-os-v2.ts`) carries only as tables and
6
+ * that its own `SAGA_OS_V3_UNEXECUTED` ledger declares "design only":
7
+ *
8
+ * §6 CATALOG-COMPILER per-row CompiledElement manifest, FAIL-CLOSED,
9
+ * WIRE-COVERAGE gate, §6.4 deterministic selection
10
+ * §7 GENRE HELM v3 human-readable BuildRequest → GenreContract,
11
+ * genre blending (Σ mix = 1), forbidden failures
12
+ * §10 ONOMASTRATA v3 NameCultureProfile phonotactics, WFST-style
13
+ * syllable generation, sound-change layers, CLDR
14
+ * field ordering, UTS#39-style confusable skeleton,
15
+ * full §10.4 six-rule cast-confusion audit,
16
+ * NameRegistry reservation, hybrid-contact rule
17
+ * §11 MORPHOGENESIS-LATTICE v2 SHA-256 SeedGenome, counter-based PRNG,
18
+ * five typed lattices, NarrativeCell, K0-ranked
19
+ * legal births, audit masks, attractor targets,
20
+ * content-addressed checkpoints, any-length modes
21
+ *
22
+ * RUNTIME HONESTY
23
+ * - Pure functions. No LLM, no network, no filesystem, no clock inside math.
24
+ * - SHA-256 is a real implementation (FIPS 180-4), verified against Node's
25
+ * `crypto` in the workspace harness — not the package's `simpleHash`.
26
+ * - The catalog is 947 rows (Empty-Space v5–v7 as shipped in the package),
27
+ * not "3,000+". The compiler is total over what exists; the number is stated.
28
+ * - Naming profiles are INVENTED phonologies with declared rules. They are
29
+ * not real-world cultures and are never presented as such (v3 §10.5).
30
+ */
31
+
32
+ import {
33
+ wireCatalog, buildRows, detectPattern, stateHash, fnv1a,
34
+ SLOT_CLASS_IDS, SLOT_PRECONDITIONS, SLOT_EFFECTS, PATTERN_ENDING,
35
+ type SlotClassId, type CostAxis, type StakesAxis,
36
+ type GenreSpec, type EntityRow, type EntityKind, type CellState, type GridTick,
37
+ type GenesisPattern,
38
+ } from "./saga-os-v2-engines";
39
+ import { GENRE_HELM, KALLOS_BASE_WEIGHTS, KALLOS_DIMENSIONS } from '@/lib/sage/saga-os-v2.ts';
40
+
41
+ export const SAGA_OS_V3_CORE_VERSION = "SAGA-OS v3 core (SYSTEM v14) — CATALOG-COMPILER · GENRE HELM v3 · ONOMASTRATA v3 · MORPHOGENESIS-LATTICE v2";
42
+ export const CATALOG_VERSION = "empty-space v5+v6+v7 (package 2sagaco48 1.0.0)";
43
+ export const RULE_TABLE_VERSION = "CA-rules v2 (B/S per genre) + morph v2 masks";
44
+ export const GRAMMAR_PACK_VERSION = "controlled-en v1 (feature-unification, restricted)";
45
+
46
+ // ═══════════════════════════════════════════════════════════════════════════
47
+ // 0. SHA-256 (FIPS 180-4), synchronous, UTF-8 input → lowercase hex
48
+ // ═══════════════════════════════════════════════════════════════════════════
49
+
50
+ const K256 = new Uint32Array([
51
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
52
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
53
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
54
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
55
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
56
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
57
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
58
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
59
+ ]);
60
+
61
+ function utf8Bytes(s: string): Uint8Array {
62
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s);
63
+ const out: number[] = [];
64
+ for (let i = 0; i < s.length; i++) {
65
+ let c = s.charCodeAt(i);
66
+ if (c < 0x80) out.push(c);
67
+ else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
68
+ else if (c >= 0xd800 && c < 0xdc00 && i + 1 < s.length) {
69
+ const d = s.charCodeAt(++i);
70
+ c = 0x10000 + ((c - 0xd800) << 10) + (d - 0xdc00);
71
+ out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
72
+ } else out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
73
+ }
74
+ return Uint8Array.from(out);
75
+ }
76
+
77
+ export function sha256Hex(input: string): string {
78
+ const msg = utf8Bytes(input);
79
+ const bitLen = msg.length * 8;
80
+ const padLen = ((msg.length + 9 + 63) >> 6) << 6;
81
+ const buf = new Uint8Array(padLen);
82
+ buf.set(msg);
83
+ buf[msg.length] = 0x80;
84
+ const dv = new DataView(buf.buffer);
85
+ dv.setUint32(padLen - 8, Math.floor(bitLen / 0x100000000), false);
86
+ dv.setUint32(padLen - 4, bitLen >>> 0, false);
87
+
88
+ let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a,
89
+ h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
90
+ const w = new Uint32Array(64);
91
+ const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n));
92
+
93
+ for (let off = 0; off < padLen; off += 64) {
94
+ for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4, false);
95
+ for (let i = 16; i < 64; i++) {
96
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
97
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
98
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
99
+ }
100
+ let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
101
+ for (let i = 0; i < 64; i++) {
102
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
103
+ const ch = (e & f) ^ (~e & g);
104
+ const t1 = (h + S1 + ch + K256[i] + w[i]) >>> 0;
105
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
106
+ const maj = (a & b) ^ (a & c) ^ (b & c);
107
+ const t2 = (S0 + maj) >>> 0;
108
+ h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
109
+ }
110
+ h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0;
111
+ h4 = (h4 + e) >>> 0; h5 = (h5 + f) >>> 0; h6 = (h6 + g) >>> 0; h7 = (h7 + h) >>> 0;
112
+ }
113
+ return [h0, h1, h2, h3, h4, h5, h6, h7].map((x) => x.toString(16).padStart(8, "0")).join("");
114
+ }
115
+
116
+ /** Counter-based PRNG derived from a SeedGenome (v3 §11.1). No state beyond the counter. */
117
+ export class GenomeRng {
118
+ private readonly k0: number;
119
+ private readonly k1: number;
120
+ private c = 0;
121
+ constructor(genomeHex: string) {
122
+ this.k0 = parseInt(genomeHex.slice(0, 8), 16) >>> 0;
123
+ this.k1 = parseInt(genomeHex.slice(8, 16), 16) >>> 0;
124
+ }
125
+ private static fmix(h: number): number {
126
+ h ^= h >>> 16; h = Math.imul(h, 0x85ebca6b);
127
+ h ^= h >>> 13; h = Math.imul(h, 0xc2b2ae35);
128
+ h ^= h >>> 16; return h >>> 0;
129
+ }
130
+ /** value at an explicit counter — order-independent, thread-safe by construction */
131
+ at(counter: number): number {
132
+ const a = GenomeRng.fmix((this.k0 ^ Math.imul(counter >>> 0, 0x9e3779b9)) >>> 0);
133
+ const b = GenomeRng.fmix((this.k1 + (counter >>> 0)) >>> 0);
134
+ return ((a ^ b) >>> 0) / 4294967296;
135
+ }
136
+ next(): number { this.c = (this.c + 1) >>> 0; return this.at(this.c); }
137
+ get counter(): number { return this.c; }
138
+ pick<T>(arr: readonly T[]): T { return arr[Math.floor(this.next() * arr.length)]; }
139
+ }
140
+
141
+ // ═══════════════════════════════════════════════════════════════════════════
142
+ // 1. CATALOG-COMPILER (§6)
143
+ // ═══════════════════════════════════════════════════════════════════════════
144
+
145
+ export type WorldLayer = "W0" | "W1" | "W2" | "W3" | "W4" | "W5" | "W6" | "W7" | "W8" | "W9";
146
+ export type SceneGrammarId =
147
+ | "ACTION" | "PROCESSING" | "INVESTIGATION" | "INSTITUTIONAL" | "MAINTENANCE"
148
+ | "RITUAL" | "TRAVEL" | "RELATIONAL" | "ENSEMBLE" | "AFTERMATH";
149
+ export type ScreenChannel =
150
+ | "VISIBLE_ACTION" | "DIALOGUE" | "SUBTEXT" | "OBJECT_OR_PROP" | "VISUAL_MOTIF"
151
+ | "SOUND" | "REACTION_SHOT" | "VOICE_OVER" | "DREAM_OR_MEMORY" | "OMISSION_WITH_APPROVED_REASON";
152
+ export type AudienceSegment = "adult" | "ya" | "children" | "literary" | "commercial";
153
+ export type NameClass =
154
+ | "PERSON" | "DYNASTY" | "HOUSEHOLD" | "PLACE" | "REGION" | "INSTITUTION" | "OFFICE"
155
+ | "ARTIFACT" | "WEAPON" | "VEHICLE" | "SPECIES" | "EVENT" | "RITUAL" | "LAW"
156
+ | "DOCUMENT" | "WORK_TITLE" | "VOLUME_TITLE" | "CHAPTER_TITLE";
157
+
158
+ export type CompiledElement = {
159
+ id: string;
160
+ sourceVersion: string;
161
+ sourceHash: string;
162
+ primarySlot: SlotClassId;
163
+ secondarySlots: SlotClassId[];
164
+ semanticDomain: string[];
165
+ preconditions: string[];
166
+ effects: string[];
167
+ conflictsWith: string[];
168
+ enables: string[];
169
+ worldLayers: WorldLayer[];
170
+ costAxes: CostAxis[];
171
+ stakesAxes: StakesAxis[];
172
+ sceneGrammars: SceneGrammarId[];
173
+ genreAffinity: Record<string, number>;
174
+ audienceAffinity: Record<AudienceSegment, number>;
175
+ nameRequirements: NameClass[];
176
+ realizationFrames: string[];
177
+ screenplayChannels: ScreenChannel[];
178
+ noveltyPrior: number;
179
+ playability: number;
180
+ harnessRequired: boolean;
181
+ provenanceStatus: "attested-clean" | "adjacent-differentiated" | "uncertain" | "band-inherited";
182
+ rowHash: string;
183
+ /** carried for prompt/UI use */
184
+ name: string;
185
+ desc: string;
186
+ band: string;
187
+ };
188
+
189
+ export type CatalogManifest = {
190
+ catalogVersion: string;
191
+ canonicalRows: number;
192
+ compiledRows: number;
193
+ failedClosed: { id: string; reason: string }[];
194
+ duplicateIdCount: number;
195
+ unresolvedSlotCount: number;
196
+ missingRealizationFrameCount: number;
197
+ coverage: number; // compiled / canonical
198
+ manifestHash: string; // SHA-256 over sorted row hashes
199
+ releaseOk: boolean; // §6.3 all four conditions
200
+ elements: CompiledElement[];
201
+ byId: Map<string, CompiledElement>;
202
+ };
203
+
204
+ const DOMAIN_RULES: [RegExp, string][] = [
205
+ [/\bwater|river|flood|tide|well|rain|drought|irrigat/i, "water"],
206
+ [/\bwaste|ash|landfill|sewage|garbage|refuse|rot\b/i, "waste"],
207
+ [/\bland|soil|field|farm|crop|harvest|acre|border/i, "land"],
208
+ [/\btime|clock|calendar|season|schedule|deadline|hour/i, "time"],
209
+ [/\blaw|court|rule|statute|verdict|jurisdiction|legal/i, "law"],
210
+ [/\bmemory|remember|forget|archive|record|ledger|receipt/i, "memory"],
211
+ [/\bfamily|kin|heir|inherit|generation|child|parent|lineage/i, "family"],
212
+ [/\blabou?r|work|repair|maintain|shift|wage|job/i, "labor"],
213
+ [/\bfaith|ritual|rite|sacred|temple|prayer|saint/i, "faith"],
214
+ [/\bwar|siege|soldier|weapon|battle|militia/i, "war"],
215
+ [/\btrade|market|price|debt|money|contract|bond|invoice/i, "trade"],
216
+ [/\bhealth|hospital|disease|medic|body|wound|illness/i, "health"],
217
+ [/\bmachine|engine|technolog|signal|system|network|device/i, "machine"],
218
+ [/\banimal|herd|beast|species|insect|bird|creature/i, "animal"],
219
+ [/\bweather|storm|wind|frost|heat|climate/i, "weather"],
220
+ [/\bname|title|office|rank|status|honou?r/i, "status"],
221
+ ];
222
+
223
+ const LAYER_RULES: [RegExp, WorldLayer][] = [
224
+ [/physics|cosmolog|immutable|law of story|impossible/i, "W0"],
225
+ [/\blaw|statute|institution|contract|ritual|rule/i, "W1"],
226
+ [/money|water|food|energy|labou?r|legitimacy|attention|debt/i, "W2"],
227
+ [/place|route|border|jurisdiction|travel|map|distance/i, "W3"],
228
+ [/season|ecology|maintenance|weather|species|infrastructure|waste/i, "W4"],
229
+ [/kinship|taboo|language|grief|status|fashion|education/i, "W5"],
230
+ [/official|record|archive|claim/i, "W6"],
231
+ [/actually happened|witness|testimony|lived/i, "W7"],
232
+ [/rumou?r|belief|propaganda|myth|false/i, "W8"],
233
+ [/secret|hidden|conceal|unknown/i, "W9"],
234
+ ];
235
+
236
+ const SLOT_GRAMMARS: Record<SlotClassId, SceneGrammarId[]> = {
237
+ "SC-A": ["ACTION", "RELATIONAL"], "SC-I": ["INSTITUTIONAL", "MAINTENANCE"], "SC-S": ["TRAVEL", "MAINTENANCE"],
238
+ "SC-T": ["ACTION", "PROCESSING"], "SC-B": ["PROCESSING", "ACTION"], "SC-L": ["RITUAL", "INSTITUTIONAL"],
239
+ "SC-R": ["RELATIONAL", "PROCESSING"], "SC-E": ["AFTERMATH"], "SC-G": ["ENSEMBLE", "ACTION"],
240
+ "SC-M": ["MAINTENANCE", "RITUAL"], "SC-N": ["PROCESSING"], "SC-W": ["INVESTIGATION"],
241
+ "SC-D": ["INVESTIGATION", "INSTITUTIONAL"], "SC-P": ["ACTION"],
242
+ };
243
+ const SLOT_FRAMES: Record<SlotClassId, string[]> = {
244
+ "SC-A": ["AGENT_ACTS_UNDER_CONSTRAINT", "AGENT_PAYS_COST"],
245
+ "SC-I": ["INSTITUTION_RULES", "INSTITUTION_EXACTS_OBLIGATION"],
246
+ "SC-S": ["PLACE_CONSTRAINS_ACTION", "PLACE_TRANSFORMS"],
247
+ "SC-T": ["DECISION_FORCED", "ALTERNATIVES_CLOSED"],
248
+ "SC-B": ["BEAT_TURN", "REACTION_THEN_DECISION"],
249
+ "SC-L": ["LAW_APPLIED_AT_EDGE", "EXCEPTION_PRICED"],
250
+ "SC-R": ["BOND_BID_AND_RESPONSE", "BOND_SHIFTS"],
251
+ "SC-E": ["CLOSURE_NAMES_RESIDUE", "SUCCESSOR_NAMED"],
252
+ "SC-G": ["MACRO_DRIVE_ESCALATES", "STRANDS_CONVERGE"],
253
+ "SC-M": ["MOTIF_RECURS_ALTERED", "THEME_LOCKS"],
254
+ "SC-N": ["FOCALIZATION_SHIFTS", "KNOWLEDGE_WITHHELD"],
255
+ "SC-W": ["REVEAL_REPRICES", "EVIDENCE_SURFACES"],
256
+ "SC-D": ["RECORD_CONSULTED", "CUSTODY_TRANSFERS"],
257
+ "SC-P": ["BRANCH_EXECUTED"],
258
+ };
259
+ const SLOT_CHANNELS: Record<SlotClassId, ScreenChannel[]> = {
260
+ "SC-A": ["VISIBLE_ACTION", "DIALOGUE", "REACTION_SHOT"], "SC-I": ["DIALOGUE", "OBJECT_OR_PROP", "VISIBLE_ACTION"],
261
+ "SC-S": ["VISUAL_MOTIF", "SOUND", "VISIBLE_ACTION"], "SC-T": ["VISIBLE_ACTION", "DIALOGUE"],
262
+ "SC-B": ["REACTION_SHOT", "SUBTEXT"], "SC-L": ["VISIBLE_ACTION", "OBJECT_OR_PROP"],
263
+ "SC-R": ["SUBTEXT", "REACTION_SHOT", "DIALOGUE"], "SC-E": ["VISUAL_MOTIF", "VISIBLE_ACTION"],
264
+ "SC-G": ["VISIBLE_ACTION", "SOUND"], "SC-M": ["VISUAL_MOTIF", "OBJECT_OR_PROP"],
265
+ "SC-N": ["SUBTEXT", "VOICE_OVER"], "SC-W": ["OBJECT_OR_PROP", "DIALOGUE", "REACTION_SHOT"],
266
+ "SC-D": ["OBJECT_OR_PROP", "DIALOGUE"], "SC-P": ["VISIBLE_ACTION"],
267
+ };
268
+ const SLOT_NAMES: Record<SlotClassId, NameClass[]> = {
269
+ "SC-A": ["PERSON"], "SC-I": ["INSTITUTION", "OFFICE"], "SC-S": ["PLACE", "REGION"], "SC-T": [],
270
+ "SC-B": [], "SC-L": ["LAW"], "SC-R": ["PERSON"], "SC-E": [], "SC-G": [], "SC-M": ["RITUAL"],
271
+ "SC-N": ["PERSON"], "SC-W": ["DOCUMENT", "ARTIFACT"], "SC-D": ["DOCUMENT"], "SC-P": ["EVENT"],
272
+ };
273
+
274
+ function audienceAffinityFor(tag: string, slot: SlotClassId, rarity: number): Record<AudienceSegment, number> {
275
+ const play = /\[play:H\]/i.test(tag) ? 0.9 : /\[play:M\]/i.test(tag) ? 0.6 : /\[play:L\]/i.test(tag) ? 0.3 : 0.5;
276
+ const literary = /\[adj:|\[rare\]/i.test(tag) ? 0.85 : 0.5 + rarity * 0.3;
277
+ return {
278
+ adult: 0.7,
279
+ ya: Math.min(1, 0.3 + play * 0.5),
280
+ children: Math.min(1, play * 0.6 * (slot === "SC-L" ? 0.6 : 1)),
281
+ literary: Math.min(1, literary),
282
+ commercial: Math.min(1, 0.4 + play * 0.4 + (slot === "SC-T" || slot === "SC-W" ? 0.2 : 0)),
283
+ };
284
+ }
285
+
286
+ let _manifest: CatalogManifest | null = null;
287
+
288
+ /**
289
+ * §6.2 — compile every catalog row into a CompiledElement. FAIL CLOSED: any
290
+ * row with an unresolved mandatory field is excluded and listed, so coverage
291
+ * can only be 1.0 when every row is genuinely materialized (§6.3).
292
+ */
293
+ export function compileCatalog(): CatalogManifest {
294
+ if (_manifest) return _manifest;
295
+ const wired = wireCatalog();
296
+ const nameIndex = wired.map((r) => ({ id: r.id, key: r.name.toLowerCase() }));
297
+ const seen = new Map<string, number>();
298
+ const failed: { id: string; reason: string }[] = [];
299
+ const elements: CompiledElement[] = [];
300
+ let unresolvedSlot = 0, missingFrames = 0;
301
+
302
+ for (const r of wired) {
303
+ seen.set(r.id, (seen.get(r.id) ?? 0) + 1);
304
+ const text = `${r.name} ${r.desc} ${r.tag}`;
305
+ const semanticDomain = DOMAIN_RULES.filter(([re]) => re.test(text)).map(([, d]) => d);
306
+ const worldLayers = [...new Set<WorldLayer>(LAYER_RULES.filter(([re]) => re.test(text)).map(([, l]) => l))];
307
+ if (!worldLayers.length) worldLayers.push(r.slotClass === "SC-S" ? "W3" : r.slotClass === "SC-I" ? "W1" : r.slotClass === "SC-L" ? "W0" : "W5");
308
+
309
+ // preconditions: slot-level + mechanism keywords
310
+ const pre = [...SLOT_PRECONDITIONS[r.slotClass].map((s) => `live:${s}`)];
311
+ if (/inherit|heir|succession/i.test(text)) pre.push("live:SC-D", "state:lineage-declared");
312
+ if (/verdict|ruling|court/i.test(text)) pre.push("live:SC-I");
313
+ if (/reveal|secret|discover/i.test(text)) pre.push("state:knowledge-asymmetry");
314
+ if (!pre.length) pre.push(r.slotClass === "SC-S" ? "context:ROOT_SETTING" : "context:BAND_DEFAULT");
315
+
316
+ const effects = [...SLOT_EFFECTS[r.slotClass]];
317
+ if (/debt|owe|price|cost/i.test(text)) effects.push("cost:debt+");
318
+ if (/repair|maintain/i.test(text)) effects.push("world:W4-drift-");
319
+ if (/split|fork|divide/i.test(text)) effects.push("relation:fork");
320
+
321
+ // conflicts: parse the catalog's own "≠ X" differentiators against row names
322
+ const conflicts: string[] = [];
323
+ const neq = [...r.tag.matchAll(/≠\s*([^;|\]\n]+)/g)].map((m) => m[1].trim().toLowerCase());
324
+ for (const phrase of neq) {
325
+ const hit = nameIndex.find((n) => n.id !== r.id && (n.key.includes(phrase) || phrase.includes(n.key)));
326
+ if (hit) conflicts.push(hit.id);
327
+ }
328
+ if (r.slotClass === "SC-L") conflicts.push("slot:SC-L(foreground>1)");
329
+
330
+ const enables: string[] = [];
331
+ if (/succession|handoff|successor/i.test(text)) enables.push("ending:Handoff", "ending:Successor");
332
+ if (/audit|ledger|record|archive/i.test(text)) enables.push("slot:SC-D");
333
+ if (/converge|shared|compact|treaty/i.test(text)) enables.push("grammar:ENSEMBLE");
334
+ if (/reveal|evidence|clue/i.test(text)) enables.push("slot:SC-W");
335
+ if (!enables.length) enables.push(`slot:${r.slotClass}`);
336
+
337
+ const stakesAxes: StakesAxis[] = [r.stakesAxis];
338
+ if (/moral|guilt|shame/i.test(text) && !stakesAxes.includes("moral")) stakesAxes.push("moral");
339
+ if (/institution|office|bureau/i.test(text) && !stakesAxes.includes("institutional")) stakesAxes.push("institutional");
340
+ const costAxes: CostAxis[] = [r.costAxis];
341
+ if (/body|wound|illness/i.test(text) && !costAxes.includes("body")) costAxes.push("body");
342
+ if (/debt|price/i.test(text) && !costAxes.includes("debt")) costAxes.push("debt");
343
+
344
+ const sceneGrammars = [...SLOT_GRAMMARS[r.slotClass]];
345
+ if (/investigat|evidence|search/i.test(text) && !sceneGrammars.includes("INVESTIGATION")) sceneGrammars.push("INVESTIGATION");
346
+ if (/aftermath|grief|residue/i.test(text) && !sceneGrammars.includes("AFTERMATH")) sceneGrammars.push("AFTERMATH");
347
+
348
+ const frames = [...SLOT_FRAMES[r.slotClass]];
349
+ const provenance: CompiledElement["provenanceStatus"] =
350
+ /\[clean\]/i.test(r.tag) ? "attested-clean" : /\[adj:/i.test(r.tag) ? "adjacent-differentiated"
351
+ : /\[unc\]|\[assum\]/i.test(r.tag) ? "uncertain" : "band-inherited";
352
+
353
+ // ── fail-closed gate (§6.2 "require at least")
354
+ const reasons: string[] = [];
355
+ if (!r.slotClass) reasons.push("no primary slot");
356
+ if (!pre.length) reasons.push("no precondition/enabling context");
357
+ if (!effects.length && !costAxes.length) reasons.push("no effect or cost");
358
+ if (!sceneGrammars.length) reasons.push("no scene grammar");
359
+ if (!Object.keys(r.genreAffinity).length) reasons.push("no genre affinity");
360
+ if (!frames.length) { reasons.push("no realization frame"); missingFrames++; }
361
+ if (!r.slotClass) unresolvedSlot++;
362
+ if (reasons.length) { failed.push({ id: r.id, reason: reasons.join("; ") }); continue; }
363
+
364
+ const sourceHash = sha256Hex(`${r.id}|${r.name}|${r.desc}|${r.tag}`).slice(0, 16);
365
+ const el: CompiledElement = {
366
+ id: r.id, sourceVersion: CATALOG_VERSION, sourceHash,
367
+ primarySlot: r.slotClass, secondarySlots: r.secondaryClasses, semanticDomain,
368
+ preconditions: pre, effects, conflictsWith: conflicts, enables, worldLayers,
369
+ costAxes, stakesAxes, sceneGrammars,
370
+ genreAffinity: r.genreAffinity,
371
+ audienceAffinity: audienceAffinityFor(r.tag, r.slotClass, r.rarity),
372
+ nameRequirements: SLOT_NAMES[r.slotClass],
373
+ realizationFrames: frames, screenplayChannels: SLOT_CHANNELS[r.slotClass],
374
+ noveltyPrior: r.rarity, playability: r.play ? 0.75 : 0.5,
375
+ harnessRequired: r.harnessRequired, provenanceStatus: provenance,
376
+ rowHash: "", name: r.name, desc: r.desc, band: r.band,
377
+ };
378
+ el.rowHash = sha256Hex(JSON.stringify({ ...el, rowHash: undefined })).slice(0, 16);
379
+ elements.push(el);
380
+ }
381
+
382
+ const duplicateIdCount = [...seen.values()].filter((n) => n > 1).length;
383
+ const coverage = wired.length ? elements.length / wired.length : 0;
384
+ const manifestHash = sha256Hex(elements.map((e) => e.rowHash).sort().join(""));
385
+ _manifest = {
386
+ catalogVersion: CATALOG_VERSION, canonicalRows: wired.length, compiledRows: elements.length,
387
+ failedClosed: failed, duplicateIdCount, unresolvedSlotCount: unresolvedSlot,
388
+ missingRealizationFrameCount: missingFrames, coverage, manifestHash,
389
+ releaseOk: coverage === 1 && duplicateIdCount === 0 && unresolvedSlot === 0 && missingFrames === 0,
390
+ elements, byId: new Map(elements.map((e) => [e.id, e])),
391
+ };
392
+ return _manifest;
393
+ }
394
+
395
+ // ── §6.4 runtime selection ──────────────────────────────────────────────────
396
+
397
+ export type SelectionContext = {
398
+ seed: string; // genome hex (or any string) for tie-break
399
+ vacantSlot: SlotClassId;
400
+ genreFamily: string;
401
+ audience: AudienceSegment;
402
+ liveSlots: Set<SlotClassId>; // for precondition satisfaction
403
+ activeStakes: StakesAxis | null;
404
+ readerLoadBudget: number; // 0..1 remaining
405
+ activeIds: Set<string>; // for conflicts + repetition
406
+ activeBands: string[]; // recent bands, most recent last
407
+ bandUsage: Map<string, number>; // usage counts → underused bonus
408
+ noveltyNeed: number; // 0..1
409
+ loopLoad: number; // 0..1 open-loop pressure
410
+ castLoad: number; // 0..1 of ≤15 window
411
+ };
412
+
413
+ export type SelectionResult = {
414
+ chosen: CompiledElement | null;
415
+ candidates: number;
416
+ scored: { id: string; score: number; auditRisk: number; unresolvedCost: number }[];
417
+ reason: string;
418
+ };
419
+
420
+ /** §6.4 — candidate intersection, ranking, and deterministic tie-break. */
421
+ export function selectCompiledElement(ctx: SelectionContext): SelectionResult {
422
+ const m = compileCatalog();
423
+ const cands = m.elements.filter((e) =>
424
+ e.primarySlot === ctx.vacantSlot &&
425
+ !ctx.activeIds.has(e.id) &&
426
+ (e.genreAffinity[ctx.genreFamily] ?? 0) >= 0.4 &&
427
+ (ctx.activeStakes == null || e.stakesAxes.includes(ctx.activeStakes) || e.stakesAxes.length > 1) &&
428
+ !e.conflictsWith.some((c) => ctx.activeIds.has(c)) &&
429
+ !(e.harnessRequired && ctx.liveSlots.has("SC-L")) &&
430
+ (e.audienceAffinity[ctx.audience] ?? 0) >= 0.25,
431
+ );
432
+ const pool = cands.length ? cands : m.elements.filter((e) => e.primarySlot === ctx.vacantSlot && !ctx.activeIds.has(e.id));
433
+ if (!pool.length) return { chosen: null, candidates: 0, scored: [], reason: "no candidate for slot" };
434
+
435
+ const scored = pool.map((e) => {
436
+ const preLive = e.preconditions.filter((p) => p.startsWith("live:"));
437
+ const causalFit = preLive.length ? preLive.filter((p) => ctx.liveSlots.has(p.slice(5) as SlotClassId)).length / preLive.length : 0.7;
438
+ const genreFit = e.genreAffinity[ctx.genreFamily] ?? 0.3;
439
+ const audFit = e.audienceAffinity[ctx.audience] ?? 0.4;
440
+ const novelty = e.noveltyPrior * ctx.noveltyNeed;
441
+ const motif = e.stakesAxes.includes(ctx.activeStakes as StakesAxis) ? 0.3 : 0;
442
+ const kallosLocal = 0.5 * novelty + 0.5 * (1 - ctx.castLoad);
443
+ const usage = ctx.bandUsage.get(e.band) ?? 0;
444
+ const underused = 1 / (1 + usage);
445
+ const repetition = ctx.activeBands.slice(-2).filter((b) => b === e.band).length * 0.5;
446
+ const cognitive = (1 - ctx.readerLoadBudget) * 0.6;
447
+ const score = causalFit + genreFit + audFit + novelty + motif + kallosLocal + underused
448
+ - ctx.castLoad - ctx.loopLoad - repetition - cognitive;
449
+ const auditRisk = (e.harnessRequired ? 0.5 : 0) + (e.provenanceStatus === "uncertain" ? 0.3 : 0) + e.conflictsWith.length * 0.05;
450
+ const unresolvedCost = e.costAxes.length * 0.1 + (e.preconditions.length - preLive.length) * 0.05;
451
+ return { id: e.id, score, auditRisk, unresolvedCost };
452
+ });
453
+ scored.sort((a, b) =>
454
+ b.score - a.score || a.auditRisk - b.auditRisk || a.unresolvedCost - b.unresolvedCost ||
455
+ (sha256Hex(`${ctx.seed}||${a.id}`) < sha256Hex(`${ctx.seed}||${b.id}`) ? -1 : 1));
456
+ return { chosen: m.byId.get(scored[0].id) ?? null, candidates: pool.length, scored: scored.slice(0, 8),
457
+ reason: cands.length ? "intersection non-empty" : "fell back to slot-only pool (genre/audience filter exhausted)" };
458
+ }
459
+
460
+ // ═══════════════════════════════════════════════════════════════════════════
461
+ // 2. GENRE HELM v3 (§7)
462
+ // ═══════════════════════════════════════════════════════════════════════════
463
+
464
+ export type ToneWord = "intimate" | "cool" | "warm" | "dread" | "wry" | "plain" | "charged" | "wonder";
465
+ export type Trajectory = "darkening" | "lightening" | "steady" | "oscillating" | "collapsing" | "converging";
466
+ export type EndingTone = "bittersweet" | "earned" | "tragic" | "open" | "custodial" | "triumphant" | "quiet";
467
+
468
+ export type BuildRequest = {
469
+ project: {
470
+ medium: "novel" | "novella" | "short" | "serial" | "screenplay";
471
+ language: "en";
472
+ length: { volumes: number; wordsPerVolume: number };
473
+ seed: number;
474
+ };
475
+ creative: {
476
+ genres: string[]; // 1..3 keys; mix weights derive from order unless given
477
+ mixWeights?: number[];
478
+ subgenres: string[];
479
+ audience: {
480
+ ageBand: "children" | "middle_grade" | "ya" | "adult";
481
+ heatLevel: "none" | "low" | "moderate" | "high";
482
+ violenceLevel: "none" | "low" | "moderate" | "high";
483
+ readingComplexity: "accessible" | "accessible_literary" | "literary" | "dense";
484
+ };
485
+ tone: { opening: ToneWord; trajectory: Trajectory; ending: EndingTone };
486
+ requests: { protagonist?: string; setting?: string; relationship?: string; premise?: string };
487
+ };
488
+ };
489
+
490
+ export type GenreProfile = {
491
+ key: string;
492
+ family: string; // GENRE_HELM family for slot/element affinity
493
+ kallosKey: keyof typeof KALLOS_BASE_WEIGHTS | string;
494
+ dominantReaderQuestion: string;
495
+ expectedPleasures: string[];
496
+ mandatoryPayoffs: string[];
497
+ forbiddenFailures: string[];
498
+ defaultEndingFamilies: string[];
499
+ stakesWeights: Partial<Record<StakesAxis, number>>;
500
+ pacingWeights: { spike: number; relief: number; dread: number; intimacy: number };
501
+ sceneGrammarWeights: Partial<Record<SceneGrammarId, number>>;
502
+ proseRegister: string;
503
+ nameRegister: string;
504
+ };
505
+
506
+ export const GENRE_PROFILES: Record<string, GenreProfile> = {
507
+ romance: {
508
+ key: "romance", family: "ROMANCE", kallosKey: "romance",
509
+ dominantReaderQuestion: "Will these two people become able to choose each other, and at what cost to who they were?",
510
+ expectedPleasures: ["charged proximity", "misread signals", "earned vulnerability", "the declaration scene"],
511
+ mandatoryPayoffs: ["central relationship receives explicit closure", "each lead changes because of the other"],
512
+ forbiddenFailures: ["romantic lead is secretly nonexistent", "relationship resolved by coincidence", "one lead is punished for the other's growth"],
513
+ defaultEndingFamilies: ["Handoff", "Maintenance", "Ring"],
514
+ stakesWeights: { relational: 0.45, identity: 0.3, moral: 0.15 },
515
+ pacingWeights: { spike: 0.3, relief: 0.6, dread: 0.2, intimacy: 0.9 },
516
+ sceneGrammarWeights: { RELATIONAL: 0.4, PROCESSING: 0.25, ACTION: 0.15 },
517
+ proseRegister: "warm, close-third, sensory, interiority-forward", nameRegister: "PERSON: euphonic, distinct initials; PLACE: lived-in",
518
+ },
519
+ psychological_thriller: {
520
+ key: "psychological_thriller", family: "THRILLER", kallosKey: "mystery_thriller",
521
+ dominantReaderQuestion: "Whose account of what is happening can be trusted, and what will believing it cost?",
522
+ expectedPleasures: ["destabilised trust", "the reread clue", "the ticking constraint", "reversal that recontextualises"],
523
+ mandatoryPayoffs: ["threat mystery is causally resolved", "every misdirection is fair on reread"],
524
+ forbiddenFailures: ["it was all a dream/hallucination unless licensed", "villain omniscience", "clue introduced only in the reveal"],
525
+ defaultEndingFamilies: ["Verdict", "Split-custody", "Decommission"],
526
+ stakesWeights: { epistemic: 0.4, physical: 0.25, identity: 0.2 },
527
+ pacingWeights: { spike: 0.8, relief: 0.3, dread: 0.8, intimacy: 0.3 },
528
+ sceneGrammarWeights: { INVESTIGATION: 0.35, ACTION: 0.25, PROCESSING: 0.2 },
529
+ proseRegister: "tight, present-tense-capable, withholding, concrete", nameRegister: "PERSON: short, plausible; INSTITUTION: flat and official",
530
+ },
531
+ mystery: {
532
+ key: "mystery", family: "MYSTERY", kallosKey: "mystery_thriller",
533
+ dominantReaderQuestion: "What actually happened, and who had reason and means?",
534
+ expectedPleasures: ["fair-play clues", "method", "the investigator's cost", "the assembled truth"],
535
+ mandatoryPayoffs: ["solution follows from on-page evidence", "investigator pays a personal price for knowing"],
536
+ forbiddenFailures: ["culprit not present before the final quarter", "evidence withheld from the reader that the detective had"],
537
+ defaultEndingFamilies: ["Verdict", "Audit", "Custodial"],
538
+ stakesWeights: { epistemic: 0.45, moral: 0.25, institutional: 0.2 },
539
+ pacingWeights: { spike: 0.5, relief: 0.4, dread: 0.5, intimacy: 0.3 },
540
+ sceneGrammarWeights: { INVESTIGATION: 0.45, INSTITUTIONAL: 0.2, PROCESSING: 0.2 },
541
+ proseRegister: "observant, procedural, dry", nameRegister: "PERSON: distinct by syllable count; DOCUMENT: numbered",
542
+ },
543
+ horror: {
544
+ key: "horror", family: "HORROR", kallosKey: "horror",
545
+ dominantReaderQuestion: "What is wrong with this place, and will anyone be allowed to leave it changed rather than destroyed?",
546
+ expectedPleasures: ["dread accumulation", "the rule you cannot break", "institutional complicity", "power reversal"],
547
+ mandatoryPayoffs: ["the source of dread receives a confrontation", "survivor bears a named residue"],
548
+ forbiddenFailures: ["threat explained away as hoax without cost", "unearned rescue", "gore substituting for dread"],
549
+ defaultEndingFamilies: ["Decommission", "Counterfactual-closure", "Residual"],
550
+ stakesWeights: { physical: 0.35, identity: 0.3, moral: 0.2 },
551
+ pacingWeights: { spike: 0.7, relief: 0.35, dread: 1, intimacy: 0.4 },
552
+ sceneGrammarWeights: { ACTION: 0.3, RITUAL: 0.2, PROCESSING: 0.2, AFTERMATH: 0.15 },
553
+ proseRegister: "flat declarative under pressure, sensory, patient", nameRegister: "PLACE: plain and ominous; INSTITUTION: benevolent-sounding",
554
+ },
555
+ fantasy: {
556
+ key: "fantasy", family: "FANTASY", kallosKey: "fantasy",
557
+ dominantReaderQuestion: "What does this world's rule cost, and who is made to pay it?",
558
+ expectedPleasures: ["progressive world revelation", "the priced exception", "alliance under pressure", "wonder with consequence"],
559
+ mandatoryPayoffs: ["every world-rule exception is priced on page", "map/knowledge expands and returns transformed"],
560
+ forbiddenFailures: ["magic solves a problem it was not established to solve", "prophecy replaces decision"],
561
+ defaultEndingFamilies: ["Handoff", "Custodial", "Ring"],
562
+ stakesWeights: { physical: 0.25, institutional: 0.25, custodial: 0.25, moral: 0.15 },
563
+ pacingWeights: { spike: 0.6, relief: 0.5, dread: 0.4, intimacy: 0.5 },
564
+ sceneGrammarWeights: { ACTION: 0.25, TRAVEL: 0.2, RITUAL: 0.15, ENSEMBLE: 0.15 },
565
+ proseRegister: "concrete wonder, grounded texture, low exposition", nameRegister: "PERSON/PLACE: single coherent phonology, declared contact layers only",
566
+ },
567
+ romantasy: {
568
+ key: "romantasy", family: "FANTASY", kallosKey: "romantasy",
569
+ dominantReaderQuestion: "Can the bond survive the world's rule, and which will bend first?",
570
+ expectedPleasures: ["bond tested by world stakes", "alliance/rivalry heat", "series escalation", "established-world return"],
571
+ mandatoryPayoffs: ["relationship and world plot each move irreversibly", "bond is chosen under cost, not fated"],
572
+ forbiddenFailures: ["bond resolved by prophecy", "world stakes reset between volumes"],
573
+ defaultEndingFamilies: ["Handoff", "Open-serial", "Ring"],
574
+ stakesWeights: { relational: 0.35, physical: 0.25, institutional: 0.2 },
575
+ pacingWeights: { spike: 0.7, relief: 0.5, dread: 0.4, intimacy: 0.8 },
576
+ sceneGrammarWeights: { RELATIONAL: 0.3, ACTION: 0.3, ENSEMBLE: 0.15 },
577
+ proseRegister: "high-heat close-third, kinetic", nameRegister: "PERSON: strong distinct initials; DYNASTY/HOUSEHOLD registers",
578
+ },
579
+ science_fiction: {
580
+ key: "science_fiction", family: "SF", kallosKey: "science_fiction",
581
+ dominantReaderQuestion: "What does this system do to the people inside it, and who notices first?",
582
+ expectedPleasures: ["premise worked out rigorously", "institutional critique", "the diagnostic turn", "consequence at scale"],
583
+ mandatoryPayoffs: ["premise's second-order consequence is dramatized", "system change has aftermath in ordinary life"],
584
+ forbiddenFailures: ["technology exception at the climax", "lecture replacing scene"],
585
+ defaultEndingFamilies: ["Decommission", "Handoff", "Reform"],
586
+ stakesWeights: { epistemic: 0.3, institutional: 0.3, ecological: 0.2 },
587
+ pacingWeights: { spike: 0.5, relief: 0.4, dread: 0.5, intimacy: 0.3 },
588
+ sceneGrammarWeights: { INVESTIGATION: 0.25, INSTITUTIONAL: 0.25, MAINTENANCE: 0.2 },
589
+ proseRegister: "precise, cool, specific nouns", nameRegister: "INSTITUTION/OFFICE: bureaucratic; ARTIFACT: numbered",
590
+ },
591
+ literary: {
592
+ key: "literary", family: "LITERARY", kallosKey: "general_fiction",
593
+ dominantReaderQuestion: "What does it cost to keep living honestly inside this arrangement?",
594
+ expectedPleasures: ["interiority with consequence", "the small irreversible decision", "texture of labour and money", "unresolved but named residue"],
595
+ mandatoryPayoffs: ["at least one irreversible private decision changes a relationship", "residue is named, not resolved away"],
596
+ forbiddenFailures: ["spectacle substituting for pressure", "epiphany without cost"],
597
+ defaultEndingFamilies: ["Maintenance", "Stable-Incomplete", "Residual"],
598
+ stakesWeights: { relational: 0.3, moral: 0.3, custodial: 0.2 },
599
+ pacingWeights: { spike: 0.25, relief: 0.5, dread: 0.3, intimacy: 0.8 },
600
+ sceneGrammarWeights: { PROCESSING: 0.3, RELATIONAL: 0.25, MAINTENANCE: 0.2 },
601
+ proseRegister: "exact, unhurried, image-true", nameRegister: "PERSON: ordinary, era-plausible",
602
+ },
603
+ institutional_maintenance: {
604
+ key: "institutional_maintenance", family: "LITERARY", kallosKey: "institutional_maintenance",
605
+ dominantReaderQuestion: "Who keeps this running, what does it quietly cost them, and who inherits the duty?",
606
+ expectedPleasures: ["procedure as drama", "the repair that reveals", "custodial handoff", "anti-spectacle"],
607
+ mandatoryPayoffs: ["custody/duty is explicitly transferred or refused", "a maintenance failure has a visible aftermath"],
608
+ forbiddenFailures: ["heroic rescue replacing maintenance", "institution becomes a villain instead of a system"],
609
+ defaultEndingFamilies: ["Maintenance", "Custodial", "Handoff"],
610
+ stakesWeights: { institutional: 0.35, custodial: 0.35, ecological: 0.15 },
611
+ pacingWeights: { spike: 0.2, relief: 0.5, dread: 0.4, intimacy: 0.5 },
612
+ sceneGrammarWeights: { MAINTENANCE: 0.35, INSTITUTIONAL: 0.3, AFTERMATH: 0.15 },
613
+ proseRegister: "plain, procedural, precise units", nameRegister: "INSTITUTION/OFFICE/DOCUMENT registers dominant",
614
+ },
615
+ historical: {
616
+ key: "historical", family: "HISTORICAL", kallosKey: "historical",
617
+ dominantReaderQuestion: "What did it feel like to be inside this moment before its outcome was known?",
618
+ expectedPleasures: ["richly drawn period", "professional and personal stakes braided", "known outcome, unknown cost"],
619
+ mandatoryPayoffs: ["period constraint shapes at least one decisive choice", "personal and public stakes converge"],
620
+ forbiddenFailures: ["anachronistic expertise or values without cost", "period as wallpaper"],
621
+ defaultEndingFamilies: ["Custodial", "Ring", "Residual"],
622
+ stakesWeights: { institutional: 0.3, moral: 0.25, identity: 0.25 },
623
+ pacingWeights: { spike: 0.4, relief: 0.5, dread: 0.4, intimacy: 0.6 },
624
+ sceneGrammarWeights: { INSTITUTIONAL: 0.25, RELATIONAL: 0.25, TRAVEL: 0.15 },
625
+ proseRegister: "plain to charged, period-plausible diction", nameRegister: "PERSON: attested-era register; PLACE: real or era-consistent",
626
+ },
627
+ family_saga: {
628
+ key: "family_saga", family: "FAMILY/SAGA", kallosKey: "general_fiction",
629
+ dominantReaderQuestion: "What passes down, what refuses to, and who ends up holding it?",
630
+ expectedPleasures: ["generational echo", "inheritance with strings", "the house/farm/shop as character"],
631
+ mandatoryPayoffs: ["at least one inheritance is accepted, refused, or transformed on page", "a pattern repeats with a changed cost"],
632
+ forbiddenFailures: ["generations as reskins", "secret revealed with no downstream consequence"],
633
+ defaultEndingFamilies: ["Handoff", "Successor", "Custodial"],
634
+ stakesWeights: { custodial: 0.35, relational: 0.3, identity: 0.2 },
635
+ pacingWeights: { spike: 0.3, relief: 0.5, dread: 0.3, intimacy: 0.7 },
636
+ sceneGrammarWeights: { RELATIONAL: 0.3, AFTERMATH: 0.2, RITUAL: 0.15 },
637
+ proseRegister: "layered, patient, object-anchored", nameRegister: "DYNASTY/HOUSEHOLD lineage system active",
638
+ },
639
+ ya: {
640
+ key: "ya", family: "CHILDREN/YA", kallosKey: "ya_saga",
641
+ dominantReaderQuestion: "Who am I allowed to become here, and who decides?",
642
+ expectedPleasures: ["identity under institutional pressure", "friendship as engine", "clear local danger with wonder", "hometown/known-world reinterpretation"],
643
+ mandatoryPayoffs: ["protagonist makes a choice an adult would not have let them make", "friendship changes state"],
644
+ forbiddenFailures: ["adult solves the climax", "moralising narration"],
645
+ defaultEndingFamilies: ["Handoff", "Ring", "Open-serial"],
646
+ stakesWeights: { identity: 0.35, relational: 0.3, physical: 0.2 },
647
+ pacingWeights: { spike: 0.6, relief: 0.6, dread: 0.4, intimacy: 0.6 },
648
+ sceneGrammarWeights: { ACTION: 0.3, RELATIONAL: 0.3, PROCESSING: 0.15 },
649
+ proseRegister: "clear, immediate, voice-forward", nameRegister: "PERSON: short, memorable, distinct initials",
650
+ },
651
+ comedy: {
652
+ key: "comedy", family: "COMEDY", kallosKey: "general_fiction",
653
+ dominantReaderQuestion: "How long can this arrangement pretend to work?",
654
+ expectedPleasures: ["escalating confusion", "institution as farce", "the dignified collapse", "reintegration"],
655
+ mandatoryPayoffs: ["confusion resolves by recognition, not by accident alone", "the institution's logic is exposed"],
656
+ forbiddenFailures: ["cruelty without recognition", "punchlines replacing decisions"],
657
+ defaultEndingFamilies: ["Ring", "Maintenance", "Reform"],
658
+ stakesWeights: { institutional: 0.35, relational: 0.3, identity: 0.2 },
659
+ pacingWeights: { spike: 0.5, relief: 0.8, dread: 0.1, intimacy: 0.5 },
660
+ sceneGrammarWeights: { INSTITUTIONAL: 0.3, ENSEMBLE: 0.25, RELATIONAL: 0.2 },
661
+ proseRegister: "dry, exact timing, escalating specificity", nameRegister: "OFFICE/INSTITUTION: over-formal; PERSON: plain",
662
+ },
663
+ };
664
+
665
+ /** Fuzzy genre key resolver (accepts "psychological_thiller", "sci-fi", "thriller", …). */
666
+ export function resolveGenreKey(raw: string): string | null {
667
+ const k = raw.toLowerCase().replace(/[\s-]+/g, "_");
668
+ if (GENRE_PROFILES[k]) return k;
669
+ const alias: Record<string, string> = {
670
+ thriller: "psychological_thriller", psychological_thiller: "psychological_thriller", psych_thriller: "psychological_thriller",
671
+ sf: "science_fiction", sci_fi: "science_fiction", scifi: "science_fiction",
672
+ saga: "family_saga", family: "family_saga", generational: "family_saga",
673
+ young_adult: "ya", children: "ya", middle_grade: "ya",
674
+ maintenance: "institutional_maintenance", institutional: "institutional_maintenance",
675
+ general_fiction: "literary", literary_fiction: "literary",
676
+ romantic_suspense: "romance", gothic_romance: "romance", cozy: "mystery", procedural: "mystery",
677
+ satire: "comedy", farce: "comedy",
678
+ };
679
+ if (alias[k]) return alias[k];
680
+ const hit = Object.keys(GENRE_PROFILES).find((g) => g.includes(k) || k.includes(g));
681
+ return hit ?? null;
682
+ }
683
+
684
+ export type GenreContract = {
685
+ dominant: GenreProfile;
686
+ secondary: GenreProfile[];
687
+ mix: { key: string; weight: number }[];
688
+ dominantReaderQuestion: string;
689
+ expectedPleasures: string[];
690
+ mandatoryPayoffs: string[];
691
+ forbiddenFailures: string[];
692
+ defaultEndingFamilies: string[];
693
+ stakesWeights: Partial<Record<StakesAxis, number>>;
694
+ pacingWeights: GenreProfile["pacingWeights"];
695
+ sceneGrammarWeights: Partial<Record<SceneGrammarId, number>>;
696
+ proseRegister: string;
697
+ nameRegister: string;
698
+ kallosBaseWeights: number[]; // 12 dims, sums to 100
699
+ audienceSegment: AudienceSegment;
700
+ audienceConstraints: string[];
701
+ toneContract: string[];
702
+ subgenres: string[];
703
+ unresolved: string[]; // genres that could not be resolved — declared, not dropped silently
704
+ family: string; // dominant GENRE_HELM family (drives slot/element affinity)
705
+ };
706
+
707
+ /** §7.2–7.3 — blend genres into one contract. Σ mix weights = 1 by construction. */
708
+ export function compileGenreContract(req: BuildRequest): GenreContract {
709
+ const resolved: { key: string; weight: number }[] = [];
710
+ const unresolved: string[] = [];
711
+ req.creative.genres.forEach((g, i) => {
712
+ const key = resolveGenreKey(g);
713
+ if (!key) { unresolved.push(g); return; }
714
+ const w = req.creative.mixWeights?.[i];
715
+ resolved.push({ key, weight: w != null && w > 0 ? w : Math.max(0.15, 1 / (i + 1)) });
716
+ });
717
+ if (!resolved.length) resolved.push({ key: "literary", weight: 1 });
718
+ const sum = resolved.reduce((a, x) => a + x.weight, 0);
719
+ const mix = resolved.map((x) => ({ key: x.key, weight: Math.round((x.weight / sum) * 1000) / 1000 }));
720
+ mix.sort((a, b) => b.weight - a.weight);
721
+ const dominant = GENRE_PROFILES[mix[0].key];
722
+ const secondary = mix.slice(1).map((x) => GENRE_PROFILES[x.key]);
723
+
724
+ // KALLOS base weights = Σ w_i × profile_i (row re-normalised to 100)
725
+ const dims = KALLOS_DIMENSIONS.length;
726
+ const kw = new Array<number>(dims).fill(0);
727
+ for (const x of mix) {
728
+ const row = KALLOS_BASE_WEIGHTS[GENRE_PROFILES[x.key].kallosKey] ?? KALLOS_BASE_WEIGHTS.general_fiction;
729
+ for (let i = 0; i < dims; i++) kw[i] += x.weight * row[i];
730
+ }
731
+ const kwSum = kw.reduce((a, b) => a + b, 0) || 1;
732
+ const kallosBaseWeights = kw.map((v) => Math.round((v / kwSum) * 1000) / 10);
733
+
734
+ const stakes: Partial<Record<StakesAxis, number>> = {};
735
+ for (const x of mix) for (const [k, v] of Object.entries(GENRE_PROFILES[x.key].stakesWeights)) {
736
+ stakes[k as StakesAxis] = (stakes[k as StakesAxis] ?? 0) + x.weight * (v ?? 0);
737
+ }
738
+ const grammars: Partial<Record<SceneGrammarId, number>> = {};
739
+ for (const x of mix) for (const [k, v] of Object.entries(GENRE_PROFILES[x.key].sceneGrammarWeights)) {
740
+ grammars[k as SceneGrammarId] = (grammars[k as SceneGrammarId] ?? 0) + x.weight * (v ?? 0);
741
+ }
742
+ const pacing = { spike: 0, relief: 0, dread: 0, intimacy: 0 };
743
+ for (const x of mix) { const p = GENRE_PROFILES[x.key].pacingWeights; pacing.spike += x.weight * p.spike; pacing.relief += x.weight * p.relief; pacing.dread += x.weight * p.dread; pacing.intimacy += x.weight * p.intimacy; }
744
+
745
+ const aud = req.creative.audience;
746
+ const audienceSegment: AudienceSegment =
747
+ aud.ageBand === "children" || aud.ageBand === "middle_grade" ? "children"
748
+ : aud.ageBand === "ya" ? "ya"
749
+ : aud.readingComplexity === "literary" || aud.readingComplexity === "dense" ? "literary"
750
+ : "adult";
751
+ const audienceConstraints = [
752
+ `age band ${aud.ageBand}`,
753
+ aud.heatLevel === "none" ? "no on-page sexual content" : `heat level ${aud.heatLevel}`,
754
+ aud.violenceLevel === "none" ? "no on-page violence" : `violence level ${aud.violenceLevel}`,
755
+ `reading complexity ${aud.readingComplexity}`,
756
+ ...(audienceSegment === "children" ? ["no graphic harm to children on page", "adult does not solve the climax"] : []),
757
+ ];
758
+ const t = req.creative.tone;
759
+ const toneContract = [
760
+ `opening register: ${t.opening}`,
761
+ `trajectory: ${t.trajectory}`,
762
+ `ending tone: ${t.ending}`,
763
+ t.trajectory === "darkening" ? "each act must remove one comfort the previous act allowed" :
764
+ t.trajectory === "lightening" ? "each act must pay one debt the previous act incurred" :
765
+ t.trajectory === "converging" ? "strands must share a cause or a cost by the final quarter" :
766
+ t.trajectory === "collapsing" ? "the governing institution loses authority in visible steps" :
767
+ t.trajectory === "oscillating" ? "alternate pressure and relief at sequence scale, not scene scale" :
768
+ "hold the opening register; change stakes, not temperature",
769
+ ];
770
+
771
+ return {
772
+ dominant, secondary, mix,
773
+ dominantReaderQuestion: dominant.dominantReaderQuestion,
774
+ expectedPleasures: [...dominant.expectedPleasures, ...secondary.flatMap((s) => s.expectedPleasures.slice(0, 2))],
775
+ mandatoryPayoffs: [...dominant.mandatoryPayoffs, ...secondary.flatMap((s) => s.mandatoryPayoffs.slice(0, 1))],
776
+ forbiddenFailures: [...new Set([...dominant.forbiddenFailures, ...secondary.flatMap((s) => s.forbiddenFailures)])],
777
+ defaultEndingFamilies: dominant.defaultEndingFamilies,
778
+ stakesWeights: stakes, pacingWeights: pacing, sceneGrammarWeights: grammars,
779
+ proseRegister: dominant.proseRegister, nameRegister: dominant.nameRegister,
780
+ kallosBaseWeights, audienceSegment, audienceConstraints, toneContract,
781
+ subgenres: req.creative.subgenres, unresolved, family: dominant.family,
782
+ };
783
+ }
784
+
785
+ /** Default request used when the studio has not composed one. */
786
+ export function defaultBuildRequest(seed: number, family?: string): BuildRequest {
787
+ const familyToKey: Record<string, string> = {
788
+ ROMANCE: "romance", THRILLER: "psychological_thriller", MYSTERY: "mystery", HORROR: "horror", FANTASY: "fantasy",
789
+ SF: "science_fiction", LITERARY: "literary", HISTORICAL: "historical", "FAMILY/SAGA": "family_saga",
790
+ "CHILDREN/YA": "ya", COMEDY: "comedy",
791
+ };
792
+ return {
793
+ project: { medium: "novel", language: "en", length: { volumes: 1, wordsPerVolume: 60000 }, seed },
794
+ creative: {
795
+ genres: [familyToKey[family ?? ""] ?? "literary"], subgenres: [],
796
+ audience: { ageBand: "adult", heatLevel: "low", violenceLevel: "moderate", readingComplexity: "accessible_literary" },
797
+ tone: { opening: "plain", trajectory: "darkening", ending: "bittersweet" },
798
+ requests: {},
799
+ },
800
+ };
801
+ }
802
+
803
+ export const BUILD_REQUEST_KEY = "veritas.saga.buildRequest.v1";
804
+ export function loadBuildRequest(): BuildRequest | null {
805
+ try { const raw = localStorage.getItem(BUILD_REQUEST_KEY); return raw ? (JSON.parse(raw) as BuildRequest) : null; } catch { return null; }
806
+ }
807
+ export function saveBuildRequest(req: BuildRequest | null): void {
808
+ try { if (req) localStorage.setItem(BUILD_REQUEST_KEY, JSON.stringify(req)); else localStorage.removeItem(BUILD_REQUEST_KEY); } catch { /* optional */ }
809
+ }
810
+
811
+ /** GENRE_HELM (v2 taxonomy) leaves available for a resolved family — for the studio form. */
812
+ export function subgenresFor(genreKey: string): string[] {
813
+ const p = GENRE_PROFILES[genreKey];
814
+ return p ? (GENRE_HELM[p.family] ?? []) : [];
815
+ }
816
+
817
+ // ═══════════════════════════════════════════════════════════════════════════
818
+ // 3. ONOMASTRATA v3 (§10)
819
+ // ═══════════════════════════════════════════════════════════════════════════
820
+
821
+ export type NameCultureProfile = {
822
+ profileId: string;
823
+ script: "Latn";
824
+ locale: string;
825
+ onsets: [string, number][]; // weighted
826
+ nuclei: [string, number][];
827
+ codas: [string, number][];
828
+ syllableTemplates: [string, number][]; // "CV" | "CVC" | "V" | "VC" | "CCV" | "CVCC"
829
+ stress: "initial" | "penultimate" | "final";
830
+ vowelHarmony?: { front: string[]; back: string[] };
831
+ morphemes: { root: string; meaning: string; classes: NameClass[] }[];
832
+ lineage: "patronymic-suffix" | "house-prefix" | "none" | "matronymic-suffix";
833
+ lineageAffix: string;
834
+ honorifics: string[];
835
+ tabooSequences: string[];
836
+ nameOrder: "given-first" | "surname-first";
837
+ nicknameRule: "first-syllable+diminutive" | "last-syllable" | "clipped";
838
+ diminutive: string;
839
+ historicalLayers: { id: string; soundChanges: [RegExp, string][] }[];
840
+ contactEvent?: string; // required for hybrids (§10.5)
841
+ };
842
+
843
+ const P = (xs: string[], w = 1): [string, number][] => xs.map((x) => [x, w]);
844
+
845
+ export const NAME_PROFILES: Record<string, NameCultureProfile> = {
846
+ RIVERINE: {
847
+ profileId: "RIVERINE", script: "Latn", locale: "x-riverine",
848
+ onsets: [...P(["m", "n", "l", "r", "s", "v", "th", "h"], 2), ...P(["d", "t", "k", "b"], 1)],
849
+ nuclei: [...P(["a", "e", "i", "o"], 2), ...P(["ae", "ei", "ou", "ia"], 1)],
850
+ codas: [...P(["n", "l", "r", "s"], 2), ...P(["th", "nd", "m"], 1)],
851
+ syllableTemplates: [["CV", 4], ["CVC", 3], ["V", 1], ["VC", 1]],
852
+ stress: "penultimate", vowelHarmony: { front: ["e", "i", "ei", "ia"], back: ["a", "o", "ou", "ae"] },
853
+ morphemes: [
854
+ { root: "mer", meaning: "river-bend", classes: ["PLACE", "REGION"] }, { root: "lin", meaning: "reed", classes: ["PLACE", "PERSON"] },
855
+ { root: "sol", meaning: "ford", classes: ["PLACE"] }, { root: "ael", meaning: "keeper", classes: ["PERSON", "OFFICE"] },
856
+ { root: "thar", meaning: "weir", classes: ["PLACE", "INSTITUTION"] }, { root: "ven", meaning: "tally", classes: ["DOCUMENT", "ARTIFACT"] },
857
+ ],
858
+ lineage: "patronymic-suffix", lineageAffix: "-sen", honorifics: ["Keeper", "Warden", "Elder"],
859
+ tabooSequences: ["kk", "xx", "gh"], nameOrder: "given-first", nicknameRule: "first-syllable+diminutive", diminutive: "i",
860
+ historicalLayers: [{ id: "old-riverine", soundChanges: [[/t(?=[aeiou])/g, "d"], [/s$/g, "z"]] }],
861
+ },
862
+ HIGHLAND: {
863
+ profileId: "HIGHLAND", script: "Latn", locale: "x-highland",
864
+ onsets: [...P(["br", "dr", "gr", "kr", "st", "sk", "th", "w"], 2), ...P(["b", "d", "g", "k", "h", "r"], 1)],
865
+ nuclei: [...P(["a", "e", "o", "u"], 2), ...P(["ei", "au"], 1)],
866
+ codas: [...P(["ck", "ld", "rn", "th", "st", "rk"], 2), ...P(["n", "r", "d"], 1)],
867
+ syllableTemplates: [["CVC", 4], ["CCVC", 3], ["CV", 1], ["CVCC", 2]],
868
+ stress: "initial",
869
+ morphemes: [
870
+ { root: "brack", meaning: "scree", classes: ["PLACE", "PERSON"] }, { root: "hold", meaning: "fortified", classes: ["PLACE", "DYNASTY"] },
871
+ { root: "dorn", meaning: "thorn", classes: ["PERSON", "WEAPON"] }, { root: "garth", meaning: "yard", classes: ["PLACE", "HOUSEHOLD"] },
872
+ { root: "wick", meaning: "settlement", classes: ["PLACE"] }, { root: "rune", meaning: "counsel", classes: ["OFFICE", "INSTITUTION"] },
873
+ ],
874
+ lineage: "house-prefix", lineageAffix: "of ", honorifics: ["Reeve", "Thane", "Marshal"],
875
+ tabooSequences: ["ll", "ss"], nameOrder: "given-first", nicknameRule: "clipped", diminutive: "",
876
+ historicalLayers: [{ id: "old-highland", soundChanges: [[/ck$/g, "k"], [/w/g, "hw"]] }],
877
+ },
878
+ SALTMARCH: {
879
+ profileId: "SALTMARCH", script: "Latn", locale: "x-saltmarch",
880
+ onsets: [...P(["f", "v", "s", "sh", "z", "l", "y"], 2), ...P(["m", "n", "h"], 1)],
881
+ nuclei: [...P(["a", "i", "u", "e"], 2), ...P(["ai", "ui", "oa"], 1)],
882
+ codas: [...P(["s", "sh", "l", "n"], 2), ...P(["f", "z"], 1)],
883
+ syllableTemplates: [["CV", 4], ["CVC", 2], ["V", 2], ["CVV", 1]],
884
+ stress: "final",
885
+ morphemes: [
886
+ { root: "sal", meaning: "salt", classes: ["PLACE", "INSTITUTION"] }, { root: "fen", meaning: "marsh", classes: ["PLACE", "REGION"] },
887
+ { root: "ysa", meaning: "gull", classes: ["PERSON"] }, { root: "voel", meaning: "net", classes: ["ARTIFACT", "PERSON"] },
888
+ { root: "shai", meaning: "tide-mark", classes: ["EVENT", "RITUAL"] },
889
+ ],
890
+ lineage: "matronymic-suffix", lineageAffix: "-dotter", honorifics: ["Netmother", "Pilot", "Tidewarden"],
891
+ tabooSequences: ["pp", "kk", "tt"], nameOrder: "surname-first", nicknameRule: "last-syllable", diminutive: "",
892
+ historicalLayers: [{ id: "old-saltmarch", soundChanges: [[/f/g, "ph"], [/y/g, "j"]] }],
893
+ },
894
+ ARCHIVAL: {
895
+ profileId: "ARCHIVAL", script: "Latn", locale: "x-archival",
896
+ onsets: [...P(["c", "p", "t", "r", "m", "s", "v", "l"], 2), ...P(["qu", "gn", "d"], 1)],
897
+ nuclei: [...P(["a", "e", "i", "o", "u"], 2)],
898
+ codas: [...P(["s", "m", "n", "r", "x"], 2), ...P(["nt", "rs"], 1)],
899
+ syllableTemplates: [["CV", 3], ["CVC", 3], ["V", 1]],
900
+ stress: "penultimate",
901
+ morphemes: [
902
+ { root: "regis", meaning: "register", classes: ["INSTITUTION", "DOCUMENT"] }, { root: "custod", meaning: "custody", classes: ["OFFICE", "INSTITUTION"] },
903
+ { root: "tabul", meaning: "tablet", classes: ["DOCUMENT", "ARTIFACT"] }, { root: "mensur", meaning: "measure", classes: ["LAW", "ARTIFACT"] },
904
+ { root: "provis", meaning: "provision", classes: ["LAW", "DOCUMENT"] }, { root: "sigill", meaning: "seal", classes: ["ARTIFACT", "OFFICE"] },
905
+ ],
906
+ lineage: "none", lineageAffix: "", honorifics: ["Registrar", "Assessor", "Adjudicator", "Custodian"],
907
+ tabooSequences: ["kk", "zz"], nameOrder: "given-first", nicknameRule: "clipped", diminutive: "",
908
+ historicalLayers: [],
909
+ },
910
+ CANTON: {
911
+ profileId: "CANTON", script: "Latn", locale: "x-canton",
912
+ onsets: [...P(["k", "t", "p", "m", "n", "l", "h", "s"], 2), ...P(["ch", "ts"], 1)],
913
+ nuclei: [...P(["a", "i", "u", "o"], 2), ...P(["aa", "ii", "uu"], 1)],
914
+ codas: [...P(["n", "ng", "k", "t"], 2), ...P(["m"], 1)],
915
+ syllableTemplates: [["CV", 4], ["CVC", 3], ["CVV", 1]],
916
+ stress: "initial",
917
+ morphemes: [
918
+ { root: "kan", meaning: "district", classes: ["PLACE", "REGION", "INSTITUTION"] }, { root: "tul", meaning: "gate", classes: ["PLACE", "PERSON"] },
919
+ { root: "min", meaning: "grain", classes: ["INSTITUTION", "EVENT"] }, { root: "hosa", meaning: "hearth", classes: ["HOUSEHOLD", "PERSON"] },
920
+ { root: "nuk", meaning: "boundary-stone", classes: ["ARTIFACT", "LAW"] },
921
+ ],
922
+ lineage: "house-prefix", lineageAffix: "", honorifics: ["Headman", "Gatekeeper", "Elder"],
923
+ tabooSequences: ["rr", "ll", "th"], nameOrder: "surname-first", nicknameRule: "first-syllable+diminutive", diminutive: "ko",
924
+ historicalLayers: [{ id: "old-canton", soundChanges: [[/ng$/g, "n"], [/ts/g, "s"]] }],
925
+ },
926
+ };
927
+
928
+ /** §10.5 — a hybrid must declare the contact event that explains the mixture. */
929
+ export function hybridProfile(a: string, b: string, contactEvent: string, id?: string): NameCultureProfile {
930
+ const A = NAME_PROFILES[a], B = NAME_PROFILES[b];
931
+ if (!A || !B) throw new Error(`hybridProfile: unknown profile ${!A ? a : b}`);
932
+ if (!contactEvent || contactEvent.trim().length < 12) throw new Error("hybridProfile: a declared historical contact event (≥12 chars) is mandatory (§10.5)");
933
+ return {
934
+ ...A, profileId: id ?? `${a}+${b}`, contactEvent,
935
+ onsets: [...A.onsets, ...B.onsets.map(([s, w]) => [s, w * 0.5] as [string, number])],
936
+ codas: [...A.codas, ...B.codas.map(([s, w]) => [s, w * 0.5] as [string, number])],
937
+ morphemes: [...A.morphemes, ...B.morphemes],
938
+ honorifics: [...A.honorifics, ...B.honorifics],
939
+ tabooSequences: [...new Set([...A.tabooSequences, ...B.tabooSequences])],
940
+ historicalLayers: [...A.historicalLayers, ...B.historicalLayers],
941
+ };
942
+ }
943
+
944
+ function weightedPick(rng: GenomeRng, xs: [string, number][]): string {
945
+ const total = xs.reduce((a, [, w]) => a + w, 0);
946
+ let x = rng.next() * total;
947
+ for (const [s, w] of xs) { x -= w; if (x <= 0) return s; }
948
+ return xs[xs.length - 1][0];
949
+ }
950
+
951
+ function syllable(rng: GenomeRng, p: NameCultureProfile, harmony: "front" | "back" | null): string {
952
+ const tpl = weightedPick(rng, p.syllableTemplates);
953
+ let nuc = weightedPick(rng, p.nuclei);
954
+ if (harmony && p.vowelHarmony) {
955
+ const set = p.vowelHarmony[harmony];
956
+ const allowed = p.nuclei.filter(([n]) => set.includes(n));
957
+ if (allowed.length) nuc = weightedPick(rng, allowed);
958
+ }
959
+ let out = "";
960
+ for (const ch of tpl) {
961
+ if (ch === "C") out += weightedPick(rng, out.length === 0 ? p.onsets : p.codas);
962
+ else if (ch === "V") out += nuc;
963
+ }
964
+ return out;
965
+ }
966
+
967
+ const VOWELS_RE = /[aeiou]/g;
968
+ export function syllableCount(s: string): number {
969
+ const clean = s.toLowerCase().replace(/[^a-z]/g, "");
970
+ if (!clean) return 0;
971
+ const m = clean.match(/[aeiouy]+/g);
972
+ return m ? m.length : 1;
973
+ }
974
+ function stressIndex(p: NameCultureProfile, n: number): number {
975
+ return p.stress === "initial" ? 0 : p.stress === "final" ? n - 1 : Math.max(0, n - 2);
976
+ }
977
+
978
+ /** UTS#39-style skeleton: fold visually confusable Latin sequences to a canonical form. */
979
+ export function confusableSkeleton(s: string): string {
980
+ return s.toLowerCase()
981
+ .replace(/rn/g, "m").replace(/vv/g, "w").replace(/cl/g, "d").replace(/[l1|]/g, "i")
982
+ .replace(/0/g, "o").replace(/5/g, "s").replace(/[fi]/g, "fi").replace(/ph/g, "f")
983
+ .replace(/ck/g, "k").replace(/[^a-z]/g, "");
984
+ }
985
+
986
+ /** Metaphone-lite phonetic key for collision detection. */
987
+ export function phoneticKey(s: string): string {
988
+ let t = s.toLowerCase().replace(/[^a-z]/g, "");
989
+ if (!t) return "";
990
+ const first = t[0];
991
+ t = t.slice(1).replace(VOWELS_RE, "")
992
+ .replace(/[bp]/g, "P").replace(/[dt]/g, "T").replace(/[kcq]/g, "K").replace(/[sz]/g, "S")
993
+ .replace(/[fv]/g, "F").replace(/[mn]/g, "N").replace(/[lr]/g, "R").replace(/[gj]/g, "J")
994
+ .replace(/(.)\1+/g, "$1");
995
+ return (first + t).slice(0, 6);
996
+ }
997
+ function editDistance(a: string, b: string): number {
998
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...new Array(b.length).fill(0)]);
999
+ for (let j = 0; j <= b.length; j++) d[0][j] = j;
1000
+ for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++)
1001
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1002
+ return d[a.length][b.length];
1003
+ }
1004
+
1005
+ export type GeneratedName = {
1006
+ nameClass: NameClass;
1007
+ profileId: string;
1008
+ full: string; // CLDR-ordered display form
1009
+ given: string;
1010
+ family: string | null;
1011
+ short: string;
1012
+ formal: string;
1013
+ addressing: string;
1014
+ referring: string;
1015
+ nickname: string;
1016
+ alias: string | null;
1017
+ semanticPayload: string;
1018
+ syllables: number;
1019
+ stressOn: number;
1020
+ skeleton: string;
1021
+ phonetic: string;
1022
+ layer: string | null;
1023
+ };
1024
+
1025
+ export type NameRegistry = {
1026
+ reserved: Map<string, GeneratedName>;
1027
+ rejections: { candidate: string; reason: string }[];
1028
+ };
1029
+ export function createNameRegistry(): NameRegistry { return { reserved: new Map(), rejections: [] }; }
1030
+
1031
+ const REAL_NAME_DENYLIST = ["harry potter", "hermione granger", "frodo baggins", "katniss everdeen", "sherlock holmes", "jane eyre", "elizabeth bennet", "jay gatsby"];
1032
+
1033
+ function titleCase1(s: string): string { return s ? s[0].toUpperCase() + s.slice(1) : s; }
1034
+
1035
+ /**
1036
+ * §10.3 — the twelve-step pipeline. Deterministic given (rng, profile, class,
1037
+ * registry). Steps 8–12 are the audits; a candidate that fails any is logged
1038
+ * in `registry.rejections` and regenerated (bounded attempts).
1039
+ */
1040
+ export function generateName(
1041
+ rng: GenomeRng, profile: NameCultureProfile, nameClass: NameClass, registry: NameRegistry,
1042
+ opts?: { payload?: string; layer?: string | null; maxAttempts?: number },
1043
+ ): GeneratedName | null {
1044
+ const maxAttempts = opts?.maxAttempts ?? 72;
1045
+ const activePeople = [...registry.reserved.values()].filter((n) => n.nameClass === "PERSON");
1046
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1047
+ // 1. semantic payload
1048
+ const roots = profile.morphemes.filter((m) => m.classes.includes(nameClass));
1049
+ const root = roots.length && rng.next() < 0.6 ? rng.pick(roots) : null;
1050
+ const payload = opts?.payload ?? root?.meaning ?? "unmarked";
1051
+ // 2–3. morphemes + phonological form
1052
+ const harmony: "front" | "back" | null = profile.vowelHarmony ? (rng.next() < 0.5 ? "front" : "back") : null;
1053
+ // Syllable target varies 1–4 for PERSON so the §10.4 syllable+stress rule
1054
+ // has room to be satisfied; stem growth is capped by LETTERS as well as
1055
+ // syllables because diphthong nuclei + codas overshoot ("Asaeourtha").
1056
+ const nSyl = nameClass === "PERSON" ? 1 + Math.floor(rng.next() * 4) : 2 + Math.floor(rng.next() * 2);
1057
+ const maxLen = nameClass === "PERSON" ? 8 : 9;
1058
+ let stem = root ? root.root : "";
1059
+ let guard = 0;
1060
+ while ((stem.length === 0 || syllableCount(stem) < nSyl) && stem.length < maxLen && guard++ < 8) stem += syllable(rng, profile, harmony);
1061
+ if (stem.length < 3) stem += syllable(rng, profile, harmony);
1062
+ if (stem.length > maxLen + 2) stem = stem.slice(0, maxLen + 2).replace(/[^aeiou]+$/, (m) => m.slice(0, 2));
1063
+ // 4. historical sound change layer
1064
+ const layer = opts?.layer === undefined ? (profile.historicalLayers.length && rng.next() < 0.25 ? profile.historicalLayers[0] : null) : profile.historicalLayers.find((l) => l.id === opts.layer) ?? null;
1065
+ if (layer) for (const [re, rep] of layer.soundChanges) stem = stem.replace(re, rep);
1066
+ stem = stem.replace(/(.)\1{2,}/g, "$1$1");
1067
+ // 5. morphology / lineage
1068
+ let given = titleCase1(stem);
1069
+ let family: string | null = null;
1070
+ if (nameClass === "PERSON" || nameClass === "DYNASTY" || nameClass === "HOUSEHOLD") {
1071
+ const fam = titleCase1(syllable(rng, profile, harmony) + syllable(rng, profile, harmony));
1072
+ family = profile.lineage === "patronymic-suffix" || profile.lineage === "matronymic-suffix" ? fam + profile.lineageAffix
1073
+ : profile.lineage === "house-prefix" ? (profile.lineageAffix + fam).trim() : fam;
1074
+ }
1075
+ if (nameClass === "INSTITUTION") given = `The ${rng.pick(["Office", "Registry", "Board", "Company", "Wardens", "Court", "Trust"])} of ${given}`;
1076
+ if (nameClass === "OFFICE") given = `${rng.pick(profile.honorifics)} ${given}`;
1077
+ if (nameClass === "LAW") given = `The ${given} ${rng.pick(["Provision", "Measure", "Ordinance", "Compact", "Rule"])}`;
1078
+ if (nameClass === "DOCUMENT") given = `The ${given} ${rng.pick(["Ledger", "Schedule", "Deed", "Census", "Warrant"])}`;
1079
+ if (nameClass === "ARTIFACT") given = `The ${given} ${rng.pick(["Seal", "Tally", "Key", "Standard", "Chain"])}`;
1080
+ if (nameClass === "EVENT" || nameClass === "RITUAL") given = `The ${given} ${rng.pick(["Reckoning", "Drawing", "Counting", "Walk", "Watch"])}`;
1081
+ if (nameClass === "WORK_TITLE" || nameClass === "VOLUME_TITLE" || nameClass === "CHAPTER_TITLE") given = `${rng.pick(["The", "After", "Under", "Before"])} ${given}`;
1082
+ // 6. CLDR-compatible field ordering
1083
+ const full = family
1084
+ ? (profile.nameOrder === "given-first" ? `${given} ${family}` : `${family} ${given}`)
1085
+ : given;
1086
+ // 7. forms
1087
+ const nick = profile.nicknameRule === "first-syllable+diminutive"
1088
+ ? titleCase1((stem.match(/^[^aeiou]*[aeiou]+[^aeiou]?/)?.[0] ?? stem.slice(0, 3)) + profile.diminutive)
1089
+ : profile.nicknameRule === "last-syllable" ? titleCase1(stem.slice(-3))
1090
+ : titleCase1(stem.slice(0, Math.max(3, Math.ceil(stem.length / 2))));
1091
+ const honor = profile.honorifics[Math.floor(rng.next() * profile.honorifics.length)];
1092
+ const cand: GeneratedName = {
1093
+ nameClass, profileId: profile.profileId, full, given, family,
1094
+ short: given, formal: family ? `${honor} ${family.replace(/^of /, "")}` : given,
1095
+ addressing: nameClass === "PERSON" ? given : full, referring: full,
1096
+ nickname: nameClass === "PERSON" ? nick : given, alias: null,
1097
+ semanticPayload: payload, syllables: syllableCount(given), stressOn: stressIndex(profile, syllableCount(given)),
1098
+ skeleton: confusableSkeleton(given), phonetic: phoneticKey(given), layer: layer?.id ?? null,
1099
+ };
1100
+ // 8. visual confusable audit 9. phonetic collision 10. taboo 11. real-name denylist 12. reserve
1101
+ const low = full.toLowerCase();
1102
+ // taboo applies to GENERATED phonology (given stem + family stem), not to
1103
+ // lineage affixes or honorific scaffolding — "-dotter" must not trip "tt".
1104
+ const generated = `${stem} ${family ? family.replace(profile.lineageAffix, "") : ""}`.toLowerCase();
1105
+ let reason: string | null = null;
1106
+ if (registry.reserved.has(low)) reason = "duplicate";
1107
+ else if (profile.tabooSequences.some((t) => generated.includes(t))) reason = `taboo sequence`;
1108
+ else if (REAL_NAME_DENYLIST.some((d) => low.includes(d))) reason = "real-name denylist";
1109
+ else if (nameClass === "PERSON") {
1110
+ const clash = activePeople.find((n) => n.skeleton === cand.skeleton || n.phonetic === cand.phonetic ||
1111
+ editDistance(n.given.toLowerCase(), given.toLowerCase()) / Math.max(n.given.length, given.length) < 0.35 ||
1112
+ n.nickname.toLowerCase() === cand.nickname.toLowerCase());
1113
+ if (clash) reason = `collides with ${clash.given} (skeleton/phonetic/edit/nickname)`;
1114
+ else if (activePeople.filter((n) => n.given[0].toLowerCase() === given[0].toLowerCase()).length >= 2) reason = `first letter "${given[0]}" already used twice`;
1115
+ else if (activePeople.filter((n) => n.syllables === cand.syllables && n.stressOn === cand.stressOn).length >= 3) reason = `syllable+stress pattern ${cand.syllables}:${cand.stressOn} already matches 3 leads`;
1116
+ }
1117
+ if (reason) { registry.rejections.push({ candidate: full, reason }); continue; }
1118
+ registry.reserved.set(low, cand);
1119
+ return cand;
1120
+ }
1121
+ return null;
1122
+ }
1123
+
1124
+ export type CastConfusionV3Finding = { rule: string; a: string; b: string; detail: string };
1125
+ /** §10.4 — all six rules over a set of major names (strings or GeneratedName). */
1126
+ export function auditCastConfusionV3(names: (string | GeneratedName)[], profile?: NameCultureProfile): CastConfusionV3Finding[] {
1127
+ const N = names.map((n) => typeof n === "string" ? n : n.given);
1128
+ const f: CastConfusionV3Finding[] = [];
1129
+ const byInitial = new Map<string, string[]>();
1130
+ for (const n of N) { const k = n[0]?.toLowerCase() ?? ""; byInitial.set(k, [...(byInitial.get(k) ?? []), n]); }
1131
+ for (const [k, g] of byInitial) if (g.length > 2) f.push({ rule: "first-letter ≤2", a: g[0], b: g.slice(1).join(", "), detail: `${g.length} major names begin with "${k}"` });
1132
+ const stressSig = (n: string) => `${syllableCount(n)}:${profile ? stressIndex(profile, syllableCount(n)) : 0}`;
1133
+ const sigCount = new Map<string, string[]>();
1134
+ for (const n of N) sigCount.set(stressSig(n), [...(sigCount.get(stressSig(n)) ?? []), n]);
1135
+ for (const [sig, g] of sigCount) if (g.length > 3) f.push({ rule: "syllable+stress ≤3 matches", a: g[0], b: g.slice(1).join(", "), detail: `${g.length} leads share pattern ${sig}` });
1136
+ for (let i = 0; i < N.length; i++) for (let j = i + 1; j < N.length; j++) {
1137
+ const a = N[i], b = N[j];
1138
+ const nd = editDistance(a.toLowerCase(), b.toLowerCase()) / Math.max(a.length, b.length, 1);
1139
+ if (nd < 0.35) f.push({ rule: "normalized edit distance ≥0.35", a, b, detail: `distance ${nd.toFixed(2)}` });
1140
+ if (phoneticKey(a) === phoneticKey(b)) f.push({ rule: "phonetic distance ≥ threshold", a, b, detail: `both key to ${phoneticKey(a)}` });
1141
+ if (confusableSkeleton(a) === confusableSkeleton(b)) f.push({ rule: "confusable skeleton must differ", a, b, detail: `skeleton ${confusableSkeleton(a)}` });
1142
+ }
1143
+ const gens = names.filter((n): n is GeneratedName => typeof n !== "string");
1144
+ for (let i = 0; i < gens.length; i++) for (let j = i + 1; j < gens.length; j++)
1145
+ if (gens[i].nickname.toLowerCase() === gens[j].nickname.toLowerCase()) f.push({ rule: "nickname collision forbidden", a: gens[i].given, b: gens[j].given, detail: `both → ${gens[i].nickname}` });
1146
+ return f;
1147
+ }
1148
+
1149
+ /** Generate a full cast + world name set for a build, one profile, deterministic. */
1150
+ export function generateNameSet(genomeHex: string, profileId: string, counts: Partial<Record<NameClass, number>>): { names: GeneratedName[]; registry: NameRegistry; audit: CastConfusionV3Finding[] } {
1151
+ const profile = NAME_PROFILES[profileId] ?? NAME_PROFILES.RIVERINE;
1152
+ const rng = new GenomeRng(sha256Hex(`onoma|${genomeHex}|${profile.profileId}`));
1153
+ const registry = createNameRegistry();
1154
+ const names: GeneratedName[] = [];
1155
+ for (const [cls, n] of Object.entries(counts) as [NameClass, number][]) {
1156
+ for (let i = 0; i < (n ?? 0); i++) { const g = generateName(rng, profile, cls, registry); if (g) names.push(g); }
1157
+ }
1158
+ return { names, registry, audit: auditCastConfusionV3(names.filter((n) => n.nameClass === "PERSON"), profile) };
1159
+ }
1160
+
1161
+ // ═══════════════════════════════════════════════════════════════════════════
1162
+ // 4. MORPHOGENESIS-LATTICE v2 (§11)
1163
+ // ═══════════════════════════════════════════════════════════════════════════
1164
+
1165
+ export type LatticeScale = "WORLD" | "CAST" | "PLOT" | "DISCOURSE" | "SERIES";
1166
+ export type NarrativeCell = {
1167
+ cellId: string;
1168
+ scale: LatticeScale;
1169
+ state: "DORMANT" | "ACTIVE" | "RETIRED";
1170
+ boundElementId: string | null;
1171
+ owner: string; // entity row id
1172
+ kind: EntityKind;
1173
+ preconditions: string[];
1174
+ effects: string[];
1175
+ neighbors: string[];
1176
+ activeCosts: CostAxis[];
1177
+ activePromises: string[];
1178
+ targetAttractor: GenesisPattern | null;
1179
+ birthTick: number;
1180
+ lastTick: number;
1181
+ };
1182
+ export type TypedDelta = { tick: number; from: LatticeScale; to: LatticeScale; kind: string; detail: string; cellId: string };
1183
+ export type Promise_ = { id: string; scope: "LOCAL" | "PHASE" | "SAGA"; openedTick: number; ownerCell: string; state: "SEEDED" | "PAID" | "TRANSFERRED" | "EXPIRED" | "RESIDUALIZED"; closedTick: number | null };
1184
+ export type MorphCheckpoint = { tick: number; hash: string; population: number; promisesOpen: number };
1185
+ export type LengthMode = "flash" | "short" | "novel" | "saga" | "serial";
1186
+
1187
+ export type MorphRun = {
1188
+ genome: string;
1189
+ userSeed: number;
1190
+ lengthMode: LengthMode;
1191
+ budgetTicks: number;
1192
+ cells: NarrativeCell[];
1193
+ ticks: GridTick[];
1194
+ stateHashes: string[];
1195
+ checkpoints: MorphCheckpoint[];
1196
+ deltas: TypedDelta[];
1197
+ promises: Promise_[];
1198
+ attractor: GenesisPattern;
1199
+ attractorDetail: string;
1200
+ attractorInTarget: boolean;
1201
+ endingFamily: string;
1202
+ targetAttractors: GenesisPattern[];
1203
+ volumes: { index: number; startTick: number; endTick: number; localClosure: boolean; seriesDelta: string }[];
1204
+ births: { tick: number; cellId: string; elementId: string; score: number; reason: string }[];
1205
+ masked: { tick: number; cellId: string; elementId: string; rule: string }[];
1206
+ compressionAdvice: string[];
1207
+ scenes: { index: number; tick: number; cellId: string; owner: string; kind: EntityKind; element: CompiledElement | null; grammar: SceneGrammarId; stakes: StakesAxis; cost: CostAxis; interior: boolean; volume: number; chapter: number }[];
1208
+ chapters: { index: number; volume: number; scenes: number; intensity: number; dominantStakes: StakesAxis | null }[];
1209
+ stats: { births: number; deaths: number; distinctElements: number; slotsExercised: number; castLoadMax: number; loopDebtMax: number };
1210
+ };
1211
+
1212
+ const ENDING_TO_ATTRACTOR: Record<string, GenesisPattern[]> = {
1213
+ Maintenance: ["STILL-LIFE"], "Stable-Incomplete": ["STILL-LIFE"], Residual: ["STILL-LIFE", "METHUSELAH"],
1214
+ Handoff: ["GLIDER"], Custodial: ["GLIDER"], Successor: ["GLIDER"], "Open-serial": ["GLIDER-GUN"], Reform: ["GLIDER", "RING"],
1215
+ Tragedy: ["EXTINCTION"], Decommission: ["EXTINCTION"], "Counterfactual-closure": ["EXTINCTION"],
1216
+ Ring: ["RING"], "Split-custody": ["SPLIT"], Verdict: ["STILL-LIFE", "RING"], Audit: ["STILL-LIFE"], Episodic: ["OSCILLATOR"],
1217
+ };
1218
+
1219
+ export function deriveSeedGenome(x: { userSeed: number; genre: GenreContract; audience: AudienceSegment; targetLength: number }): string {
1220
+ return sha256Hex([
1221
+ String(x.userSeed >>> 0),
1222
+ JSON.stringify({ mix: x.genre.mix, family: x.genre.family, end: x.genre.defaultEndingFamilies }),
1223
+ x.audience, String(x.targetLength), CATALOG_VERSION, RULE_TABLE_VERSION, GRAMMAR_PACK_VERSION,
1224
+ ].join("||"));
1225
+ }
1226
+
1227
+ export type MorphOpts = {
1228
+ userSeed: number;
1229
+ contract: GenreContract;
1230
+ spec: GenreSpec; // v2 GenreSpec for the CA rule table + topology
1231
+ lengthMode?: LengthMode;
1232
+ targetWords?: number;
1233
+ budget?: number;
1234
+ audience?: AudienceSegment;
1235
+ };
1236
+
1237
+ const SLOT_FOR_KIND: Record<EntityKind, SlotClassId[]> = {
1238
+ agent: ["SC-A", "SC-R", "SC-N", "SC-M"],
1239
+ institution: ["SC-I", "SC-D", "SC-L"],
1240
+ setting: ["SC-S", "SC-L"],
1241
+ strand: ["SC-T", "SC-B", "SC-W", "SC-G", "SC-E"],
1242
+ };
1243
+ const SCALE_FOR_KIND: Record<EntityKind, LatticeScale> = { agent: "CAST", institution: "WORLD", setting: "WORLD", strand: "PLOT" };
1244
+
1245
+ /**
1246
+ * §11.4 evolution. Legal births come from CATALOG-COMPILER via §6.4 selection
1247
+ * (genre/audience/precondition/conflict-aware); audit masks zero out illegal
1248
+ * births (≤1 foreground LAW, cast window ≤15, loop debt, reader-load); winners
1249
+ * are ranked by causal + genre + audience + KALLOS_local + underused − load −
1250
+ * loop_debt − repetition; effects propagate as TYPED DELTAS between lattices;
1251
+ * attractor is detected each tick and matched against the contract's target
1252
+ * ending families; every tick is checkpointed by content hash.
1253
+ */
1254
+ export function runMorphogenesis(opts: MorphOpts): MorphRun {
1255
+ const contract = opts.contract;
1256
+ const audience = opts.audience ?? contract.audienceSegment;
1257
+ const lengthMode: LengthMode = opts.lengthMode ?? (opts.spec.V >= 13 ? "serial" : opts.spec.V >= 2 ? "saga" : (opts.targetWords ?? 60000) < 8000 ? "short" : "novel");
1258
+ const targetWords = opts.targetWords ?? 60000;
1259
+ const genome = deriveSeedGenome({ userSeed: opts.userSeed, genre: contract, audience, targetLength: targetWords });
1260
+ const rng = new GenomeRng(genome);
1261
+ const budget = Math.max(6, Math.min(120, opts.budget ??
1262
+ (lengthMode === "flash" ? 8 : lengthMode === "short" ? 14 : lengthMode === "novel" ? 40 : lengthMode === "saga" ? 72 : 96)));
1263
+ const targetAttractors = [...new Set(contract.defaultEndingFamilies.flatMap((e) => ENDING_TO_ATTRACTOR[e] ?? []))];
1264
+ const manifest = compileCatalog();
1265
+
1266
+ // topology from v2 (same seed family so v2 grid and v3 lattice describe one world)
1267
+ const rows: EntityRow[] = buildRows(opts.spec, opts.userSeed >>> 0);
1268
+ const byRow = new Map(rows.map((r) => [r.id, r]));
1269
+ const cells: NarrativeCell[] = rows.map((r) => ({
1270
+ cellId: `${SCALE_FOR_KIND[r.kind]}:${r.id}`, scale: SCALE_FOR_KIND[r.kind], state: "DORMANT", boundElementId: null,
1271
+ owner: r.id, kind: r.kind, preconditions: [], effects: [], neighbors: [], activeCosts: [], activePromises: [],
1272
+ targetAttractor: targetAttractors[0] ?? null, birthTick: -1, lastTick: -1,
1273
+ }));
1274
+ const cellByOwner = new Map(cells.map((c) => [c.owner, c]));
1275
+ const neighboursOf = (rowId: string): string[] => {
1276
+ const r = byRow.get(rowId); if (!r) return [];
1277
+ const out = new Set<string>(r.relations);
1278
+ for (const s of r.settings) for (const o of rows) if (o.id !== rowId && o.settings.includes(s)) out.add(o.id);
1279
+ return [...out];
1280
+ };
1281
+ for (const c of cells) c.neighbors = neighboursOf(c.owner).map((id) => cellByOwner.get(id)?.cellId ?? id);
1282
+
1283
+ const deltas: TypedDelta[] = [];
1284
+ const promises: Promise_[] = [];
1285
+ const births: MorphRun["births"] = [];
1286
+ const masked: MorphRun["masked"] = [];
1287
+ const checkpoints: MorphCheckpoint[] = [];
1288
+ const ticks: GridTick[] = [];
1289
+ const hashes: string[] = [];
1290
+ const activeIds = new Set<string>();
1291
+ const bandUsage = new Map<string, number>();
1292
+ const recentBands: string[] = [];
1293
+ const B = opts.spec.caRuleTableId.includes("LITERARY") || opts.spec.caRuleTableId.includes("DOMESTIC") ? [3, 4] : [3];
1294
+ const S = opts.spec.caRuleTableId.includes("LITERARY") || opts.spec.caRuleTableId.includes("DOMESTIC") ? [2, 3, 4] : [2, 3];
1295
+ let castLoadMax = 0, loopDebtMax = 0, deaths = 0;
1296
+ let lawLive = 0; // foreground LAW count, updated by bind/retire so intra-tick births are masked too
1297
+ const stakesOrder = (Object.entries(contract.stakesWeights) as [StakesAxis, number][]).sort((a, b) => b[1] - a[1]).map(([k]) => k);
1298
+
1299
+ const bind = (cell: NarrativeCell, el: CompiledElement, tick: number, score: number, reason: string) => {
1300
+ cell.state = "ACTIVE"; cell.boundElementId = el.id; cell.birthTick = tick; cell.lastTick = tick;
1301
+ cell.preconditions = el.preconditions; cell.effects = el.effects; cell.activeCosts = el.costAxes;
1302
+ if (el.harnessRequired) lawLive++;
1303
+ activeIds.add(el.id); bandUsage.set(el.band, (bandUsage.get(el.band) ?? 0) + 1); recentBands.push(el.band);
1304
+ births.push({ tick, cellId: cell.cellId, elementId: el.id, score, reason });
1305
+ // typed deltas: PLOT births open promises; WORLD births change layers; CAST births change relations
1306
+ if (cell.scale === "PLOT" && (el.primarySlot === "SC-T" || el.primarySlot === "SC-W" || el.primarySlot === "SC-G")) {
1307
+ const pid = `P${promises.length + 1}`;
1308
+ promises.push({ id: pid, scope: lengthMode === "saga" || lengthMode === "serial" ? (rng.next() < 0.3 ? "SAGA" : "PHASE") : "LOCAL", openedTick: tick, ownerCell: cell.cellId, state: "SEEDED", closedTick: null });
1309
+ cell.activePromises.push(pid);
1310
+ deltas.push({ tick, from: "PLOT", to: "SERIES", kind: "promise-opened", detail: `${pid} ${el.name}`, cellId: cell.cellId });
1311
+ }
1312
+ if (cell.scale === "WORLD") deltas.push({ tick, from: "WORLD", to: "PLOT", kind: "world-layer", detail: `${el.worldLayers.join("/")} via ${el.name}`, cellId: cell.cellId });
1313
+ if (cell.scale === "CAST") deltas.push({ tick, from: "CAST", to: "PLOT", kind: el.primarySlot === "SC-R" ? "relation-delta" : "intention", detail: el.name, cellId: cell.cellId });
1314
+ deltas.push({ tick, from: cell.scale, to: "DISCOURSE", kind: "scene-stub", detail: `${el.sceneGrammars[0]} · ${el.name}`, cellId: cell.cellId });
1315
+ };
1316
+ const retire = (cell: NarrativeCell, tick: number, how: "isolation" | "overcrowd") => {
1317
+ cell.state = "RETIRED"; cell.lastTick = tick; deaths++;
1318
+ if (cell.boundElementId) {
1319
+ if (manifest.byId.get(cell.boundElementId)?.harnessRequired) lawLive = Math.max(0, lawLive - 1);
1320
+ activeIds.delete(cell.boundElementId);
1321
+ }
1322
+ for (const pid of cell.activePromises) {
1323
+ const p = promises.find((x) => x.id === pid); if (!p || p.state !== "SEEDED") continue;
1324
+ p.state = how === "overcrowd" ? "PAID" : rng.next() < 0.5 ? "EXPIRED" : "TRANSFERRED"; p.closedTick = tick;
1325
+ deltas.push({ tick, from: "PLOT", to: "SERIES", kind: `promise-${p.state.toLowerCase()}`, detail: pid, cellId: cell.cellId });
1326
+ }
1327
+ cell.activePromises = []; cell.boundElementId = null;
1328
+ };
1329
+ const snapshot = (t: number, b: GridTick["births"], d: GridTick["deaths"]): GridTick => {
1330
+ const active: CellState[] = cells.filter((c) => c.state === "ACTIVE").map((c) => ({ rowId: c.owner, elementId: c.boundElementId }));
1331
+ const turnover = b.length + d.length;
1332
+ const intensity = Math.max(0, Math.min(100, Math.round(((active.length + 2 * turnover) / (3 * rows.length)) * 100)));
1333
+ return { t, active, births: b, deaths: d, activeCount: active.length, population: rows.length, intensity };
1334
+ };
1335
+
1336
+ // ── §4.3-style seed: kernel conflict + agents + institution + setting + theme, ~30 % density
1337
+ const ctxBase = (slot: SlotClassId): SelectionContext => ({
1338
+ seed: genome, vacantSlot: slot, genreFamily: contract.family, audience,
1339
+ liveSlots: new Set(cells.filter((c) => c.state === "ACTIVE" && c.boundElementId).map((c) => manifest.byId.get(c.boundElementId!)?.primarySlot ?? "SC-B")),
1340
+ activeStakes: stakesOrder[0] ?? null, readerLoadBudget: 1, activeIds, activeBands: recentBands, bandUsage,
1341
+ noveltyNeed: 0.6, loopLoad: 0, castLoad: 0,
1342
+ });
1343
+ const target = Math.max(5, Math.round(rows.length * 0.3));
1344
+ const seedPlan: [EntityKind, SlotClassId][] = [["strand", "SC-T"], ["institution", "SC-I"], ["setting", "SC-S"], ["agent", "SC-A"], ["agent", "SC-A"], ["agent", "SC-M"], ["setting", "SC-S"], ["strand", "SC-T"]];
1345
+ let placed = 0;
1346
+ const shuffled = cells.map((c) => ({ c, k: rng.next() })).sort((a, b) => a.k - b.k).map((x) => x.c);
1347
+ for (const [kind, slot] of seedPlan) {
1348
+ const cell = shuffled.find((c) => c.kind === kind && c.state === "DORMANT"); if (!cell) continue;
1349
+ const sel = selectCompiledElement(ctxBase(slot)); if (!sel.chosen) continue;
1350
+ if (sel.chosen.harnessRequired && lawLive >= 1) { masked.push({ tick: 0, cellId: cell.cellId, elementId: sel.chosen.id, rule: "≤1 foreground LAW" }); continue; }
1351
+ bind(cell, sel.chosen, 0, sel.scored[0]?.score ?? 0, "seed"); placed++;
1352
+ if (placed >= target) break;
1353
+ }
1354
+ while (placed < target) {
1355
+ const cell = shuffled.find((c) => c.state === "DORMANT"); if (!cell) break;
1356
+ const sel = selectCompiledElement(ctxBase(SLOT_FOR_KIND[cell.kind][0])); if (!sel.chosen) break;
1357
+ if (sel.chosen.harnessRequired && lawLive >= 1) { masked.push({ tick: 0, cellId: cell.cellId, elementId: sel.chosen.id, rule: "≤1 foreground LAW" }); cell.state = "RETIRED"; continue; }
1358
+ bind(cell, sel.chosen, 0, sel.scored[0]?.score ?? 0, "seed-scatter"); placed++;
1359
+ }
1360
+ ticks.push(snapshot(0, [], [])); hashes.push(stateHash(ticks[0].active));
1361
+ checkpoints.push({ tick: 0, hash: sha256Hex(hashes[0]).slice(0, 16), population: ticks[0].activeCount, promisesOpen: promises.filter((p) => p.state === "SEEDED").length });
1362
+
1363
+ // ── evolution
1364
+ let stableConfirm = 0, budgetHit = false;
1365
+ const volumeBounds: number[] = [0];
1366
+ for (let t = 1; t <= budget; t++) {
1367
+ const prev = new Map(cells.map((c) => [c.cellId, c.state === "ACTIVE"]));
1368
+ const isLive = (cellId: string) => prev.get(cellId) === true;
1369
+ const b: GridTick["births"] = [], d: GridTick["deaths"] = [];
1370
+ const openLoops = promises.filter((p) => p.state === "SEEDED").length;
1371
+ const loopLoad = Math.min(1, openLoops / 8);
1372
+ const castNow = cells.filter((c) => c.state === "ACTIVE" && c.kind === "agent").length;
1373
+ const castLoad = Math.min(1, castNow / 15);
1374
+ castLoadMax = Math.max(castLoadMax, castNow); loopDebtMax = Math.max(loopDebtMax, openLoops);
1375
+ const liveSlots = new Set(cells.filter((c) => c.state === "ACTIVE" && c.boundElementId).map((c) => manifest.byId.get(c.boundElementId!)?.primarySlot ?? "SC-B"));
1376
+ const activeStakes = stakesOrder[(t - 1) % Math.max(1, stakesOrder.length)] ?? null;
1377
+
1378
+ for (const cell of cells) {
1379
+ const A = cell.neighbors.filter(isLive).length;
1380
+ if (cell.state === "ACTIVE") {
1381
+ if (A < Math.min(...S)) { d.push({ rowId: cell.owner, elementId: cell.boundElementId ?? "", kind: "isolation", reason: `A=${A}` }); retire(cell, t, "isolation"); }
1382
+ else if (A > Math.max(...S)) { d.push({ rowId: cell.owner, elementId: cell.boundElementId ?? "", kind: "overcrowd", reason: `A=${A}` }); retire(cell, t, "overcrowd"); }
1383
+ else cell.lastTick = t;
1384
+ continue;
1385
+ }
1386
+ if (!B.includes(A)) continue;
1387
+ // §6.4 selection with §11.4 ranking terms
1388
+ const slots = SLOT_FOR_KIND[cell.kind].filter((s) => SLOT_PRECONDITIONS[s].length === 0 || SLOT_PRECONDITIONS[s].some((p) => liveSlots.has(p)));
1389
+ if (!slots.length) continue;
1390
+ const slot = slots[Math.floor(rng.at(t * 7919 + fnv1a(cell.cellId)) * slots.length)];
1391
+ const sel = selectCompiledElement({
1392
+ seed: genome, vacantSlot: slot, genreFamily: contract.family, audience, liveSlots, activeStakes,
1393
+ readerLoadBudget: Math.max(0, 1 - (b.length * 0.2)), activeIds, activeBands: recentBands, bandUsage,
1394
+ noveltyNeed: 0.4 + 0.4 * (1 - Math.min(1, activeIds.size / 20)), loopLoad, castLoad,
1395
+ });
1396
+ if (!sel.chosen) continue;
1397
+ const el = sel.chosen;
1398
+ // ── audit masks (constraint-propagating, not post-hoc)
1399
+ let rule: string | null = null;
1400
+ const openSubstantial = promises.filter((p) => p.state === "SEEDED" && p.scope !== "LOCAL").length;
1401
+ if (el.harnessRequired && lawLive >= 1) rule = "≤1 foreground LAW";
1402
+ else if (cell.kind === "agent" && castNow + 1 > 15) rule = "SAGA_CAST_LOAD ≤15";
1403
+ else if ((el.primarySlot === "SC-T" || el.primarySlot === "SC-W") && (openSubstantial >= 5 || openLoops >= 16)) rule = "loop budget (v1 §15.3: ≤5 substantial, ≤16 total)";
1404
+ else if (b.length >= 6) rule = "reader-load: ≤6 births per tick";
1405
+ else if (el.conflictsWith.some((c) => activeIds.has(c))) rule = "canon conflict (≠ differentiator)";
1406
+ if (rule) { masked.push({ tick: t, cellId: cell.cellId, elementId: el.id, rule }); continue; }
1407
+ bind(cell, el, t, sel.scored[0]?.score ?? 0, sel.reason);
1408
+ b.push({ rowId: cell.owner, elementId: el.id, reason: `A=${A} ∈ B${JSON.stringify(B)} · ${sel.reason}` });
1409
+ }
1410
+ const tk = snapshot(t, b, d); ticks.push(tk);
1411
+ const h = stateHash(tk.active); hashes.push(h);
1412
+ checkpoints.push({ tick: t, hash: sha256Hex(hashes.join("|")).slice(0, 16), population: tk.activeCount, promisesOpen: promises.filter((p) => p.state === "SEEDED").length });
1413
+ if (tk.activeCount === 0) break;
1414
+ const firstSeen = hashes.slice(0, -1).indexOf(h);
1415
+ if (firstSeen >= 0) {
1416
+ const period = hashes.length - 1 - firstSeen;
1417
+ stableConfirm++;
1418
+ // quasi-stable state = volume boundary in saga/serial modes
1419
+ if ((lengthMode === "saga" || lengthMode === "serial") && t - volumeBounds[volumeBounds.length - 1] >= 6) volumeBounds.push(t);
1420
+ if (stableConfirm >= Math.max(1, Math.min(period, 3))) {
1421
+ const det = detectPattern(ticks, hashes, false);
1422
+ if (lengthMode === "flash" || targetAttractors.length === 0 || targetAttractors.includes(det.pattern) || stableConfirm >= 6) break;
1423
+ }
1424
+ } else stableConfirm = 0;
1425
+ if (t === budget) budgetHit = tk.activeCount > 0;
1426
+ }
1427
+
1428
+ // v3 correction of a v2 dead branch: under a hard budget the structural
1429
+ // classifiers (RING / SPLIT / GLIDER / GLIDER-GUN / METHUSELAH) were never
1430
+ // reachable because `budgetHit` short-circuited to BUDGET-TRUNCATED. §11.5
1431
+ // treats a translating or mirrored pattern at budget as a legitimate
1432
+ // attractor; only an unclassifiable live state is truncation.
1433
+ let det = detectPattern(ticks, hashes, false);
1434
+ if (budgetHit && ticks[ticks.length - 1].activeCount > 0) {
1435
+ const tail = ticks.slice(-4).map((x) => x.activeCount);
1436
+ const mean = tail.reduce((a, b2) => a + b2, 0) / Math.max(1, tail.length);
1437
+ const settled = tail.every((x) => Math.abs(x - mean) <= Math.max(1, mean * 0.2));
1438
+ if (det.pattern === "METHUSELAH" && !settled) det = detectPattern(ticks, hashes, true); // → BUDGET-TRUNCATED
1439
+ }
1440
+ const attractorInTarget = targetAttractors.includes(det.pattern);
1441
+ const endingFamily = attractorInTarget ? contract.defaultEndingFamilies.find((e) => (ENDING_TO_ATTRACTOR[e] ?? []).includes(det.pattern)) ?? PATTERN_ENDING[det.pattern] : PATTERN_ENDING[det.pattern];
1442
+ const compressionAdvice: string[] = [];
1443
+ if (det.pattern === "BUDGET-TRUNCATED") compressionAdvice.push("Budget exhausted before an attractor: merge parallel strands, retire one institution, or raise the tick budget. Realization must NOT begin (§11.6).");
1444
+ if (!attractorInTarget && det.pattern !== "BUDGET-TRUNCATED") compressionAdvice.push(`Attractor ${det.pattern} is outside the contract's target set [${targetAttractors.join(", ")}] — accept as a declared deviation or re-seed.`);
1445
+ const openAtEnd = promises.filter((p) => p.state === "SEEDED");
1446
+ for (const p of openAtEnd) { p.state = "RESIDUALIZED"; p.closedTick = ticks.length - 1; }
1447
+
1448
+ // ── compile scenes/chapters/volumes
1449
+ if (volumeBounds[volumeBounds.length - 1] !== ticks.length - 1) volumeBounds.push(ticks.length - 1);
1450
+ const volumeOf = (t: number) => Math.max(1, volumeBounds.findIndex((vb) => t <= vb) || 1);
1451
+ const scenes: MorphRun["scenes"] = [];
1452
+ const grammarWeights = contract.sceneGrammarWeights;
1453
+ for (const tk of ticks) {
1454
+ for (const a of tk.active) {
1455
+ const cell = cellByOwner.get(a.rowId); const el = a.elementId ? manifest.byId.get(a.elementId) ?? null : null;
1456
+ if (!cell) continue;
1457
+ const g = (el?.sceneGrammars ?? ["ACTION"]).slice().sort((x, y) => (grammarWeights[y] ?? 0) - (grammarWeights[x] ?? 0))[0];
1458
+ scenes.push({
1459
+ index: scenes.length + 1, tick: tk.t, cellId: cell.cellId, owner: cell.owner, kind: cell.kind, element: el, grammar: g,
1460
+ stakes: el?.stakesAxes[0] ?? "moral", cost: el?.costAxes[0] ?? "attention",
1461
+ interior: g === "PROCESSING" || g === "AFTERMATH" || g === "RELATIONAL" || (el?.costAxes.includes("attention") ?? false),
1462
+ volume: volumeOf(tk.t), chapter: tk.t + 1,
1463
+ });
1464
+ }
1465
+ }
1466
+ const chapters: MorphRun["chapters"] = ticks.map((tk) => {
1467
+ const sc = scenes.filter((s) => s.tick === tk.t);
1468
+ const cnt = new Map<StakesAxis, number>(); sc.forEach((s) => cnt.set(s.stakes, (cnt.get(s.stakes) ?? 0) + 1));
1469
+ return { index: tk.t + 1, volume: volumeOf(tk.t), scenes: sc.length, intensity: tk.intensity, dominantStakes: [...cnt.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null };
1470
+ });
1471
+ const volumes: MorphRun["volumes"] = [];
1472
+ for (let i = 1; i < volumeBounds.length; i++) {
1473
+ const start = volumeBounds[i - 1], end = volumeBounds[i];
1474
+ const paid = promises.filter((p) => p.closedTick != null && p.closedTick > start && p.closedTick <= end && p.state === "PAID").length;
1475
+ volumes.push({ index: i, startTick: start, endTick: end, localClosure: paid > 0 || i === volumeBounds.length - 1,
1476
+ seriesDelta: deltas.filter((x) => x.tick > start && x.tick <= end && x.to === "SERIES").length + " series-scale deltas" });
1477
+ }
1478
+
1479
+ return {
1480
+ genome, userSeed: opts.userSeed >>> 0, lengthMode, budgetTicks: budget, cells, ticks, stateHashes: hashes, checkpoints,
1481
+ deltas, promises, attractor: det.pattern, attractorDetail: det.detail, attractorInTarget, endingFamily, targetAttractors,
1482
+ volumes, births, masked, compressionAdvice, scenes, chapters,
1483
+ stats: {
1484
+ births: births.length, deaths, distinctElements: new Set(births.map((x) => x.elementId)).size,
1485
+ slotsExercised: new Set(births.map((x) => manifest.byId.get(x.elementId)?.primarySlot)).size,
1486
+ castLoadMax, loopDebtMax,
1487
+ },
1488
+ };
1489
+ }
1490
+
1491
+ export const SLOT_CLASS_LIST = SLOT_CLASS_IDS;