# SCCD-X Engine v2.00

Qualitative structural engine for world politics. Takes `sccd` v1.00 —
**Sovereign-Civilian Capacity Dynamics**, Vi Nhật Sơn, Parts I–V — and plugs in
two forcing blocks built from two further papers by the same author:

| Block | Paper | Tag |
|---|---|---|
| **D** | *The Wheel of Fate: From Ancient "Prosperity Trap" to the Modern Fertility Crisis* | `DEGROW` |
| **A** | *The Displacement Hypothesis*, 2nd ed., Feb 2026 | `DISPL` |

```bash
pip install numpy          # scipy optional; see "Solver" below
python serve.py            # → http://127.0.0.1:8777
python validate2.py        # 119 checks
```

**Two languages, fully separate.** The page has a `VI` / `EN` tab pair. Press
`VI` and everything is Vietnamese — chrome, loop details, gate meanings,
calibration warnings, the narrative, the whole provenance tab. Press `EN` and
everything is English. There is no bilingual mode and no half-translated state;
see §9.

---

## 1. Why these two papers plug in *here*

`sccd` v1.00 leaves two terms of its own ODE system purely exogenous:

```
dS₃/dt = **S₃_natural**    − ρ_b(1−λ/λmax)² − ζ·C₅^ψ·(S₃−S₃floor)
dC₅/dt = **C₅_structural** + ε_p·C₅(1−C₅)·RevPol + η·C₅²·C₃ + ξ·Ĉ₄Ĉ₁(1−C₅)
```

Those two starred terms are the engine's largest hole. They set the drift of the
two variables that control the whole non-linear core — S₃ drives λ, which
multiplies *all* of S; C₅ is the C denominator *and* the (1−C₅) factor in Ψ —
and v1.00 has nothing whatever to say about either.

**The Wheel of Fate supplies a mechanism for `S₃_natural` and `C₅_structural`
on a demographic timescale. The Displacement Hypothesis supplies one on an
AI-adoption timescale.** That is the join. Everything else follows from it.

---

## 2. The four structural results the v2 layer produces

### 2.1 Both of Degrow's channels terminate in the same object: θ₂₅

Degrow's claim is that the ancient explosion-then-collapse and the modern
fertility freeze are "different manifestations of the same fundamental law."
Written in `sccd`'s variables, that claim becomes specific — and checkable.

Block D is a **router**, not another pressure. A quantity of adjustment pressure
is generated by load; what the paper actually varies is *which channel carries
it*, and the split variable is the availability of voluntary instruments
("contraception, financial planning, and risk-management thinking"):

```
violent_share = (1 − d₃)^route_sharpness
```

The two channels then discharge into **different** `sccd` variables, and that
asymmetry is the whole point:

| channel | path | terminal gate |
|---|---|---|
| voluntary / aging | d₄ accumulates → Ŝ₅ compressed | **Ŝ₅ < 0.15** (PAPER) |
| violent / youth | surplus youth → Ŝ₂ eroded | **Ŝ₂ < 0.25** (PAPER) |

Both gates sit inside the *same* `sccd` object — the threshold factor θ₂₅ —
and either closure sends `Ψ = λ·θ₂₅·(1−C₅) → 0`, tripping the third condition
of the bifurcation test eq (23). **Same terminus, two doors.** The Wheel of Fate,
in `sccd`'s own terms, is a statement about θ₂₅.

### 2.2 AI is the first windfall in the model that raises G while lowering S₃

Everywhere in `sccd` v1.00 a generation windfall and legitimacy move *together*
— Part V's 1941–45 build-up is the type case, the war dividend raising both.
The Displacement Hypothesis describes a windfall that does not, because what it
erodes is *agency*, and agency is what consent is made of.

The engine flags the condition explicitly (`decoupling_active`) rather than
letting it hide inside a trajectory, because its consequence breaks a reading
convention: **Δ_norm and Ψ can now move in opposite directions.** The five Δ
bands of Part II Table 2 cannot express that on their own, which is why v2 adds
a second axis.

On the US preset this is not hypothetical. Over a 15-year run, Δ_norm stays
inside the single band `Equilibrium (Stable or Gridlock)` the entire time while
Ψ falls 0.257 → 0.014 and θ₂₅ falls 0.95 → 0.38. **A v1.00 reading would have
reported "stable" throughout.**

### 2.3 Haven flight is exit of a kind `sccd` cannot see

`sccd`'s Ĉ₄ is built from capital-account openness (Chinn-Ito) and emigration
options: exit by **jurisdiction**. Gold in a domestic vault is exit by **asset
form** — the holder does not leave, the wealth simply stops being legible, and
therefore stops being taxable, inflatable or repressible.

`sccd`'s own re-extraction term already has the right shape for this:
`τ_eff = τ_max·Ŝ₁·(1−Ĉ₄)·λ/λ_max`. Push Ĉ₄ up and T falls. The paper's third
prediction — that physical gold commands a premium over paper gold — is
precisely the claim that the flight is into the non-legible form *specifically*.

### 2.4 The two new papers are the same paper at loop L6

Both arrive independently at one failure mode: **a state raising extraction on a
shrinking legible base.**

> Degrow, Stage 2.1 — the late-Ming state raising rates on centuries-stale land
> registers, "like continuing to milk a cow that was already emaciated to the
> bone."
>
> Displacement, §5–8 — the base relocating into tangible, non-legible metal.

Same spiral, six centuries apart, reached from demography and from psychology.
The engine makes it **L6 Legibility–Extraction Spiral**, driven by
`squeeze = τ_demanded − τ_feasible`. The asymmetry is what makes it hard to
exit: raising a₄ is a private decision taken once; lowering it requires the
holder to trust the register again. Capital controls shut jurisdiction exit in a
weekend — no decree recalls metal from a vault.

---

## 3. What the engine outputs

A **qualitative triple**, not a single band:

```
Equilibrium / Incoherent ; adjustment mode VOLUNTARY ;
θ₂₅ S5_GATE_CLOSING (~14 yr) ; loops active L3+L4+L5+L6+L7 ; G/S₃ decoupled
```

| | axis | source |
|---|---|---|
| 1 | Δ band — 5 bands | `sccd` Part II Table 2 (PAPER) |
| 2 | **coherence tier** — Coherent / Strained / Incoherent | Ψ_crit=0.40 PAPER + one ANALYST cut |
| 3 | **adjustment mode** — VOLUNTARY / VIOLENT_EXPOSED / MIXED / SLACK | Block D router |

plus eight loops, θ₂₅ gate projections with time-to-gate, the bifurcation test,
a 10-state trajectory, Monte-Carlo measurement bands, an OAT fragility table,
and a bilingual (VI/EN) narrative.

### The eight loops

| | name | source |
|---|---|---|
| L1–L4 | Capture / Legitimacy Death Spiral / Polarisation Trap / Exit-Fragmentation | `sccd` Part IV §2 |
| **L5** | Agency–Legitimacy Spiral | DISPL §4.4 drain × `sccd`'s own λ return leg |
| **L6** | Legibility–Extraction Spiral | DEGROW 2.1 × DISPL 5.4 — *the cross-paper loop* |
| **L7** | Demographic Inversion Trap | DEGROW, two arms the paper never joins |
| **L8** | Youth-Surplus Discharge | DEGROW Stages 2–3 |

**L8 is why the engine can read the young half of the world at all.** L5–L7 are
all rich-aging-society loops; without L8 the instrument would report Nigeria as
"quiet" simply because none of its mechanisms are the live one there.

**CAI is reported twice and the two are not interchangeable.** `cai_core` is
L1–L4 scored and banded exactly as Part IV eq (CAI) specifies, directly
comparable to any v1.00 output. `cai_ext` is L1–L8 with the band edges rescaled
4/8, flagged `UNCALIBRATED-EXT`. Reporting only the extended number would
silently inflate every historical comparison; reporting only the core one would
discard the new loops.

---

## 4. Two things v1.00 could not do

**dΔ/dt is computed, not asked for.** `sccd`'s eq (23) needs three conditions,
one of which is momentum `dΔ/dt < −δ_c`. v1.00's `diagnose()` takes that as a
*caller-supplied argument* — the analyst has to already know the answer to the
most forward-looking part of the question. With Ŝ₁/Ŝ₂/Ŝ₄/Ŝ₅ promoted to state
variables, the trajectory supplies it. Passing it explicitly still overrides.

**The sensitivity protocol runs against the verdict.** Part IV eq (SA) mandates
OAT ±20%. The engine runs it on the *qualitative verdict* — band, tier, loop
set, R_B — and reports which parameters flip it. This is not decoration: the
first version of this engine had the entire θ₂₅ collapse resting on one ANALYST
coefficient, and `fragility` is where a reader finds that out.

---

## 5. The two design rules that keep the layer honest

### 5.1 LEVEL vs DRIFT — the double-counting trap

A new block may shift a component's **level** or set its **drift**. Choosing
wrongly double counts, and the trap is easy to fall into. Worked example:
`sccd` Part III defines Ŝ₅ Temporal Leverage as the **uncommitted** revenue
horizon. Pension liabilities are revenue pre-commitment, so a measured Ŝ₅
*already has them netted out*. Subtracting a further `compress_s5·d₄` from the
level charges the same pensions twice. What Block D legitimately knows that the
measurement does not is how fast pre-commitment is still *growing* — a rate.

So:

- **S₁–S₅ take drifts only.** `sccd`'s S proxies are annual flow measures that
  already embed present conditions. Only the forward rate is new information.
- **C₁, C₂, C₄ take level shifts.** Their proxies *structurally omit* the new
  channels — Chinn-Ito plus OECD DIOC contain no term whatever for asset-form
  exit; V-Dem lags a counter-tech revival by years. Blind spots, not double
  counts.
- **C₅ takes a drift**, because it is the variable `sccd` itself integrates.

`apply_forcing` **raises** on an S-side level shift rather than accepting it, and
`validate2.py` injects the bug to prove the guard fires.

### 5.2 `sccd`'s own CRITICAL defects are patched ON by default

v1.00 ships `patch=False, c1_dynamic=False` to reproduce the paper verbatim.
v2 **inverts that default**, because the new blocks supply sourced mechanisms
for the terms the paper's own spec says are missing:

| `sccd` defect | what v2 supplies |
|---|---|
| **INC-7** `dC₅/dt` has only positive terms, so C₅ rises monotonically and Ψ collapses on *every* path | DISPL §8.5 counter-tech organising raises Ĉ₂, which generates the `−ν·C₅·C₂` depolarisation term |
| **INC-8** `dS₃/dt` has only negative terms, so the model cannot produce the legitimacy reconstitution Part V documents for 1980–86 | DEGROW delivery performance through Ŝ₄ drives `+σ·(S₃_target − S₃)` |
| **INC-9** `[S₃,C₅]` is an autonomous subsystem — "there is NO closed feedback loop in eq (NL) at all" | C₁ promoted to a state, closing L1 and L4 |

Pass `faithful_v1=True` to reproduce the paper verbatim instead. `validate2.py`
asserts that this setting *does* reproduce the monotone-C₅ defect — the engine
has to be able to demonstrate the flaw it repairs.

And v2 repeats the INC-9 lesson one layer up: **d₄ is a state variable, not a
parameter.** With d₄ frozen, the Ŝ₅ and Ŝ₁ drifts are constants and loop L7
exists only in its activation test — the test would report a trap the equations
do not contain. With d₄ integrated, the loop closes inside the ODE.

---

## 6. Calibration — read this before quoting anything

```
36 of 83 v1 parameters carry a value stated in the source paper; 46 do not.
ALL 46 v2 parameters are ANALYST or DERIVED2.
```

The two new papers are a qualitative essay and a qualitative working paper.
Between them they contain **zero coefficients usable as an ODE rate.** What *is*
sourced is the causal structure: which latent variable pushes which, and with
what sign. The magnitudes are invented, and `spec2.py` says so on every line.

The engine is therefore an **ordinal instrument**: it ranks scenarios and
locates threshold crossings. It does not produce trustworthy absolute levels,
dates or probabilities. `sccd` Part V §1 sets the standard for itself —

> "We do not, at this stage, claim predictive precision — generating
> quantitatively accurate forecasts from an incompletely calibrated formal
> model would be epistemically irresponsible. The validation standard is
> qualitative-structural."

— and the v2 layer is weaker again. `GET /api/v2/audit` returns the full
census, the v2 open-problem register (7 entries, 2 HIGH) and `sccd`'s own nine
known inconsistencies.

### The two open problems that matter most

**OP2-2 — the load-bearing claim is the least evidenced.** The agency →
legitimacy link carries the whole v2 layer. Displacement documents agency
erosion and documents capital reallocation; it does **not** document a fall in
regime legitimacy, and offers no survey series on consent. The engine asserts
the link on the ground that consent of the governed is constituted by exactly
the self-authorship the paper says is dissolving. *That is an argument, not a
measurement.* If it is wrong, L5 is spurious.

**OP2-1 — the router's conservation assumption is the engine's reading, not
Degrow's statement.** The paper never says total pressure is conserved across
the split. If voluntary adjustment is cheaper in total pressure terms — it
prevents rather than clears overload — then d₃ should shrink `P_demo` itself and
not merely redirect it. Exposed as `router_mode: "conserve" | "discount"` so the
assumption is a dial rather than a hidden choice.

### A note on the presets, and on `compress_s5`

`compress_s5` was revised **down** from 0.045 to 0.018 against a direct
counter-example: Japan has sat within ~0.03 of θ₂₅'s 0.15 gate for two decades
without the discontinuity the gate is supposed to mark. At 0.045 the gate closed
inside 15 years for every aging polity in the preset set, which makes the
diagnostic unfalsifiable rather than informative. The constant-drain form still
assumes *no* policy response — no retirement-age move, no benefit reindexation —
so it remains an upper bound on the compression rate.

The ten preset rows are analyst priors. **Four values in the whole file are
paper-stated** (US s₃=0.38, c₅=0.74, c₁=0.82, s₂≈0.90). Nothing was produced by
running `sccd`'s Part III proxy pipeline, and no value has a confidence
interval. Note also `sccd`'s own `api_readiness()`: 7 of 10 components are
machine-retrievable, and the three that are **not** — S₃ legitimacy, S₄
administrative penetration, C₅ fragmentation — are precisely the ones driving λ,
S and the C denominator. The blocked components are the decisive ones.

---

## 7. API

All endpoints take a language — `?lang=vi|en` on GET, `"lang"` in a POST body.
See §9.

| | endpoint | returns |
|---|---|---|
| GET | `/api/v2/health` | versions, ODE backend |
| GET | `/api/v2/schema` | every input field with definition, default, data status |
| GET | `/api/v2/events` | 50 events across 11 groups with declared effects |
| GET | `/api/v2/presets` | 2026 rows for 10 polities |
| GET | `/api/v2/audit` | provenance census, open problems, v1 inconsistencies |
| POST | `/api/v2/diagnose` | one polity |
| POST | `/api/v2/world` | many polities + the system they form |

Both POST bodies are permissive: every field is optional, `preset` fills a whole
row, anything you supply overrides it.

```bash
curl -s -X POST http://127.0.0.1:8777/api/v2/diagnose \
  -H 'Content-Type: application/json' -d '{
    "preset": "US", "year": 2026, "horizon_years": 15, "lang": "vi",
    "sub":  {"s3": 0.34},
    "demo": {"d2": 0.85},
    "disp": {"a3": 0.66, "a4": 0.70},
    "events": [
      {"id": "haven_flight_surge",           "intensity": 0.8, "weight": 1.2},
      {"id": "housing_affordability_crisis", "intensity": 0.9, "weight": 1.0},
      {"id": "whitecollar_displacement",     "intensity": 0.6, "weight": 1.0}
    ]
  }' | python -m json.tool
```

```bash
curl -s -X POST http://127.0.0.1:8777/api/v2/world \
  -H 'Content-Type: application/json' \
  -d '{"year":2026,"codes":["US","CN","EU","JP","KR","IN","RU","BR","GB","NG"],
       "horizon_years":15,"sync_risk_prior":0.58,"lang":"vi"}'
```

### Events

An event is a **level shift added on top of** your baseline components — for
something that has not happened yet, or has happened but is not yet in the
numbers you entered. It is **not** for re-describing what your baseline already
contains. Set `s3 = 0.38` because polling already shows a slump *and* switch on
`contested_election` at full intensity, and you have charged the same slump
twice. The engine cannot detect this — it has no way to know what your 0.38 was
built from — so the discipline is yours. `events.audit` prints every shift
applied so you can at least see the total.

All 50 effect magnitudes are ANALYST. The catalogue's value is that the mapping
is explicit and reviewable rather than implicit in an analyst's head; disagree
with a number and change it.

---

## 8. Files

```
sccd/           vendored v1.00, unchanged except one import made scipy-optional
sccdx/
  i18n.py       the template table + the resolver — the ONLY source of prose
  i18n_long.py  long reference texts: 7 open problems, 9 inconsistencies,
                46 parameter notes, 50 event notes, 10 preset rationales
  i18n_ui.py    160 UI strings, served to the page through /api/v2/schema
  i18n_render.py walks an engine payload and renders every code
  spec2.py      46 v2 parameters, each with provenance + the sentence it came from
  forcing.py    Blocks D and A → effective components, drifts, gate projections
  events.py     the 50-event catalogue
  loops2.py     L5–L8 + the two-number CAI
  dynamics2.py  the 10-state ODE  [S, C, S₃, C₅, C₁, S₁, S₂, S₄, S₅, d₄]
  engine2.py    orchestrator, measured dΔ/dt, OAT fragility
  world.py      Ψ_G with two-part Sync_Risk, Thucydides, talent gradient
  narrative.py  VI/EN prose, template-driven on purpose
  presets.py    2026 analyst priors, 10 polities
  integrate.py  DP45 fallback for when scipy is absent
serve.py        stdlib API + static server          ← start here
api_fastapi.py  optional ASGI adapter, same contract
validate2.py    119 checks: v1 parity, v1 replication, mechanism, discipline
validate_lang.py  section E: the language-separation sweep
web/index.html  the form and dashboard
```

### Solver

Part IV §8.1 mandates an adaptive **stiff** solver (LSODA / Radau / BDF). With
scipy installed the engine uses it. Without, it falls back to the explicit DP45
in `integrate.py`, and says so in `trajectory.solver` and in the returned notes
— DP45 is *not* a stiff solver. On this system at these coefficient magnitudes
(all O(0.01–0.2)/yr) it tracks to ~1e-6; install scipy before raising any rate
by an order of magnitude or adding sub-year shock pulses.

### Charts

The dashboard uses **small multiples** — one variable per panel, each with its
own scale and its own PAPER threshold drawn as a reference line with the breach
region shaded. Ψ (0–2.5), Δ_norm (−0.4–1.0) and Ŝ₃ (0–1) on one plot would need
two y-axes, and a dual-axis chart invents a correlation that is not in the data.
Palette validated for colour-vision deficiency in both light and dark mode.

---

## 9. Language separation

### The engine emits codes, not prose

`LoopState2.detail_key` is `"loop.L6.detail"` plus a dict of numbers.
`GateProjection.meaning_key` is `"gate.s5"`. A warning is
`("warn.oat_flips", {"n": 4})`. The gate verdict is `"S5_GATE_NEAR"` with a
year count beside it. **Nothing downstream of the engine contains a sentence**,
so there is no path by which a Vietnamese reading can leak an English fragment
or the reverse.

That is stricter than display alone requires, and deliberately so. The first
build localised at the view layer, and the result was a page that was
Vietnamese in its headings and English in every string the engine produced —
loop details, gate meanings, calibration warnings, the entire provenance tab.
Codes make the mixed state *impossible to construct* rather than merely
discouraged.

It also makes the tests language-independent: `validate2` asserts
`gate_code == "S5_GATE_NEAR"`, not an English substring a translation pass
could silently break.

### What stays untranslated, and why

Variable names (`S₃`, `C₅`, `θ₂₅`, `Ψ`, `λ`, `L1`–`L8`), provenance tags
(`PAPER`, `FREE`, `ANALYST`, `BLOCKED`), paper references (`INC-7`, `OP2-2`,
`Part IV §2.3`) and data-source names (`Chinn-Ito`, `V-Dem`, `UCDP`) read
identically in both languages. They are the formalism's identifiers; a reader
comparing two runs needs them to line up. Quotations from the source papers
also stay in English inside the Vietnamese text — translating a quotation would
misrepresent it, and a reader checking the claim against the PDF needs the
words that are in the PDF.

The language picker labels each language in its own name (`Tiếng Việt`,
`English`), which is the one place a Vietnamese string is correct on the
English page.

### How the page stays honest

`web/index.html` contains **zero** Vietnamese characters and no English prose.
Every label, heading, hint and column header arrives from
`/api/v2/schema?lang=…`; every engine string arrives already rendered.
Switching language re-fetches the reference payloads and **re-runs** whatever is
on screen rather than patching fragments — which is why VI and EN are two
views, not one view with some words swapped. Form state (slider values, chosen
events, their intensities and weights) is preserved across the switch.

A missing template **raises** under strict mode instead of falling back to
English, so a gap is a loud failure during development rather than a quiet
language leak in production.

### Tested, not asserted

`validate_lang.py` walks every string in every payload — diagnosis, world,
schema, audit, events — and checks:

- EN payloads contain **zero** Vietnamese characters
- VI payloads contain **zero** English marker phrases, drawn from *engine*
  output rather than chrome, because the chrome was never the part that leaked
- one narrative per response, not a bilingual pair
- switching language changes **prose only**: every enum code, number and
  trajectory point is identical across VI and EN
- all 405 templates and 160 UI strings have both sides
- all 50 events, 10 presets, 7 open problems, 9 inconsistencies and 46
  parameter notes are translated
- `web/index.html` holds no prose of its own

The sweep caught a real leak on its first run: event audit rows were shipping
both `label_vi` and `label_en`, so an English response still carried the
Vietnamese label. Fixed by dropping both source fields once the label is
resolved.

### API

Every endpoint takes a language. `GET` endpoints read `?lang=vi|en`; `POST`
bodies take `"lang"`. The response states which language it is in its `lang`
field.

```bash
curl -s "http://127.0.0.1:8777/api/v2/schema?lang=en"
curl -s "http://127.0.0.1:8777/api/v2/audit?lang=vi"
curl -s -X POST http://127.0.0.1:8777/api/v2/diagnose   -H 'Content-Type: application/json'   -d '{"preset":"JP","lang":"en","horizon_years":15}'
```

---

## 10. What this engine will not tell you

Not because of missing features — because 36 PAPER + 46 FREE + 46 ANALYST
parameters do not support it:

- a probability
- a date
- an absolute Δ level you can act on
- *"this will happen"*

What it will tell you: the current regime band, coherence tier and adjustment
mode; which loops are armed and which are cross-coupled; which PAPER threshold
is nearest and in which direction; the **order** in which gates close; how
scenarios **rank** against each other; and — in `fragility` — what would have to
be true for any of it to be wrong.
