Methodology

The specification below was frozen before any analysis was run. It is reproduced from the repository at build time, and the hash block lets you confirm that what you are reading is what was frozen.

Verify this yourself

The specification is a file in a public repository, fixed by an annotated tag. The hash is of the file's contents at that tag. If the text below has been altered, the hash will not match — that is the whole point of publishing it.

Tag
freeze/fr-pres-2027-spec-v0.1
Commit
da2b786a0145295845a2a1b6af6f7149a15a5d04
Specification SHA-256 at the tag (frozen, never changes)
b7420e97b0897aee5debb2f88027613421d00516fa53c0eeebfdd60b05bf11c6

Reproduce it with: git cat-file blob <commit>:FR_PRES_2027_SPEC.md | sha256sum

The specification appears twice below with two different hashes, and that is the regime working rather than a discrepancy. The hash above is the frozen text at the tag and never moves. The hash on the document is of the file as published here, which is the frozen text plus every amendment appended since. The frozen bytes are still the opening bytes of the current file, so the second is checkable against the first.

How it can change

Additions only. The frozen text is never edited. Every change is a dated amendment appended at the end, each with its reasoning, and the original bytes remain a prefix of the current file — which is itself checkable. The amendments in force are listed below with the date each one landed.

  • A3 · 2026-08-24 · prospective amendment to §11 (vintage schedule)
  • A2 · 2026-08-24 · schema addition to §9: historical poll table `fr_polls_hist`
  • A4 · 2026-08-24 · new measured pin P18: R1 non-valid mass

The documents

Forecast specification

File
FR_PRES_2027_SPEC.md
SHA-256 (as published here — frozen text plus amendments)
371c0c6447f7398dd1f513dba3c62d2f271cb750f9742bd962e5432a7b8c0f5d
Size
348 lines, 32028 bytes
# French Presidential Election 2027 — Forecast Specification
## v0.1 — FROZEN

**Document control**

- Status: FROZEN v0.1. No analysis run preceded this freeze; runs are permitted from tag `freeze/fr-pres-2027-spec-v0.1`.
- Regime: additions-only. Amendments are dated addenda under §14; no silent edits.
- Freeze tag (proposed): `freeze/fr-pres-2027-spec-v0.1`, annotated, blob SHA-256 recorded in the tag message.
- EOL class: derived document, LF-canonical.
- Scope: one national contest, two rounds. Out of scope: the June 2027 legislatives, any seat model, any fundamentals- or approval-weighted prior, the publication-surface decision, and any comparison against external price-based signals (separate document).
- Labour split: planning/spec — Claude Chat; implementation, DB writes, git — Claude Code; pin ratification, diff review, ruling authority — Jim.

---

## 0. Forecast object

A joint distribution over three things in sequence:

1. First-round (R1) valid-vote share for every candidate on the ballot.
2. The R2 pairing (top two from R1).
3. R2 valid-vote share for each member of the pairing, plus the no-valid-vote share of the registered electorate.

Headline outputs (80% and 95% intervals wherever continuous):

| Symbol | Meaning | Role |
|---|---|---|
| `P_qual(c)` | Probability candidate c reaches R2 | Primary |
| `P_pair(a,b)` | Probability of each R2 pairing | Primary |
| `R1_share(c)` | R1 valid-vote share | Primary |
| `R2_share(c \| pair)`, `P_win(c \| pair)` | Conditional on pairing | Primary |
| `R2_nonvalid` | Registered electorate casting no valid vote in R2 (abstention + blank + null) | Reported |
| `P_win(c)` | Marginal win probability | Derived (mixture over pairings), not primary |

Design statement: the R1 leader's share is the best-measured and least decision-relevant quantity in the system. The decision-relevant quantities are the second-slot race and the R2 transfer structure. The architecture is built around those two.

---

## 1. Electoral mechanics encoded

| Item | Rule | Model treatment |
|---|---|---|
| Dates | R1 Sun 18 Apr 2027, R2 Sun 2 May 2027 (announced; confirm against the décret de convocation when published). Art. 7: R1 must fall 20–35 days before expiry of the incumbent's term (13 May 2027). | Hard constants; verification row in Phase 1. |
| Winner | Absolute majority of valid votes in R1, else top two to R2. | P(R1 outright) = 0 (never occurred). |
| Qualification | The two highest R1 totals, after any withdrawal of a better-placed candidate (Art. 7). | Withdrawal probability = 0 in v0.1 (no precedent). |
| Ballot access | 500 parrainages; deadline the sixth Friday before R1 at 18:00 → Fri 12 Mar 2027. Conseil constitutionnel publishes the list shortly after (2022 precedent: +3 days) → ~15 Mar 2027. | Candidate universe replaced in full by the official list via addendum on publication day. Branch register resolves that day. |
| Incapacity | Art. 7: death or incapacity of a candidate after the list (and, at the Conseil's discretion, in the final week before the parrainage deadline) → postponement or full re-run, not substitution. | Post-list candidate-loss tail is modelled as postponement, not as a substitution branch. |
| Denominator | Official shares are % of valid votes (exprimés); blank and null excluded. | All model shares are % of valid votes. Pollster bases (e.g. inscrits certains d'aller voter) converted on ingest. |
| Poll blackout | No publication, diffusion or commentary of polls from 00:00 on the Saturday before each round until close of polls. | Final R1 vintage frozen Fri 16 Apr 2027 23:59 CEST; final R2 vintage Fri 30 Apr 2027 23:59 CEST. |
| Results | Ministère de l'Intérieur provisional (election night, per commune and bureau); Conseil constitutionnel proclamation definitive (days later). | Provisional for provisional scoring; definitive for the ledger. Reconciliation addendum if any scored quantity moves ≥0.1pp. |
| Electorate | ~48.7m registered (2022). | Reported; not a driver. |

---

## 2. Architecture

```
L0  Candidate-field branches   (exogenous; named; weighted; dated triggers)
 └─ L1  R1 share model | field  (compositional; bloc-structured error; horizon-scaled)
     └─ L2  Qualification        (deterministic given an R1 draw)
         └─ L3  R2 transfer model | pairing   (segment × {A, B, non-valid}; coordination term)
             └─ L4  Monte Carlo  (joint draws; latent factors shared across L1 and L3)
```

Neither existing engine ports. The UK proportional-flow/IPF tooling is reused in exactly one place: ecological estimation of R1→R2 transfer priors from commune-level results (§6.2). The US national-environment / regional-cluster hierarchy is replaced by a bloc hierarchy over candidates (§5.3). Geography is not a first-class dimension in v0.1.

---

## 3. Candidate universe (working, v0.1)

Replaced in full by the Conseil constitutionnel list via addendum (~15 Mar 2027).

| Bloc | Candidate | Party | Status at 21 Aug 2026 |
|---|---|---|---|
| Far-right | Marine Le Pen | RN | Declared (7 Jul 2026) |
| Far-right | Jordan Bardella | RN | Substitution-only (branch B-RN) |
| Far-right | Éric Zemmour | Reconquête | Presumptive |
| Far-right | Nicolas Dupont-Aignan | DLF | Presumptive |
| Right | Bruno Retailleau | LR | Presumptive |
| Centre | Édouard Philippe | Horizons | Declared |
| Centre | Gabriel Attal | Renaissance | Conditional |
| Centre | Dominique de Villepin | — | Presumptive |
| Left | Jean-Luc Mélenchon | LFI | Presumptive |
| Left | Raphaël Glucksmann | Place Publique | Presumptive (PS endorsement path open) |
| Left | United-left primary winner | Écologistes / L'Après / Debout! / Génération.s (+PS?) | Primary 11 Oct 2026 |
| Left | François Hollande | — | Conditional |
| Left | Fabien Roussel | PCF | Presumptive |
| Far-left | Nathalie Arthaud | LO | Presumptive |
| Far-left | NPA candidate | NPA | Unknown |
| Other | Jean Lassalle and any other qualifier | — | Unknown |

Status vocabulary: Declared / Presumptive / Conditional / Substitution-only / Unknown. Status changes are addenda.

**Bloc-leader rule** (used by §5.3): within each bloc, the candidate with the highest aggregate R1 mean at the vintage date. Pre-specified, not discretionary.

---

## 4. L0 — Branch register

Governance copied from the UK scenario model: named branches with weights summing to (1 − residual); residual fixed at freeze; weights move only at named trigger events, by dated addendum, with the evidence class stated. Every branch maps to the pollster configuration(s) that measure it. Configurations are the measurement instrument for branches — the structural advantage over the UK branches, which were unmeasured.

| ID | Branch | Alternatives | Trigger / resolution | Weight |
|---|---|---|---|---|
| B-RN | RN candidate | Le Pen (default) / Bardella substitution | Hard: Conseil list ~15 Mar 2027. Sub-triggers: any PNF charging decision; Bardella–Le Pen R2 gap in autumn waves; Cassation hearing date once fixed. | Le Pen 0.96 / Bardella 0.04 (P1) |
| B-LEFT | Left field | (a) Mélenchon + Glucksmann + primary winner + Roussel; (b) PS joins primary → single non-LFI left candidate; (c) Hollande runs; (d) combinations | Primary 11 Oct 2026; PS decision; parrainages | ⚑ P2 |
| B-CTR | Centre field | Philippe only / Philippe + Attal / Attal only | Declarations; parrainages | ⚑ P3 |
| B-RGT | Right and far-right minor field | Retailleau present; Zemmour present; DA present | Parrainages | ⚑ P4 |
| B-ENV | Cassation ruling | Confirmation before R1 / quash before R1 / after R2 | Cour de cassation: ruling by early April 2027 at the latest; hearing January at best | ⚑ P5 |
| R | Unnamed residual | — | — | 10% ⚑ P6 |

**B-RN weight decomposition**, recorded for the pin: judicial route ≈ 0 (firm ineligibility served; prosecution lodged no pourvoi, so Cassation can only confirm or quash; procureur général states no bracelet before the vote in any scenario); incapacity <1% actuarially over the horizon; voluntary or party substitution is the residual and the only component without a base rate. Statements of intent are labels; the Conseil list is the measurement.

**B-ENV** is an environment branch, not an identity branch: it shifts L1/L3 parameters (P5 specifies direction and magnitude; default zero-mean), not the universe.

Branch arithmetic: B-LEFT, B-CTR and B-RGT are sampled **jointly** — one draw selects a single row of the combination table below, so every sampled field is by construction a configuration that has been measured. B-RN and B-ENV are independent overlays applied to that draw. No other dependency is specified at v0.1; any addition is a dated addendum.

**Combination table** (populates P2–P4 until each branch's trigger resolves): the distinct branch combinations tested in the trailing 60 days, weighted uniformly, where tested means a published configuration from an organisation that filed a notice with the Commission des sondages for the poll concerned. Cascade, applied in order: (i) combinations tested by ≥2 such organisations; (ii) failing that, ≥1; (iii) failing that, the tested set of the most recent wave. The tier applied is recorded in the vintage row. Weights reset by dated addendum at each trigger (11 Oct 2026 for B-LEFT).

---

## 5. L1 — R1 share model

### 5.1 Inputs
- Every published 2027 R1 intention poll, one row per poll × configuration × candidate.
- Metadata from Commission des sondages notices (authoritative for n, fieldwork dates, method, base). The fr.wikipedia list is used for discovery only; every row reconciles to a notice ID.
- Share base converted to % of valid votes among intending voters.

### 5.2 Aggregation → mean vector per configuration
- House effects: per pollster × bloc, from 2022 R1 final-fortnight polls vs result and 2024 European final polls vs result, shrunk toward zero. ⚑ P7: apply or not, given panel changes (Ipsos → Ipsos-BVA; Harris → Toluna Harris; Kantar → Vérian).
- Recency: exponential decay. ⚑ P8 half-life schedule (proposed 30 d to 28 Feb; 14 d from 1 Mar; 7 d from list publication).
- Sample size: √n weighting, capped. No trend extrapolation (the UK delta retrospective stands: poll levels ≈ random walk).
- Configuration mapping: each poll configuration maps to the branch combination it measures. The mean is computed per branch combination from matching configurations; unmatched combinations inherit the nearest configuration with a documented offset. For the jointly sampled branches this fallback cannot fire and it applies only to B-RN (§4). Nothing is silently pooled.

### 5.3 Error model (compositional)
Shares are modelled in additive-log-ratio space (reference category: other). Error = bloc shift + within-bloc consolidation + cross-bloc tactical + idiosyncratic, all scaled by horizon.

**Units convention** (frozen; governs P9, P11, P12, P14): every pin is stated in share points at the final-week horizon. Flow terms (`γ`, `τ`) are applied as mass transfers in share space on the aggregate mean before the level draw; transfers conserve total mass, so no separate renormalisation applies. Level terms (`β`, `ε`) are applied in ALR space, their SDs converted from share points at the vintage mean via the Jacobian; `h(t)` multiplies after conversion. `κ` (§6.3) is applied on the logit of R2 share, its SD converted at p = 0.5.

| Component | Definition | Prior source | Pin |
|---|---|---|---|
| Bloc shift `β_b` | Common error to every candidate in bloc b | Pooled 2002–2022 R1 final-poll errors by bloc | ⚑ P9 |
| Consolidation `γ_b` | Transfer from non-leaders to the bloc leader | Leader-vs-others final-poll errors, 2002/2007/2012/2017/2022 | ⚑ P10 |
| Cross-bloc tactical `τ` | Left → centre shift (second-slot insurance) and right → far-right leakage | No clean precedent for left → centre: in 2022 the centrist was a safe qualifier. Two-sided prior; sign not assumed. | ⚑ P11 |
| Idiosyncratic `ε_c` | Candidate-specific | Residual of pooled errors | ⚑ P12 |
| Horizon `h(t)` | Multiplier on every SD as a function of days to R1 | Fitted on the Jennings–Wlezien (2018) dataset, presidential subset (Dataverse — throttled; archive snapshot required). Appended before V0. | ⚑ P13 |

**Recorded finding for P10** (structure before numbers): consolidation toward the bloc leader has a positive mean but is not sign-stable across cycles. 2002 is a counterexample (left leader Jospin under-ran as the left fragmented); 2012 shows it (Hollande over, Mélenchon under); 2022 shows it (Mélenchon over; Jadot/Hidalgo under; Zemmour/Pécresse under, Macron over); 2017 ≈ 0. It is therefore a mean shift plus a variance term, not a deterministic adjustment.

**τ is the least-evidenced block in the model** — the analogue of the Reform vote-source row in the UK matrix. Evidence pathways before the final vintage: pollster second-choice and vote-utile questions; the trajectory of the Philippe–Mélenchon second-slot gap. τ is never tuned to the 2022 backtest, which contains no instance of it.

### 5.4 Reference table — R1 final-poll error by cycle
Indicative. The poll column is a label until replaced by measured values from the archived poll tables at Phase 1; no pin is ratified off this table as it stands.

| Cycle | Candidate | Final-poll avg (indicative) | Result | Error |
|---|---|---|---|---|
| 2002 | Jospin | ~17.5 | 16.2 | −1.3 |
| 2002 | J-M Le Pen | ~13 | 16.9 | +3.9 |
| 2007 | Sarkozy | ~28.5 | 31.2 | +2.7 |
| 2007 | J-M Le Pen | ~13.5 | 10.4 | −3.1 |
| 2012 | Mélenchon | ~14 | 11.1 | −2.9 |
| 2012 | M Le Pen | ~16.5 | 17.9 | +1.4 |
| 2017 | Top four | within ~1.5 each | — | — |
| 2022 | Mélenchon | ~17.5 | 22.0 | +4.5 |
| 2022 | Zemmour | ~9.5 | 7.1 | −2.4 |
| 2022 | Pécresse | ~8.5 | 4.8 | −3.7 |

---

## 6. L3 — R2 transfer model

### 6.1 Object
For each pairing p = (A, B), a transfer matrix `T_p`: rows = R1 segments (each R1 candidate's voters, plus R1 non-voters); columns = {A, B, non-valid}.

`R2_share(A) = Σ_rows mass_row · T_p[row, A] / Σ_rows mass_row · (T_p[row, A] + T_p[row, B])`

`R2_nonvalid` reported on the registered electorate. Reference: ~34% of the registered electorate cast no valid vote in both 2017 and 2022 R2 — a block several times any plausible margin, and the denominator.

Pairing set at v0.1: Le Pen × {Philippe, Mélenchon, Attal, Glucksmann, Retailleau, primary winner, Hollande}; Bardella × the same. Non-RN pairings receive a generic prior and negligible mass.

### 6.2 Prior sources, in order of authority
1. Ecological inference from commune-level R1/R2 results, 2017 and 2022 (Ministère de l'Intérieur via data.gouv.fr, ~35k communes): constrained IPF/regression. The one reuse of the UK flow tooling.
2. Day-of surveys (Ipsos/Cevipof, Ifop, Elabe, Harris "sociologie des électorats", 2017 and 2022), as cross-check on (1).
3. 2024 legislative R2 transfer and désistement behaviour, as evidence on anti-RN coordination — not as a direct row prior (different contest).
4. Current published reports de voix from hypothetical duels: stated-intention measurements, horizon-discounted by h(t), used to update rows, never to set the headline. Consistency check: a pollster's published duel headline must be reproducible from its own R1 configuration and its own transfer table to within rounding; failures are logged.

Indicative 2022 rows (to be measured at Phase 1): Mélenchon voters → Macron ~42 / Le Pen ~17 / non-valid ~41; Zemmour voters → Le Pen ~78 / Macron ~7 / non-valid ~15.

### 6.3 Coordination term `κ`
Documented pattern: in every presidential runoff against a Le Pen, the opponent's R2 share has exceeded final polls — 2002 (Chirac ~78–80 polled, 82.2), 2017 (Macron ~62–63, 66.1), 2022 (Macron ~56–57, 58.5) — with declining magnitude; and R1-night seat projections in the 2024 legislatives (~230–300 for RN and allies) under-called the coordination that delivered ~143. κ is a mean shift against the RN candidate's R2 share with its own SD.

⚑ P14 specifies mean, SD and the difference-from-anchor discount. Anchor differences to be stated in the pin: no incumbent opponent; RN R1 at ~35 rather than ~23; the LR electorate has drifted right (Retailleau voters currently ~5/10 Le Pen vs ~4/10 Philippe in duel tables); a left opponent inverts the coordination (centre/PS voters ~6/10 non-valid against Mélenchon in current duel tables). Zero lies inside κ's 95% interval unless an addendum argues otherwise.

### 6.4 Segment uncertainty and shared factors
Each `T_p` row is a Dirichlet draw around its mean, concentration ⚑ P15. Rows are not independent: two latent factors are shared across rows and with L1.

- `F_left` — left mobilisation: moves the L1 consolidation draw in the left bloc and the left-segment non-valid columns in L3.
- `F_coord` — anti-RN coordination: moves κ and the centre/left → opponent columns.

Loadings and correlations ⚑ P16. `ρ` denotes the induced correlation between the affected quantities, not the loading. Loadings are derived as `λ_i = σ_i·√ρ` on a standard-normal factor, so the marginal SDs pinned in P9, P10, P14 and P15 are preserved: the factor accounts for share ρ of each variance and the idiosyncratic remainder for 1 − ρ.

### 6.5 Between-round update
V-inter (Mon 19 Apr): row masses replaced by actual R1 shares; duel polls for the realised pairing update rows through 30 Apr; κ retained with its SD, not reset on the strength of one poll.

---

## 7. L4 — Simulation

Per draw: sample one alternative per branch → sample `F_left`, `F_coord` → sample R1 shares in ALR space → top two → sample `T_p` rows → compute R2 shares and non-valid share → record. N = 100,000 (⚑ P17). Seed, code commit hash, input-table checksums and spec blob hash recorded with every vintage. Outputs are append-only rows in `fr_forecast_vintages`.

---

## 8. Estimation protocol and Goodhart guard

Single pre-gate pass (US protocol reused).

- **Estimated from data:** poll aggregate means and covariances per configuration; house effects (if P7 = apply); ecological transfer priors; the h(t) fit.
- **Set by pin, never tuned:** σ scales (P9, P12), γ (P10), τ (P11), κ (P14), Dirichlet concentration (P15), factor structure (P16), half-life schedule (P8).
- **Validation, not tuning targets:** (i) 2022 machinery backtest on vintage-reconstructed polls at T−240, T−90, T−30, T−1 days — pass criterion: 95% intervals cover the realised R1 share for every candidate ≥3%, and the realised pairing sits inside the top-two `P_pair` mass; (ii) 2017, same. Both cycles are in-sample for the P9/P10 derivation and are recorded as such; the backtest validates machinery and interval width, not the priors. No parameter moves on the basis of these checks; a failure is recorded as a finding and addressed only by a dated addendum whose reasoning is independent of the failing statistic.

---

## 9. Data foundation (Phase 1)

Supabase analytics; append-only; version-tagged. DDL via migration; DML via SQL.

| Table | Grain | Source |
|---|---|---|
| `fr_candidates` | candidate | §3, then the Conseil list |
| `fr_configurations` | poll × configuration → candidate set → branch combination | Derived on ingest |
| `fr_polls_r1` | poll × configuration × candidate | Pollster publications; notice ID |
| `fr_polls_r2` | poll × duel × (headline + transfer rows) | Same |
| `fr_poll_notices` | notice ID | commission-des-sondages.fr |
| `fr_results_presidential` | cycle × round × candidate (national); 2017/2022 × round × commune | data.gouv.fr (Intérieur) |
| `fr_results_other` | 2024 legislative R1/R2 (national + circonscription); 2024 European | data.gouv.fr |
| `fr_transfer_priors` | pairing × segment × column; source tag (eco / day-of / duel) | §6.2 |
| `fr_pollster_house_effects` | pollster × bloc × cycle | Derived |
| `fr_branch_register` | branch × alternative × weight × addendum ref | §4 |
| `fr_forecast_vintages` | vintage × quantity × value × intervals | L4 |
| `fr_calibration_events` | event × forecast × resolution × score | §10 |

Provenance: source archives hashed with SHA-256 manifests under `backtrack/fr/source/` (bytes-as-served class); derived tables LF-canonical. Blocked or rate-limited official sources require a dated archive snapshot (standing rule).

**Data-complete check** (gate for V0): poll row count reconciles to the Commission des sondages notice register for the 2027 tag; every configuration mapped; results tables checksummed against a second source (Conseil constitutionnel proclamations); transfer-prior tables populated for every pairing in §6.1 with at least one source tag; h(t) table appended.

---

## 10. Scoring and calibration ledger (pre-registered)

| Event class | Quantity | Scored on | Score |
|---|---|---|---|
| Qualification | `P_qual(c)`, every listed candidate | 18 Apr (definitive on proclamation) | Brier |
| Pairing | `P_pair`, top five pairings by mass at V-R1 | 18 Apr | Brier |
| Winner | `P_win(c)`, every listed candidate | 2 May | Brier |
| R1 shares | 80% and 95% intervals, every candidate with forecast mean ≥3% | 18 Apr | Coverage |
| R2 share | 80% and 95% intervals, winner's share | 2 May | Coverage |
| R2 non-valid | 80% interval | 2 May | Coverage |
| Branch realisations | B-RN, B-LEFT, B-CTR, B-RGT | 15 Mar | Recorded, not scored (inputs, not forecasts) |

Scored vintage: the final pre-blackout vintage for each round. Earlier vintages are retained and reported as trajectory, not scored.

Honest statement: this is a one-shot forecast. There is no by-election-style calibration stream before April; the first real score is the election. Interval coverage on shares is the primary test; Brier is secondary (few binary events).

---

## 11. Vintage schedule

| Vintage | Date | Trigger |
|---|---|---|
| V0 | On Phase 1 gate | Internal baseline |
| V1 | w/c 12 Oct 2026 | United-left primary (11 Oct) |
| V2–V6 | First Monday monthly, Nov–Mar | Calendar |
| V-list | Conseil list publication (~15 Mar 2027) | Universe replaced |
| V-R1 | Fri 16 Apr 2027 23:59 CEST | Final R1 freeze (blackout) |
| V-inter | Mon 19 Apr 2027 | R1 result ingested; R2-only vintage |
| V-R2 | Fri 30 Apr 2027 23:59 CEST | Final R2 freeze |

Event vintages may be inserted by addendum at any B-* trigger. The US midterm (3 Nov 2026) sits between V1 and V2; no French vintage is scheduled in the fortnight either side of it.

---

## 12. Known limitations (recorded at freeze)

- Calibration base: six cycles with usable polling (1995–2022). σ priors are set from pooled errors with n ≈ 6, not estimated; they are pins and are labelled as such in every published interval.
- Horizon: at ~240 days, year-ahead polls have missed individual candidates by >10pp (2022 LFI). Wide intervals at early vintages are the correct output.
- τ has no precedent and cannot be validated before the event.
- B-RN (P1) is judgemental and rests on a component with no base rate. B-LEFT/B-CTR/B-RGT weights are rule-derived from the tested configuration set and therefore inherit pollsters' choices of what to test, which track newsworthiness rather than probability.
- Duel polls at long horizons are weakly validated; they update transfer rows, not headlines.
- Geography is not modelled; overseas and diaspora votes enter only through national totals.

---

## 13. Pins

Pins are ratified at freeze. Eleven carry values below. Six — P9, P10, P12, P13, P14, P15 — are defined as measurements and cannot exist before Phase 1; their derivation rules are frozen here and the values land as Addendum A1 before V0. A measured pin is still a pin: derived once by the frozen rule, then never tuned (§8).

| Pin | Parameter | Ratified at freeze |
|---|---|---|
| P1 | B-RN Bardella weight | Le Pen 0.96 / Bardella 0.04. Decomposition §4: judicial 0; incapacity ≈0.007; voluntary or party substitution ≈0.03 |
| P2 | B-LEFT alternative weights | Rule, not numbers: combination table §4. Resets by addendum at the 11 Oct 2026 primary |
| P3 | B-CTR alternative weights | Rule, not numbers: combination table §4. Resets by addendum at declarations and parrainages |
| P4 | B-RGT presence weights | Rule, not numbers: combination table §4. Resets by addendum at parrainages |
| P5 | B-ENV effect | Zero-mean, direction-agnostic |
| P6 | Unnamed residual | 10% |
| P7 | House effects | Do not apply. One cycle per pollster on a reconstituted panel labels 2022 noise, not 2027 bias; the 2022 errors are stored as a descriptor in `fr_pollster_house_effects`. Revisit by addendum only if a persistent bloc-level pollster spread appears |
| P8 | Half-life schedule | 30 d to 28 Feb; 14 d from 1 Mar; 7 d from list publication |
| P9 | Bloc-shift SD (final week, major blocs) | MEASURED — Addendum A1 before V0. Derivation frozen: pooled 2002–2022 R1 final-poll errors by bloc, final-week horizon, share points per §5.3 |
| P10 | Consolidation mean / SD | MEASURED — A1 before V0. Derivation frozen: leader-vs-others final-poll errors 2002/2007/2012/2017/2022; positive mean, sign-unstable; a mean shift plus a variance term, not a deterministic adjustment |
| P11 | τ prior | Zero-mean, two-sided. SD 2.0 share points on the left → centre flow, 1.0 on right → far-right, horizon-scaled by h(t). Judgement, no precedent — labelled as such in §12 |
| P12 | Idiosyncratic SD | MEASURED — A1 before V0. Derivation frozen: residual of the pooled §5.4 errors after β and γ |
| P13 | h(t) | MEASURED — A1 before V0. Derivation frozen: Jennings–Wlezien (2018) presidential subset, archive snapshot hashed under `backtrack/fr/source/` |
| P14 | κ mean / SD / anchor discount | MEASURED — A1 before V0. Derivation frozen: 2002/2017/2022 R2 final-poll errors on the logit scale per §5.3; anchor discount stated against the four differences in §6.3; zero inside the 95% interval |
| P15 | Dirichlet concentration | MEASURED — A1 before V0. Derivation frozen: dispersion of day-of surveys against the ecological estimates, per §6.2 |
| P16 | Factor loadings / correlations | ρ = 0.5 for `F_left` (left-bloc consolidation, left-segment non-valid columns) and ρ = 0.5 for `F_coord` (κ, centre/left → opponent columns); factors independent. Loadings per §6.4 |
| P17 | Draws N | 100,000 |

---

## 14. Addenda

(none at freeze)

A1 (before V0): measured values for P9, P10, P12, P13, P14, P15 by the derivation rules in §5.3–5.4 and §6.2–6.3, and nothing else.

**A3 — 2026-08-24 — prospective amendment to §11 (vintage schedule).**

*Reason.* Two facts established after freeze. (1) The engine implementing L1–L4 (§5–§7) was unscoped at freeze; its build is sequenced after the US final freeze of 2 Nov 2026, by ruling of 21 Aug 2026, on the grounds that §10 scores only the final pre-blackout vintages and that the competing deadline is a scored public record. (2) §11 as frozen is internally inconsistent: the first Monday of November 2026 is 2 Nov, which sits inside the fortnight around 3 Nov that §11 itself protects (20 Oct – 17 Nov).

*Amendments.*
(i) V1 (w/c 12 Oct 2026) is withdrawn. The 11 Oct united-left primary is recorded on its date through the B-LEFT resolution addendum that §4 requires at a named trigger, without a vintage. The measurement keeps its date; only the output is deferred.
(ii) Calendar vintages are: V2 Mon 7 Dec 2026; V3 Mon 4 Jan 2027; V4 Mon 1 Feb 2027; V5 Mon 1 Mar 2027; V6 Mon 5 Apr 2027 (between V-list and V-R1). Labels are preserved; no vintage falls in the protected fortnight.
(iii) V0's trigger is stated in full: Phase 1 gate (§9), A1 landed, and engine acceptance per §12 of `FR_ENGINE_DESIGN.md` v0.3, committed in the same PR as this addendum at blob SHA-256 `21335c99f7b7f3ad4a9bf6498b349d6fd952ec2a81aa6e436ba495af313044b6`. Later revisions of that document do not alter this trigger; a change to the acceptance criteria is a further addendum. V0 is expected late November 2026.

*Unchanged.* §10 scored vintages remain V-R1 and V-R2; V-list, V-R1, V-inter and V-R2 keep their dates and triggers; event vintages at B-* triggers remain available by addendum. The withdrawal of V1 costs one trajectory point and enters no score.

**A2 — 2026-08-24 — schema addition to §9: historical poll table `fr_polls_hist`.**

*Reason.* The §5.4 and §6.3 measured columns, and the P9, P10, P12 and P14 derivations, require the archived 2002–2022 poll tables; and §8 validates the machinery on vintage-reconstructed polls at T−240, T−90, T−30 and T−1 days for 2017 and 2022. §9's twelve tables have nowhere to hold any of it: without this table the A1 pass cannot run and the §8 backtest cannot be fed.

*Addition.* Table `fr_polls_hist` (`cycle`, `round`, `poll_key`, `hypothesis_id`, `candidate_set` text[], `organisation`, `fieldwork_start`, `fieldwork_end`, `publication_date`, `sample_n`, `candidate_name`, `party`, `share`, `source_url`, `source_snapshot_sha256`, `revision_seq`, `ingest_version`, `created_at`). Scope: every R1 and R2 intention poll listed for the cycles 2002, 2007, 2012, 2017 and 2022, from the earliest listed poll of each cycle to that cycle's blackout — at minimum spanning T−240 to T−1 — hypothesis-aware, because the field was unsettled for most of each cycle and the §8 reconstructions read those polls through the same configuration logic the 2027 model uses. `publication_date` is the reconstruction filter.

*Scope.* Schema only; no parameter is added and §14's A1 bound is untouched. The three derivation windows in the frozen text (P9 "final week"; §5.2 "final-fortnight"; §5.3–5.4 "final-poll") are pinned as queries by the A1 protocol document, which is hash-frozen before this table's coverage report exists; a fuller table is neutral to that guard and mandatory for §8.

**A4 — 2026-08-24 — new measured pin P18: R1 non-valid mass.**

*Reason.* §6.1 gives the R2 transfer matrix a `nonvalid` row (R1 abstention + blank + null) and §10 scores `R2_nonvalid` by interval coverage, but no pin fixes the mass of that row. Engine design v0.1 proposed a zero-uncertainty constant (C2); that would make a scored interval narrow by construction, and the R1 non-valid share of the registered electorate has varied materially across the calibration base; the measured range is reported with the derivation.

*Addition.* P18 — R1 non-valid share of the registered electorate, drawn per simulation as Normal(μ₁₈, σ₁₈) truncated to [0, 1]. Derivation rule, frozen here before measurement: six observations, one per cycle (1995, 2002, 2007, 2012, 2017, 2022), each being `(inscrits − exprimés) / inscrits` for that cycle's distinct national R1 pair taken as `SELECT DISTINCT cycle, inscrits, exprimes FROM fr_results_presidential WHERE round = 1 AND level = 'national' AND cycle IN (1995, 2002, 2007, 2012, 2017, 2022)`. `fr_results_presidential` is candidate-grain, so the query must return exactly six rows, and the derivation aborts on any other count: more than six means the pair is inconsistent across candidate rows within a cycle; fewer than six means a cycle is missing from the table. Either is a PR C finding, and in neither case is a partial series used. μ₁₈ is the unweighted mean of the six; σ₁₈ the sample standard deviation on n − 1 = 5. No trend term; no cycle weighting; no candidate-count weighting. P18 is drawn independently of F_left and F_coord at v0.1.

*Scope.* P18 enters `R2_nonvalid` at first order and `R2_share` at second order only, through the abstainer-mobilisation cells of the transfer matrix; it does not enter L1. Its value lands under this heading, not under A1, whose bound (§14) is unchanged; operationally it may be computed in the same single pass. Added to §13 as a measured pin and to the pins file with this derivation as provenance.

Derivation protocol

The run sheet for the six measured parameters. It fixes every window, threshold and cap as a query before any error is computed — so the numbers arrive under rules written before they existed.

File
FR_A1_PROTOCOL.md
SHA-256 (as published here — frozen text plus amendments)
df51d6e3191564df29137ff3552ef6bac2780642ace0c7a54f164a73bc45bce6
Size
97 lines, 10256 bytes
# FR Presidential 2027 — A1 single-pass protocol v0.3
## Derivation run sheet for P9, P10, P12, P13, P14, P15 (§14 A1 bound) and P18 (A4), against `FR_PRES_2027_SPEC.md` blob `b7420e97…11c6`

**Status.** v0.3, authored 24 Aug 2026 (v0.3 pre-freeze clarifications: §2 window choice stated; §8 closed-interval reading stated); §8's λ_A, §9's α cap and §3's flagged assignments ratified 24 Aug 2026 without amendment (ratification by deferral, recorded as such). Amendment window: a dated revision of this file before PR E's coverage report lands is legitimate; after that report exists, the values are closed whether or not the pass has run. Freeze = commit of this file with its blob SHA-256 quoted in the A1 PR body; the freeze must precede the existence of PR E's coverage report (counts per cycle × round × organisation), so that no window below is chosen with poll-density knowledge. One execution; re-run only from an abort condition in §11, each re-run reported.

**What this protocol is not.** It derives no per-vintage quantity (aggregate means, covariances, branch tables — vintage-time, engine). It tunes nothing: every threshold, window, discount and cap is fixed in this text before any error is computed. Descriptors are reported, never fed back.

---

## 1. Inputs and checksums

| Input | Table / file | Checksum recorded |
|---|---|---|
| Historical polls | `fr_polls_hist` (A2), highest `revision_seq` per natural key | SHA-256 of sorted result set |
| Historical results | `fr_results_presidential` (national rows) | same |
| Transfer bases | `fr_transfer_priors` (`eco_*`, `dayof_*`) | same |
| Horizon data | Jennings–Wlezien archive snapshot (PR E manifest) | manifest hash |
| This protocol | committed blob | blob SHA-256 |
| Code | analysis scripts at a clean commit | commit hash |

R1 dates: 2002-04-21, 2007-04-22, 2012-04-22, 2017-04-23, 2022-04-10. R2 dates: 2002-05-05, 2007-05-06, 2012-05-06, 2017-05-07, 2022-04-24.

## 2. Universal definitions (fixed)

- **Final R1 poll, per organisation, per cycle:** the organisation's last poll with `fieldwork_end` in [R1−8, R1−1] and `publication_date` ≤ R1−2 (blackout eve). Final R2 poll: same with R2 dates. The windows here and in §12 are deliberately one day wider than the frozen labels ("final week", "final fortnight"): historical fieldwork-end dates carry ±1-day precision noise, and last-poll-per-organisation selection means the extra day admits organisations, never additional polls per organisation. This operationalisation supersedes the prose labels — making those labels executable is this document's job.
- **Final-poll average:** unweighted mean across organisations' final polls, per candidate. Shares on the expressed basis; any final poll published on another basis is converted and the conversion logged.
- **Error:** `result − final-poll average`, in points of valid votes.
- **Candidate universe per cycle:** candidates with final-poll average ≥ 1%.
- **Bloc assignment:** §3 table of this protocol, fixed here. Any universe candidate absent from it is an abort (§11).
- **Bloc leader (historical):** highest final-poll average within the bloc — the spec's §3 rule applied to the cycle.
- **Hypotheses:** final polls are single-field (the ballot is set); if any final poll carries multiple hypotheses, the hypothesis matching the official candidate list is used and the case logged.

## 3. Historical bloc assignment (fixed before any error is computed)

| Cycle | far_right | right | centre | left | far_left | other |
|---|---|---|---|---|---|---|
| 2002 | J-M Le Pen, Mégret | Chirac, Madelin, Boutin | Bayrou, Lepage | Jospin, Chevènement*, Mamère, Hue, Taubira | Laguiller, Besancenot | Saint-Josse |
| 2007 | J-M Le Pen, de Villiers* | Sarkozy | Bayrou | Royal, Buffet, Voynet, Bové | Besancenot, Laguiller | Nihous |
| 2012 | M Le Pen, Dupont-Aignan | Sarkozy | Bayrou | Hollande, Mélenchon, Joly | Poutou, Arthaud | — |
| 2017 | M Le Pen, Dupont-Aignan | Fillon | Macron | Mélenchon, Hamon | Poutou, Arthaud | Lassalle |
| 2022 | M Le Pen, Zemmour, Dupont-Aignan | Pécresse | Macron | Mélenchon, Jadot, Roussel, Hidalgo | Poutou, Arthaud | Lassalle |

\* Judgement calls, named: Chevènement (MDC, ex-PS souverainiste) → left; de Villiers (MPF) → far_right. Both flagged in the run report; neither is revisable post-measurement.

## 4. P9 — bloc-shift SD

Per cycle per bloc: bloc-total error = Σ candidate errors in the bloc. P9(bloc) = sample SD across the five cycle values, n−1 = 4, for the four major blocs (far_right, right, centre, left). far_left and other receive the pooled SD across all twenty (cycle, major-bloc) values. β is zero-mean by the frozen spec; the per-bloc mean error is a descriptor, and any persistent sign is a finding, not a parameter.

## 5. P10 — consolidation mean and SD

Observation set: every (cycle, bloc) with ≥ 2 universe candidates and bloc final-poll total ≥ 10 points. Per observation:

`g = error_leader − bloc_total_error × (leader_poll_share / bloc_poll_share)`

— the leader's error net of its pro-rata share of the common bloc shift, i.e. the within-bloc reallocation. μ_γ = mean of all observations pooled across blocs and cycles; σ_γ = sample SD. One (μ_γ, σ_γ) pair applied to every bloc; the per-bloc breakdown is a descriptor.

## 6. P12 — idiosyncratic SD

Per universe candidate with final-poll average ≥ 3%: residual `r = error − bloc-shift component − consolidation component`, where the bloc-shift component is `bloc_total_error × (poll_share / bloc_poll_share)` and the consolidation component is `+g` for the leader and `−g × (poll_share / non-leader bloc poll share)` for non-leaders, using that (cycle, bloc)'s g where one exists, else 0. σ_ε = pooled sample SD of residuals. One value.

## 7. P13 — h(t)

From the Jennings–Wlezien snapshot: presidential-election rows; entities (candidates/parties) with final result ≥ 5%; error = poll share − result, in points. Column mapping from the archive's codebook is recorded in the run report before computation. Buckets of days-to-election: [0,7], (7,30], (30,60], (60,90], (90,150], (150,240]. Per bucket: RMSE, pooled across all presidential elections in the dataset. h(bucket) = RMSE(bucket) / RMSE([0,7]); monotone non-increasing toward the election enforced by pool-adjacent-violators; d > 240 uses the (150,240] value. The pin is the six-value table. France-only column reported as a descriptor, not fitted.

## 8. P14 — κ mean and SD

Observations: 2002, 2017, 2022 R2 (the RN runoffs). Per cycle: κ_obs = anti-RN candidate's result − final-poll average of that candidate's two-way share, in R2 share points. μ_raw = mean of the three; σ_raw = sample SD, n−1 = 2.

Fixed pre-measurement, for ratification with this protocol:
- **Anchor discount λ_A = 0.5, applied to the mean only:** μ_κ = λ_A × μ_raw; σ_κ = σ_raw undiscounted. Grounds, per §6.3: no incumbent opponent; RN R1 base ~35 not ~23; LR-electorate drift; declining magnitude 2002 → 2022. The discount halves the claimed effect and leaves the uncertainty whole.
- **Zero-inside-95% enforcement (frozen §6.3/§13):** if 0 ∉ μ_κ ± 1.96 σ_κ after the discount, σ_κ is raised to |μ_κ|/1.96 and the inflation is reported. No discretion at run time. "Inside" is the closed interval, and the boundary is the intent: the requirement is that the pin may not assert κ's sign at 95%, and σ = |μ_κ|/1.96 is that requirement's minimal operationalisation — strict interiority has no minimal form and would need an arbitrary ε.
- **Disclosure carried to the run report:** all three κ observations come from runoffs with a centre or right opponent; the pin applies to every pairing and is weakest-grounded where the opponent is from the left — §6.3's inversion note. σ_κ, not μ_κ, is what covers that gap.

## 9. P15 — Dirichlet concentration

For each year (2017, 2022), each segment present in both `eco_<year>` and at least one `dayof_*_<year>`, each of the three cells: m = mean of the two estimates, d = survey − eco. Conservative rule fixed here: all divergence is attributed to prior uncertainty — `1/(α+1) = mean over cells of d² / (m(1−m))`, pooled across both years. Cap α ≤ 200 (a floor on transfer uncertainty of ≈ 3.5 points on a 50% cell), applied only if the pooled estimate exceeds it, and reported if applied. Per-year α is a descriptor.

## 10. P18 — per A4, verbatim

`SELECT DISTINCT cycle, inscrits, exprimes FROM fr_results_presidential WHERE round = 1 AND level = 'national' AND cycle IN (1995, 2002, 2007, 2012, 2017, 2022)` — exactly six rows or abort (either direction, per A4). μ₁₈, σ₁₈ on `(inscrits − exprimés)/inscrits`, n−1 = 5.

## 11. Abort conditions (the only permitted re-run triggers)

1. P18 row count ≠ 6 (A4).
2. A universe candidate absent from the §3 bloc table → abort; the fix is a dated protocol addendum adding the row, before re-run.
3. An input checksum differing from the PR-E-gate value → abort; reconcile provenance first.
4. A final-poll set empty for any (cycle, organisation ≥ 3 organisations required per cycle) → abort; ingest defect, back to PR E.

Anything else found mid-run is a finding in the report, not a reason to touch the pass.

## 12. Descriptors (reported, never fed back)

House-effect table per §5.2 (per organisation × bloc; 2022 window [R1−15, R1−1], plus 2024 European final polls), `role = descriptor`, P7 unchanged. Per-bloc mean errors (P9), per-bloc consolidation breakdown (P10), per-year α (P15), France-only h(t) (P13), and the full observation tables behind every pin.

## 13. Outputs and landing

One run produces: the A1 addendum values table (six pins + P18 under its A4 heading), `pins/fr_v0.1.yaml` (pins + constants per engine design §2, this protocol's blob hash as provenance for the measured entries), and the run report under `backtrack/fr/derived/a1_run/` (input checksums, code commit, every observation and descriptor table, judgement flags from §3, any §8 inflation or §9 cap application). A1 lands as a dated §14 append citing this protocol's blob hash; the prefix assertion applies as always.

Engine design

How the specification becomes code. No engine code is written before 2 November 2026; this document is what the build starts against.

File
FR_ENGINE_DESIGN.md
SHA-256 (as published here — frozen text plus amendments)
21335c99f7b7f3ad4a9bf6498b349d6fd952ec2a81aa6e436ba495af313044b6
Size
197 lines, 15068 bytes
# FR Presidential 2027 — Engine design v0.3
## Implementation design for L1–L4 of `FR_PRES_2027_SPEC.md` (frozen, blob `b7420e97…11c6`) — v0.3

*Change log. v0.2 → v0.3 (21 Aug): §10 — 2012 priors withdrawn (§8 pass criteria are R1-only; 2012's PS–LR runoff is not a like-for-like prior); R2 diagnostic provenance stated per cycle. v0.1 → v0.2: C2 withdrawn in favour of P18 (A4) with an SD; §10 backtest inputs made explicit.*

**Status.** Design only, dated 21 Aug 2026. No engine code before the US final freeze of 2 Nov 2026 (ruling of 21 Aug). The build starts against this document, not a blank page. Design decisions the spec does not make are collected in §9 for ruling; none is taken silently.

**Relationship to the spec.** This document implements §5–§7 under the §8 protocol. It adds no parameter. Every number the engine uses comes from one of: the §9 tables; the branch register; or a machine-readable pins file (§2) whose hash is quoted in the addendum that ratified it. A number with no provenance is a build defect.

---

## 1. Package layout

```
fr_engine/
  io/         readers for fr_* tables and views (read-only); vintage writer (append-only)
  pins.py     loads and hash-verifies the pins file; hard-fails on any missing key
  branches.py joint combination table (B-LEFT × B-CTR × B-RGT) per the P2–P4 rule; B-RN, B-ENV overlays
  aggregate.py per-configuration weighted means (P8 half-life, √n cap); matched view only
  compose.py  share ↔ ALR transforms; Jacobian conversions pp → ALR at the vintage mean
  errors.py   flow terms γ, τ in share space; level terms β, ε in ALR; h(t)
  qualify.py  top two
  transfer.py T_p assembly by §6.2 authority order; duel update; κ; factor loadings; Dirichlet rows
  simulate.py the per-draw loop, vectorised (numpy Generator, fixed seed)
  outputs.py  quantities, intervals, provenance block; append to fr_forecast_vintages
  backtest.py §8 machinery validation: vintage reconstruction for 2017 and 2022
  cli.py      two commands only: `vintage` and `backtest`
```

No module holds a default value for any model quantity. No CLI flag overrides a pin. There is no `preview` command: every run that produces numbers writes a vintage row (or a backtest artefact) with full provenance, so there is no such thing as an unlogged look.

---

## 2. Pins file

`pins/fr_v0.1.yaml`, committed with the A1 addendum PR, SHA-256 quoted in A1. Two sections:

- `pins`: the §13 values and the six A1-derived values, keyed P1–P17, each with `unit` and `spec_ref`.
- `constants`: operational constants the spec is silent on (listed in §9 of this document), each with `provenance`. Any change to either section is a dated addendum; the engine records the file hash in every vintage row and refuses to run if the file fails verification.

---

## 3. Inputs per vintage

| Input | Source | Filter |
|---|---|---|
| R1 polls | view `fr_polls_r1_matched` (highest `revision_seq` per natural key) | `publication_date ≤ t_v` |
| R2 duel tables | `fr_polls_r2`, matched | `publication_date ≤ t_v` |
| Configurations | `fr_configurations` | `mapping_status = mapped` |
| Branch register | `fr_branch_register` | latest `addendum_ref` per branch |
| Transfer priors | `fr_transfer_priors` | all bases |
| Candidates | `fr_candidates` | status as of `t_v` |
| R1 non-valid series | `fr_results_presidential` | national R1 rows, 1995–2022, for P18 (A4) |

Input checksums: SHA-256 of each filtered result set, rows sorted on the natural key, serialised LF-canonical. Recorded in the vintage row.

---

## 4. L0 — branches (`branches.py`)

1. Combination table: distinct (B-LEFT alt, B-CTR alt, B-RGT presence) triples among configurations with `fieldwork_end` in the trailing 60 days, tier cascade ≥2 notice-filing organisations → ≥1 → most recent wave. Uniform weights across the surviving triples. Tier applied is written to `fr_branch_register.rule_tier_applied` for the vintage. After a branch's trigger resolves by addendum, that branch's alternative is fixed and the table is built over the remaining branches only.
2. B-RN: independent Bernoulli, P1. A Le Pen draw uses the sampled triple's Le Pen configuration(s). A Bardella draw uses the Bardella configuration with the highest candidate-set Jaccard similarity to the sampled triple; the offset (candidates present in one and not the other) is logged per vintage (§5.2 fallback, B-RN only).
3. B-ENV: zero-mean at v0.1 (P5); the hook exists and is a no-op.

---

## 5. L1 — R1 shares (`aggregate.py`, `compose.py`, `errors.py`)

**5.1 Aggregate mean per configuration k.** For poll i with fieldwork end `t_i`, size `n_i`, shares `s_ic` (expressed):

`w_i = sqrt(min(n_i, n_cap)) · 2^(−(t_v − t_i) / HL(t_v))`, HL per P8 by vintage date.
`m_kc = Σ_i w_i s_ic / Σ_i w_i`. Effective sample size `(Σ w)² / Σ w²` reported. No house effects (P7). No trend term.

When several configurations map to one combination, their means are pooled with weights `Σ_i w_i` per configuration.

**5.2 Flow terms, share space, applied first.** Let `d = days to R1`, `h = h(d)` (P13).

- Consolidation, per bloc b: `g_b ~ N(μ_γ,b, σ_γ,b) · h` in share points. If `g_b > 0`: move `min(g_b, M_b)` from non-leaders to the bloc leader, pro-rata to non-leader shares, where `M_b` is the non-leader mass. If `g_b < 0`: move `min(|g_b|, leader share)` from the leader to non-leaders pro-rata. Leader per the §3 rule. F_left enters `g_left` (§7).
- Tactical, left → centre: `τ_LC ~ N(0, σ_LC) · h`; source set = left bloc minus the LFI candidate; target set = centre bloc; pro-rata both sides; negative values reverse direction.
- Tactical, right → far-right: `τ_RF ~ N(0, σ_RF) · h`; source = LR candidate; target = RN candidate; negative reverses.

Transfers conserve total; no renormalisation step.

**5.3 Level terms, ALR space.** Reference category `other` (aggregate of non-listed candidates), floored at `p_floor`. `y_c = ln(p_c / p_other)`.

- Bloc shift: one draw `δ_b ~ N(0, σ_δ,b)` added to every `y_c` in bloc b, with `σ_δ,b = σ_β,b · h / (P_b (1 − P_b))`, `P_b` the bloc's share at the vintage mean (Jacobian of bloc total w.r.t. a common log shift).
- Idiosyncratic: `e_c ~ N(0, σ_e,c)` added to `y_c`, `σ_e,c = σ_ε · h / (p_c (1 − p_c))`.
- Back-transform by closure. Shares sum to one by construction.

**5.4 Qualification (`qualify.py`).** Top two by share. Ties (measure-zero) broken by seed order and logged.

---

## 6. L3 — R2 transfer (`transfer.py`)

**6.1 Row set.** One row per R1 candidate in the sampled combination, plus `nonvalid` (R1 abstention + blank + null). Row masses: per draw, `m_nv ~ N(μ_18, σ_18)` truncated to [0, 1] (P18, A4: R1 non-valid share of the registered electorate); candidate rows = L1 shares × (1 − m_nv). `m_nv` enters `R2_nonvalid` at first order and `R2_share` only at second order, through the abstainer-mobilisation cells; it is drawn independently of F_left and F_coord at v0.1 and that independence is recorded.

**6.2 Prior mean per (pairing, segment), authority order (§6.2).**
1. `eco_2022` row for the segment via the segment map; `eco_2017` as the secondary where 2022 has no analogue; bloc-level eco row where neither has one (new candidates). Fallback level is logged per segment.
2. Day-of rows are a reported cross-check, not an input to the mean (their role is P15).
3. 2024 legislative evidence enters only through κ (A1).
4. Duel update: for each published duel matching the pairing, with publication `≤ t_v`, the published transfer share for the segment updates the prior mean by precision weighting: `m = (π_p m_p + π_q m_q) / (π_p + π_q)`, `π_p = 1/σ_p²` with `σ_p` the eco-vs-day-of dispersion for that segment class (A1 report), `π_q = 1/(σ_q · h(d))²` with `σ_q` the sampling SE of the published share given duel `n` and segment size. Means only; the row's dispersion is P15, not the duel.

Consistency check, every vintage: each pollster's duel headline recomputed from its own R1 configuration and its own transfer table; mismatches beyond rounding logged (§6.2).

**6.3 Row draws.** Each row ~ Dirichlet(α' · mean), with α from P15 and α' per §7 so total cell variance is preserved after the factor contribution.

**6.4 κ.** `κ ~ N(μ_κ, σ_κ)` in R2 share points (A1, P14), shared with F_coord (§7). Applied on the logit of the RN candidate's R2 share: `logit(s') = logit(s) − 4κ` (slope of the logit at p = 0.5; κ as a fraction).

**6.5 R2 arithmetic.** `s_A = Σ_r m_r T[r,A] / Σ_r m_r (T[r,A] + T[r,B])`; `R2_nonvalid = Σ_r m_r T[r,nonvalid]` on the registered electorate.

**6.6 Between rounds (V-inter).** Row masses replaced by actual R1 shares and actual R1 non-valid; duel updates continue for the realised pairing; κ retained with its SD.

---

## 7. Shared factors (`transfer.py`, `errors.py`)

Per draw, `F_left, F_coord ~ N(0,1)`, independent (P16). For any affected quantity with pinned marginal SD `σ_i` and factor F:

`q_i = μ_i + σ_i ( sqrt(ρ) · F + sqrt(1 − ρ) · z_i )`, `z_i ~ N(0,1)` — marginal SD preserved, induced correlation ρ.

- F_left → `g_left` (5.2) and the `nonvalid` cell of every left-segment row.
- F_coord → κ (6.4) and the opponent cell of every centre- and left-segment row.

For Dirichlet cells, the factor shifts the cell mean on the logit scale by `sqrt(ρ) · σ_cell,logit · F`, the row is renormalised, and the Dirichlet concentration is raised to `α' = (α + 1)/(1 − ρ) − 1` so the cell's total variance equals the P15 variance. Unit-tested on synthetic rows: empirical cell SD within 2% of the P15 value; empirical correlation within 0.03 of ρ.

---

## 8. L4 — simulation and outputs (`simulate.py`, `outputs.py`)

Per draw: combination → B-RN → (F_left, F_coord) → R1 (5.1–5.3) → top two → T_p rows (6.2–6.3) → κ → R2 (6.5) → record. N = P17. numpy `Generator(PCG64(seed))`, seed = SHA-256 of (`vintage_id`, pins hash) truncated, recorded.

Outputs (80% and 95% central intervals; point = median): `P_qual(c)`, `P_pair(a,b)`, `R1_share(c)`, `R2_share(c|pair)`, `P_win(c|pair)`, `P_win(c)`, `R2_nonvalid`; per-combination breakdowns; tier applied; B-RN offset log.

Vintage row provenance: `vintage_id`, `vintage_date`, seed, `code_commit` (clean tree required; dirty tree aborts), spec blob SHA-256, pins file SHA-256, input checksums (§3). Append-only. Two runs with the same seed and inputs must produce byte-identical output artefacts; this is an acceptance test.

---

## 9. Decisions the spec does not make — for ruling, recorded in `constants`

| ID | Constant | Proposed | Effect |
|---|---|---|---|
| C1 | `n_cap` for √n weighting | 2,000 | Caps the influence of very large online panels |
| C2 | *(withdrawn — became P18 by A4; a scored interval may not be narrow by construction)* | — | — |
| C3 | Duel precision rule | as 6.2(4) | How hypothetical duels move transfer means |
| C4 | Factor variance split | `α' = (α+1)/(1−ρ) − 1` | Preserves P15 marginal variance under F |
| C5 | Segment fallback order | eco_2022 → eco_2017 → bloc-level eco | New candidates without a 2022 analogue |
| C6 | Negative flow handling | reverse direction, same pro-rata rule | γ, τ draws below zero |
| C7 | `p_floor` for the ALR reference | 0.1% | Avoids log of zero when `other` vanishes |
| C8 | B-RN configuration matching | highest Jaccard on candidate set | §5.2 fallback, B-RN only |
| C9 | Trailing window for combination table | 60 days (from the P2–P4 rule) | Stated here for completeness; already ruled |

Every constant is listed in `pins/fr_v0.1.yaml` under `constants` with provenance; a change is an addendum. None of the surviving constants touches a scored quantity directly.

---

## 10. Backtest harness (`backtest.py`) — §8 validation, not tuning

For cycles 2017 and 2022: reconstruct vintages at T−240, T−90, T−30, T−1 days using only `fr_polls_hist` rows with `publication_date ≤` the vintage date; run the engine with the frozen pins, the cycle's own candidate set and bloc map (in-repo `backtrack/fr/derived/hist_blocs_<cycle>.csv`, leader by the §3 rule), the cycle's own hypotheses through §4's configuration logic, and transfer priors for the R2 diagnostic (2022 on eco_2017, held out and a like-for-like RN runoff; 2017 on eco_2022, which is anachronistic, and the report names it — no earlier commune-level RN-runoff matrix exists, 2012's runoff being PS–LR); P13's h(t) is in-sample for 2017 if the Jennings–Wlezien data include that cycle, and the report says so. §8's pass criteria are R1-only: 95% coverage of realised R1 shares for candidates ≥3%, and the realised pairing inside the top-two `P_pair` mass. The R2 margin is reported as a diagnostic and scores nothing. Output to `backtrack/fr/derived/backtest_<cycle>_<run>.csv` with provenance, never to `fr_forecast_vintages`. Pass/fail recorded as a finding. No parameter moves; a failure is addressed only by a dated addendum with reasoning independent of the failing statistic (§8). P9/P10/P12 are in-sample for these cycles and the report says so.

---

## 11. Tests

- ALR round-trip; closure sums to one; Jacobian conversions reproduce a pp perturbation at the mean to first order.
- Flow terms conserve total to 1e-12; negative flows reverse correctly; transfers never exceed source mass.
- Dirichlet rows sum to one; factor split meets the §7 tolerances on synthetic data.
- Top-two with known vectors; R2 arithmetic reproduces a published duel headline from that pollster's own R1 configuration and transfer table (§6.2 check) on at least two archived examples.
- Pins loader hard-fails on a missing key, a wrong hash, or an unknown key.
- Golden run: fixed seed, fixed synthetic inputs, byte-identical artefact.

---

## 12. Acceptance before V0

1. All §11 tests green in CI.
2. Backtest harness run for both cycles, report committed, findings recorded (pass or fail).
3. Vintage writer round-trip verified against the DB by query.
4. Reproducibility: two runs, same seed and inputs, identical artefact hashes.
5. Code review against this document: every constant in §9 present in the pins file; no default value in code; no override flag.

---

## 13. Build plan (after 2 Nov 2026; for ruling)

| PR | Content | Target |
|---|---|---|
| E1 | skeleton, `io`, `pins.py`, CI | w/c 2 Nov |
| E2 | `aggregate.py`, `branches.py` | w/c 9 Nov |
| E3 | `compose.py`, `errors.py`, `qualify.py` | w/c 9 Nov |
| E4 | `transfer.py`, factors | w/c 16 Nov |
| E5 | `simulate.py`, `outputs.py`, vintage writer | w/c 16 Nov |
| E6 | `backtest.py`, report | w/c 23 Nov |
| Acceptance (§12) → V0 | | by Fri 27 Nov |
| V2 | first calendar vintage per A3 | Mon 7 Dec |

A1 does not depend on the engine: it is a single pass over Phase 1 tables plus the h(t) fitting code from PR E. It can hold its 5 Oct slot if the 2 Oct gate holds, which fixes the six measured pins before any machinery exists — the stronger order for the Goodhart guard.