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