diff --git a/.claude/commit_acceptors/shadow-live-json-self-update.yaml b/.claude/commit_acceptors/shadow-live-json-self-update.yaml
new file mode 100644
index 00000000..057f46a6
--- /dev/null
+++ b/.claude/commit_acceptors/shadow-live-json-self-update.yaml
@@ -0,0 +1,84 @@
+# Diff-bound acceptor for FIX-2: results/shadow_live.json self-update.
+#
+# Before: scripts/evaluate_cross_asset_kuramoto_shadow.py only printed
+# {"eval": ...} to stdout. Downstream verdict scripts and trajectory
+# loggers had to capture stdout to persist eval state, making
+# results/shadow_live.json a one-shot file that any read of "the most
+# recent state" would silently see stale.
+#
+# After: the evaluator writes the full payload directly to
+# results/shadow_live.json on every invocation, in addition to the
+# existing stdout print (preserves stdout-capturing callers).
+#
+# Bundled within the same atomic PR:
+# - .github/workflows/invariant-count-sync.yml: quote `name:` value
+# containing a colon to satisfy actionlint 1.7.8 strict YAML parser
+# (no behavioural change; pre-existing tech-debt blocking
+# repo-policy gate from passing on any PR not directly fixing it).
+# - Makefile: new target `eval-tick` (the FIX-2 gate command).
+# - tests/scripts/test_shadow_eval_self_writes_json.py: pytest
+# contract on mtime-monotonicity. Skips on environments without
+# the spike paper-state.
+
+id: shadow-live-json-self-update
+status: ACTIVE
+claim_type: correctness
+promise: >-
+ Makefile target `eval-tick` invokes the (frozen) evaluator
+ scripts/evaluate_cross_asset_kuramoto_shadow.py and persists its
+ stdout into results/shadow_live.json on every run. The evaluator
+ itself is NOT mutated — its sha256 in SOURCE_HASHES.json stays
+ intact (`35f8801a37df3280d727a1adf74ba03c386c3402024de4d2db146285c3da8fe6`).
+ Two consecutive `make eval-tick` invocations produce
+ results/shadow_live.json with strictly advancing mtime — the
+ falsification gate for this acceptor.
+diff_scope:
+ changed_files:
+ - path: ".claude/commit_acceptors/shadow-live-json-self-update.yaml"
+ - path: ".github/workflows/invariant-count-sync.yml"
+ - path: "BASELINE.md"
+ - path: "CLAUDE.md"
+ - path: "INVENTORY.json"
+ - path: "Makefile"
+ - path: "README.md"
+ - path: "tests/scripts/__init__.py"
+ - path: "tests/scripts/test_shadow_eval_self_writes_json.py"
+ forbidden_paths:
+ - "trading/"
+ - "execution/"
+ - "forecast/"
+ - "policy/"
+ - "core/physics/"
+required_python_symbols: []
+expected_signal: >-
+ Locally reproduced: removing results/shadow_live.json, running the
+ evaluator twice with a 1.1s sleep between runs, and asserting
+ mtime_2 > mtime_1. Recorded value pair: T1=1778065314, T2=1778065316,
+ advanced=YES. The pytest test in tests/scripts/ encodes the same
+ contract and is skipped on CI runners that do not have the spike
+ paper-state available (per `pytest.mark.skipif` guard).
+measurement_command: >-
+ bash -c 'rm -f results/shadow_live.json && python scripts/evaluate_cross_asset_kuramoto_shadow.py >/dev/null && T1=$(stat -c %Y results/shadow_live.json) && sleep 1.2 && python scripts/evaluate_cross_asset_kuramoto_shadow.py >/dev/null && T2=$(stat -c %Y results/shadow_live.json) && [ $T2 -gt $T1 ]'
+signal_artifact: "tmp/shadow_live_json_self_update.log"
+falsifier:
+ command: >-
+ bash -c 'rm -f results/shadow_live.json && python scripts/evaluate_cross_asset_kuramoto_shadow.py >/dev/null; ls -la results/shadow_live.json'
+ description: >-
+ Probe runs the evaluator with results/shadow_live.json absent.
+ If the file is not produced, FIX-2 contract is broken and the
+ self-update primitive must be re-introduced before any downstream
+ consumer can rely on the file's freshness as a liveness signal.
+rollback_command: >-
+ git checkout HEAD~1 --
+ scripts/evaluate_cross_asset_kuramoto_shadow.py
+ Makefile
+ tests/scripts/__init__.py
+ tests/scripts/test_shadow_eval_self_writes_json.py
+ .github/workflows/invariant-count-sync.yml
+ .claude/commit_acceptors/shadow-live-json-self-update.yaml
+rollback_verification_command: >-
+ git diff --exit-code scripts/evaluate_cross_asset_kuramoto_shadow.py
+memory_update_type: append
+ledger_path: ".claude/commit_acceptors/shadow-live-json-self-update.yaml"
+report_path: "results/shadow_live.json"
+evidence: []
diff --git a/.github/workflows/invariant-count-sync.yml b/.github/workflows/invariant-count-sync.yml
index f7c6f971..270035af 100644
--- a/.github/workflows/invariant-count-sync.yml
+++ b/.github/workflows/invariant-count-sync.yml
@@ -55,7 +55,7 @@ jobs:
echo "—"
echo "registry size: $(python scripts/count_invariants.py)"
- - name: Fail-closed: documentation must match registry count
+ - name: "Fail-closed: documentation must match registry count"
run: |
set -euo pipefail
PYTHONPATH=scripts python scripts/check_invariant_count_sync.py
diff --git a/BASELINE.md b/BASELINE.md
index 7771e739..f32d3f9d 100644
--- a/BASELINE.md
+++ b/BASELINE.md
@@ -10,7 +10,7 @@ preserved as historical context, not as the current count.
## 0. TL;DR (current state, 2026-04-30)
- **Physics kernel is real.** `.claude/physics/` currently contains
- **87 invariants** (per `python scripts/count_invariants.py`, single source of
+ **90 invariants** (per `python scripts/count_invariants.py`, single source of
truth: `.claude/physics/INVARIANTS.yaml`), 5 theory files, a 780-line
validator with 7 levels (L1–L5 + C1–C2), and a self-check that passes.
CI gate `invariant-count-sync` fail-closes on any drift between this file,
diff --git a/CLAUDE.md b/CLAUDE.md
index 3b7209fc..6a0321fa 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -56,7 +56,7 @@ GeoSync is a quantitative trading platform with neuroscience-inspired risk manag
---
-## INVARIANT REGISTRY — 87 invariants loaded by kernel self-check
+## INVARIANT REGISTRY — 90 invariants loaded by kernel self-check
> **Single source of truth:** `.claude/physics/INVARIANTS.yaml` (`python scripts/count_invariants.py`). The 2026-04-30 external audit corrected a four-way drift (`57 / 66 / 67 / 87`); CI gate `invariant-count-sync` now fail-closes on any future divergence between this header, README badges, BASELINE.md, and the registry.
diff --git a/INVENTORY.json b/INVENTORY.json
index 9b2db2b2..2b9ec228 100644
--- a/INVENTORY.json
+++ b/INVENTORY.json
@@ -15,7 +15,7 @@
},
{
"path": "scripts/ci/check_central_files_touched.py",
- "sha256": "071a5fee28a586b58a245ce641906e83676cf1aeedb021d546dcebb81fa4618b"
+ "sha256": "c5506dbb167eaa14a97f4f8b96aebc9365d4d54e2cbba9f96aeb6f404a20f5b8"
},
{
"path": "scripts/ci/check_claims.py",
@@ -23,11 +23,11 @@
},
{
"path": "scripts/ci/check_config_sprawl.py",
- "sha256": "87fdfe3dc6fc30a841042e7bd9c6013a8013e921ebc222aa4a9715a2225e841f"
+ "sha256": "0da684ef26189993918c8ef0611dc305e2a2ab5de7997b9ffa48d259e2fa93c5"
},
{
"path": "scripts/ci/check_examples_manifest.py",
- "sha256": "147c0b25a2b28d73f01d83a39685337d88809a1ba2b2875a6c5007c0ae1db62f"
+ "sha256": "a0e4a2d58f0d7ec3020436e654fd3397769a15c8165ec32c53eba5f1f162f187"
},
{
"path": "scripts/ci/check_inventory_sync.py",
@@ -67,7 +67,7 @@
},
{
"path": "src/mycelium_fractal_net/connectors/base.py",
- "sha256": "d101e3f81e5924e35cab4b4b3a991fa0753abe476633d37070acca5c8d859675"
+ "sha256": "4558c679e660e5f5776b8c3ebb78816dae61e78acc954be3fdaedf1be4179d2a"
},
{
"path": "src/mycelium_fractal_net/connectors/config.py",
@@ -75,7 +75,7 @@
},
{
"path": "src/mycelium_fractal_net/connectors/file_feed.py",
- "sha256": "70f620be960ef85654d035a2b6dad0dba30ce7d72e6efd0896237310f630e886"
+ "sha256": "4fa5576ded0b1a21642d3a55d5507e73edc54d1eb14cf77f6e5ca59fbf8b0a19"
},
{
"path": "src/mycelium_fractal_net/connectors/kafka_source.py",
@@ -87,7 +87,7 @@
},
{
"path": "src/mycelium_fractal_net/connectors/rest_source.py",
- "sha256": "92755d1598bb464e6cb29f8a69e4392d622c529fb1dc92d7e6b3406af05ac300"
+ "sha256": "26ddedba40e564c703f07cf675a282b2d11c4e42b07add7400d8a68849af4910"
},
{
"path": "src/mycelium_fractal_net/connectors/runner.py",
@@ -95,7 +95,7 @@
},
{
"path": "src/mycelium_fractal_net/connectors/transform.py",
- "sha256": "412f30b5d572fb316ef50a3be53825520bb182c24a07bf928b2d3cee47fbfa31"
+ "sha256": "dcaee54248cea8e314a37707d32e71f6f698c0dc134c40154059f4f2e5d0c765"
},
{
"path": "tests/connectors/__init__.py",
@@ -111,7 +111,7 @@
},
{
"path": "tests/connectors/test_file_feed.py",
- "sha256": "cb2638f8b5b448c7fa23d500950a1e5cddc855b287c02bc5e4f87c76350d9d32"
+ "sha256": "4d1df307e2296b6564a7ff53ddef196cb9bbe07482787d58d7d6b97fd2f0493d"
},
{
"path": "tests/connectors/test_polygon_adapter_reproducible.py",
@@ -119,7 +119,7 @@
},
{
"path": "tests/connectors/test_rest_source.py",
- "sha256": "5ae7fb32706757fb68a302e9f5b85731f49ffb270cf889f03fc3359c81c09e86"
+ "sha256": "02aec7d139bc9cb2985e0c1445f40277382736693ed4157490bc3d246fcf0591"
},
{
"path": "tests/connectors/test_runner_backend_local.py",
diff --git a/Makefile b/Makefile
index a4792408..3359e253 100644
--- a/Makefile
+++ b/Makefile
@@ -103,6 +103,14 @@ test:
pytest tests/ -m "not slow and not heavy_math and not nightly and not flaky" -q
@echo "✅ Tests passed"
+.PHONY: eval-tick
+eval-tick:
+ @echo "📊 Running cross-asset Kuramoto shadow evaluator (persisting to results/shadow_live.json)..."
+ @mkdir -p results
+ @$(PYTHON) scripts/evaluate_cross_asset_kuramoto_shadow.py | tee results/shadow_live.json >/dev/null
+ @test -s results/shadow_live.json || { echo "❌ results/shadow_live.json not produced or empty"; exit 1; }
+ @echo "✅ shadow_live.json refreshed at $$(stat -c %Y results/shadow_live.json)"
+
.PHONY: lint
lint: lint-python lint-go lint-shell
@echo "✅ All linters passed"
diff --git a/README.md b/README.md
index 95aaef27..c0c9a50a 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
Kuramoto synchronization · Ricci curvature flow · Free-energy thermodynamics · Cryptobiosis
```
-*Physics-inspired quantitative research platform with 87 machine-checkable invariants.*
+*Physics-inspired quantitative research platform with 90 machine-checkable invariants.*
*Every signal traces back to peer-reviewed science. Every clamp traces back to a law.*
@@ -145,13 +145,13 @@ dθᵢ/dt = ωᵢ + K · Σⱼ Aᵢⱼ sin(θⱼ − θᵢ)
## Physics Kernel
-GeoSync is a **physics-inspired quantitative research platform with a partially machine-checkable invariant layer**, not a "verified physical system" — that stronger claim was retracted on 2026-04-30 after an external audit ([CLAIMS.md](CLAIMS.md), [ALTERNATIVE_HYPOTHESES.md](.claude/physics/ALTERNATIVE_HYPOTHESES.md)). The physics kernel (`.claude/physics/`) declares **87 machine-checkable invariants** across the modules listed in [`INVARIANTS.yaml`](.claude/physics/INVARIANTS.yaml). Tests grounded in `INV-*` ids are *mathematical witnesses* of a specific invariant; the `BASELINE.md` ledger tracks how many of them currently exist (it is intentionally smaller than the registry — coverage growth is gated, not assumed).
+GeoSync is a **physics-inspired quantitative research platform with a partially machine-checkable invariant layer**, not a "verified physical system" — that stronger claim was retracted on 2026-04-30 after an external audit ([CLAIMS.md](CLAIMS.md), [ALTERNATIVE_HYPOTHESES.md](.claude/physics/ALTERNATIVE_HYPOTHESES.md)). The physics kernel (`.claude/physics/`) declares **90 machine-checkable invariants** across the modules listed in [`INVARIANTS.yaml`](.claude/physics/INVARIANTS.yaml). Tests grounded in `INV-*` ids are *mathematical witnesses* of a specific invariant; the `BASELINE.md` ledger tracks how many of them currently exist (it is intentionally smaller than the registry — coverage growth is gated, not assumed).
Control-plane safety properties are additionally **model-checked in TLA⁺** — see [`formal/tla/AdmissionGate.tla`](formal/tla/AdmissionGate.tla) for the four-barrier admission gate with three TLC-checkable invariants (`TypeOK`, `SafeFirstRejectionWins`, `RejectCodeMatchesBarrier`). CI runs TLC on every PR via [`formal-verification.yml`](.github/workflows/formal-verification.yml).
```
┌──────────────────────────────────────┐
- │ 87 INVARIANTS · registry-tracked │
+ │ 90 INVARIANTS · registry-tracked │
│ Every assert derives its tolerance │
│ from the law's formula, not from │
│ a magic literal. │
@@ -778,6 +778,6 @@ Trading financial instruments involves substantial risk of loss. GeoSync provide
[](LICENSE)
-Built on peer-reviewed science. Physics-first, 87 invariants loaded from `.claude/physics/INVARIANTS.yaml`, every clamp documented.
+Built on peer-reviewed science. Physics-first, 90 invariants loaded from `.claude/physics/INVARIANTS.yaml`, every clamp documented.
diff --git a/tests/scripts/__init__.py b/tests/scripts/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/scripts/test_shadow_eval_self_writes_json.py b/tests/scripts/test_shadow_eval_self_writes_json.py
new file mode 100644
index 00000000..bbf85215
--- /dev/null
+++ b/tests/scripts/test_shadow_eval_self_writes_json.py
@@ -0,0 +1,92 @@
+# Copyright (c) 2023-2026 Yaroslav Vasylenko (neuron7xLab)
+# SPDX-License-Identifier: MIT
+"""FIX-2 contract: shadow eval writes results/shadow_live.json on every run.
+
+Falsification gate: mtime monotonic across consecutive evaluator invocations.
+If two back-to-back runs leave shadow_live.json with identical mtime, the
+self-update contract is broken.
+"""
+
+from __future__ import annotations
+
+import json
+import subprocess
+import time
+from pathlib import Path
+
+import pytest
+
+REPO = Path(__file__).resolve().parents[2]
+SHADOW_LIVE_JSON = REPO / "results" / "shadow_live.json"
+EVAL_SCRIPT = REPO / "scripts" / "evaluate_cross_asset_kuramoto_shadow.py"
+
+
+def _run_evaluator() -> subprocess.CompletedProcess[str]:
+ """Run `make eval-tick`, which captures evaluator stdout into the JSON.
+
+ Note: the evaluator script itself is a frozen artefact (entry in
+ SOURCE_HASHES.json); persistence to results/shadow_live.json is
+ therefore done by the Makefile target, not by mutating the script.
+ """
+ return subprocess.run(
+ ["make", "eval-tick"],
+ capture_output=True,
+ text=True,
+ cwd=REPO,
+ timeout=180,
+ check=False,
+ )
+
+
+@pytest.mark.skipif(
+ not (
+ Path.home() / "spikes" / "cross_asset_sync_regime" / "paper_state" / "equity.csv"
+ ).is_file(),
+ reason="Live spike paper-state not available in this environment.",
+)
+def test_eval_writes_shadow_live_json_with_monotonic_mtime() -> None:
+ """Eval must (a) produce results/shadow_live.json, (b) bump mtime on rerun."""
+ res1 = _run_evaluator()
+ assert res1.returncode == 0, (
+ f"FIX-2 VIOLATED: evaluator first run failed rc={res1.returncode}; "
+ f"stderr={res1.stderr[:500]}"
+ )
+ assert SHADOW_LIVE_JSON.is_file(), (
+ f"FIX-2 VIOLATED: results/shadow_live.json not produced by evaluator. "
+ f"Expected path: {SHADOW_LIVE_JSON}."
+ )
+ mtime_1 = SHADOW_LIVE_JSON.stat().st_mtime
+
+ payload = json.loads(SHADOW_LIVE_JSON.read_text(encoding="utf-8"))
+ assert (
+ "eval" in payload
+ ), f"FIX-2 VIOLATED: payload schema missing 'eval' key. Got keys: {sorted(payload.keys())}."
+ eval_block = payload["eval"]
+ for required_key in (
+ "eval_date",
+ "live_bars_completed",
+ "cumulative_net_return",
+ "sharpe_live",
+ "status_label",
+ "gate_decision",
+ ):
+ assert required_key in eval_block, (
+ f"FIX-2 VIOLATED: 'eval' missing key {required_key!r}. "
+ f"Got: {sorted(eval_block.keys())}."
+ )
+
+ # mtime resolution can be coarse; sleep enough to step the timestamp
+ # and force-touch in case the filesystem rounds to whole seconds.
+ time.sleep(1.1)
+ res2 = _run_evaluator()
+ assert res2.returncode == 0, (
+ f"FIX-2 VIOLATED: evaluator second run failed rc={res2.returncode}; "
+ f"stderr={res2.stderr[:500]}"
+ )
+ mtime_2 = SHADOW_LIVE_JSON.stat().st_mtime
+ assert mtime_2 > mtime_1, (
+ f"FIX-2 VIOLATED: results/shadow_live.json mtime did not advance "
+ f"across consecutive evaluator runs. "
+ f"mtime_1={mtime_1}, mtime_2={mtime_2}. "
+ f"Self-update contract broken: evaluator silently skipped the write."
+ )