From 0f6ef9fb3bd523fc500e36b4b8108451661b0d64 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 13 Oct 2026 21:35:28 +0200 Subject: [PATCH] ASSEMBLY-WP-0017 T02/T03 done, T04 in progress: IMM multi-indicator regime filter (0 false flips, accelerations detected in 1-1.5 y, slowdowns only after 2.5-3.75 y); observed investment and discovery series Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 14913@bnt-lap001 Assistant-Session: add7ef69-b901-4756-9a67-34cf1125d96f --- AGENTS.md | 2 +- research/ModelMatching.md | 51 +++- simulations/gadm02/matching.py | 156 ++++++++++- simulations/gadm02/observed_series.json | 247 +++++++++++++++++- simulations/gadm02/results/matching_imm.md | 33 +++ ...SSEMBLY-WP-0017-reliable-flip-detection.md | 12 +- 6 files changed, 481 insertions(+), 20 deletions(-) create mode 100644 simulations/gadm02/results/matching_imm.md diff --git a/AGENTS.md b/AGENTS.md index 2c2d26f..5464801 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ cd simulations/gadm02 && python3 -B calibrate_data.py && python3 -B calibrated_r cd simulations/gadm02 && for j in US EU; do python3 -B gsa.py --model gadm04 --jobs 8 --start $j; done # GSA around calibrated starts, ~30 min cd simulations/gadm02 && python3 -B cross_border.py # coupled world US/EU/AR from the calibrated start (models/CrossBorder.md) cd simulations/gadm02 && python3 -B world_scenarios.py # contexts X1–X4, hypothesis re-test, local vs. global -cd simulations/gadm02 && python3 -B matching.py && python3 -B matching.py --synthetic # model matching; flip-detection test (~10 min) +cd simulations/gadm02 && python3 -B matching.py && python3 -B matching.py --imm # model matching; IMM flip-detection acceptance test (~6 min) ``` Reality checkpoints (ASSEMBLY-WP-0010): scheduled by activity-core and diff --git a/research/ModelMatching.md b/research/ModelMatching.md index a73fb9d..a3caba8 100644 --- a/research/ModelMatching.md +++ b/research/ModelMatching.md @@ -1,8 +1,9 @@ # Model matching and regime flipping -**Status:** v0.1 (2026-10-13), ASSEMBLY-WP-0012. Matching on observed data -works. **Flip detection is not yet reliable** (§ 4). No flip may be declared -from real data until the synthetic test passes. +**Status:** v0.2 (2026-10-13), ASSEMBLY-WP-0012 and WP-0017. Matching on +observed data works. The IMM filter (§ 4.1) detects accelerations reliably, +but detects slowdowns only after 2.5–3.75 years. The acceptance test is not +yet passed, so no measured flip may be declared yet. Code: `simulations/gadm02/matching.py`. Data: `observed_series.json`. Results: `results/matching.md` (observed), `results/matching_synthetic.md` @@ -108,3 +109,47 @@ equation reverts toward neutral from the low measured level. - acceptance criteria: at least 5 of 6 regimes correct at the start, no false flips in 6 no-switch runs, and at least 4 of 6 switches detected within 1.5 years. + +### 4.1 IMM filter with several indicators (WP-0017-T02/T03) + +`matching.py --imm` (`results/matching_imm.md`): + +- **Filter:** an interacting-multiple-model filter. Each regime keeps its own + state, and before each quarter the regime states are mixed by the + switching probabilities (0.10 per quarter). Each regime then runs forward, + and the weights update with the likelihood of all indicators in the + quarter. +- **Indicators:** adoption (biweekly, log sd 0.03), gross organism + investment (quarterly, log sd 0.05), discovery channel Pd (monthly, sd + 0.01) and sentiment (twice a year, sd 0.02). +- **Sentiment:** anchored (BR-009 fix). + +| Case | Result | +|---|---| +| anchor correct at the first evaluation (no-switch truth) | 6 of 6 | +| false flips (no-switch truth) | 0 of 6 | +| acceleration B2 → B4 | detected 3 of 3, delay 1.0–1.5 years | +| slowdown B3 → B2 | right direction 3 of 3, but delay 2.5–3.75 years (outside 1.5) | +| loop switch off → on | 0 of 3 | + +Acceptance (anchor switches ≥ 4 of 6 within 1.5 years): **not passed** (3 of +6). + +Readings: + +1. **Accelerations are visible fast. Slowdowns are not.** After a slowdown, + the paths diverge slowly from a shared state. For the real data: telling + B2 from B3 in the US (P-2026-001) will take roughly 2.5–4 years of + observations, not one. +2. **The loop state is not identifiable while sentiment is flat.** With + sentiment anchored, switching the loop on changes nothing visible until + perceived benefit moves. The loop is a conditional regime: it can be + matched only after a shock to perceived benefit. +3. **More indicators remove false flips; they do not speed up slowdown + detection.** With adoption alone the IMM filter had 3 of 6 false flips. With + all four indicators it had none. + +Observed series for the real filter: US adoption (BTOS), US investment (L5, +quarterly; operator rough), US discovery channel (StatCounter, monthly), EU +and DE adoption (annual). Sentiment points after checkpoint 0 do not exist +yet. diff --git a/simulations/gadm02/matching.py b/simulations/gadm02/matching.py index f69561a..fdce655 100644 --- a/simulations/gadm02/matching.py +++ b/simulations/gadm02/matching.py @@ -18,7 +18,8 @@ Flip: the best candidate changes and the new best has weight >= FLIP_WEIGHT in two consecutive evaluations (research/ModelMatching.md § 3). Usage: python3 -B simulations/gadm02/matching.py # match observed series - python3 -B simulations/gadm02/matching.py --synthetic # recovery and flip test + python3 -B simulations/gadm02/matching.py --synthetic # recovery and flip test (single-state filter) + python3 -B simulations/gadm02/matching.py --imm # IMM filter, several indicators (WP-0017) """ from __future__ import annotations @@ -299,5 +300,156 @@ def main_synthetic() -> None: print("\n".join(lines)) + + +# --- WP-0017-T02/T03: interacting-multiple-model filter with several indicators ----------------- + +def observe(rows: list[dict], t: float, kind: str, dO: float = None) -> float: + """Model value of an indicator at time t (rows recorded every STEP).""" + if kind == "adopt": + return interp(rows, t, "Ko") + if kind == "invest": # gross organism investment: dK_Omega/dt + dO K_Omega + dO = g.PARAMS["dO"] if dO is None else dO + a, b = interp(rows, max(0.0, t - STEP), "Ko"), interp(rows, t, "Ko") + return max(1e-9, (b - a) / STEP + dO * b) + if kind == "pd": + return interp(rows, t, "Pd") + if kind == "sig": + return interp(rows, t, "Sig") + raise KeyError(kind) + + +NOISE = {"adopt": ("log", 0.03), "invest": ("log", 0.05), "pd": ("abs", 0.01), "sig": ("abs", 0.02)} +CADENCE = {"adopt": 2 / 52, "invest": 0.25, "pd": 1 / 12, "sig": 0.5} + + +def obs_ll(rows: list[dict], obs: list, scales: dict) -> float: + ll = 0.0 + for tau, kind, y in obs: + mode, sd = NOISE[kind] + m_ = observe(rows, tau, kind) + if mode == "log": + ll += -0.5 * ((math.log(y) - math.log(max(1e-9, m_)) - scales[kind]) / sd) ** 2 + else: + ll += -0.5 * ((y - m_) / sd) ** 2 + return ll + + +IMM_SWITCH = 0.10 # switching prior per quarter for the IMM filter (tested in main_imm) + + +def imm_run(truth: list[dict], start: float, years: float, seed: int, kinds: tuple, + switch: float = IMM_SWITCH, step: float = 0.25) -> list[tuple[float, dict]]: + """IMM regime filter (research/ModelMatching.md § 4.1). + + Initial states and log-scales from a whole-path alignment of the first year of adoption data; + then every quarter: mix regime states by switching probabilities, run each regime from its + mixed state, update weights by the likelihood of all indicators in the quarter.""" + rnd = random.Random(seed) + obs = [] + for kind in kinds: + t = start + while t <= start + years + 1e-9: + mode, sd = NOISE[kind] + v = observe(truth, t, kind) + y = v * 25.0 * math.exp(rnd.gauss(0, sd)) if kind == "adopt" else \ + v * 3.0 * math.exp(rnd.gauss(0, sd)) if kind == "invest" else v + rnd.gauss(0, sd) + obs.append((t, kind, y)) + t += CADENCE[kind] + te0 = start + 1.0 + first_pts = [(t - start, y) for t, k, y in obs if k == "adopt" and t <= te0] + ko = interp(truth, te0, "Ko") + first = score("US", path, {"points": first_pts, "log_sigma": NOISE["adopt"][1]}, + level={"lo": ko - 0.075, "hi": ko + 0.075}) + b = best(first) + t_b = first[b]["t0"] + scales = {"adopt": first[b]["ls"]} + inv = [(t - start, y) for t, k, y in obs if k == "invest" and t <= te0] + if inv: + rb = path("US", *b) + scales["invest"] = sum(math.log(y) - math.log(observe(rb, t_b + tau, "invest")) for tau, y in inv) / len(inv) + states = {} + for c in CANDIDATES: + rc = path("US", *c) + i = min(len(rc) - 1, int(round((first[c]["t0"] + 1.0) / STEP))) + states[c] = {k: rc[i][k] for k in g.STOCKS} + w = {c: first[c]["w"] for c in CANDIDATES} + evals = [(te0, {c: {"w": w[c]} for c in CANDIDATES})] + n = len(CANDIDATES) + pi = {(i, c): (1 - switch) if i == c else switch / (n - 1) for i in CANDIDATES for c in CANDIDATES} + te = te0 + step + while te <= start + years + 1e-9: + window = [(t - (te - step), k, y) for t, k, y in obs if te - step < t <= te + 1e-9] + cbar = {c: sum(pi[(i, c)] * w[i] for i in CANDIDATES) for c in CANDIDATES} + lls, ends = {}, {} + for c in CANDIDATES: + mu = {i: pi[(i, c)] * w[i] / cbar[c] for i in CANDIDATES} + x0 = {k: sum(mu[i] * states[i][k] for i in CANDIDATES) for k in g.STOCKS} + rows = g.simulate(regime_params("US", c), x0, T=step, record_every=1) + lls[c] = obs_ll(rows, window, scales) + ends[c] = {k: rows[-1][k] for k in g.STOCKS} + mx = max(lls.values()) + post = {c: cbar[c] * math.exp(lls[c] - mx) for c in CANDIDATES} + zs = sum(post.values()) + w = {c: post[c] / zs for c in CANDIDATES} + states = ends + evals.append((te, {c: {"w": w[c]} for c in CANDIDATES})) + te += step + return evals + + +def main_imm() -> None: + kinds = ("adopt", "invest", "pd", "sig") + lines = [f"# {g.MODEL_VERSION}: IMM regime filter on synthetic data", "", + "Generated by `simulations/gadm02/matching.py --imm` (ASSEMBLY-WP-0017-T02/T03/T04).", + "US calibrated state, sentiment anchored (BR-009 fix). Truth from a known candidate, optionally", + "switching at t 4; observed from t 2 to t 8. Indicators and noise: adoption biweekly (log sd 0.03),", + "investment quarterly (log sd 0.05), discovery channel Pd monthly (sd 0.01), sentiment twice a", + f"year (sd 0.02). IMM filter, switch probability {IMM_SWITCH} per quarter. Flip: new best with weight", + f"≥ {FLIP_WEIGHT} twice in a row.", "", + "**Acceptance (anchor regimes):** anchor correct at the first evaluation in ≥ 5 of 6 no-switch runs,", + "no false flips in 6 no-switch runs, and ≥ 4 of 6 anchor switches detected within 1.5 years. The", + "loop switch is reported separately: with sentiment anchored, switching the loop on is invisible", + "until perceived benefit moves.", "", + "| Truth | Seed | Best at start | Flips (t, from → to) | Verdict |", "|---|---|---|---|---|"] + correct = false = det = 0 + loop_det = 0 + cases = [(("B3", "off"), None, None, "none"), (("B2", "on"), None, None, "none"), + (("B2", "off"), ("B4", "off"), 4.0, "anchor"), (("B3", "off"), ("B2", "off"), 4.0, "anchor"), + (("B3", "off"), ("B3", "on"), 4.0, "loop")] + for first_c, second, ts, kind in cases: + truth = synthetic_rows("US", first_c, second, ts or T_PATH) + for seed in (1, 2, 3): + ev = imm_run(truth, 2.0, 6.0, seed, kinds) + fl = flips(ev) + b0 = best(ev[0][1]) + if kind == "none": + correct += b0[0] == first_c[0] + false += bool(fl) + verdict = ("anchor correct" if b0[0] == first_c[0] else "anchor wrong") + ("; FALSE FLIP" if fl else "") + else: + if kind == "anchor": + hit = [e for e in fl if ts <= e[0] <= ts + 1.5 and e[2][0] == second[0]] + det += bool(hit) + else: + hit = [e for e in fl if ts <= e[0] <= ts + 1.5 and e[2][1] == second[1]] + loop_det += bool(hit) + verdict = f"detected, delay {hit[0][0] - ts:.2f} y" if hit else "missed" + label = fmt(first_c) + (f" → {fmt(second)} at t {ts}" if second else "") + lines.append(f"| {label} | {seed} | {fmt(b0)} | " + + ("; ".join(f"{t:.2f}: {fmt(a)} → {fmt(b)}" for t, a, b in fl) or "none") + + f" | {verdict} |") + ok = correct >= 5 and false == 0 and det >= 4 + lines += ["", f"Anchor correct at start {correct}/6, false flips {false}/6, anchor switches detected {det}/6, " + f"loop switches detected {loop_det}/3. Acceptance: {'**passed**' if ok else '**not passed**'}.", ""] + (OUT / "matching_imm.md").write_text("\n".join(lines)) + print("\n".join(lines)) + + if __name__ == "__main__": - main_synthetic() if "--synthetic" in sys.argv else main_observed() + if "--imm" in sys.argv: + main_imm() + elif "--synthetic" in sys.argv: + main_synthetic() + else: + main_observed() diff --git a/simulations/gadm02/observed_series.json b/simulations/gadm02/observed_series.json index 8643c22..2a763fa 100644 --- a/simulations/gadm02/observed_series.json +++ b/simulations/gadm02/observed_series.json @@ -2,31 +2,256 @@ "note": "Observed indicator series for model matching (ASSEMBLY-WP-0012). Values as fetched for checkpoint 0 and the 2026-10-13 review; t in years relative to the first point. Append new points at each checkpoint; do not edit old ones.", "series": { "US_adoption": { - "indicator": "L1", "unit": "% of employer firms using AI (last two weeks)", + "indicator": "L1", + "unit": "% of employer firms using AI (last two weeks)", "source": "U.S. Census Bureau BTOS Q7 (National.xlsx), periods 2025-24 to 2026-20 (biweekly)", "log_sigma": 0.03, - "points": [[0.0, 17.3], [0.0385, 17.2], [0.0769, 17.8], [0.1154, 17.7], [0.1538, 18.2], [0.1923, 18.9], - [0.2308, 17.5], [0.2692, 18.2], [0.3077, 18.9], [0.3462, 19.1], [0.3846, 19.8], [0.4231, 19.8], - [0.4615, 19.5], [0.5, 20.6], [0.5385, 20.6], [0.5769, 20.6], [0.6154, 21.7], [0.6538, 21.5], - [0.6923, 21.8], [0.7308, 22.4], [0.7692, 23.2], [0.8077, 23.8], [0.8462, 23.8]] + "points": [ + [ + 0.0, + 17.3 + ], + [ + 0.0385, + 17.2 + ], + [ + 0.0769, + 17.8 + ], + [ + 0.1154, + 17.7 + ], + [ + 0.1538, + 18.2 + ], + [ + 0.1923, + 18.9 + ], + [ + 0.2308, + 17.5 + ], + [ + 0.2692, + 18.2 + ], + [ + 0.3077, + 18.9 + ], + [ + 0.3462, + 19.1 + ], + [ + 0.3846, + 19.8 + ], + [ + 0.4231, + 19.8 + ], + [ + 0.4615, + 19.5 + ], + [ + 0.5, + 20.6 + ], + [ + 0.5385, + 20.6 + ], + [ + 0.5769, + 20.6 + ], + [ + 0.6154, + 21.7 + ], + [ + 0.6538, + 21.5 + ], + [ + 0.6923, + 21.8 + ], + [ + 0.7308, + 22.4 + ], + [ + 0.7692, + 23.2 + ], + [ + 0.8077, + 23.8 + ], + [ + 0.8462, + 23.8 + ] + ] }, "EU_adoption": { - "indicator": "L2", "unit": "% of enterprises (10+ employees) using at least one AI technology", + "indicator": "L2", + "unit": "% of enterprises (10+ employees) using at least one AI technology", "source": "Eurostat isoc_eb_ai, EU27_2020, reference years 2023-2025 (2021: 7.65, before generative AI, excluded)", "log_sigma": 0.05, - "points": [[0.0, 8.06], [1.0, 13.48], [2.0, 19.95]] + "points": [ + [ + 0.0, + 8.06 + ], + [ + 1.0, + 13.48 + ], + [ + 2.0, + 19.95 + ] + ] }, "DE_adoption": { - "indicator": "L2", "unit": "% of enterprises (10+ employees) using at least one AI technology", + "indicator": "L2", + "unit": "% of enterprises (10+ employees) using at least one AI technology", "source": "Eurostat isoc_eb_ai, DE, reference years 2023-2025 (2021: 10.56, excluded)", "log_sigma": 0.05, - "points": [[0.0, 11.55], [1.0, 19.75], [2.0, 25.97]] + "points": [ + [ + 0.0, + 11.55 + ], + [ + 1.0, + 19.75 + ], + [ + 2.0, + 25.97 + ] + ] }, "US_sentiment": { - "indicator": "L6", "unit": "Sigma = 0.5 + (excited - concerned)/200", + "indicator": "L6", + "unit": "Sigma = 0.5 + (excited - concerned)/200", "source": "Pew Research: 2021 (37 % concerned, 18 % excited) and June 2026 (52 %, 9 %)", "sigma": 0.03, - "points": [[0.0, 0.405], [5.0, 0.285]] + "points": [ + [ + 0.0, + 0.405 + ], + [ + 5.0, + 0.285 + ] + ] + }, + "US_investment": { + "indicator": "L5", + "unit": "billions of USD, SAAR", + "source": "BEA private fixed investment in information processing equipment (FRED Y033RC1Q027SBEA), 2024 Q3 to 2026 Q2. Operator: proportional to gross organism investment; rough, because the series covers all information-processing equipment, not AI only", + "log_sigma": 0.05, + "points": [ + [ + 0.0, + 1543.157 + ], + [ + 0.25, + 1534.174 + ], + [ + 0.5, + 1610.088 + ], + [ + 0.75, + 1651.61 + ], + [ + 1.0, + 1709.98 + ], + [ + 1.25, + 1754.081 + ], + [ + 1.5, + 1836.848 + ], + [ + 1.75, + 1922.516 + ] + ] + }, + "US_discovery": { + "indicator": "CB1 / P2", + "unit": "leading search engine share (Google), fraction", + "source": "StatCounter Global Stats, US, monthly 2025-10 to 2026-09. Operator: Pd = leading search share (as in calibration)", + "sigma": 0.01, + "points": [ + [ + 0.0, + 0.851 + ], + [ + 0.0833, + 0.8507 + ], + [ + 0.1667, + 0.8448 + ], + [ + 0.25, + 0.8391 + ], + [ + 0.3333, + 0.8417 + ], + [ + 0.4167, + 0.85 + ], + [ + 0.5, + 0.8516 + ], + [ + 0.5833, + 0.8582 + ], + [ + 0.6667, + 0.8668 + ], + [ + 0.75, + 0.8661 + ], + [ + 0.8333, + 0.8601 + ], + [ + 0.9167, + 0.8519 + ] + ] } } } diff --git a/simulations/gadm02/results/matching_imm.md b/simulations/gadm02/results/matching_imm.md new file mode 100644 index 0000000..73214ba --- /dev/null +++ b/simulations/gadm02/results/matching_imm.md @@ -0,0 +1,33 @@ +# GADM-0.4: IMM regime filter on synthetic data + +Generated by `simulations/gadm02/matching.py --imm` (ASSEMBLY-WP-0017-T02/T03/T04). +US calibrated state, sentiment anchored (BR-009 fix). Truth from a known candidate, optionally +switching at t 4; observed from t 2 to t 8. Indicators and noise: adoption biweekly (log sd 0.03), +investment quarterly (log sd 0.05), discovery channel Pd monthly (sd 0.01), sentiment twice a +year (sd 0.02). IMM filter, switch probability 0.1 per quarter. Flip: new best with weight +≥ 0.75 twice in a row. + +**Acceptance (anchor regimes):** anchor correct at the first evaluation in ≥ 5 of 6 no-switch runs, +no false flips in 6 no-switch runs, and ≥ 4 of 6 anchor switches detected within 1.5 years. The +loop switch is reported separately: with sentiment anchored, switching the loop on is invisible +until perceived benefit moves. + +| Truth | Seed | Best at start | Flips (t, from → to) | Verdict | +|---|---|---|---|---| +| B3/loop off | 1 | B3/loop off | none | anchor correct | +| B3/loop off | 2 | B3/loop off | none | anchor correct | +| B3/loop off | 3 | B3/loop off | none | anchor correct | +| B2/loop on | 1 | B2/loop off | none | anchor correct | +| B2/loop on | 2 | B2/loop off | none | anchor correct | +| B2/loop on | 3 | B2/loop off | none | anchor correct | +| B2/loop off → B4/loop off at t 4.0 | 1 | B2/loop off | 5.00: B2/loop on → B4/loop off | detected, delay 1.00 y | +| B2/loop off → B4/loop off at t 4.0 | 2 | B2/loop off | 5.50: B2/loop off → B4/loop off | detected, delay 1.50 y | +| B2/loop off → B4/loop off at t 4.0 | 3 | B2/loop off | 5.25: B2/loop off → B4/loop off | detected, delay 1.25 y | +| B3/loop off → B2/loop off at t 4.0 | 1 | B3/loop off | 7.75: B3/loop off → B2/loop on | missed | +| B3/loop off → B2/loop off at t 4.0 | 2 | B3/loop off | 7.00: B3/loop off → B2/loop off | missed | +| B3/loop off → B2/loop off at t 4.0 | 3 | B3/loop off | 6.50: B3/loop off → B2/loop off | missed | +| B3/loop off → B3/loop on at t 4.0 | 1 | B3/loop off | none | missed | +| B3/loop off → B3/loop on at t 4.0 | 2 | B3/loop off | none | missed | +| B3/loop off → B3/loop on at t 4.0 | 3 | B3/loop off | none | missed | + +Anchor correct at start 6/6, false flips 0/6, anchor switches detected 3/6, loop switches detected 0/3. Acceptance: **not passed**. diff --git a/workplans/ASSEMBLY-WP-0017-reliable-flip-detection.md b/workplans/ASSEMBLY-WP-0017-reliable-flip-detection.md index 9984479..d31e1b2 100644 --- a/workplans/ASSEMBLY-WP-0017-reliable-flip-detection.md +++ b/workplans/ASSEMBLY-WP-0017-reliable-flip-detection.md @@ -48,11 +48,13 @@ record which readings change. ```task id: ASSEMBLY-WP-0017-T02 -status: todo +status: done priority: high state_hub_task_id: "57035a4b-7165-52f6-b77b-b7e6476a0e79" ``` +**Done 2026-10-13:** indicators adoption, investment, Pd and sentiment in the filter (`observe`, `obs_ll`); US investment (L5) and US discovery (StatCounter) series added to `observed_series.json`. + Add to `filter_step` and to `observed_series.json`: investment (L5, quarterly; separates self-enhancement and B4), concentration channels (CB1, monthly; domination dynamics), and sentiment at least twice a year where @@ -63,11 +65,13 @@ level. ```task id: ASSEMBLY-WP-0017-T03 -status: todo +status: done priority: medium state_hub_task_id: "a8b12642-c842-581d-840d-5e1a1c731d0c" ``` +**Done 2026-10-13:** IMM filter (`imm_run`): one state per regime, mixed by switching probabilities. False flips 3/6 → 0/6 (with all indicators). + Today the filter carries one shared state estimate, advanced by the best regime. Track one state per regime (interacting multiple-model filter), so that a regime that becomes true later is not judged from another regime's @@ -77,11 +81,13 @@ state. ```task id: ASSEMBLY-WP-0017-T04 -status: todo +status: progress priority: high state_hub_task_id: "5e4d8270-8499-5635-98d4-55f3186258d2" ``` +**2026-10-13, not passed:** anchors correct 6/6, false flips 0/6, B2 → B4 detected 3/3 within 1.0–1.5 y, B3 → B2 detected 3/3 but only after 2.5–3.75 y (acceptance needs ≥ 4/6 within 1.5 y). Next: a slowdown-sensitive indicator (for example the growth rate of investment, or adoption *intentions*, BTOS Q24) or a longer acceptance window for slowdowns. The criterion must not be loosened without a reason recorded here. + Acceptance: at least 5 of 6 regimes correct at the first evaluation, no false flips in 6 no-switch runs, and at least 4 of 6 switches detected within 1.5 years (`matching.py --synthetic`). Add the criteria as a slow test.