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,1161 @@
1
+ /**
2
+ * SAGA-OS v4 — AUREATE-SCRIBE (SYSTEM v15)
3
+ * ---------------------------------------------------------------------------
4
+ * Type C net-new. Real, deterministic, offline implementations of the v4
5
+ * subsystems that the package (`saga-os-v4.ts`) carries only as canonical
6
+ * tables plus two helpers.
7
+ *
8
+ * §4 STYLEBOOK-COMPILER nine executable style layers (Williams, Pinker,
9
+ * Strunk, Dreyer, Booth, Le Guin, Zinsser, Clark,
10
+ * Tufte) as passes over real text
11
+ * §5 STYLE-GENOME formal genotype + genre defaults + pack overlay
12
+ * §6 SENTENCE ENGINE SentencePlan, 16 syntax forms, selection rule,
13
+ * characters-as-subjects / actions-as-verbs analyzer
14
+ * §7 PARAGRAPH ENGINE 10 paragraph types, move detection, PAR-AUD-001..006
15
+ * §8 IMAGECHAIN ENGINE motif family extraction, recurrence, IMG-001..006
16
+ * §9 SOUND / RHYTHM 10 sound variables, 8 rhythm profiles, RHY-AUD-001..005
17
+ * §10 POV / FOCALIZATION POV-AUD-001..005
18
+ * §11 DIALOGUE ENGINE speech acts, 11-dim idiolect vector, DIA-AUD-001..006
19
+ * §12 ARGUMENT / WARRANT passage classification, ARG-AUD-001..005
20
+ * §13 REVISION STACK PASS-001..015 executed in order over real prose
21
+ * §14 KALLOS-S 12 weighted dims + 5 hard gates (fail ⇒ 0)
22
+ * §15 STYLE-CGOL style-state automaton over discourse units
23
+ * §17 STYLE PACKS 7 packs as genome deltas
24
+ * §19 STYLE-TEST-001..015 executable acceptance tests
25
+ *
26
+ * RUNTIME HONESTY
27
+ * - Pure functions. No LLM, no network, no filesystem, no clock inside math.
28
+ * - Every analyzer is LEXICAL/HEURISTIC over real English text: regex families,
29
+ * closed word lists, syllable counting, positional statistics. There is NO
30
+ * parser and NO semantic model here. That ceiling is declared on every report
31
+ * via `method`, and STYLE-TEST rows that need a parser or a human are marked
32
+ * NOT_EXECUTABLE with the concrete blocker rather than faked.
33
+ * - v4 §0's own non-overclaim boundary is preserved verbatim in `V4_HONESTY`.
34
+ */
35
+
36
+ import { computeProseMetrics } from '@/lib/sage/judge.ts';
37
+ import { STYLE_GENOME_GENRE_DEFAULTS, selectSyntaxForm, auditWanIntensifiers } from '@/lib/sage/saga-os-v4.ts';
38
+ import { sha256Hex, GenomeRng } from "./saga-os-v3-core";
39
+
40
+
41
+ export const SAGA_OS_V4_AUREATE_VERSION =
42
+ "SAGA-OS v4 AUREATE-SCRIBE (SYSTEM v15) — STYLEBOOK-COMPILER · STYLE-GENOME · SENTENCE/PARAGRAPH/IMAGE/SOUND/POV/DIALOGUE/ARGUMENT engines · REVISION STACK · KALLOS-S · STYLE-CGOL";
43
+
44
+ export const V4_HONESTY = [
45
+ "SAGA-OS v4 converts style into explicit, audited generative machinery, but no system can guarantee award-winning prose without implementation, corpus calibration, human evaluation, and iterative editorial testing.",
46
+ "Every analyzer below is lexical/heuristic. There is no syntactic parser and no semantic model in this build; a violation written in plain, well-formed words can pass an audit it should fail.",
47
+ "Hemingway-style readability checks are used as MINOR audits only and are explicitly prevented from flattening literary prose (v4 §18).",
48
+ ] as const;
49
+
50
+ const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
51
+ const words = (t: string) => (t.trim().match(/[A-Za-z'’-]+/g) || []);
52
+ const sentencesOf = (t: string) =>
53
+ (t.replace(/\s+/g, " ").match(/[^.!?]+[.!?]+(?=\s|$)|[^.!?]+$/g) || []).map((s) => s.trim()).filter((s) => s.length > 1);
54
+ /**
55
+ * Discourse units. Prose paragraphs are blank-line separated, but dialogue is
56
+ * conventionally one speaker per LINE with no blank line between turns.
57
+ * Splitting only on blank lines merged whole dialogue exchanges into a single
58
+ * "paragraph" and hid every per-turn audit — verified defect.
59
+ */
60
+ const paragraphsOf = (t: string): string[] => {
61
+ const out: string[] = [];
62
+ for (const block of t.split(/\n\s*\n/)) {
63
+ const lines = block.split("\n").map((l) => l.trim()).filter(Boolean);
64
+ if (!lines.length) continue;
65
+ let buf: string[] = [];
66
+ const flush = () => { if (buf.length) { out.push(buf.join(" ")); buf = []; } };
67
+ for (const line of lines) {
68
+ if (/^["“]/.test(line)) { flush(); out.push(line); } else buf.push(line);
69
+ }
70
+ flush();
71
+ }
72
+ return out;
73
+ };
74
+ function syllables(w: string): number {
75
+ const s = w.toLowerCase().replace(/[^a-z]/g, "");
76
+ if (!s) return 0;
77
+ const m = s.replace(/e$/, "").match(/[aeiouy]+/g);
78
+ return Math.max(1, m ? m.length : 1);
79
+ }
80
+
81
+ // ═══════════════════════════════════════════════════════════════════════════
82
+ // §5 STYLE-GENOME
83
+ // ═══════════════════════════════════════════════════════════════════════════
84
+
85
+ export type ProseMode =
86
+ | "classic" | "transparent" | "lyrical" | "spare" | "baroque"
87
+ | "comic" | "gothic" | "procedural" | "intimate" | "epic";
88
+ export type FocalDistance = "close" | "medium" | "far" | "roving";
89
+
90
+ export type StyleGenome = {
91
+ clarityLevel: number; ornamentLevel: number; sentenceMusic: number;
92
+ syntacticComplexity: number; concreteness: number; interiority: number;
93
+ compression: number; ambiguityTolerance: number; dialogueDensity: number;
94
+ imageDensity: number; soundDensity: number; humorDensity: number;
95
+ rhetoricalFormality: number; focalDistance: FocalDistance; proseMode: ProseMode;
96
+ };
97
+
98
+ export const STYLE_GENOME_KEYS: (keyof StyleGenome)[] = [
99
+ "clarityLevel", "ornamentLevel", "sentenceMusic", "syntacticComplexity", "concreteness",
100
+ "interiority", "compression", "ambiguityTolerance", "dialogueDensity", "imageDensity",
101
+ "soundDensity", "humorDensity", "rhetoricalFormality",
102
+ ];
103
+
104
+ /** §5.1 genre defaults, read from the package table (12 numeric columns). */
105
+ const GENRE_ROW_ORDER = [
106
+ "clarityLevel", "ornamentLevel", "sentenceMusic", "syntacticComplexity", "concreteness",
107
+ "interiority", "compression", "ambiguityTolerance", "dialogueDensity", "imageDensity",
108
+ "soundDensity", "rhetoricalFormality",
109
+ ] as const;
110
+
111
+ const MODE_FOR_GENRE: Record<string, ProseMode> = {
112
+ romance: "intimate", thriller: "spare", mystery: "classic", literary: "lyrical",
113
+ fantasy: "epic", horror: "gothic", institutional: "procedural",
114
+ };
115
+ const FOCAL_FOR_GENRE: Record<string, FocalDistance> = {
116
+ romance: "close", thriller: "close", mystery: "medium", literary: "close",
117
+ fantasy: "roving", horror: "close", institutional: "far",
118
+ };
119
+
120
+ /** Map a v3 GenreContract family/key onto the v4 genre-default rows. */
121
+ export function genreDefaultKey(genreKey: string, family?: string): string {
122
+ const k = genreKey.toLowerCase();
123
+ if (STYLE_GENOME_GENRE_DEFAULTS[k]) return k;
124
+ if (/thriller/.test(k)) return "thriller";
125
+ if (/mystery|cozy|procedural/.test(k)) return "mystery";
126
+ if (/horror/.test(k)) return "horror";
127
+ if (/romantasy|fantasy/.test(k)) return "fantasy";
128
+ if (/romance/.test(k)) return "romance";
129
+ if (/institutional|maintenance/.test(k)) return "institutional";
130
+ if (/literary|general|historical|family|saga|ya|comedy|science/.test(k)) return "literary";
131
+ const f = (family ?? "").toUpperCase();
132
+ if (f === "THRILLER") return "thriller";
133
+ if (f === "MYSTERY") return "mystery";
134
+ if (f === "HORROR") return "horror";
135
+ if (f === "FANTASY") return "fantasy";
136
+ if (f === "ROMANCE") return "romance";
137
+ return "literary";
138
+ }
139
+
140
+ export function buildStyleGenome(genreKey: string, family?: string, overrides?: Partial<StyleGenome>): StyleGenome {
141
+ const key = genreDefaultKey(genreKey, family);
142
+ const row = STYLE_GENOME_GENRE_DEFAULTS[key] ?? STYLE_GENOME_GENRE_DEFAULTS.literary;
143
+ const g = {} as StyleGenome;
144
+ GENRE_ROW_ORDER.forEach((field, i) => { (g as unknown as Record<string, number>)[field] = row[i] ?? 0.5; });
145
+ g.humorDensity = key === "literary" ? 0.25 : key === "romance" ? 0.35 : 0.15;
146
+ g.proseMode = MODE_FOR_GENRE[key] ?? "classic";
147
+ g.focalDistance = FOCAL_FOR_GENRE[key] ?? "medium";
148
+ return { ...g, ...overrides };
149
+ }
150
+
151
+ // §17 production style packs — deltas applied on top of the genre genome
152
+ export type StylePackId =
153
+ | "STYLEPACK-SPARE" | "STYLEPACK-CLASSIC" | "STYLEPACK-GRACE" | "STYLEPACK-LYRICAL"
154
+ | "STYLEPACK-COPYCHIEF" | "STYLEPACK-ARGUMENT" | "STYLEPACK-POPULAR";
155
+
156
+ export const STYLE_PACK_DELTAS: Record<StylePackId, { note: string; delta: Partial<StyleGenome> }> = {
157
+ "STYLEPACK-SPARE": { note: "Strunk/Zinsser-heavy: high compression, low ornament, high concreteness", delta: { compression: 0.9, ornamentLevel: 0.12, concreteness: 0.9, syntacticComplexity: 0.28, proseMode: "spare" } },
158
+ "STYLEPACK-CLASSIC": { note: "Pinker/Williams-heavy: reader gaze, clear syntax, topic-stress", delta: { clarityLevel: 0.92, ornamentLevel: 0.3, syntacticComplexity: 0.45, proseMode: "classic" } },
159
+ "STYLEPACK-GRACE": { note: "Williams/Tufte-heavy: balanced syntax, emphatic endings, cadence", delta: { clarityLevel: 0.85, sentenceMusic: 0.72, syntacticComplexity: 0.6, ornamentLevel: 0.45, proseMode: "classic" } },
160
+ "STYLEPACK-LYRICAL": { note: "Le Guin/Tufte-heavy: sound, image, long curves, omission", delta: { sentenceMusic: 0.88, imageDensity: 0.85, soundDensity: 0.85, syntacticComplexity: 0.75, compression: 0.35, proseMode: "lyrical" } },
161
+ "STYLEPACK-COPYCHIEF": { note: "Dreyer-heavy: grammar hygiene, house style, punctuation judgment", delta: { clarityLevel: 0.9, ornamentLevel: 0.25, compression: 0.7, rhetoricalFormality: 0.6, proseMode: "transparent" } },
162
+ "STYLEPACK-ARGUMENT": { note: "Booth-heavy: claim-reason-evidence-warrant passages", delta: { clarityLevel: 0.9, rhetoricalFormality: 0.7, interiority: 0.7, ambiguityTolerance: 0.25, proseMode: "classic" } },
163
+ "STYLEPACK-POPULAR": { note: "Clark/Zinsser/Pinker hybrid: subject/verb drive, pattern/twist, low clutter", delta: { clarityLevel: 0.88, compression: 0.75, ornamentLevel: 0.28, dialogueDensity: 0.7, proseMode: "transparent" } },
164
+ };
165
+
166
+ export function applyStylePack(g: StyleGenome, pack: StylePackId | null): StyleGenome {
167
+ return pack ? { ...g, ...STYLE_PACK_DELTAS[pack].delta } : g;
168
+ }
169
+
170
+ export const STYLE_PACK_KEY = "veritas.saga.stylePack.v1";
171
+ export function loadStylePack(): StylePackId | null {
172
+ try { const v = localStorage.getItem(STYLE_PACK_KEY); return v && v in STYLE_PACK_DELTAS ? (v as StylePackId) : null; } catch { return null; }
173
+ }
174
+ export function saveStylePack(p: StylePackId | null): void {
175
+ try { if (p) localStorage.setItem(STYLE_PACK_KEY, p); else localStorage.removeItem(STYLE_PACK_KEY); } catch { /* optional */ }
176
+ }
177
+
178
+ // ═══════════════════════════════════════════════════════════════════════════
179
+ // Shared lexicons (closed lists — the declared ceiling of this build)
180
+ // ═══════════════════════════════════════════════════════════════════════════
181
+
182
+ const NOMINALIZATION_RE = /\b[a-z]{3,}(?:tion|sion|ment|ance|ence|ity|ness|ing)\b/gi;
183
+ const BE_VERBS = /\b(is|are|was|were|be|been|being|am)\b/gi;
184
+ /**
185
+ * Williams' central diagnosis is not "a be-verb plus a noun" — it is that the
186
+ * real ACTION has been buried in a nominalization while the verb slot is filled
187
+ * by an empty one. Light/support verbs ("made a determination", "gave
188
+ * consideration to", "took an examination") are the canonical case and were
189
+ * previously invisible to the detector: `beVerbCount` was 0, so
190
+ * `actionIsVerb` returned true for prose that is exactly what Style warns
191
+ * against. Verified by adversarial attack A1.
192
+ */
193
+ const LIGHT_VERB_NOM_RE = /\b(?:make|makes|made|making|give|gives|gave|given|giving|take|takes|took|taken|taking|have|has|had|having|do|does|did|doing|perform|performs|performed|conduct|conducts|conducted|undertake|undertakes|undertook|undertaken|provide|provides|provided|carry\s+out|carries\s+out|carried\s+out|reach|reaches|reached)\s+(?:a|an|the|some|any|no)?\s*[a-z]{3,}(?:tion|sion|ment|ance|ence|ity|ness|al|ing)\b/gi;
194
+ const PASSIVE_RE = /\b(is|are|was|were|be|been|being)\s+(\w+(?:ed|en))\b/gi;
195
+ const ADVERB_LY_RE = /\b\w{4,}ly\b/gi;
196
+ const HEDGE_RE = /\b(somewhat|rather|quite|fairly|perhaps|maybe|possibly|arguably|seemingly|apparently|sort of|kind of)\b/gi;
197
+ const CLUTTER_RE = /\b(in order to|due to the fact that|at this point in time|the fact that|in terms of|for all intents and purposes|it should be noted that|there (?:is|are|was|were)\b)/gi;
198
+ const CONCRETE_SENSE_RE = /\b(salt|ash|iron|rust|dust|smoke|glass|stone|wood|cloth|paper|water|rain|mud|blood|bone|skin|hair|bread|oil|wire|nail|rope|thread|coal|clay|wool|leather|brick|frost|grease|copper|tin|silt|lime|straw|hay|sand|resin|tar|chalk|wax|soot|grain|milk|honey|vinegar)\b/gi;
199
+ const ABSTRACT_RE = /\b(freedom|justice|beauty|truth|reality|society|existence|meaning|purpose|identity|concept|notion|aspect|situation|condition|process|system|structure|element|factor|issue|matter)\b/gi;
200
+ const CLICHE_LIST = [
201
+ "at the end of the day", "avoid like the plague", "back to square one", "beat around the bush",
202
+ "better late than never", "bite the bullet", "blessing in disguise", "break the ice",
203
+ "calm before the storm", "cat got your tongue", "cold as ice", "cry over spilled milk",
204
+ "dead as a doornail", "diamond in the rough", "easier said than done", "every cloud has a silver lining",
205
+ "few and far between", "fit as a fiddle", "give it your all", "heart of gold",
206
+ "in the nick of time", "last but not least", "leave no stone unturned", "light as a feather",
207
+ "needle in a haystack", "nip it in the bud", "only time will tell", "pale as a ghost",
208
+ "piece of cake", "quiet as a mouse", "read between the lines", "silence was deafening",
209
+ "sea of faces", "sharp as a tack", "sight for sore eyes", "time heals all wounds",
210
+ "tip of the iceberg", "under the weather", "white as a sheet", "world at your feet",
211
+ "heart skipped a beat", "blood ran cold", "breath caught in", "let out a breath she didn't know",
212
+ "little did", "eyes widened", "shiver down", "deafening silence",
213
+ ];
214
+ const SPEECH_ACT_LEX: [RegExp, string][] = [
215
+ [/\?\s*["'”’]?\s*$/, "QUESTION"],
216
+ [/\b(please|would you|could you|will you|can you)\b/i, "REQUEST"],
217
+ [/\b(don't|do not|stop|never|no\.|get out|leave)\b/i, "PROHIBIT"],
218
+ [/\b(i (?:promise|swear|will)|we will)\b/i, "COMMIT"],
219
+ [/\b(sorry|apolog|forgive)\b/i, "APOLOGISE"],
220
+ [/\b(you (?:always|never)|your fault|because of you)\b/i, "ACCUSE"],
221
+ [/\b(i think|i believe|i suppose|maybe|perhaps)\b/i, "HEDGE"],
222
+ [/!\s*["'”’]?\s*$/, "EXCLAIM"],
223
+ ];
224
+ const ARG_CLAIM_RE = /\b(therefore|thus|so it (?:is|was)|which means|the point is|that is why|it follows)\b/i;
225
+ const ARG_REASON_RE = /\b(because|since|as a result of|owing to|given that|for this reason)\b/i;
226
+ const ARG_EVIDENCE_RE = /\b(\d|the ledger|the record|the letter|she saw|he saw|the report|the form|the invoice|showed|proved|witness)\b/i;
227
+ const ARG_QUALIFY_RE = /\b(unless|except|although|though|admittedly|granted|but only if)\b/i;
228
+ const FUTURE_LEAK_RE = /\b(later (?:she|he|they) would|would (?:soon )?(?:learn|discover|realise|realize)|years later|little did (?:she|he|they) know|would come to)\b/i;
229
+ const EMOTION_LABEL_RE = /\b(felt (?:sad|angry|happy|afraid|guilty|ashamed)|was (?:sad|angry|happy|afraid|guilty|ashamed)|felt an? \w+)\b/gi;
230
+ const FIGURATIVE_RE = /\b(like a|like the|as if|as though|resembl\w+|reminded \w+ of)\b/gi;
231
+
232
+ // ═══════════════════════════════════════════════════════════════════════════
233
+ // §6 SENTENCE ENGINE — plans + real analysis of finished sentences
234
+ // ═══════════════════════════════════════════════════════════════════════════
235
+
236
+ export type SyntaxFormId =
237
+ | "SYN-001" | "SYN-002" | "SYN-003" | "SYN-004" | "SYN-005" | "SYN-006" | "SYN-007" | "SYN-008"
238
+ | "SYN-009" | "SYN-010" | "SYN-011" | "SYN-012" | "SYN-013" | "SYN-014" | "SYN-015" | "SYN-016";
239
+
240
+ export const SYNTAX_FORM_NAMES: Record<SyntaxFormId, string> = {
241
+ "SYN-001": "Kernel sentence", "SYN-002": "Cumulative sentence", "SYN-003": "Periodic sentence",
242
+ "SYN-004": "Balanced sentence", "SYN-005": "Antithetical sentence", "SYN-006": "Appositive expansion",
243
+ "SYN-007": "Parallel triad", "SYN-008": "Fragment with prior license", "SYN-009": "Question sentence",
244
+ "SYN-010": "Negative sentence", "SYN-011": "Inverted sentence", "SYN-012": "Dialogue beat sentence",
245
+ "SYN-013": "Image sentence", "SYN-014": "Procedural sentence", "SYN-015": "Lyrical long sentence",
246
+ "SYN-016": "Hard stop sentence",
247
+ };
248
+
249
+ export type RhythmClass = "short" | "medium" | "long" | "very-long";
250
+
251
+ export type SentenceAnalysis = {
252
+ index: number;
253
+ text: string;
254
+ wordCount: number;
255
+ rhythmClass: RhythmClass;
256
+ syntaxForm: SyntaxFormId;
257
+ /** Williams: is the first noun phrase also the actor? (heuristic) */
258
+ subjectIsCharacter: boolean;
259
+ /** Williams: is the main action a finite verb rather than a nominalization? */
260
+ actionIsVerb: boolean;
261
+ nominalizations: string[];
262
+ /** Williams: an empty light/support verb carrying the real action as a noun. */
263
+ lightVerbNominalization: boolean;
264
+ passive: boolean;
265
+ beVerbCount: number;
266
+ /** old→new: does the opening overlap with the previous sentence's tail? */
267
+ oldBeforeNew: boolean | null;
268
+ /** stress: does it end on content rather than a function-word tail? */
269
+ emphaticEnd: boolean;
270
+ clutterHits: string[];
271
+ hedges: string[];
272
+ adverbs: string[];
273
+ concreteHits: string[];
274
+ abstractHits: string[];
275
+ figurative: number;
276
+ isDialogue: boolean;
277
+ syllablesPerWord: number;
278
+ };
279
+
280
+ const FUNCTION_TAIL = new Set(["the", "a", "an", "of", "to", "in", "on", "at", "for", "with", "and", "or", "but", "that", "it", "is", "was", "as", "by", "from", "this", "there", "been", "very", "too", "so"]);
281
+ const PRONOUN_SUBJ = new Set(["he", "she", "they", "i", "we", "you", "it"]);
282
+
283
+ function detectSyntaxForm(s: string, w: string[], prior: SentenceAnalysis | null): SyntaxFormId {
284
+ const t = s.trim();
285
+ if (/^["'“]/.test(t) || /["'”]\s*$/.test(t)) return "SYN-012";
286
+ if (/\?$/.test(t)) return "SYN-009";
287
+ if (w.length <= 4 && !/[.!?]$/.test(t)) return "SYN-008";
288
+ if (w.length <= 5) return "SYN-016";
289
+ if (/\bnot\b|\bnever\b|\bno\b/i.test(t) && w.length < 20) return "SYN-010";
290
+ if (/,\s*(?:a|an|the)\s+[a-z]+[^,]*,/i.test(t)) return "SYN-006";
291
+ if (/\b\w+,\s*\w+,\s*and\s+\w+/i.test(t)) return "SYN-007";
292
+ if (/\bbut\b|\byet\b|\bwhereas\b|\bwhile\b/i.test(t) && /,/.test(t)) return "SYN-005";
293
+ if (/^(?:Not|Never|Only|Down|Up|Into|Across|Behind|Beneath)\b/.test(t)) return "SYN-011";
294
+ if (/^[A-Z][a-z]*(?:ing|ed)\b|^(?:After|Before|When|Because|Although|Since|If)\b/.test(t) && w.length > 14) return "SYN-003";
295
+ if (w.length > 32) return "SYN-015";
296
+ if (/,\s*\w+ing\b/.test(t)) return "SYN-002";
297
+ if (FIGURATIVE_RE.test(t)) { FIGURATIVE_RE.lastIndex = 0; return "SYN-013"; }
298
+ if (/\b(then|next|first|second|after that|meanwhile)\b/i.test(t)) return "SYN-014";
299
+ if (/;|\bboth\b.*\band\b/.test(t) && w.length > 12) return "SYN-004";
300
+ return prior?.syntaxForm === "SYN-001" && w.length > 18 ? "SYN-002" : "SYN-001";
301
+ }
302
+
303
+ export function analyzeSentences(text: string): SentenceAnalysis[] {
304
+ const out: SentenceAnalysis[] = [];
305
+ const sents = sentencesOf(text);
306
+ for (let i = 0; i < sents.length; i++) {
307
+ const s = sents[i];
308
+ const w = words(s);
309
+ const lower = s.toLowerCase();
310
+ const prior = out[i - 1] ?? null;
311
+ const firstWords = w.slice(0, 4).map((x) => x.toLowerCase());
312
+ // Williams: the sentence's main character should BE the grammatical subject.
313
+ // A capital letter is not evidence of that — "There was a realization…" and
314
+ // "The implementation of…" both opened with a capital and were scored as
315
+ // character-subjects (verified defect). Reject expletive openings and
316
+ // openings whose head noun is an abstraction/nominalization.
317
+ const bare = s.replace(/^["'“]\s*/, "");
318
+ const headMatch = /^(?:The|A|An)\s+([A-Za-z-]+)/i.exec(bare);
319
+ const headNoun = (headMatch?.[1] ?? "").toLowerCase();
320
+ const expletive = /^(?:there|it)\s+(?:is|are|was|were|has|have|had|seems|appears)\b/i.test(bare);
321
+ const headIsAbstract = !!headNoun && (NOMINALIZATION_RE.test(headNoun) || ABSTRACT_RE.test(headNoun));
322
+ NOMINALIZATION_RE.lastIndex = 0; ABSTRACT_RE.lastIndex = 0;
323
+ const properOpen = /^[A-Z][a-z]+\b/.test(bare) && !/^(?:The|A|An|There|It|This|That|These|Those)\b/.test(bare);
324
+ const subjectIsCharacter =
325
+ !expletive && !headIsAbstract &&
326
+ (firstWords.some((x) => PRONOUN_SUBJ.has(x)) || properOpen || (!!headNoun && !headIsAbstract));
327
+ const noms = s.match(NOMINALIZATION_RE) ?? [];
328
+ const realNoms = noms.filter((n) => !/^(?:something|nothing|morning|evening|during|building|ceiling|meeting)$/i.test(n));
329
+ const beCount = (s.match(BE_VERBS) ?? []).length;
330
+ const passive = PASSIVE_RE.test(s); PASSIVE_RE.lastIndex = 0;
331
+ LIGHT_VERB_NOM_RE.lastIndex = 0;
332
+ const lightVerbNom = LIGHT_VERB_NOM_RE.test(s);
333
+ LIGHT_VERB_NOM_RE.lastIndex = 0;
334
+ // The action is a real finite verb only if it is NOT buried behind a
335
+ // be-verb + nominalization, NOT buried behind a light/support verb +
336
+ // nominalization, and NOT passivised.
337
+ const actionIsVerb = !(beCount > 0 && realNoms.length > 0) && !lightVerbNom && !passive;
338
+ let oldBeforeNew: boolean | null = null;
339
+ if (prior) {
340
+ const priorTail = new Set(words(prior.text).slice(-6).map((x) => x.toLowerCase()));
341
+ const head = w.slice(0, 5).map((x) => x.toLowerCase());
342
+ oldBeforeNew = head.some((x) => priorTail.has(x) || PRONOUN_SUBJ.has(x));
343
+ }
344
+ const lastWord = (w[w.length - 1] ?? "").toLowerCase();
345
+ const emphaticEnd = !FUNCTION_TAIL.has(lastWord) && lastWord.length > 2;
346
+ const clutterHits = s.match(CLUTTER_RE) ?? [];
347
+ const hedges = s.match(HEDGE_RE) ?? [];
348
+ const adverbs = s.match(ADVERB_LY_RE) ?? [];
349
+ const concreteHits = s.match(CONCRETE_SENSE_RE) ?? [];
350
+ const abstractHits = s.match(ABSTRACT_RE) ?? [];
351
+ const fig = (s.match(FIGURATIVE_RE) ?? []).length;
352
+ const rhythmClass: RhythmClass = w.length <= 8 ? "short" : w.length <= 18 ? "medium" : w.length <= 32 ? "long" : "very-long";
353
+ out.push({
354
+ index: i, text: s, wordCount: w.length, rhythmClass,
355
+ syntaxForm: detectSyntaxForm(s, w, prior),
356
+ subjectIsCharacter, actionIsVerb, nominalizations: realNoms, lightVerbNominalization: lightVerbNom, passive, beVerbCount: beCount,
357
+ oldBeforeNew, emphaticEnd, clutterHits, hedges, adverbs, concreteHits, abstractHits,
358
+ figurative: fig, isDialogue: /^["'“]/.test(s.trim()),
359
+ syllablesPerWord: w.length ? w.reduce((a, x) => a + syllables(x), 0) / w.length : 0,
360
+ });
361
+ void lower;
362
+ }
363
+ return out;
364
+ }
365
+
366
+ /** §6.1 SentencePlan — the generative side (used by the planner UI + directive). */
367
+ export type SentencePlan = {
368
+ index: number;
369
+ mainCharacter: string; grammaticalSubject: string;
370
+ mainAction: string; finiteVerb: string;
371
+ oldInformation: string; newInformation: string;
372
+ topicPosition: string; stressPosition: string;
373
+ image?: string; soundPattern?: string;
374
+ rhythmClass: RhythmClass; syntaxForm: SyntaxFormId;
375
+ register: string; povDistance: FocalDistance;
376
+ warrantRole?: "CLAIM" | "REASON" | "EVIDENCE" | "WARRANT" | null;
377
+ };
378
+
379
+ /** §6.3 selection rule — delegates to the package selector, then records the plan. */
380
+ export function planSentences(x: {
381
+ seedHex: string; count: number; genome: StyleGenome;
382
+ sceneForce: "clarity" | "emotional" | "lyrical" | "argument" | "neutral";
383
+ character: string; action: string; oldInfo: string; newInfo: string;
384
+ image?: string;
385
+ }): SentencePlan[] {
386
+ const rng = new GenomeRng(sha256Hex(`aureate|plan|${x.seedHex}|${x.sceneForce}`));
387
+ const prior: string[] = [];
388
+ const plans: SentencePlan[] = [];
389
+ for (let i = 0; i < x.count; i++) {
390
+ const form = selectSyntaxForm({
391
+ clarityRequired: x.sceneForce === "clarity",
392
+ emotionalPeak: x.sceneForce === "emotional",
393
+ lyricalMode: x.sceneForce === "lyrical",
394
+ argumentMode: x.sceneForce === "argument",
395
+ }, prior as never[]) as SyntaxFormId;
396
+ prior.push(form);
397
+ const long = form === "SYN-015" || form === "SYN-002" || form === "SYN-003";
398
+ const shortF = form === "SYN-016" || form === "SYN-008";
399
+ plans.push({
400
+ index: i,
401
+ mainCharacter: x.character, grammaticalSubject: x.character,
402
+ mainAction: x.action, finiteVerb: x.action.split(/\s+/)[0] ?? x.action,
403
+ oldInformation: i === 0 ? x.oldInfo : x.newInfo,
404
+ newInformation: x.newInfo,
405
+ topicPosition: i === 0 ? x.oldInfo : x.newInfo,
406
+ stressPosition: x.newInfo,
407
+ image: x.image && rng.next() < x.genome.imageDensity ? x.image : undefined,
408
+ soundPattern: rng.next() < x.genome.soundDensity ? (rng.next() < 0.5 ? "liquid" : "plosive") : undefined,
409
+ rhythmClass: shortF ? "short" : long ? "long" : "medium",
410
+ syntaxForm: form,
411
+ register: x.genome.rhetoricalFormality > 0.5 ? "formal" : "plain",
412
+ povDistance: x.genome.focalDistance,
413
+ warrantRole: x.sceneForce === "argument" ? (i === 0 ? "CLAIM" : i === 1 ? "REASON" : i === 2 ? "EVIDENCE" : "WARRANT") : null,
414
+ });
415
+ }
416
+ return plans;
417
+ }
418
+
419
+ // ═══════════════════════════════════════════════════════════════════════════
420
+ // §7 PARAGRAPH ENGINE
421
+ // ═══════════════════════════════════════════════════════════════════════════
422
+
423
+ export type ParagraphTypeId =
424
+ | "PAR-001" | "PAR-002" | "PAR-003" | "PAR-004" | "PAR-005"
425
+ | "PAR-006" | "PAR-007" | "PAR-008" | "PAR-009" | "PAR-010";
426
+ export const PARAGRAPH_TYPE_NAMES: Record<ParagraphTypeId, string> = {
427
+ "PAR-001": "Action", "PAR-002": "Sensory", "PAR-003": "Interior-processing", "PAR-004": "Argument / reflection",
428
+ "PAR-005": "Dialogue", "PAR-006": "Institutional", "PAR-007": "Transition", "PAR-008": "Ritual",
429
+ "PAR-009": "Memory", "PAR-010": "Shock",
430
+ };
431
+
432
+ export type ParagraphAnalysis = {
433
+ index: number; text: string; type: ParagraphTypeId;
434
+ sentences: SentenceAnalysis[];
435
+ hasAnchor: boolean; oldNewChainRatio: number; finalSentenceStress: boolean;
436
+ abstractionAnchored: boolean; lengthSentences: number;
437
+ move: { anchor: string; development: string; turn: string; stress: string };
438
+ };
439
+
440
+ function classifyParagraph(p: string, sents: SentenceAnalysis[]): ParagraphTypeId {
441
+ const dial = sents.filter((s) => s.isDialogue).length / Math.max(1, sents.length);
442
+ if (dial >= 0.4) return "PAR-005";
443
+ if (/\b(remember|remembered|years ago|when (?:she|he|they) was|used to)\b/i.test(p)) return "PAR-009";
444
+ if (/\b(office|registry|form|permit|ruling|committee|clerk|statute|ordinance|court)\b/i.test(p)) return "PAR-006";
445
+ if (/\b(rite|ritual|ceremony|procession|liturgy|vigil|oath)\b/i.test(p)) return "PAR-008";
446
+ if (sents.length <= 2 && sents.some((s) => s.wordCount <= 8)) return "PAR-010";
447
+ if (ARG_CLAIM_RE.test(p) || ARG_REASON_RE.test(p)) return "PAR-004";
448
+ if (/\b(thought|wondered|considered|decided|knew|realised|realized|wanted)\b/i.test(p)) return "PAR-003";
449
+ if ((p.match(CONCRETE_SENSE_RE) ?? []).length >= 2) return "PAR-002";
450
+ if (/\b(walked|ran|opened|closed|lifted|pushed|pulled|crossed|struck|carried)\b/i.test(p)) return "PAR-001";
451
+ return "PAR-007";
452
+ }
453
+
454
+ export function analyzeParagraphs(text: string): ParagraphAnalysis[] {
455
+ return paragraphsOf(text).map((p, i) => {
456
+ const sents = analyzeSentences(p);
457
+ const chain = sents.slice(1);
458
+ const oldNew = chain.length ? chain.filter((s) => s.oldBeforeNew).length / chain.length : 1;
459
+ const last = sents[sents.length - 1];
460
+ const abstractCount = sents.reduce((a, s) => a + s.abstractHits.length, 0);
461
+ const concreteCount = sents.reduce((a, s) => a + s.concreteHits.length, 0);
462
+ return {
463
+ index: i, text: p, type: classifyParagraph(p, sents), sentences: sents,
464
+ hasAnchor: !!sents[0] && (sents[0].subjectIsCharacter || sents[0].concreteHits.length > 0),
465
+ oldNewChainRatio: oldNew,
466
+ finalSentenceStress: !!last?.emphaticEnd,
467
+ abstractionAnchored: abstractCount === 0 || concreteCount >= 1,
468
+ lengthSentences: sents.length,
469
+ move: {
470
+ anchor: sents[0]?.text.slice(0, 60) ?? "",
471
+ development: sents[1]?.text.slice(0, 60) ?? "",
472
+ turn: sents.length > 2 ? sents[Math.floor(sents.length / 2)].text.slice(0, 60) : "",
473
+ stress: last?.text.slice(-60) ?? "",
474
+ },
475
+ };
476
+ });
477
+ }
478
+
479
+ // ═══════════════════════════════════════════════════════════════════════════
480
+ // §8 IMAGECHAIN ENGINE
481
+ // ═══════════════════════════════════════════════════════════════════════════
482
+
483
+ export type ImageFamily = {
484
+ motifId: string; sourceDomain: string; sensoryChannel: string;
485
+ occurrences: { paragraph: number; sentence: number; context: string }[];
486
+ contextsDiffer: boolean; transformed: boolean;
487
+ };
488
+
489
+ const IMAGE_DOMAINS: [string, RegExp, string][] = [
490
+ ["water", /\b(water|river|tide|rain|flood|current|salt|wave|damp|drip|well)\w*\b/gi, "touch/sight"],
491
+ ["fire", /\b(fire|flame|ember|ash|smoke|burn|scorch|coal|soot)\w*\b/gi, "sight/smell"],
492
+ ["metal", /\b(iron|steel|copper|tin|rust|nail|wire|blade|hinge|chain)\w*\b/gi, "touch/sound"],
493
+ ["cloth", /\b(cloth|thread|wool|linen|seam|hem|stitch|weave|coat)\w*\b/gi, "touch"],
494
+ ["stone", /\b(stone|brick|gravel|slate|granite|wall|floor|step)\w*\b/gi, "touch/sight"],
495
+ ["light", /\b(light|shadow|glare|dusk|dawn|lamp|glint|dark)\w*\b/gi, "sight"],
496
+ ["bone", /\b(bone|skull|rib|spine|tooth|marrow)\w*\b/gi, "touch"],
497
+ ["paper", /\b(paper|ledger|page|ink|letter|receipt|form|record)\w*\b/gi, "sight/touch"],
498
+ ["plant", /\b(root|leaf|branch|seed|grain|straw|hay|moss|vine)\w*\b/gi, "sight/smell"],
499
+ ["sound", /\b(hum|creak|echo|silence|bell|knock|scrape|whistle)\w*\b/gi, "hearing"],
500
+ ];
501
+
502
+ export function extractImageChains(text: string): ImageFamily[] {
503
+ const paras = paragraphsOf(text);
504
+ const out: ImageFamily[] = [];
505
+ for (const [domain, re, channel] of IMAGE_DOMAINS) {
506
+ const occ: ImageFamily["occurrences"] = [];
507
+ paras.forEach((p, pi) => {
508
+ const sents = sentencesOf(p);
509
+ sents.forEach((s, si) => {
510
+ re.lastIndex = 0;
511
+ // count EVERY hit in the sentence, not just the first: "the gate stuck"
512
+ // and "the iron" in one sentence are two occurrences of the metal family.
513
+ const hits = s.match(re);
514
+ if (hits) for (let h = 0; h < hits.length; h++) occ.push({ paragraph: pi, sentence: si, context: s.slice(0, 90) });
515
+ });
516
+ });
517
+ if (occ.length === 0) continue;
518
+ // Compare DISTINCT host sentences. Multiple hits inside one sentence share
519
+ // the same context string, so comparing raw occurrences made every family
520
+ // look un-transformed (verified defect). IMG-002 is about the motif
521
+ // returning in a changed context, which is a between-sentence property.
522
+ const seenCtx = new Set<string>();
523
+ const distinctCtx: string[] = [];
524
+ for (const o of occ) {
525
+ const key = `${o.paragraph}:${o.sentence}`;
526
+ if (seenCtx.has(key)) continue;
527
+ seenCtx.add(key); distinctCtx.push(o.context);
528
+ }
529
+ const ctxWords = distinctCtx.map((c) => new Set(words(c).map((w) => w.toLowerCase())));
530
+ let differ = ctxWords.length >= 2;
531
+ for (let i = 1; i < ctxWords.length; i++) {
532
+ const a = ctxWords[i - 1], b = ctxWords[i];
533
+ const inter = [...b].filter((w) => a.has(w)).length;
534
+ const jaccard = inter / Math.max(1, new Set([...a, ...b]).size);
535
+ if (jaccard > 0.6) { differ = false; break; }
536
+ }
537
+ out.push({
538
+ motifId: `IMG-${domain.toUpperCase()}`, sourceDomain: domain, sensoryChannel: channel,
539
+ occurrences: occ, contextsDiffer: differ,
540
+ // recurrence is measured in DISTINCT host sentences, not raw hits
541
+ transformed: distinctCtx.length >= 2 && differ,
542
+ });
543
+ }
544
+ return out.sort((a, b) => b.occurrences.length - a.occurrences.length);
545
+ }
546
+
547
+ // ═══════════════════════════════════════════════════════════════════════════
548
+ // §9 SOUND AND RHYTHM ENGINE
549
+ // ═══════════════════════════════════════════════════════════════════════════
550
+
551
+ export type SoundVariables = {
552
+ assonance: number; consonance: number; alliteration: number; sibilance: number;
553
+ plosiveDensity: number; liquidDensity: number; vowelOpenness: number;
554
+ sentenceCadence: number; stressPattern: number; pausePattern: number;
555
+ };
556
+ export type RhythmProfileId = "RHY-001" | "RHY-002" | "RHY-003" | "RHY-004" | "RHY-005" | "RHY-006" | "RHY-007" | "RHY-008";
557
+ export const RHYTHM_PROFILE_NAMES: Record<RhythmProfileId, string> = {
558
+ "RHY-001": "Spare pulse", "RHY-002": "Procedural cadence", "RHY-003": "Lyrical wave", "RHY-004": "Comic snap",
559
+ "RHY-005": "Gothic accumulation", "RHY-006": "Epic roll", "RHY-007": "Domestic murmur", "RHY-008": "Thriller chop",
560
+ };
561
+
562
+ export function computeSoundVariables(text: string): SoundVariables {
563
+ const w = words(text);
564
+ const n = Math.max(1, w.length);
565
+ const letters = text.toLowerCase().replace(/[^a-z]/g, "");
566
+ const L = Math.max(1, letters.length);
567
+ const count = (re: RegExp) => (letters.match(re) ?? []).length;
568
+ const vowels = letters.match(/[aeiou]/g) ?? [];
569
+ const open = vowels.filter((v) => "aeo".includes(v)).length;
570
+ let allit = 0;
571
+ for (let i = 1; i < w.length; i++) if (w[i][0]?.toLowerCase() === w[i - 1][0]?.toLowerCase()) allit++;
572
+ const vowelPairs = new Map<string, number>();
573
+ for (const x of w) { const v = (x.toLowerCase().match(/[aeiou]+/) ?? [""])[0]; if (v) vowelPairs.set(v, (vowelPairs.get(v) ?? 0) + 1); }
574
+ const assonance = [...vowelPairs.values()].filter((c) => c > 1).reduce((a, b) => a + b, 0) / n;
575
+ const endings = new Map<string, number>();
576
+ for (const x of w) { const e = x.toLowerCase().slice(-2); if (e.length === 2) endings.set(e, (endings.get(e) ?? 0) + 1); }
577
+ const consonance = [...endings.values()].filter((c) => c > 1).reduce((a, b) => a + b, 0) / n;
578
+ const sents = sentencesOf(text);
579
+ const lens = sents.map((s) => words(s).length);
580
+ const mean = lens.length ? lens.reduce((a, b) => a + b, 0) / lens.length : 0;
581
+ const sd = lens.length ? Math.sqrt(lens.reduce((a, x) => a + (x - mean) ** 2, 0) / lens.length) : 0;
582
+ const commas = (text.match(/[,;:—–]/g) ?? []).length;
583
+ return {
584
+ assonance: clamp01(assonance), consonance: clamp01(consonance),
585
+ alliteration: clamp01(allit / n * 4), sibilance: clamp01(count(/s|z|sh/g) / L * 6),
586
+ plosiveDensity: clamp01(count(/[pbtdkg]/g) / L * 4), liquidDensity: clamp01(count(/[lrmn]/g) / L * 3),
587
+ vowelOpenness: clamp01(open / Math.max(1, vowels.length)),
588
+ sentenceCadence: clamp01(mean / 30), stressPattern: clamp01(sd / 12),
589
+ pausePattern: clamp01(commas / Math.max(1, sents.length) / 3),
590
+ };
591
+ }
592
+
593
+ export function detectRhythmProfile(sv: SoundVariables, sents: SentenceAnalysis[]): RhythmProfileId {
594
+ const mean = sents.length ? sents.reduce((a, s) => a + s.wordCount, 0) / sents.length : 0;
595
+ const shortShare = sents.filter((s) => s.rhythmClass === "short").length / Math.max(1, sents.length);
596
+ if (mean < 10 && shortShare > 0.5) return sv.plosiveDensity > 0.5 ? "RHY-008" : "RHY-001";
597
+ if (mean > 28) return sv.liquidDensity > 0.5 ? "RHY-003" : "RHY-006";
598
+ if (sv.pausePattern > 0.6 && mean > 20) return "RHY-005";
599
+ if (shortShare > 0.35 && sv.plosiveDensity > 0.45) return "RHY-004";
600
+ if (/\b(then|next|first|after)\b/i.test(sents.map((s) => s.text).join(" "))) return "RHY-002";
601
+ return "RHY-007";
602
+ }
603
+
604
+ // ═══════════════════════════════════════════════════════════════════════════
605
+ // §11 DIALOGUE ENGINE
606
+ // ═══════════════════════════════════════════════════════════════════════════
607
+
608
+ export type IdiolectVector = {
609
+ sentenceLength: number; contractionRate: number; formality: number; directness: number;
610
+ figurativeLanguage: number; questionRate: number; interruptionRate: number; hedging: number;
611
+ emotionalNaming: number; specialVocabulary: number; silenceTendency: number;
612
+ };
613
+ export type DialogueTurnAnalysis = {
614
+ index: number; speaker: string | null; line: string;
615
+ speechAct: string; hasSubtext: boolean; wordCount: number;
616
+ };
617
+
618
+ const CONTRACTION_RE = /\b\w+['’](?:t|s|re|ve|ll|d|m)\b/gi;
619
+ const FORMAL_RE = /\b(shall|whom|thus|hence|indeed|moreover|therefore|regarding|pursuant)\b/gi;
620
+
621
+ export function extractDialogue(text: string): DialogueTurnAnalysis[] {
622
+ const out: DialogueTurnAnalysis[] = [];
623
+ const re = /["“]([^"”]{2,})["”]\s*(?:,?\s*(?:said|asked|replied|answered|murmured|snapped|whispered)\s+([A-Z][a-z]+))?/g;
624
+ let m: RegExpExecArray | null; let i = 0;
625
+ while ((m = re.exec(text)) !== null) {
626
+ const line = m[1].trim();
627
+ const speaker = m[2] ?? null;
628
+ let act = "ASSERT";
629
+ for (const [rx, name] of SPEECH_ACT_LEX) { if (rx.test(line)) { act = name; break; } }
630
+ const hasSubtext = /\b(nothing|fine|it's fine|whatever|if you say so|of course)\b/i.test(line) ||
631
+ (act === "ASSERT" && /\?/.test(line)) || /\.\.\./.test(line);
632
+ out.push({ index: i++, speaker, line, speechAct: act, hasSubtext, wordCount: words(line).length });
633
+ }
634
+ return out;
635
+ }
636
+
637
+ export function idiolectVector(lines: string[]): IdiolectVector {
638
+ const all = lines.join(" ");
639
+ const w = words(all); const n = Math.max(1, w.length);
640
+ const sents = Math.max(1, lines.length);
641
+ return {
642
+ sentenceLength: clamp01(n / sents / 25),
643
+ contractionRate: clamp01((all.match(CONTRACTION_RE) ?? []).length / n * 10),
644
+ formality: clamp01((all.match(FORMAL_RE) ?? []).length / n * 20),
645
+ directness: clamp01(1 - (all.match(HEDGE_RE) ?? []).length / n * 12),
646
+ figurativeLanguage: clamp01((all.match(FIGURATIVE_RE) ?? []).length / sents),
647
+ questionRate: clamp01(lines.filter((l) => /\?/.test(l)).length / sents),
648
+ interruptionRate: clamp01(lines.filter((l) => /[—–]\s*$|\.\.\.$/.test(l)).length / sents),
649
+ hedging: clamp01((all.match(HEDGE_RE) ?? []).length / n * 12),
650
+ emotionalNaming: clamp01((all.match(EMOTION_LABEL_RE) ?? []).length / sents),
651
+ specialVocabulary: clamp01(new Set(w.map((x) => x.toLowerCase())).size / n),
652
+ silenceTendency: clamp01(lines.filter((l) => words(l).length <= 3).length / sents),
653
+ };
654
+ }
655
+
656
+ export function idiolectDistance(a: IdiolectVector, b: IdiolectVector): number {
657
+ const ks = Object.keys(a) as (keyof IdiolectVector)[];
658
+ return Math.sqrt(ks.reduce((s, k) => s + (a[k] - b[k]) ** 2, 0) / ks.length);
659
+ }
660
+
661
+ // ═══════════════════════════════════════════════════════════════════════════
662
+ // §12 ARGUMENT / WARRANT ENGINE
663
+ // ═══════════════════════════════════════════════════════════════════════════
664
+
665
+ export type PassageClass = "ARGUMENTATIVE" | "LYRICAL" | "PERCEPTUAL" | "EMOTIONAL" | "PROCEDURAL";
666
+ export type ArgumentUnit = {
667
+ paragraph: number; passageClass: PassageClass;
668
+ claim: string | null; reason: string | null; evidence: string | null;
669
+ warrantExplicit: boolean; qualification: string | null; complete: boolean;
670
+ };
671
+
672
+ export function classifyPassage(p: ParagraphAnalysis): PassageClass {
673
+ if (p.type === "PAR-004" || ARG_CLAIM_RE.test(p.text)) return "ARGUMENTATIVE";
674
+ if (p.type === "PAR-006" || p.type === "PAR-001" || /\b(then|next|first|after that)\b/i.test(p.text)) return "PROCEDURAL";
675
+ if (p.sentences.reduce((a, s) => a + s.figurative, 0) >= 2) return "LYRICAL";
676
+ if (p.type === "PAR-002") return "PERCEPTUAL";
677
+ if (p.type === "PAR-003" || EMOTION_LABEL_RE.test(p.text)) { EMOTION_LABEL_RE.lastIndex = 0; return "EMOTIONAL"; }
678
+ return "PERCEPTUAL";
679
+ }
680
+
681
+ export function analyzeArguments(paras: ParagraphAnalysis[]): ArgumentUnit[] {
682
+ return paras.map((p) => {
683
+ const cls = classifyPassage(p);
684
+ if (cls !== "ARGUMENTATIVE") {
685
+ return { paragraph: p.index, passageClass: cls, claim: null, reason: null, evidence: null, warrantExplicit: false, qualification: null, complete: true };
686
+ }
687
+ const claimS = p.sentences.find((s) => ARG_CLAIM_RE.test(s.text)) ?? p.sentences[0];
688
+ const reasonS = p.sentences.find((s) => ARG_REASON_RE.test(s.text));
689
+ const evidenceS = p.sentences.find((s) => ARG_EVIDENCE_RE.test(s.text));
690
+ const qualS = p.sentences.find((s) => ARG_QUALIFY_RE.test(s.text));
691
+ return {
692
+ paragraph: p.index, passageClass: cls,
693
+ claim: claimS?.text.slice(0, 90) ?? null,
694
+ reason: reasonS?.text.slice(0, 90) ?? null,
695
+ evidence: evidenceS?.text.slice(0, 90) ?? null,
696
+ warrantExplicit: !!reasonS && !!evidenceS,
697
+ qualification: qualS?.text.slice(0, 90) ?? null,
698
+ complete: !!claimS && !!reasonS && !!evidenceS,
699
+ };
700
+ });
701
+ }
702
+
703
+ // ═══════════════════════════════════════════════════════════════════════════
704
+ // §13 AUREATE REVISION STACK — PASS-001..015 executed over real prose
705
+ // ═══════════════════════════════════════════════════════════════════════════
706
+
707
+ export type PassResult = {
708
+ id: string; name: string; severity: "pass" | "warn" | "fail";
709
+ score: number; detail: string; findings: string[]; executable: boolean;
710
+ };
711
+
712
+ export type AureateContext = {
713
+ genome: StyleGenome;
714
+ /** reader-safe knowledge terms; anything outside is a curse-of-knowledge risk */
715
+ readerKnownTerms?: string[];
716
+ /** focal character name for POV audits */
717
+ focalCharacter?: string | null;
718
+ /** all cast names for dialogue attribution + idiolect distance */
719
+ castNames?: string[];
720
+ /** genre key for profile-relative thresholds */
721
+ genreKey?: string;
722
+ };
723
+
724
+ export function runRevisionStack(text: string, ctx: AureateContext): PassResult[] {
725
+ const sents = analyzeSentences(text);
726
+ const paras = analyzeParagraphs(text);
727
+ const g = ctx.genome;
728
+ const n = Math.max(1, sents.length);
729
+ const wAll = words(text); const W = Math.max(1, wAll.length);
730
+ const R: PassResult[] = [];
731
+ const push = (id: string, name: string, score: number, detail: string, findings: string[] = [], executable = true, warnAt = 0.65, failAt = 0.4) =>
732
+ R.push({ id, name, score: clamp01(score), detail, findings: findings.slice(0, 6), executable, severity: !executable ? "warn" : score >= warnAt ? "pass" : score >= failAt ? "warn" : "fail" });
733
+
734
+ // PASS-001 logic-to-prose alignment — every paragraph has an anchor
735
+ const anchored = paras.filter((p) => p.hasAnchor).length / Math.max(1, paras.length);
736
+ push("PASS-001", "Logic-to-prose alignment", anchored,
737
+ `${paras.filter((p) => p.hasAnchor).length}/${paras.length} paragraphs open on an actor or a concrete perception`,
738
+ paras.filter((p) => !p.hasAnchor).map((p) => `¶${p.index + 1}: "${p.text.slice(0, 50)}…"`));
739
+
740
+ // PASS-002 characters-as-subjects / actions-as-verbs (Williams)
741
+ const subjOk = sents.filter((s) => s.subjectIsCharacter).length / n;
742
+ const verbOk = sents.filter((s) => s.actionIsVerb).length / n;
743
+ const p002 = 0.5 * subjOk + 0.5 * verbOk;
744
+ push("PASS-002", "Characters-as-subjects / actions-as-verbs", p002,
745
+ `${(subjOk * 100).toFixed(0)}% sentences lead with an actor; ${(verbOk * 100).toFixed(0)}% carry the action in a finite verb`,
746
+ sents.filter((s) => !s.actionIsVerb).map((s) =>
747
+ `${s.lightVerbNominalization ? "light verb + nominalization" : s.passive ? "passive" : "nominalized"}: "${s.text.slice(0, 52)}…" [${s.nominalizations.slice(0, 2).join(", ")}]`));
748
+
749
+ // PASS-003 old-new information flow
750
+ const chain = sents.slice(1);
751
+ const oldNew = chain.length ? chain.filter((s) => s.oldBeforeNew).length / chain.length : 1;
752
+ push("PASS-003", "Old-before-new information flow", oldNew,
753
+ `${(oldNew * 100).toFixed(0)}% of sentences open on information the previous sentence established`,
754
+ chain.filter((s) => !s.oldBeforeNew).map((s) => `¶ break at "${s.text.slice(0, 50)}…"`));
755
+
756
+ // PASS-004 topic-stress / emphatic endings
757
+ const stress = sents.filter((s) => s.emphaticEnd).length / n;
758
+ push("PASS-004", "Topic-stress / emphatic endings", stress,
759
+ `${(stress * 100).toFixed(0)}% of sentences end on a content word rather than a function-word tail`,
760
+ sents.filter((s) => !s.emphaticEnd).map((s) => `weak end: "…${s.text.slice(-45)}"`));
761
+
762
+ // PASS-005 concision and clutter (Zinsser/Strunk) — profile-relative
763
+ const clutter = sents.reduce((a, s) => a + s.clutterHits.length, 0);
764
+ const wan = auditWanIntensifiers(text);
765
+ // NOTE: the package's `auditWanIntensifiers` returns `total` = TOTAL WORD
766
+ // COUNT (not hit count) and `density` = hits/words. Summing `findings` gives
767
+ // the actual hit count. Using `wan.total` here drove clutterRate to ~1.0 on
768
+ // every text and pinned PASS-005 to 0 — verified defect, fixed.
769
+ const wanHits = wan.findings.reduce((a, f) => a + f.count, 0);
770
+ const clutterRate = (clutter + wanHits) / W;
771
+ const cap = 0.012 + (1 - g.compression) * 0.02;
772
+ push("PASS-005", "Concision / clutter removal", clamp01(1 - clutterRate / Math.max(1e-6, cap)),
773
+ `clutter+intensifier rate ${(clutterRate * 100).toFixed(2)}% vs profile cap ${(cap * 100).toFixed(2)}% (compression ${g.compression})`,
774
+ [...new Set(sents.flatMap((s) => s.clutterHits))].concat(wan.findings.slice(0, 3).map((f) => `${f.word}×${f.count}`)));
775
+
776
+ // PASS-006 concrete-language strengthening (Strunk)
777
+ const concrete = sents.reduce((a, s) => a + s.concreteHits.length, 0);
778
+ const abstract = sents.reduce((a, s) => a + s.abstractHits.length, 0);
779
+ const concRate = concrete / Math.max(1, concrete + abstract);
780
+ const floor = 0.35 + g.concreteness * 0.35;
781
+ push("PASS-006", "Concrete-language strengthening", clamp01(concRate / Math.max(1e-6, floor)),
782
+ `concrete:abstract = ${concrete}:${abstract} (ratio ${(concRate * 100).toFixed(0)}%, profile floor ${(floor * 100).toFixed(0)}%)`,
783
+ [...new Set(sents.flatMap((s) => s.abstractHits))].map((x) => `abstract noun "${x}"`));
784
+
785
+ // PASS-007 copy-chief grammar hygiene (Dreyer) — HARD errors only
786
+ const hard: string[] = [];
787
+ if (/\ba\s+[aeiou]/i.test(text)) hard.push('article: "a" before a vowel sound');
788
+ if (/\bthe the\b|\band and\b|\bof of\b/i.test(text)) hard.push("doubled function word");
789
+ if (/\s[,.;:]\s?[,.;:]/.test(text)) hard.push("doubled punctuation");
790
+ const dq = (text.match(/"/g) ?? []).length, cq = (text.match(/[“”]/g) ?? []).length;
791
+ if (dq % 2 === 1 || cq % 2 === 1) hard.push("unbalanced quotation marks");
792
+ if (text.trim() && !/[.!?"'”’)\]]$/.test(text.trim())) hard.push("text does not terminate");
793
+ push("PASS-007", "Copy-chief grammar hygiene", hard.length === 0 ? 1 : 0,
794
+ hard.length ? `${hard.length} hard error(s) — these block release, unlike style preferences` : "no hard grammar errors detected (lexical check)",
795
+ hard, true, 0.99, 0.99);
796
+
797
+ // PASS-008 POV / focalization consistency
798
+ const povFindings: string[] = [];
799
+ if (FUTURE_LEAK_RE.test(text)) povFindings.push("future-knowledge leak into present focalization (POV-AUD-004)");
800
+ const others = (ctx.castNames ?? []).filter((c) => c && c !== ctx.focalCharacter);
801
+ for (const o of others) {
802
+ const re = new RegExp(`\\b${o}\\s+(?:felt|thought|knew|realised|realized|wondered)\\b`, "i");
803
+ if (re.test(text)) povFindings.push(`interior access to non-focal character "${o}" (POV-AUD-003)`);
804
+ }
805
+ const labels = (text.match(EMOTION_LABEL_RE) ?? []).length; EMOTION_LABEL_RE.lastIndex = 0;
806
+ if (g.focalDistance === "far" && labels > 0) povFindings.push(`${labels} direct emotion label(s) at far focal distance (POV-AUD-005)`);
807
+ push("PASS-008", "POV / focalization consistency", povFindings.length === 0 ? 1 : clamp01(1 - povFindings.length * 0.34),
808
+ povFindings.length ? `${povFindings.length} focalization issue(s)` : `focal distance ${g.focalDistance}; no leaks detected`, povFindings);
809
+
810
+ // PASS-009 image-chain coherence
811
+ const chains = extractImageChains(text);
812
+ const distinctHosts = (c: ImageFamily) => new Set(c.occurrences.map((o) => `${o.paragraph}:${o.sentence}`)).size;
813
+ const primary = chains.filter((c) => distinctHosts(c) >= 2);
814
+ const transformed = primary.filter((c) => c.transformed).length;
815
+ const tooMany = primary.length > 2;
816
+ const imgScore = primary.length === 0 ? (g.imageDensity > 0.6 ? 0.35 : 0.7)
817
+ : clamp01(transformed / primary.length) * (tooMany ? 0.7 : 1);
818
+ push("PASS-009", "Image-chain coherence", imgScore,
819
+ primary.length === 0 ? `no recurring image family (IMG-001: each major motif needs one)` :
820
+ `${primary.length} recurring famil${primary.length === 1 ? "y" : "ies"} (${primary.map((c) => c.sourceDomain).join(", ")}); ${transformed} recur in changed context${tooMany ? "; >2 primaries per scene (IMG-005)" : ""}`,
821
+ primary.filter((c) => !c.transformed).map((c) => `"${c.sourceDomain}" repeats without changing context (IMG-002)`));
822
+
823
+ // PASS-010 sound / rhythm variation
824
+ const sv = computeSoundVariables(text);
825
+ const rhythm = detectRhythmProfile(sv, sents);
826
+ let runs = 0, cur = 1;
827
+ for (let i = 1; i < sents.length; i++) { if (sents[i].rhythmClass === sents[i - 1].rhythmClass) { cur++; if (cur > 3) runs++; } else cur = 1; }
828
+ const lens = sents.map((s) => s.wordCount);
829
+ const mean = lens.reduce((a, b) => a + b, 0) / n;
830
+ const cv = mean > 0 ? Math.sqrt(lens.reduce((a, x) => a + (x - mean) ** 2, 0) / n) / mean : 0;
831
+ const rhyScore = clamp01(0.6 * clamp01(cv / 0.45) + 0.4 * (runs === 0 ? 1 : Math.max(0, 1 - runs * 0.3)));
832
+ push("PASS-010", "Sound / rhythm variation", rhyScore,
833
+ `profile ${rhythm} (${RHYTHM_PROFILE_NAMES[rhythm]}); length CV ${(cv * 100).toFixed(0)}%; ${runs} run(s) of >3 same-length sentences (RHY-AUD-001)`,
834
+ runs > 0 ? [`${runs} monotony run(s) — vary sentence length`] : []);
835
+
836
+ // PASS-011 dialogue idiolect and subtext
837
+ const turns = extractDialogue(text);
838
+ const bySpeaker = new Map<string, string[]>();
839
+ for (const t of turns) if (t.speaker) bySpeaker.set(t.speaker, [...(bySpeaker.get(t.speaker) ?? []), t.line]);
840
+ const vecs = [...bySpeaker.entries()].map(([k, v]) => ({ k, v: idiolectVector(v) }));
841
+ let minDist = 1;
842
+ for (let i = 0; i < vecs.length; i++) for (let j = i + 1; j < vecs.length; j++) minDist = Math.min(minDist, idiolectDistance(vecs[i].v, vecs[j].v));
843
+ const subtext = turns.length ? turns.filter((t) => t.hasSubtext).length / turns.length : 0;
844
+ const dialScore = turns.length === 0 ? (g.dialogueDensity > 0.6 ? 0.4 : 0.75)
845
+ : clamp01(0.5 * (vecs.length >= 2 ? clamp01(minDist / 0.18) : 0.6) + 0.5 * clamp01(subtext / 0.3));
846
+ push("PASS-011", "Dialogue idiolect and subtext", dialScore,
847
+ turns.length === 0 ? `no dialogue found (genome dialogueDensity ${g.dialogueDensity})` :
848
+ `${turns.length} turn(s), ${vecs.length} attributed speaker(s); min idiolect distance ${minDist.toFixed(3)} (DIA-AUD-002 ≥0.18); subtext in ${(subtext * 100).toFixed(0)}% of turns`,
849
+ vecs.length >= 2 && minDist < 0.18 ? ["two speakers share a near-identical idiolect vector"] : []);
850
+
851
+ // PASS-012 argument / warrant check
852
+ const args = analyzeArguments(paras);
853
+ const argUnits = args.filter((a) => a.passageClass === "ARGUMENTATIVE");
854
+ const argOk = argUnits.length === 0 ? 1 : argUnits.filter((a) => a.complete).length / argUnits.length;
855
+ push("PASS-012", "Argument / warrant check", argOk,
856
+ argUnits.length === 0 ? "no argumentative passage — lyrical/perceptual passages are exempt (§12.1)" :
857
+ `${argUnits.filter((a) => a.complete).length}/${argUnits.length} argumentative passages carry claim + reason + evidence`,
858
+ argUnits.filter((a) => !a.complete).map((a) => `¶${a.paragraph + 1}: missing ${!a.reason ? "reason" : "evidence"} (ARG-AUD-00${!a.reason ? "1" : "2"})`));
859
+
860
+ // PASS-013 cliché and stale phrase removal
861
+ const low = text.toLowerCase();
862
+ const cliches = CLICHE_LIST.filter((c) => low.includes(c));
863
+ push("PASS-013", "Cliché / stale phrase removal", cliches.length === 0 ? 1 : clamp01(1 - cliches.length * 0.25),
864
+ cliches.length ? `${cliches.length} cliché(s) from a ${CLICHE_LIST.length}-entry blacklist` : `0 hits against a ${CLICHE_LIST.length}-entry blacklist`,
865
+ cliches.map((c) => `"${c}"`));
866
+
867
+ // PASS-014 KALLOS-S is computed separately; record its inputs are ready
868
+ push("PASS-014", "KALLOS-S style scoring", 1, "computed by evaluateKallosS() from these passes", []);
869
+
870
+ // PASS-015 human-readability gate — NOT executable without a human
871
+ const pm = computeProseMetrics(text);
872
+ push("PASS-015", "Human-readability gate", 0.5,
873
+ `NOT EXECUTABLE: requires a human reader. Machine proxies only — Flesch ${pm.fleschReadingEase.toFixed(0)} (${pm.fleschLabel}), sentence σ ${pm.sentenceLenStdev.toFixed(1)}, distinct-word ${(pm.distinctWordRatio * 100).toFixed(0)}%.`,
874
+ ["v4 §19 STYLE-TEST-015 and v3 TEST-025 both require human review"], false);
875
+
876
+ return R;
877
+ }
878
+
879
+ // ═══════════════════════════════════════════════════════════════════════════
880
+ // §14 KALLOS-S
881
+ // ═══════════════════════════════════════════════════════════════════════════
882
+
883
+ export const KALLOS_S_WEIGHTS: Record<string, number> = {
884
+ clarity: 0.14, cohesion: 0.10, concision: 0.10, concreteness: 0.09, rhythm: 0.09,
885
+ image_chain: 0.08, voice_distinction: 0.08, dialogue_subtext: 0.08, focal_integrity: 0.08,
886
+ syntactic_variety: 0.07, argument_warranting: 0.05, surprise_without_confusion: 0.04,
887
+ };
888
+
889
+ export type KallosSDim = { key: string; label: string; score: number; weight: number; detail: string };
890
+ export type KallosSGate = { id: string; pass: boolean; detail: string; executable: boolean };
891
+ export type KallosSReport = {
892
+ dims: KallosSDim[]; gates: KallosSGate[]; gatesPass: boolean;
893
+ composite: number; weakest: KallosSDim | null;
894
+ rhythmProfile: RhythmProfileId; soundVariables: SoundVariables;
895
+ passes: PassResult[]; method: string;
896
+ };
897
+
898
+ export function evaluateKallosS(text: string, ctx: AureateContext): KallosSReport {
899
+ const passes = runRevisionStack(text, ctx);
900
+ const byId = new Map(passes.map((p) => [p.id, p]));
901
+ const get = (id: string) => byId.get(id)?.score ?? 0.5;
902
+ const sents = analyzeSentences(text);
903
+ const sv = computeSoundVariables(text);
904
+ const rhythmProfile = detectRhythmProfile(sv, sents);
905
+
906
+ const distinctForms = new Set(sents.map((s) => s.syntaxForm)).size;
907
+ const variety = clamp01(distinctForms / 6);
908
+ const pm = computeProseMetrics(text);
909
+ const surprise = clamp01(0.5 * pm.distinctWordRatio * 1.6 + 0.5 * (1 - Math.min(1, pm.repeatedPhraseDensity * 20)));
910
+
911
+ const dims: KallosSDim[] = [
912
+ { key: "clarity", label: "clarity", score: 0.5 * get("PASS-002") + 0.5 * get("PASS-004"), weight: KALLOS_S_WEIGHTS.clarity, detail: "characters-as-subjects + topic/stress" },
913
+ { key: "cohesion", label: "cohesion", score: get("PASS-003"), weight: KALLOS_S_WEIGHTS.cohesion, detail: "old-before-new chain" },
914
+ { key: "concision", label: "concision", score: get("PASS-005"), weight: KALLOS_S_WEIGHTS.concision, detail: "clutter + intensifier rate vs profile cap" },
915
+ { key: "concreteness", label: "concreteness", score: get("PASS-006"), weight: KALLOS_S_WEIGHTS.concreteness, detail: "concrete:abstract ratio vs profile floor" },
916
+ { key: "rhythm", label: "rhythm", score: get("PASS-010"), weight: KALLOS_S_WEIGHTS.rhythm, detail: `length variance + monotony runs (${rhythmProfile})` },
917
+ { key: "image_chain", label: "image chain", score: get("PASS-009"), weight: KALLOS_S_WEIGHTS.image_chain, detail: "recurrence with changed context" },
918
+ { key: "voice_distinction", label: "voice distinction", score: get("PASS-011"), weight: KALLOS_S_WEIGHTS.voice_distinction, detail: "idiolect distance between speakers" },
919
+ { key: "dialogue_subtext", label: "dialogue subtext", score: get("PASS-011"), weight: KALLOS_S_WEIGHTS.dialogue_subtext, detail: "share of turns carrying subtext" },
920
+ { key: "focal_integrity", label: "focal integrity", score: get("PASS-008"), weight: KALLOS_S_WEIGHTS.focal_integrity, detail: "POV-AUD-001..005" },
921
+ { key: "syntactic_variety", label: "syntactic variety", score: variety, weight: KALLOS_S_WEIGHTS.syntactic_variety, detail: `${distinctForms} distinct syntax forms in use` },
922
+ { key: "argument_warranting", label: "argument warranting", score: get("PASS-012"), weight: KALLOS_S_WEIGHTS.argument_warranting, detail: "claim/reason/evidence on argumentative passages only" },
923
+ { key: "surprise_without_confusion", label: "surprise without confusion", score: surprise, weight: KALLOS_S_WEIGHTS.surprise_without_confusion, detail: "lexical freshness minus repeated phrases" },
924
+ ].map((d) => ({ ...d, score: Math.round(clamp01(d.score) * 1000) / 1000 }));
925
+
926
+ // §3.4 admissibility — "prose correct but dead".
927
+ // The v4 failure set (§3.4) explicitly includes "main action hidden in
928
+ // nominalization", "sentence rhythm monotonous" and "prose correct but dead".
929
+ // KALLOS-S §14 is a weighted SUM, so a passage can zero several dimensions and
930
+ // still clear the threshold on the strength of the others — verified by
931
+ // adversarial attack A1 ("Mara made a determination." ×5 scored 67.8).
932
+ // §3.4 is therefore enforced as its own admissibility gate rather than being
933
+ // folded into the score, which keeps §14's arithmetic spec-faithful.
934
+ const verbRate = sents.length ? sents.filter((x) => x.actionIsVerb).length / sents.length : 1;
935
+ const varietyScore = variety;
936
+ const concScore = get("PASS-006");
937
+ const rhythmScore = get("PASS-010");
938
+ const deadSignals = [
939
+ verbRate < 0.35 ? "action buried in nominalizations" : null,
940
+ varietyScore < 0.25 ? "syntactic monoculture" : null,
941
+ concScore < 0.20 ? "no concrete language" : null,
942
+ rhythmScore < 0.25 ? "monotonous rhythm" : null,
943
+ ].filter((x): x is string => x !== null);
944
+ const proseIsDead = sents.length >= 3 && deadSignals.length >= 3;
945
+
946
+ // §14 hard gates
947
+ const grammarPass = (byId.get("PASS-007")?.score ?? 0) >= 0.99;
948
+ const placeholder = /\{\{|\[TODO|TBD|lorem ipsum|\[insert/i.test(text);
949
+ const readerSafe = !FUTURE_LEAK_RE.test(text);
950
+ const povPass = (byId.get("PASS-008")?.score ?? 0) >= 0.65;
951
+ const logicPass = (byId.get("PASS-001")?.score ?? 0) >= 0.4;
952
+ const gates: KallosSGate[] = [
953
+ { id: "grammar", pass: grammarPass, executable: true, detail: byId.get("PASS-007")?.detail ?? "" },
954
+ { id: "logic", pass: logicPass, executable: true, detail: `paragraph anchoring ${(get("PASS-001") * 100).toFixed(0)}%` },
955
+ { id: "no_placeholder", pass: !placeholder, executable: true, detail: placeholder ? "placeholder token found" : "no placeholder tokens" },
956
+ { id: "reader_safe", pass: readerSafe, executable: true, detail: readerSafe ? "no future-knowledge leak" : "future-knowledge leak (curse of knowledge / POV-AUD-004)" },
957
+ { id: "POV", pass: povPass, executable: true, detail: byId.get("PASS-008")?.detail ?? "" },
958
+ { id: "not_dead", pass: !proseIsDead, executable: true,
959
+ detail: proseIsDead
960
+ ? `§3.4 "prose correct but dead": ${deadSignals.join("; ")}`
961
+ : `§3.4 alive — ${deadSignals.length}/4 death signals (needs ≥3 to fail)` },
962
+ ];
963
+ const gatesPass = gates.every((x) => x.pass);
964
+ const wsum = dims.reduce((a, d) => a + d.weight, 0) || 1;
965
+ const raw = dims.reduce((a, d) => a + d.weight * d.score, 0) / wsum;
966
+ const composite = gatesPass ? Math.round(raw * 1000) / 10 : 0;
967
+ const weakest = dims.reduce((a, d) => (d.score < a.score ? d : a), dims[0]);
968
+
969
+ return {
970
+ dims, gates, gatesPass, composite, weakest, rhythmProfile, soundVariables: sv, passes,
971
+ method:
972
+ "KALLOS-S is a lexical/heuristic style proxy: regex families, closed word lists, positional statistics and " +
973
+ "syllable counts over real text. No parser, no semantic model. Any hard gate failing sets KALLOS-S to 0 " +
974
+ "(v4 §14), including the §3.4 prose-correct-but-dead admissibility gate. " +
975
+ "PASS-015 (human readability) is NOT executable here and is excluded from the composite.",
976
+ };
977
+ }
978
+
979
+ // ═══════════════════════════════════════════════════════════════════════════
980
+ // §15 STYLE-CGOL — prose morphogenesis over discourse units
981
+ // ═══════════════════════════════════════════════════════════════════════════
982
+
983
+ export type StyleState = "spare" | "concrete" | "lyrical" | "procedural" | "comic" | "gothic" | "intimate" | "epic" | "reflective" | "tense";
984
+ export type StyleCell = {
985
+ index: number; scale: "paragraph" | "sentence";
986
+ state: StyleState; support: number; neighbors: number[];
987
+ event: "BIRTH" | "SURVIVAL" | "DEATH" | "OVERPOPULATION" | "UNDERPOPULATION";
988
+ note: string;
989
+ };
990
+ export type StyleCgolRun = {
991
+ cells: StyleCell[];
992
+ pattern: "STILL-LIFE" | "OSCILLATOR" | "GLIDER" | "MIXED" | "EXTINCTION";
993
+ detail: string;
994
+ advice: string[];
995
+ };
996
+
997
+ function stateOfParagraph(p: ParagraphAnalysis, g: StyleGenome): { state: StyleState; support: number } {
998
+ const s = p.sentences; const mean = s.length ? s.reduce((a, x) => a + x.wordCount, 0) / s.length : 0;
999
+ const fig = s.reduce((a, x) => a + x.figurative, 0);
1000
+ const conc = s.reduce((a, x) => a + x.concreteHits.length, 0);
1001
+ if (p.type === "PAR-005") return { state: "intimate", support: 0.7 };
1002
+ if (p.type === "PAR-006") return { state: "procedural", support: 0.8 };
1003
+ if (p.type === "PAR-004") return { state: "reflective", support: 0.7 };
1004
+ if (p.type === "PAR-010" || mean < 9) return { state: "tense", support: 0.75 };
1005
+ if (fig >= 2 && mean > 20) return { state: "lyrical", support: clamp01(g.ornamentLevel + 0.2) };
1006
+ if (mean > 30) return { state: g.proseMode === "gothic" ? "gothic" : "epic", support: 0.6 };
1007
+ if (conc >= 2) return { state: "concrete", support: 0.8 };
1008
+ if (mean < 14) return { state: "spare", support: clamp01(g.compression) };
1009
+ return { state: "reflective", support: 0.5 };
1010
+ }
1011
+
1012
+ export function runStyleCgol(text: string, g: StyleGenome): StyleCgolRun {
1013
+ const paras = analyzeParagraphs(text);
1014
+ // A 2-paragraph passage cannot exhibit an oscillator or a glider. The v4 §15.1
1015
+ // StyleCell scale is explicitly {word|phrase|sentence|paragraph|scene|chapter},
1016
+ // so fall back to SENTENCE scale when the paragraph lattice is too small to
1017
+ // carry a pattern — otherwise every short passage reported "MIXED" (defect).
1018
+ if (paras.length < 4) {
1019
+ const sent = analyzeSentences(text);
1020
+ if (sent.length >= 4) {
1021
+ const synth: ParagraphAnalysis[] = sent.map((sa, i) => ({
1022
+ index: i, text: sa.text, type: classifyParagraph(sa.text, [sa]), sentences: [sa],
1023
+ hasAnchor: sa.subjectIsCharacter || sa.concreteHits.length > 0,
1024
+ oldNewChainRatio: sa.oldBeforeNew === false ? 0 : 1,
1025
+ finalSentenceStress: sa.emphaticEnd,
1026
+ abstractionAnchored: sa.abstractHits.length === 0 || sa.concreteHits.length >= 1,
1027
+ lengthSentences: 1,
1028
+ move: { anchor: sa.text.slice(0, 60), development: "", turn: "", stress: sa.text.slice(-40) },
1029
+ }));
1030
+ return cgolOver(synth, g, "sentence");
1031
+ }
1032
+ }
1033
+ return cgolOver(paras, g, "paragraph");
1034
+ }
1035
+
1036
+ function cgolOver(paras: ParagraphAnalysis[], g: StyleGenome, scale: "paragraph" | "sentence"): StyleCgolRun {
1037
+ const cells: StyleCell[] = [];
1038
+ for (let i = 0; i < paras.length; i++) {
1039
+ const { state, support } = stateOfParagraph(paras[i], g);
1040
+ const prev = cells[i - 1];
1041
+ const effectCount =
1042
+ (paras[i].sentences.reduce((a, s) => a + s.figurative, 0) >= 2 ? 1 : 0) +
1043
+ (paras[i].sentences.some((s) => s.rhythmClass === "very-long") ? 1 : 0) +
1044
+ (paras[i].sentences.reduce((a, s) => a + s.adverbs.length, 0) >= 3 ? 1 : 0) +
1045
+ (paras[i].sentences.reduce((a, s) => a + s.concreteHits.length, 0) >= 4 ? 1 : 0);
1046
+ let event: StyleCell["event"] = "BIRTH"; let note = `${state} enters`;
1047
+ if (prev && prev.state === state) { event = "SURVIVAL"; note = `${state} persists (supported ${support.toFixed(2)})`; }
1048
+ if (prev && prev.state === state && support < 0.45) { event = "DEATH"; note = `${state} unsupported — retire the register`; }
1049
+ if (effectCount >= 3) { event = "OVERPOPULATION"; note = `${effectCount} style effects in one paragraph — simplify`; }
1050
+ if (effectCount === 0 && support < 0.5) { event = "UNDERPOPULATION"; note = "bland paragraph — inject image, rhythm or voice contrast"; }
1051
+ cells.push({ index: i, scale, state, support, neighbors: [i - 1, i + 1].filter((x) => x >= 0 && x < paras.length), event, note });
1052
+ }
1053
+ const states = cells.map((c) => c.state);
1054
+ const uniq = new Set(states).size;
1055
+ let pattern: StyleCgolRun["pattern"] = "MIXED";
1056
+ let detail = `${uniq} distinct style states across ${cells.length} ${scale}s`;
1057
+ if (cells.length === 0) { pattern = "EXTINCTION"; detail = "no paragraphs"; }
1058
+ else if (uniq === 1) { pattern = "STILL-LIFE"; detail = `stable ${states[0]} register throughout`; }
1059
+ else if (cells.length >= 4 && states.every((s, i) => i < 2 || s === states[i - 2])) { pattern = "OSCILLATOR"; detail = "period-2 alternation of registers"; }
1060
+ else if (uniq >= 3 && states[0] !== states[states.length - 1]) { pattern = "GLIDER"; detail = `register migrates ${states[0]} → ${states[states.length - 1]}`; }
1061
+ const advice = cells.filter((c) => c.event === "OVERPOPULATION" || c.event === "UNDERPOPULATION" || c.event === "DEATH")
1062
+ .map((c) => `¶${c.index + 1}: ${c.note}`);
1063
+ return { cells, pattern, detail, advice };
1064
+ }
1065
+
1066
+ // ═══════════════════════════════════════════════════════════════════════════
1067
+ // §19 STYLE-TEST-001..015
1068
+ // ═══════════════════════════════════════════════════════════════════════════
1069
+
1070
+ export type StyleTest = { id: string; name: string; status: "PASS" | "FAIL" | "NOT_EXECUTABLE"; detail: string };
1071
+
1072
+ export function runStyleTests(text: string, ctx: AureateContext, opts?: { replayHashA?: string; replayHashB?: string }): StyleTest[] {
1073
+ const k = evaluateKallosS(text, ctx);
1074
+ const byId = new Map(k.passes.map((p) => [p.id, p]));
1075
+ const g = ctx.genome;
1076
+ const sents = analyzeSentences(text);
1077
+ const t: StyleTest[] = [];
1078
+ const add = (id: string, name: string, status: StyleTest["status"], detail: string) => t.push({ id, name, status, detail });
1079
+ const gate = (id: string, name: string, score: number, floor: number, detail: string) =>
1080
+ add(id, name, score >= floor ? "PASS" : "FAIL", `${detail} (score ${score.toFixed(2)} vs floor ${floor})`);
1081
+
1082
+ gate("STYLE-TEST-001", "characters/actions alignment", byId.get("PASS-002")?.score ?? 0, 0.55, byId.get("PASS-002")?.detail ?? "");
1083
+ gate("STYLE-TEST-002", "old-new information chain", byId.get("PASS-003")?.score ?? 0, 0.5, byId.get("PASS-003")?.detail ?? "");
1084
+ gate("STYLE-TEST-003", "topic-stress endings", byId.get("PASS-004")?.score ?? 0, 0.6, byId.get("PASS-004")?.detail ?? "");
1085
+ gate("STYLE-TEST-004", "clutter ratio below cap", byId.get("PASS-005")?.score ?? 0, 0.5, byId.get("PASS-005")?.detail ?? "");
1086
+ gate("STYLE-TEST-005", "concrete-language density", byId.get("PASS-006")?.score ?? 0, 0.45, byId.get("PASS-006")?.detail ?? "");
1087
+ const cl = byId.get("PASS-013");
1088
+ add("STYLE-TEST-006", "cliché blacklist zero", (cl?.findings.length ?? 0) === 0 ? "PASS" : "FAIL", cl?.detail ?? "");
1089
+ gate("STYLE-TEST-007", "POV metaphor ownership", byId.get("PASS-008")?.score ?? 0, 0.65, byId.get("PASS-008")?.detail ?? "");
1090
+ const turns = extractDialogue(text);
1091
+ const bySpeaker = new Map<string, string[]>();
1092
+ for (const x of turns) if (x.speaker) bySpeaker.set(x.speaker, [...(bySpeaker.get(x.speaker) ?? []), x.line]);
1093
+ if (bySpeaker.size < 2) add("STYLE-TEST-008", "idiolect distance between leads", "NOT_EXECUTABLE", `needs ≥2 attributed speakers; found ${bySpeaker.size}`);
1094
+ else {
1095
+ const vs = [...bySpeaker.values()].map(idiolectVector);
1096
+ let md = 1; for (let i = 0; i < vs.length; i++) for (let j = i + 1; j < vs.length; j++) md = Math.min(md, idiolectDistance(vs[i], vs[j]));
1097
+ add("STYLE-TEST-008", "idiolect distance between leads", md >= 0.18 ? "PASS" : "FAIL", `min distance ${md.toFixed(3)} vs floor 0.18`);
1098
+ }
1099
+ const distinctHosts2 = (c: ImageFamily) => new Set(c.occurrences.map((o) => `${o.paragraph}:${o.sentence}`)).size;
1100
+ const chains = extractImageChains(text).filter((c) => distinctHosts2(c) >= 2);
1101
+ if (chains.length === 0) add("STYLE-TEST-009", "image-chain recurrence transformed", "NOT_EXECUTABLE", "no image family recurs; nothing to transform");
1102
+ else add("STYLE-TEST-009", "image-chain recurrence transformed", chains.every((c) => c.transformed) ? "PASS" : "FAIL",
1103
+ `${chains.filter((c) => c.transformed).length}/${chains.length} families recur in a changed context`);
1104
+ gate("STYLE-TEST-010", "sound/rhythm variance", byId.get("PASS-010")?.score ?? 0, 0.5, byId.get("PASS-010")?.detail ?? "");
1105
+ gate("STYLE-TEST-011", "argument/warrant complete", byId.get("PASS-012")?.score ?? 0, 0.99, byId.get("PASS-012")?.detail ?? "");
1106
+ add("STYLE-TEST-012", "grammar hard errors zero", (byId.get("PASS-007")?.findings.length ?? 0) === 0 ? "PASS" : "FAIL", byId.get("PASS-007")?.detail ?? "");
1107
+ const ph = /\{\{|\[TODO|TBD|lorem ipsum|\[insert/i.test(text) || (text.trim() !== "" && !/[.!?"'”’)\]]$/.test(text.trim()));
1108
+ add("STYLE-TEST-013", "no placeholders/truncation", ph ? "FAIL" : "PASS", ph ? "placeholder or truncated ending detected" : "no placeholders; text terminates");
1109
+ const thresh = Math.round((45 + g.clarityLevel * 25));
1110
+ add("STYLE-TEST-014", "KALLOS-S ≥ release threshold", k.composite >= thresh ? "PASS" : "FAIL", `KALLOS-S ${k.composite} vs profile threshold ${thresh}${k.gatesPass ? "" : " (hard gate failed ⇒ 0)"}`);
1111
+ if (opts?.replayHashA && opts?.replayHashB) add("STYLE-TEST-015", "seed replay byte-identical", opts.replayHashA === opts.replayHashB ? "PASS" : "FAIL", `${opts.replayHashA.slice(0, 12)} vs ${opts.replayHashB.slice(0, 12)}`);
1112
+ else add("STYLE-TEST-015", "seed replay byte-identical", "NOT_EXECUTABLE", "supply two replay hashes of the same seed to execute this test");
1113
+ void sents;
1114
+ return t;
1115
+ }
1116
+
1117
+ // ═══════════════════════════════════════════════════════════════════════════
1118
+ // Directive block — what v4 contributes to the LLM prompt
1119
+ // ═══════════════════════════════════════════════════════════════════════════
1120
+
1121
+ const MODE_GUIDANCE: Record<ProseMode, string> = {
1122
+ classic: "Classic style: the prose points at something in the world and lets the reader see it. No throat-clearing, no meta-commentary.",
1123
+ transparent: "Transparent style: the sentence disappears; only the event remains. Plain verbs, ordinary syntax.",
1124
+ lyrical: "Lyrical style: sound carries meaning. Long syntactic curves are allowed, but every curve must land on an image.",
1125
+ spare: "Spare style: short declaratives. Cut every word that does not change the picture.",
1126
+ baroque: "Baroque style: accumulation and subordination, but each clause must add a fact, not an adjective.",
1127
+ comic: "Comic style: setup and snap. Put the funny word last.",
1128
+ gothic: "Gothic style: accumulation toward dread. Concrete decay imagery, slow clauses, hard stops.",
1129
+ procedural: "Procedural style: sequence, measurement, and consequence. Numbers carry units.",
1130
+ intimate: "Intimate style: close focal distance, small physical gestures, restrained emotion naming.",
1131
+ epic: "Epic style: wide focal distance, long roll, but ground every abstraction in one concrete object.",
1132
+ };
1133
+
1134
+ export function buildAureateDirective(g: StyleGenome, pack: StylePackId | null, opts?: { chapterWords?: number }): string {
1135
+ const wordsTarget = Math.max(300, opts?.chapterWords ?? 1200);
1136
+ const clutterCap = Math.round((0.012 + (1 - g.compression) * 0.02) * wordsTarget);
1137
+ const concreteFloor = Math.max(3, Math.round((0.35 + g.concreteness * 0.35) * (wordsTarget / 120)));
1138
+ const longAllowed = g.syntacticComplexity > 0.55;
1139
+ return [
1140
+ "AUREATE-SCRIBE STYLE CONTRACT (v4 · apply silently; never name these rules in the prose):",
1141
+ `- Prose mode: ${g.proseMode}. ${MODE_GUIDANCE[g.proseMode]}`,
1142
+ pack ? `- Style pack: ${pack} — ${STYLE_PACK_DELTAS[pack].note}.` : "",
1143
+ `- Focal distance: ${g.focalDistance}. Every metaphor, every judgement and every sensory detail must belong to that position. No future knowledge, no access to a mind the focal position cannot reach.`,
1144
+ "- Williams: the main character of each sentence is its grammatical subject; the main action is a finite verb, not a noun ending in -tion/-ment/-ance.",
1145
+ "- Williams: open each sentence on information the previous sentence established, and end on the new, stressed idea. Do not end on a function word.",
1146
+ `- Strunk/Zinsser: cut clutter. Budget ≈ ${clutterCap} clutter phrases or intensifiers for this chapter, and prefer fewer.`,
1147
+ `- Strunk: prefer definite, concrete, specific language. Aim for ≥ ${concreteFloor} concrete physical nouns in this chapter.`,
1148
+ `- Le Guin/Tufte: vary sentence length deliberately. Never write more than three consecutive sentences of the same length class.${longAllowed ? " Long syntactic curves are permitted when they land on an image." : " Keep syntax simple; long curves are not in this profile."}`,
1149
+ `- Image discipline: at most 2 primary image families in a scene; when a motif returns, its context must have changed. No dead metaphor unless a character's voice requires it.`,
1150
+ "- Dreyer: hard grammar errors block release; style preferences are negotiable. No doubled punctuation, no unbalanced quotation marks.",
1151
+ "- Dialogue: every line performs a speech act (question, request, refusal, accusation, commitment, evasion). Two characters must never sound alike. Emotionally loaded scenes carry subtext, not statement.",
1152
+ "- Booth: if a passage makes an argument, it must carry claim → reason → evidence. Lyrical and perceptual passages are exempt and must not be forced into essay logic.",
1153
+ "- Pinker: write from what the reader currently knows. Do not assume the reader shares knowledge the text has not supplied.",
1154
+ "- Forbidden: clichés from the common stock (\"heart skipped a beat\", \"deafening silence\", \"little did she know\", \"let out a breath she didn't know she was holding\"), placeholders, and unfinished sentences.",
1155
+ ].filter(Boolean).join("\n");
1156
+ }
1157
+
1158
+ /** Compact genome line for the archetype set. */
1159
+ export function genomeSummary(g: StyleGenome, pack: StylePackId | null): string {
1160
+ return `${g.proseMode}/${g.focalDistance}${pack ? ` · ${pack}` : ""} · clarity ${g.clarityLevel.toFixed(2)} · ornament ${g.ornamentLevel.toFixed(2)} · music ${g.sentenceMusic.toFixed(2)} · syntax ${g.syntacticComplexity.toFixed(2)} · concrete ${g.concreteness.toFixed(2)} · compression ${g.compression.toFixed(2)}`;
1161
+ }