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