hypermind 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. hmctl/__init__.py +7 -0
  2. hmctl/cli.py +947 -0
  3. hypermind/__init__.py +208 -0
  4. hypermind/_deprecations.py +108 -0
  5. hypermind/agent.py +2896 -0
  6. hypermind/agent_card.py +127 -0
  7. hypermind/api/__init__.py +32 -0
  8. hypermind/api/app.py +686 -0
  9. hypermind/capability.py +923 -0
  10. hypermind/consult.py +493 -0
  11. hypermind/consult_bias.py +112 -0
  12. hypermind/contract_net.py +219 -0
  13. hypermind/crypto/__init__.py +21 -0
  14. hypermind/crypto/cose_encrypt.py +126 -0
  15. hypermind/crypto/dkg.py +246 -0
  16. hypermind/crypto/domain.py +59 -0
  17. hypermind/crypto/frost.py +771 -0
  18. hypermind/crypto/hashing.py +38 -0
  19. hypermind/crypto/hlc.py +163 -0
  20. hypermind/crypto/hpke.py +277 -0
  21. hypermind/crypto/kms.py +196 -0
  22. hypermind/crypto/kms_backends/__init__.py +56 -0
  23. hypermind/crypto/kms_backends/aws.py +128 -0
  24. hypermind/crypto/kms_backends/azure.py +114 -0
  25. hypermind/crypto/kms_backends/gcp.py +97 -0
  26. hypermind/crypto/kms_backends/vault.py +120 -0
  27. hypermind/crypto/namespace_root.py +204 -0
  28. hypermind/crypto/pq.py +152 -0
  29. hypermind/crypto/room_epoch.py +97 -0
  30. hypermind/crypto/signing.py +245 -0
  31. hypermind/deliberation.py +355 -0
  32. hypermind/did_web.py +256 -0
  33. hypermind/dispute.py +858 -0
  34. hypermind/errors.py +218 -0
  35. hypermind/eval/__init__.py +49 -0
  36. hypermind/eval/calibration.py +307 -0
  37. hypermind/eval/comparison.py +251 -0
  38. hypermind/eval/replay.py +202 -0
  39. hypermind/eval/swarm_iq.py +718 -0
  40. hypermind/knowledge/__init__.py +17 -0
  41. hypermind/knowledge/citation_graph.py +141 -0
  42. hypermind/knowledge/correlated_agreement.py +167 -0
  43. hypermind/knowledge/embedding_registry.py +115 -0
  44. hypermind/knowledge/kernel_store.py +197 -0
  45. hypermind/knowledge/rekor.py +173 -0
  46. hypermind/knowledge/reputation.py +364 -0
  47. hypermind/knowledge/swarm_memory.py +523 -0
  48. hypermind/knowledge/transparency.py +409 -0
  49. hypermind/mcp/__init__.py +60 -0
  50. hypermind/mcp/bridge.py +151 -0
  51. hypermind/mcp/client.py +142 -0
  52. hypermind/mcp/provenance.py +214 -0
  53. hypermind/mcp/relata_tools.py +152 -0
  54. hypermind/mcp/server.py +248 -0
  55. hypermind/mcp/transport.py +206 -0
  56. hypermind/mind/__init__.py +150 -0
  57. hypermind/mind/active.py +234 -0
  58. hypermind/mind/belief.py +369 -0
  59. hypermind/mind/deliberator.py +625 -0
  60. hypermind/mind/diversity.py +144 -0
  61. hypermind/mind/evolve.py +184 -0
  62. hypermind/mind/federation.py +154 -0
  63. hypermind/mind/goals.py +117 -0
  64. hypermind/mind/integration.py +190 -0
  65. hypermind/mind/mediator.py +74 -0
  66. hypermind/mind/memory_store.py +129 -0
  67. hypermind/mind/policy.py +307 -0
  68. hypermind/mind/records.py +260 -0
  69. hypermind/mind/roles.py +190 -0
  70. hypermind/mind/sybil_guard.py +41 -0
  71. hypermind/mind/world_model.py +166 -0
  72. hypermind/namespace.py +515 -0
  73. hypermind/namespace_policy.py +254 -0
  74. hypermind/observability/__init__.py +25 -0
  75. hypermind/observability/asgi.py +214 -0
  76. hypermind/observability/cost.py +277 -0
  77. hypermind/observability/otel_export.py +200 -0
  78. hypermind/observability/recorder.py +344 -0
  79. hypermind/observability/swarm_trace.py +438 -0
  80. hypermind/pin_policy.py +116 -0
  81. hypermind/py.typed +0 -0
  82. hypermind/responders/__init__.py +87 -0
  83. hypermind/responders/anthropic.py +159 -0
  84. hypermind/responders/base.py +162 -0
  85. hypermind/responders/openai.py +199 -0
  86. hypermind/responders/router.py +254 -0
  87. hypermind/responders/structured.py +287 -0
  88. hypermind/responders/tools.py +444 -0
  89. hypermind/revocation.py +270 -0
  90. hypermind/rule_ids.py +135 -0
  91. hypermind/schemas/encrypted_statement.cddl +28 -0
  92. hypermind/schemas/namespace_accept.cddl +20 -0
  93. hypermind/schemas/namespace_create.cddl +25 -0
  94. hypermind/schemas/namespace_founding_attest.cddl +30 -0
  95. hypermind/schemas/namespace_invite.cddl +22 -0
  96. hypermind/schemas/wire-v0.1.cddl +73 -0
  97. hypermind/simlab/__init__.py +15 -0
  98. hypermind/simlab/_persona_registry.py +93 -0
  99. hypermind/simlab/_scenario_impl.py +2778 -0
  100. hypermind/simlab/app.py +2538 -0
  101. hypermind/simlab/backends/__init__.py +27 -0
  102. hypermind/simlab/backends/anchor.py +88 -0
  103. hypermind/simlab/backends/local.py +32 -0
  104. hypermind/simlab/backends/server.py +79 -0
  105. hypermind/simlab/cli_command.py +108 -0
  106. hypermind/simlab/config.py +106 -0
  107. hypermind/simlab/deployment.py +26 -0
  108. hypermind/simlab/knowledge.py +716 -0
  109. hypermind/simlab/learning.py +295 -0
  110. hypermind/simlab/modes/__init__.py +115 -0
  111. hypermind/simlab/modes/_eval_real_llm.py +373 -0
  112. hypermind/simlab/modes/aar.py +611 -0
  113. hypermind/simlab/modes/backtest.py +610 -0
  114. hypermind/simlab/modes/binary_forecast.py +69 -0
  115. hypermind/simlab/modes/conformance.py +1869 -0
  116. hypermind/simlab/modes/evaluation.py +2271 -0
  117. hypermind/simlab/modes/governance.py +648 -0
  118. hypermind/simlab/modes/redteam.py +534 -0
  119. hypermind/simlab/modes/tabletop.py +797 -0
  120. hypermind/simlab/modes/tournament.py +448 -0
  121. hypermind/simlab/modes/whatif.py +523 -0
  122. hypermind/simlab/namespace.py +125 -0
  123. hypermind/simlab/personas.py +477 -0
  124. hypermind/simlab/prompt_generator.py +172 -0
  125. hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
  126. hypermind/simlab/question_sets/governance_q5.json +67 -0
  127. hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
  128. hypermind/simlab/question_sets/whatif_q3.json +38 -0
  129. hypermind/simlab/registry.py +325 -0
  130. hypermind/simlab/runner.py +239 -0
  131. hypermind/simlab/scenario.py +147 -0
  132. hypermind/simlab/swarms.py +180 -0
  133. hypermind/simlab/tool_handlers/__init__.py +4 -0
  134. hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
  135. hypermind/simlab/tool_handlers/brave.py +46 -0
  136. hypermind/simlab/tool_handlers/newsapi.py +50 -0
  137. hypermind/simlab/tool_handlers/openweather.py +46 -0
  138. hypermind/simlab/tool_handlers/pinecone.py +52 -0
  139. hypermind/simlab/tool_handlers/qdrant.py +57 -0
  140. hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
  141. hypermind/simlab/tool_handlers/relata_recall.py +61 -0
  142. hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
  143. hypermind/simlab/tool_handlers/serper.py +46 -0
  144. hypermind/simlab/tool_handlers/tavily.py +53 -0
  145. hypermind/simlab/tool_handlers/weaviate.py +65 -0
  146. hypermind/simlab/tool_handlers/wikipedia.py +64 -0
  147. hypermind/simlab/tool_handlers/wolfram.py +48 -0
  148. hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
  149. hypermind/simlab/tool_registry.py +568 -0
  150. hypermind/simlab/topic_adapter.py +338 -0
  151. hypermind/simlab/topic_questions.py +259 -0
  152. hypermind/storage/__init__.py +69 -0
  153. hypermind/storage/backend.py +71 -0
  154. hypermind/storage/relata.py +245 -0
  155. hypermind/storage/sqlite.py +258 -0
  156. hypermind/sync.py +191 -0
  157. hypermind/tal.py +175 -0
  158. hypermind/tasks.py +334 -0
  159. hypermind/testing.py +16 -0
  160. hypermind/tools/__init__.py +32 -0
  161. hypermind/tools/registry.py +61 -0
  162. hypermind/transport/__init__.py +22 -0
  163. hypermind/transport/anti_entropy.py +708 -0
  164. hypermind/transport/bus.py +206 -0
  165. hypermind/transport/knows_delta.py +204 -0
  166. hypermind/transport/libp2p.py +298 -0
  167. hypermind/transport/placement.py +58 -0
  168. hypermind/transport/tcp.py +797 -0
  169. hypermind/transport/tls_profile.py +417 -0
  170. hypermind/types.py +59 -0
  171. hypermind/uncertainty.py +222 -0
  172. hypermind/wire.py +897 -0
  173. hypermind/workflow/__init__.py +72 -0
  174. hypermind/workflow/engine.py +655 -0
  175. hypermind/workflow/plan.py +182 -0
  176. hypermind/workflow/repair.py +203 -0
  177. hypermind-0.11.0.dist-info/METADATA +524 -0
  178. hypermind-0.11.0.dist-info/RECORD +181 -0
  179. hypermind-0.11.0.dist-info/WHEEL +4 -0
  180. hypermind-0.11.0.dist-info/entry_points.txt +2 -0
  181. hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,523 @@
1
+ """What-If counterfactual scenario tree mode.
2
+
3
+ The user enters a *hypothesis* ("UAE leaves OPEC in 2026") plus 2–3
4
+ *driver variables* with named values they want explored ("oil_price:
5
+ 60/80/100", "us_saudi_relations: warm/cool/cold"). The mode produces:
6
+
7
+ * a **scenario tree** with one root and up to ``MAX_BRANCHES_PER_DRIVER``
8
+ children at each level (capped at depth :data:`MAX_DEPTH` so the
9
+ combinatorial explosion stays demo-legible);
10
+ * per-leaf **consensus probability** for the hypothesis under that
11
+ combination of driver values, plus **dissent counter-probability**
12
+ from the most-disagreeing reviewer;
13
+ * per-leaf **leading indicators** — concrete things the user can watch
14
+ for that would shift the leaf's posterior;
15
+ * an **anti-herding flag** when the panel converges below the §20.3
16
+ stdev floor (default 0.05) without any dissent — surfacing the case
17
+ where the swarm should have been polled harder.
18
+
19
+ Like ``governance``, this mode is deterministic given the input — no
20
+ LLM round-trip required. A future expansion can swap the
21
+ synthetic-scoring helpers for a real responder. The scoring is
22
+ intentionally explainable so the demo tree reads naturally.
23
+
24
+ Spec hooks:
25
+ * §20.3 Anti-Herding Trigger — surfaces in trace as ``anti_herding_fired``
26
+ * §19.5 DissentRecord — each leaf carries a dissent counter-probability
27
+ * §20.3 InformationGainPlanner — picks the next driver to query
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import hashlib
33
+ import json
34
+ import time
35
+ from dataclasses import asdict, dataclass
36
+ from itertools import product
37
+ from pathlib import Path
38
+ from typing import Any
39
+
40
+ from . import ModeSpec, register
41
+
42
+ MAX_BRANCHES_PER_DRIVER = 3
43
+ MAX_DEPTH = 2
44
+ ANTI_HERDING_FLOOR = 0.05
45
+
46
+
47
+ # --- Result types ----------------------------------------------------------
48
+
49
+
50
+ @dataclass
51
+ class LeadingIndicator:
52
+ name: str
53
+ direction: str # "if rises" | "if falls" | "if observed"
54
+ points_toward: str # branch label this indicator strengthens
55
+
56
+
57
+ @dataclass
58
+ class BranchDissent:
59
+ reviewer_role: str # "scout" | "sceptic" | "synthesist" | "counterfactualist"
60
+ counter_probability: float
61
+ rationale: str
62
+
63
+
64
+ @dataclass
65
+ class ScenarioBranch:
66
+ branch_id: str
67
+ path: list[dict[str, str]] # [{driver: value}, ...] — drivers fixed at this leaf
68
+ consensus_probability: float
69
+ confidence: float
70
+ dissents: list[BranchDissent]
71
+ leading_indicators: list[LeadingIndicator]
72
+ anti_herding_fired: bool
73
+ rationale: str
74
+
75
+
76
+ @dataclass
77
+ class WhatIfResult:
78
+ hypothesis: str
79
+ horizon_days: int
80
+ drivers: list[dict[str, Any]] # input drivers
81
+ branches: list[ScenarioBranch]
82
+ most_likely_path: list[dict[str, str]]
83
+ most_likely_probability: float
84
+ information_gain_next_driver: str | None # which driver to pin first
85
+ information_gain_score: float
86
+ panel_size: int
87
+
88
+
89
+ # --- Input schema ----------------------------------------------------------
90
+
91
+ INPUT_SCHEMA: dict[str, Any] = {
92
+ "type": "object",
93
+ "title": "What-If counterfactual scenario tree",
94
+ "required": ["hypothesis", "drivers"],
95
+ "properties": {
96
+ "hypothesis": {
97
+ "type": "string",
98
+ "title": "Hypothesis",
99
+ "format": "textarea",
100
+ "description": (
101
+ "The thing you want the swarm to evaluate under each "
102
+ "branch. e.g. 'UAE leaves OPEC by end of 2026'."
103
+ ),
104
+ },
105
+ "horizon_days": {
106
+ "type": "integer",
107
+ "title": "Time horizon (days)",
108
+ "minimum": 7,
109
+ "maximum": 3650,
110
+ "default": 365,
111
+ },
112
+ "drivers": {
113
+ "type": "array",
114
+ "title": "Driver variables (2–3, comma-separated values per driver)",
115
+ "description": (
116
+ "Each driver: 'name=val1|val2|val3'. e.g. "
117
+ "'oil_price=60|80|100,us_saudi=warm|cool|cold'. Max 3 "
118
+ "drivers, 3 values each."
119
+ ),
120
+ "examples": ["oil_price=60|80|100", "us_saudi=warm|cool|cold"],
121
+ "default": [],
122
+ },
123
+ },
124
+ }
125
+
126
+
127
+ RESULT_SCHEMA: dict[str, Any] = {
128
+ "type": "object",
129
+ "properties": {
130
+ "mode": {"type": "string", "const": "what-if-tree"},
131
+ "hypothesis": {"type": "string"},
132
+ "horizon_days": {"type": "integer"},
133
+ "branches": {"type": "array"},
134
+ "most_likely_path": {"type": "array"},
135
+ "most_likely_probability": {"type": "number"},
136
+ "information_gain_next_driver": {"type": ["string", "null"]},
137
+ "anti_herding_fired_anywhere": {"type": "boolean"},
138
+ },
139
+ }
140
+
141
+
142
+ # --- Driver parsing --------------------------------------------------------
143
+
144
+
145
+ def _parse_drivers(raw: list[str]) -> list[dict[str, Any]]:
146
+ """Parse 'name=v1|v2|v3' strings into structured drivers.
147
+
148
+ Permissive: discards malformed entries. Capped at MAX_DEPTH drivers
149
+ and MAX_BRANCHES_PER_DRIVER values per driver to keep the tree from
150
+ exploding.
151
+ """
152
+ out: list[dict[str, Any]] = []
153
+ for item in raw[: MAX_DEPTH + 1]: # one extra for the IG-suggestion driver
154
+ if not item or "=" not in item:
155
+ continue
156
+ name, _, vals = item.partition("=")
157
+ name = name.strip()
158
+ values = [v.strip() for v in vals.split("|") if v.strip()]
159
+ if not name or not values:
160
+ continue
161
+ values = values[:MAX_BRANCHES_PER_DRIVER]
162
+ out.append({"name": name, "values": values})
163
+ return out
164
+
165
+
166
+ def _seed_drivers_from_hypothesis(hypothesis: str) -> list[dict[str, Any]]:
167
+ """When the user gives no drivers, fall back to two synthetic ones
168
+ so the demo always produces a tree."""
169
+ h = hypothesis.lower()
170
+ if "opec" in h or "oil" in h:
171
+ return [
172
+ {"name": "oil_price_usd", "values": ["60", "80", "100"]},
173
+ {"name": "us_saudi_relations", "values": ["warm", "cool", "cold"]},
174
+ ]
175
+ if "fed" in h or "rates" in h or "inflation" in h:
176
+ return [
177
+ {"name": "core_inflation_yoy", "values": ["below_2", "2_to_3", "above_3"]},
178
+ {"name": "labor_market", "values": ["soft", "balanced", "tight"]},
179
+ ]
180
+ if "cyber" in h or "incident" in h or "breach" in h:
181
+ return [
182
+ {"name": "actor_capability", "values": ["nation_state", "criminal", "insider"]},
183
+ {"name": "blast_radius", "values": ["single_tenant", "regional", "global"]},
184
+ ]
185
+ return [
186
+ {"name": "key_driver_a", "values": ["low", "base", "high"]},
187
+ {"name": "key_driver_b", "values": ["bear", "base", "bull"]},
188
+ ]
189
+
190
+
191
+ # --- Synthetic scoring -----------------------------------------------------
192
+
193
+
194
+ def _value_lift(driver_name: str, value: str) -> float:
195
+ """Map a driver value to a [-0.30, +0.30] lift on the hypothesis
196
+ posterior. Deterministic — same value always yields the same lift.
197
+ Encodes a small amount of domain intuition for the demo:
198
+
199
+ * Extreme tail values ("cold", "above_3", "global") shift the
200
+ probability further from the base case in the direction implied
201
+ by the value's polarity.
202
+ * Stable / "base" values are lift=0.
203
+ * The hash makes the mapping deterministic for any value the user
204
+ types that we don't have an explicit rule for.
205
+ """
206
+ polarity = {
207
+ "warm": +0.10,
208
+ "cool": -0.05,
209
+ "cold": -0.20,
210
+ "soft": -0.10,
211
+ "balanced": 0.0,
212
+ "tight": +0.15,
213
+ "below_2": -0.10,
214
+ "2_to_3": 0.0,
215
+ "above_3": +0.20,
216
+ "60": -0.15,
217
+ "80": 0.0,
218
+ "100": +0.18,
219
+ "low": -0.15,
220
+ "base": 0.0,
221
+ "high": +0.18,
222
+ "bear": -0.20,
223
+ "bull": +0.20,
224
+ "nation_state": +0.18,
225
+ "criminal": +0.05,
226
+ "insider": +0.10,
227
+ "single_tenant": -0.15,
228
+ "regional": 0.0,
229
+ "global": +0.25,
230
+ }
231
+ if value in polarity:
232
+ return polarity[value]
233
+ # Unknown values get a small deterministic pseudo-lift in [-0.10, 0.10]
234
+ h = int(hashlib.sha256(f"{driver_name}::{value}".encode()).hexdigest(), 16)
235
+ return ((h % 21) - 10) / 100.0 # [-0.10, 0.10]
236
+
237
+
238
+ def _branch_consensus(
239
+ base_prior: float,
240
+ path: list[dict[str, str]],
241
+ ) -> tuple[float, float]:
242
+ """Compose per-branch lift from the path. Returns (mean, stdev)."""
243
+ means: list[float] = []
244
+ for _ in range(4): # synthesise 4 reviewers w/ small noise
245
+ m = base_prior
246
+ for step in path:
247
+ for d, v in step.items():
248
+ m += _value_lift(d, v)
249
+ means.append(max(0.0, min(1.0, m)))
250
+ # Add a small dispersion so the panel isn't perfectly converged.
251
+ n = len(means)
252
+ mean = sum(means) / n
253
+ var = sum((x - mean) ** 2 for x in means) / max(n - 1, 1)
254
+ return mean, var**0.5
255
+
256
+
257
+ def _dissent_for_branch(
258
+ consensus: float,
259
+ path: list[dict[str, str]],
260
+ ) -> list[BranchDissent]:
261
+ """The Counterfactualist always asks 'what would have to be true
262
+ for the opposite to happen?' — and records that as a dissent when
263
+ the panel's posterior is strongly one-sided (so the demo always
264
+ has a counter-narrative to read)."""
265
+ dissents: list[BranchDissent] = []
266
+ if consensus >= 0.65:
267
+ # Counterfactualist argues the downside.
268
+ dissents.append(
269
+ BranchDissent(
270
+ reviewer_role="counterfactualist",
271
+ counter_probability=round(max(0.05, 1.0 - consensus - 0.10), 3),
272
+ rationale=(
273
+ "Counterfactual: assumes drivers stay anchored at this "
274
+ "branch. A regime change would invalidate the lift; "
275
+ "tail risk is asymmetric here."
276
+ ),
277
+ )
278
+ )
279
+ elif consensus <= 0.35:
280
+ # Counterfactualist argues the upside.
281
+ dissents.append(
282
+ BranchDissent(
283
+ reviewer_role="counterfactualist",
284
+ counter_probability=round(min(0.95, consensus + 0.30), 3),
285
+ rationale=(
286
+ "Counterfactual: assumes the structural arguments "
287
+ "outweigh political surprises. A single high-impact "
288
+ "catalyst could move this leaf upward."
289
+ ),
290
+ )
291
+ )
292
+ return dissents
293
+
294
+
295
+ def _leading_indicators(path: list[dict[str, str]]) -> list[LeadingIndicator]:
296
+ """Tie each driver value to a watchable signal."""
297
+ out: list[LeadingIndicator] = []
298
+ for step in path:
299
+ for d, v in step.items():
300
+ label = f"{d}={v}"
301
+ if d == "oil_price_usd":
302
+ out.append(
303
+ LeadingIndicator(
304
+ name="Brent crude weekly close",
305
+ direction="if rises"
306
+ if v in {"100", "high"}
307
+ else "if falls"
308
+ if v in {"60", "low"}
309
+ else "if observed",
310
+ points_toward=label,
311
+ )
312
+ )
313
+ elif d == "us_saudi_relations":
314
+ out.append(
315
+ LeadingIndicator(
316
+ name="Joint statements / sanctions activity",
317
+ direction="if observed",
318
+ points_toward=label,
319
+ )
320
+ )
321
+ elif d == "core_inflation_yoy":
322
+ out.append(
323
+ LeadingIndicator(
324
+ name="Core PCE month-over-month",
325
+ direction="if rises"
326
+ if v == "above_3"
327
+ else "if falls"
328
+ if v == "below_2"
329
+ else "if observed",
330
+ points_toward=label,
331
+ )
332
+ )
333
+ elif d == "actor_capability":
334
+ out.append(
335
+ LeadingIndicator(
336
+ name="MITRE ATT&CK technique fingerprints",
337
+ direction="if observed",
338
+ points_toward=label,
339
+ )
340
+ )
341
+ else:
342
+ out.append(
343
+ LeadingIndicator(
344
+ name=f"{d} indicator",
345
+ direction="if observed",
346
+ points_toward=label,
347
+ )
348
+ )
349
+ return out
350
+
351
+
352
+ # --- Information-gain helper ----------------------------------------------
353
+
354
+
355
+ def _information_gain(
356
+ drivers: list[dict[str, Any]],
357
+ branches: list[ScenarioBranch],
358
+ ) -> tuple[str | None, float]:
359
+ """Approximate the ``InformationGainPlanner`` for the demo: which
360
+ driver, when pinned to a value, narrows the posterior most? We
361
+ score each driver by the variance of branch posteriors over its
362
+ values, holding the other drivers averaged out. The driver with
363
+ the *highest* variance is the one worth querying first.
364
+
365
+ This is a synthetic mirror of ``mind.active.InformationGainPlanner``;
366
+ a future patch will route through the real planner.
367
+ """
368
+ if not drivers or not branches:
369
+ return None, 0.0
370
+ best_name: str | None = None
371
+ best_score = -1.0
372
+ for d in drivers:
373
+ groups: dict[str, list[float]] = {v: [] for v in d["values"]}
374
+ for b in branches:
375
+ for step in b.path:
376
+ if d["name"] in step and step[d["name"]] in groups:
377
+ groups[step[d["name"]]].append(b.consensus_probability)
378
+ means = [sum(g) / len(g) if g else 0.5 for g in groups.values()]
379
+ if not means:
380
+ continue
381
+ m = sum(means) / len(means)
382
+ v = sum((x - m) ** 2 for x in means) / max(len(means) - 1, 1)
383
+ if v > best_score:
384
+ best_score = v
385
+ best_name = d["name"]
386
+ return best_name, round(best_score, 4)
387
+
388
+
389
+ # --- Run function ----------------------------------------------------------
390
+
391
+
392
+ async def _run_whatif(args: Any, recorder: Any | None = None) -> None:
393
+ payload = getattr(args, "mode_payload", None) or {}
394
+ hypothesis = (payload.get("hypothesis") or "").strip()
395
+ if not hypothesis:
396
+ hypothesis = "UAE leaves OPEC by end of 2026."
397
+ horizon_days = int(payload.get("horizon_days") or 365)
398
+
399
+ raw_drivers = payload.get("drivers") or []
400
+ if isinstance(raw_drivers, str):
401
+ raw_drivers = [raw_drivers]
402
+ drivers = _parse_drivers(list(raw_drivers))
403
+ if not drivers:
404
+ drivers = _seed_drivers_from_hypothesis(hypothesis)
405
+ drivers = drivers[:MAX_DEPTH] # hard cap
406
+
407
+ started_at = time.time()
408
+
409
+ # Base prior derived from the hypothesis itself — synthetic but
410
+ # deterministic. Encodes a small amount of "this kind of claim is
411
+ # usually true 30% of the time" prior so the tree isn't centered.
412
+ h_hash = int(hashlib.sha256(hypothesis.encode("utf-8")).hexdigest(), 16)
413
+ base_prior = 0.20 + (h_hash % 41) / 100.0 # [0.20, 0.60]
414
+
415
+ # Cartesian over all drivers — every combination becomes a leaf.
416
+ value_grid = [d["values"] for d in drivers]
417
+ leaves: list[ScenarioBranch] = []
418
+ anti_herding_anywhere = False
419
+ for combo in product(*value_grid):
420
+ path = [{drivers[i]["name"]: combo[i]} for i in range(len(drivers))]
421
+ mean, stdev = _branch_consensus(base_prior, path)
422
+ dissents = _dissent_for_branch(mean, path)
423
+ anti_herding = (stdev < ANTI_HERDING_FLOOR) and not dissents
424
+ if anti_herding:
425
+ anti_herding_anywhere = True
426
+ # Force a counterfactual on suspicious-converged branches —
427
+ # this is the §20.3 anti-herding intervention surfaced.
428
+ dissents.append(
429
+ BranchDissent(
430
+ reviewer_role="counterfactualist",
431
+ counter_probability=round(
432
+ max(0.05, mean - 0.20) if mean >= 0.5 else min(0.95, mean + 0.20),
433
+ 3,
434
+ ),
435
+ rationale=(
436
+ "Anti-herding triggered: panel converged below the "
437
+ f"{ANTI_HERDING_FLOOR:.2f} stdev floor without "
438
+ "spontaneous dissent. The Counterfactualist was "
439
+ "consulted to surface a minority signal."
440
+ ),
441
+ )
442
+ )
443
+ branch_id = "/".join(f"{d['name']}={v}" for d, v in zip(drivers, combo, strict=True))
444
+ leaves.append(
445
+ ScenarioBranch(
446
+ branch_id=branch_id,
447
+ path=path,
448
+ consensus_probability=round(mean, 4),
449
+ confidence=round(max(0.0, 1.0 - stdev * 5.0), 3),
450
+ dissents=dissents,
451
+ leading_indicators=_leading_indicators(path),
452
+ anti_herding_fired=anti_herding,
453
+ rationale=(
454
+ f"Under {branch_id}, the panel's posterior on the "
455
+ f"hypothesis sits at {mean:.2f} (stdev {stdev:.3f})."
456
+ ),
457
+ )
458
+ )
459
+
460
+ # Most-likely path = leaf with highest consensus.
461
+ best = max(leaves, key=lambda b: b.consensus_probability)
462
+ ig_driver, ig_score = _information_gain(drivers, leaves)
463
+
464
+ result = WhatIfResult(
465
+ hypothesis=hypothesis,
466
+ horizon_days=horizon_days,
467
+ drivers=drivers,
468
+ branches=leaves,
469
+ most_likely_path=best.path,
470
+ most_likely_probability=best.consensus_probability,
471
+ information_gain_next_driver=ig_driver,
472
+ information_gain_score=ig_score,
473
+ panel_size=4,
474
+ )
475
+
476
+ elapsed = time.time() - started_at
477
+ trace = {
478
+ "mode": "what-if-tree",
479
+ "started_at": started_at,
480
+ "elapsed_s": round(elapsed, 3),
481
+ "n_agents": result.panel_size,
482
+ "n_questions": len(leaves),
483
+ "hypothesis": hypothesis,
484
+ "horizon_days": horizon_days,
485
+ "drivers": drivers,
486
+ "branches": [asdict(b) for b in leaves],
487
+ "most_likely_path": best.path,
488
+ "most_likely_probability": best.consensus_probability,
489
+ "information_gain_next_driver": ig_driver,
490
+ "information_gain_score": ig_score,
491
+ "anti_herding_fired_anywhere": anti_herding_anywhere,
492
+ "collective_iq": {
493
+ # Surfaced for parity with binary traces consumed by SimList.
494
+ "score": round(
495
+ sum(b.consensus_probability for b in leaves) / len(leaves),
496
+ 4,
497
+ ),
498
+ },
499
+ }
500
+
501
+ Path(args.trace).write_text(json.dumps(trace, indent=2, default=str))
502
+
503
+
504
+ # --- Registration ----------------------------------------------------------
505
+
506
+ SPEC = ModeSpec(
507
+ slug="what-if-tree",
508
+ label="What-If Scenario Tree",
509
+ description=(
510
+ "Explore a hypothesis under 2 driver variables, each with up to "
511
+ "3 values. The swarm assigns a per-branch consensus probability "
512
+ "with dissent counter-probabilities, leading indicators, and an "
513
+ "InformationGainPlanner suggestion for which driver to pin next."
514
+ ),
515
+ input_schema=INPUT_SCHEMA,
516
+ result_schema=RESULT_SCHEMA,
517
+ run_fn=_run_whatif,
518
+ uses_legacy_fields=False,
519
+ expected_roles=["scout", "sceptic", "synthesist", "counterfactualist"],
520
+ )
521
+
522
+
523
+ register(SPEC)
@@ -0,0 +1,125 @@
1
+ """Namespace registry — each namespace has a slug, label, and DeploymentProfile."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import threading
8
+ from dataclasses import asdict, dataclass, field
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ from .registry import home
13
+
14
+ _ns_lock = threading.Lock()
15
+
16
+ _SLUG_RE = re.compile(r"^[a-z][a-z0-9\-]{0,62}$")
17
+
18
+
19
+ @dataclass
20
+ class DeploymentProfile:
21
+ tier: Literal["local", "server", "decentralised"] = "local"
22
+ storage_url: str | None = None # None=~/.hypermind; sqlite:///path; postgres://...
23
+ visibility: Literal["private", "shared", "public"] = "private"
24
+ anchor_chain: str | None = None # "base" | "ethereum" | "polygon"
25
+ anchor_contract: str | None = None
26
+ anchor_rpc_url: str | None = None
27
+
28
+ def to_dict(self) -> dict:
29
+ return {k: v for k, v in asdict(self).items() if v is not None or k == "tier"}
30
+
31
+ @classmethod
32
+ def from_dict(cls, d: dict) -> DeploymentProfile:
33
+ valid_tiers = {"local", "server", "decentralised"}
34
+ tier = d.get("tier", "local")
35
+ if tier not in valid_tiers:
36
+ raise ValueError(f"Invalid tier: {tier!r}")
37
+ return cls(
38
+ tier=tier,
39
+ storage_url=d.get("storage_url"),
40
+ visibility=d.get("visibility", "private"),
41
+ anchor_chain=d.get("anchor_chain"),
42
+ anchor_contract=d.get("anchor_contract"),
43
+ anchor_rpc_url=d.get("anchor_rpc_url"),
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class Namespace:
49
+ slug: str
50
+ label: str
51
+ profile: DeploymentProfile = field(default_factory=DeploymentProfile)
52
+ created_at_ms: int | None = None
53
+
54
+ def to_dict(self) -> dict:
55
+ return {
56
+ "slug": self.slug,
57
+ "label": self.label,
58
+ "profile": self.profile.to_dict(),
59
+ "created_at_ms": self.created_at_ms,
60
+ }
61
+
62
+ @classmethod
63
+ def from_dict(cls, d: dict) -> Namespace:
64
+ return cls(
65
+ slug=d["slug"],
66
+ label=d.get("label", d["slug"]),
67
+ profile=DeploymentProfile.from_dict(d.get("profile", {})),
68
+ created_at_ms=d.get("created_at_ms"),
69
+ )
70
+
71
+
72
+ def _namespaces_dir() -> Path:
73
+ d = home() / "namespaces"
74
+ d.mkdir(parents=True, exist_ok=True)
75
+ return d
76
+
77
+
78
+ def _ns_path(slug: str) -> Path:
79
+ return _namespaces_dir() / f"{slug}.json"
80
+
81
+
82
+ def list_namespaces() -> list[Namespace]:
83
+ ns = []
84
+ for p in sorted(_namespaces_dir().glob("*.json")):
85
+ try:
86
+ ns.append(Namespace.from_dict(json.loads(p.read_text())))
87
+ except Exception:
88
+ pass
89
+ if not ns:
90
+ # Auto-create default namespace on first call
91
+ default = Namespace(
92
+ slug="default", label="Default", profile=DeploymentProfile(tier="local")
93
+ )
94
+ save_namespace(default)
95
+ ns = [default]
96
+ return ns
97
+
98
+
99
+ def get_namespace(slug: str) -> Namespace | None:
100
+ p = _ns_path(slug)
101
+ if not p.exists():
102
+ return None
103
+ return Namespace.from_dict(json.loads(p.read_text()))
104
+
105
+
106
+ def save_namespace(ns: Namespace) -> None:
107
+ import time
108
+
109
+ with _ns_lock:
110
+ if ns.created_at_ms is None:
111
+ ns.created_at_ms = int(time.time() * 1000)
112
+ _ns_path(ns.slug).write_text(json.dumps(ns.to_dict(), indent=2))
113
+
114
+
115
+ def validate_slug(slug: str) -> None:
116
+ if not _SLUG_RE.match(slug):
117
+ raise ValueError(f"Invalid namespace slug {slug!r} — must match [a-z][a-z0-9-]{{0,62}}")
118
+
119
+
120
+ def ensure_default() -> None:
121
+ """Call at server startup to guarantee 'default' namespace exists."""
122
+ if not _ns_path("default").exists():
123
+ save_namespace(
124
+ Namespace(slug="default", label="Default", profile=DeploymentProfile(tier="local"))
125
+ )