# Landing Screen — Data Contract

> Auto-maintained. Updated whenever a landing screen file changes.
> Schema reference: `backend/chanakya/tatasteel/schema.sql` (live tables: `ts4_*`).
> Static dataset: `frontend/src/data/tata/normalized.json` (used by the landing screen today; mirrors the `ts4_*` tables on `2026-04-22`).
> Last updated: 2026-05-08 (Bucket labels renamed: "Timeline Risk"→"Timeline Impact", "Budget Pressure"→"Budget Impact". Card illustrations enlarged (viewBox 60px tall, font sizes 14–16px). Budget/combined card data distributed proportionally to match totals: tl+co timeline = 45 days, bd+co budget = ₹38.6 Cr.)

---

## Phase 1 — Program Health (added 2026-05-08)

Auto-advances to Phase 2 when the typewriter finishes and the statement settles.
Components: `frontend/src/components/landing/story/ProgramHealthPhase/`
Data source: `PROGRAM_HEALTH` export in `landingStoryCards.ts`

### Headline

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| static string | Opening greeting | `"Hi, Here is what needs your attention today."` | Fixed greeting — no data binding | — static copy |

### Health Gauge

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `PROGRAM_HEALTH.score` | Program Health | `87` | Composite score 0–100 penalising: contracts with open quotations (×1.5 pts each, cap 30), open early warnings (×0.3 pts each, cap 25), stalled past-due quotations (×2 pts each, cap 20) | Derived from `ts_quotations`, `ts_early_warnings` — see `programHealthScore()` in `landingStoryCards.ts` |

### Colored Statement

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `PROGRAM_HEALTH.contractsWithOpenQuotations` | Contracts with open quotations | `14` | Count of distinct contracts that have at least one quotation in a pending review status | `ts_quotations → COUNT(DISTINCT contract_id) WHERE status IN ('Submitted','Assessment','In_Review','Internal_Review','Revised_Quotation')` |
| `PROGRAM_HEALTH.totalContracts` | Total contracts | `32` | Total number of active contracts in the portfolio | `ts_contracts → COUNT(*)` |
| `PROGRAM_HEALTH.varianceCr` | Variance (₹ Cr) | `38.6` | Sum of `change_to_prices` for all pending-review quotations, converted GBP→INR Crore (÷10M, ×107) | `ts_quotations → SUM(change_to_prices) WHERE status IN review statuses → converted to ₹ Cr` |
| `PROGRAM_HEALTH.slippageDays` | Potential slippage (days) | `22` | Estimated further delay in days — proxy: average open EW age in days divided by 7 | `ts_early_warnings → AVG(age_days) WHERE status = 'Open'` ÷ 7 |

---

## Phase 2 — Quotation Chain (added 2026-05-08)

Auto-advances to NceTriageView once all four nodes are visible and settled (1.8 s pause).
Components: `frontend/src/components/landing/story/QuotationChainPhase/`
Data source: `QUOTATION_CHAIN` export in `landingStoryCards.ts`

### Chain Nodes

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `QUOTATION_CHAIN[0].count` | Contracts | `32` | Total contracts in the portfolio | `ts_contracts → COUNT(*)` |
| `QUOTATION_CHAIN[0].metaLine` | Initial Budget | `"₹615 Cr initial budget"` | Sum of initial contract values converted to INR Crore | `ts_contracts → SUM(initial_value) → converted to ₹ Cr` |
| `QUOTATION_CHAIN[1].count` | Early Warnings | `89` | Total early warnings raised (open + closed) | `ts_early_warnings → COUNT(*)` |
| `QUOTATION_CHAIN[1].metaLine` | EW description | `"Contractor-raised scope signals"` | Fixed label describing EW business purpose | — static copy |
| `QUOTATION_CHAIN[2].count` | NCEs Raised | `47` | Total compensation events (NCEs) raised across all contracts | `ts_nce → COUNT(*)` |
| `QUOTATION_CHAIN[2].metaLine` | NCE description | `"Confirmed scope changes"` | Fixed label describing NCE business purpose | — static copy |
| `QUOTATION_CHAIN[3].count` | Quotations | `175` | Total quotation records across all NCEs | `ts_quotations → COUNT(*)` |
| `QUOTATION_CHAIN[3].metaLine` | Quotation description | `"Awaiting your review"` | Fixed label emphasising PM action required | — static copy |

---

## NCE Triage View (active landing screen — added 2026-05-07)

The landing screen now opens with the **NCE Triage** canvas. 175 Quotation particles animate into four impact-dimension clusters. The user clicks a cluster to reveal contract cards below. Greeting text and program subtitle have been removed; the big-number title now reads "175 Quotations".

Components: `frontend/src/components/landing/nceTriageView/`

### Canvas cluster labels

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `BUCKET_META[0].label` | Timeline Impact | `"Timeline Impact"` | Quotations that will add days to the programme schedule if not resolved promptly (58 quotations, +45 days combined) | `ts_quotations → COUNT(*) WHERE timeline_days > 0` |
| `BUCKET_META[1].label` | Budget Impact | `"Budget Impact"` | Quotations adding budget pressure — ₹20.2 Cr exposure across 58 quotations (bd share of ₹38.6 Cr total) | `ts_quotations → COUNT(*) WHERE change_to_prices > 0 AND timeline_days = 0` |
| `BUCKET_META[2].label` | Combined Impact | `"Combined Impact"` | Quotations hitting both timeline and budget simultaneously — 59 quotations, +38 days combined, ₹18.4 Cr locked | `ts_quotations → COUNT(*) WHERE timeline_days > 0 AND change_to_prices > 0` |

### Card: Timeline Impact bucket (tl)

**Insight copy:** `"58 quotations are shifting timelines — adding a combined +45 days, a 12% schedule deviation across active contracts."`
**Card layout:** title → area badge → timeline illustration (+Nd bar) → date raised → Revise / Submit to Finance actions

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `BUCKET_DATA.tl.insight` | Bucket insight | `"58 quotations are shifting timelines…"` | Summary impact statement shown above the card carousel | — static copy derived from totals |
| `BUCKET_DATA.tl.cards[i].title` | Quotation title | `"Operational Spare Parts"` | Name of the work package / quotation item | `ts_quotations → description` |
| `BUCKET_DATA.tl.cards[i].area` | Area | `"Melt Shop"` | Plant area or department associated with the quotation | `ts_contracts → area` |
| `BUCKET_DATA.tl.cards[i].dateRaised` | Date raised | `"4 Jan 2025"` | Date the quotation was submitted for review | `ts_quotations → submitted_date` |
| `BUCKET_DATA.tl.cards[i].timelineDays` | Timeline days | `1–6` | Number of days this quotation adds to the programme if unresolved (seeded 1–6 per card; 58 cards sum to proportional share of 45 days) | `ts_quotations → timeline_days WHERE status IN pending statuses` |

### Card: Budget Impact bucket (bd)

**Insight copy:** `"58 quotations are adding budget pressure — ₹24.6 Cr total exposure, an 8.4% cost deviation on the active programme."`
**Card layout:** title → area badge → budget illustration (coin stack + amount) → date raised → Revise / Submit to Finance actions

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `BUCKET_DATA.bd.insight` | Bucket insight | `"58 quotations are adding budget pressure…"` | Summary impact statement shown above the card carousel | — static copy derived from totals |
| `BUCKET_DATA.bd.cards[i].title` | Quotation title | `"Mechanical Seal Package"` | Name of the work package / quotation item | `ts_quotations → description` |
| `BUCKET_DATA.bd.cards[i].area` | Area | `"Melt Shop"` | Plant area or department | `ts_contracts → area` |
| `BUCKET_DATA.bd.cards[i].dateRaised` | Date raised | `"6 Jan 2025"` | Date the quotation was submitted | `ts_quotations → submitted_date` |
| `BUCKET_DATA.bd.cards[i].budgetAmount` | Budget amount | `"₹0.4 Cr"` | Rupee exposure for this quotation (distributed proportionally across 58 cards to sum to ₹20.2 Cr, the bd share of the ₹38.6 Cr total) | `ts_quotations → change_to_prices WHERE status IN pending statuses → converted to ₹ Cr` |

### Card: Combined Impact bucket (co)

**Insight copy:** `"59 quotations are hitting both timeline and budget simultaneously — +38 days combined shift and ₹18.4 Cr locked, compounding risk daily."`
**Card layout:** title → area badge → combined illustration (timeline + coin) → date raised → Revise / Submit to Finance actions

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `BUCKET_DATA.co.insight` | Bucket insight | `"59 quotations are hitting both timeline and budget simultaneously…"` | Summary impact statement shown above the card carousel | — static copy derived from totals |
| `BUCKET_DATA.co.cards[i].title` | Quotation title | `"Piping Spool Fabrication"` | Name of the work package / quotation item | `ts_quotations → description` |
| `BUCKET_DATA.co.cards[i].area` | Area | `"Melt Shop"` | Plant area or department | `ts_contracts → area` |
| `BUCKET_DATA.co.cards[i].dateRaised` | Date raised | `"8 Jan 2025"` | Date the quotation was submitted | `ts_quotations → submitted_date` |
| `BUCKET_DATA.co.cards[i].timelineDays` | Timeline days | `1–5` | Days added to schedule by this combined-impact quotation (seeded 1–5 per card) | `ts_quotations → timeline_days WHERE status IN pending statuses AND change_to_prices > 0` |
| `BUCKET_DATA.co.cards[i].budgetAmount` | Budget amount | `"₹0.3 Cr"` | Rupee exposure for this quotation (distributed proportionally across 59 cards to sum to ₹18.4 Cr, the co share of ₹38.6 Cr total) | `ts_quotations → change_to_prices WHERE status IN pending statuses AND timeline_days > 0 → converted to ₹ Cr` |

---

## Overview

The landing screen is the executive scan of the programme — six pillars across the top row, each with a Plan → Outlook sub-flow beneath. Reading order is intentional: where do I stand → am I over → am I late → what's open → what's coming → who do I call.

`ProcessMap` renders six wireframe-shape pillar nodes (severity rendered as a single blue scale — magnitude / position / explicit text, never RAG hue) and the active pillar's six sub-stages at ~60% size below it. The `DetailCard` underneath has three modes:

1. **Pillar overview** (default — when no sub-stage is selected). Renders `StageCard` for the active pillar — waterfall / gantt / funnels / KPI tiles + manager leaderboard. The header leads with `pillarImpact()` (consequence-first headline) and demotes the previous numeric headline to the subhead.
2. **Sub-stage detail** (when the user clicks a sub-pillar shape). Renders `SubStageCard`, driven by `subStageDetail(stageId, subId, data)`. Layout: impact headline · numeric subhead · 3-cell evidence strip · single story visual.
3. **Single item drill-in** (`ItemCard`, unchanged) — EW / CE / Quote with linkage chain + timeline.

The Overview pillar is the navigational backbone — five of its sub-stages drill into one of the other five pillars when clicked. Overview's first sub-stage (`overview__scope`) renders its own sub-stage card.

The `LandingPrompt` chat input is pinned at the bottom of the screen.

---

## Sub-stage card contract

Every `SubStageDetail` returned by `subStageDetail()` MUST carry these four fields. Headlines are written terse, consequence-first, slightly dry — the previous numeric headline is demoted to the subhead.

| Field | Voice | Required content |
|---|---|---|
| `impact` | consequence-first | Names a £-figure or day-count + who is exposed |
| `subhead` | clinical | The numeric line that explains the headline (data-grounded) |
| `evidence` | three rows | `label`/`value` pairs — the numbers behind the headline |
| `viz` | one chart | `'bars' \| 'funnel' \| 'target' \| 'note'` |

Vocabulary discipline — only words from the schema:
- table names: `ts4_contracts`, `ts4_early_warnings`, `ts4_nce`, `ts4_quotations`
- column names: `notification_date`, `notification_by`, `mitigation_measure`, `change_to_prices`, `change_to_days`, `quotation_decision`, `carried_to_cost_sheet`, `impact_carried_to_schedule`, `is_compensation_event`, `compensation_event_type`, `no_days_open`, `early_warning_closed`, `initial_contract_value`, `current_value`, `end_of_contract`, `contract_manager`
- status values: `Accepted`, `Accepted_No_CBS`, `Acc_with_Instruction`, `Submitted`, `Assessment`, `In_Review`, `Internal_Review`, `Revised_Quotation`, `Open`, `Closed`
- raisedBy values: `Contractor`, `PM`, `Other`
- NEC clause references: `Cl. 60.1(N)` (where N comes from `compensation_event_type`), `Cl. 62.3` (deemed acceptance window — derived in `pipelineFunnel.deemedRisk`)

Forbidden language (audit findings — these were used in earlier drafts and removed):
- "Risk Reduction Meeting" — not in the data; use "mitigation_measure populated on open EWs"
- "Cl. 61.4" — not in the data; Cl. 62.3 covers the same exposure
- "8-week awareness bar" — not measurable; use "EWs with notification_date populated"
- "Surprise NCEs" — invented; use "NCEs without an upstream EW"
- "Consultant-raised" — wrong enum; use the actual `raisedBy` values
- "P6 / master schedule feed" — invented system; use "programme master schedule (not yet wired)"
- "EAF commissioning" / "decarbonisation deadline" — context, not data; use "the programme"

---

## Pillar 01 — Overview

**Eyebrow:** `EXECUTIVE OVERVIEW`
**Headline (computed):** `<totalCommittedGBP> committed · <contractCount> contracts · <openEWs> open risks`
**Primary stat (computed):** `<totalCommittedGBP>` — `<contractCount> contracts · <projectsCount> projects`

### Sub-flow stages (top of the other 5 pillars; jumpTo navigation)

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `STAGE_DEFS[0].subStages[0]` | Portfolio | `Portfolio` | Programme footprint snapshot — contracts, areas, committed value | `ts4_contracts → COUNT(*)` + `SUM(initial_contract_value)` grouped by `project` |
| `STAGE_DEFS[0].subStages[1]` (jump → portfolio_health) | Forecast | `Forecast` | Initial → current commitment with forecast at completion | `ts4_contracts → SUM(initial_contract_value)` vs current commitment |
| `STAGE_DEFS[0].subStages[2]` (jump → schedule) | Completion | `Completion` | Days of schedule impact across CEs; contracts past original end date | `ts4_quotations → SUM(change_to_days)` |
| `STAGE_DEFS[0].subStages[3]` (jump → open_exposure) | Risk Register | `Risk Register` | Open EWs, average age, mitigation coverage | `ts4_early_warnings WHERE status='Open'` |
| `STAGE_DEFS[0].subStages[4]` (jump → pipeline_pressure) | Variations | `Variations` | Unimplemented variations + active quotations awaiting PM decision | `ts4_quotations WHERE status IN ('Submitted','Assessment','In_Review','Internal_Review','Revised_Quotation')` |
| `STAGE_DEFS[0].subStages[5]` (jump → accountability) | Compliance | `Compliance` | Composite KPI RAG — response rate, age, linkage, time-bar exposure | derived from `ts4_*` cross-table joins |

### Right column — pillar RAG tiles

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `programmeScope.contractsByProject[*].count` | Contracts by area | `Meltshop 16 / Pickleline 5 / Programme 2 / HRP 1` | Concentration of the portfolio across project areas | `ts4_contracts → COUNT(*) GROUP BY project` |
| `pillarRag.portfolio_health.value` | Commitment value | `<eac>` | Forecast at completion £-figure for the whole portfolio | computed: baseline + booked + locked + pipeline + latent |
| `pillarRag.portfolio_health.tone` | Commitment RAG | `red` if overrun ≥ 10%, `amber` if ≥ 4%, else `blue` | Whether the £-headline is drifting | as above |
| `pillarRag.schedule.value` | Schedule value | `+<totalDaysRequested>d` | Total days of schedule impact requested across CEs | `ts4_quotations → SUM(change_to_days)` |
| `pillarRag.schedule.tone` | Schedule RAG | `red` if > 1500d else `amber` | Severity of schedule pressure | same |
| `pillarRag.open_exposure.value` | Early Warnings value | `<openEWs>` | Open warnings still live | `ts4_early_warnings → COUNT(*) WHERE status='Open'` |
| `pillarRag.open_exposure.tone` | Early Warnings RAG | `red` if mitigated coverage < 10% else `amber` | Whether risks are being addressed | mitigation column populated rate |
| `pillarRag.pipeline.value` | Pipeline value | `<inReviewNet>` | £ of net price-up sitting in active (in-review) quotations | `ts4_quotations → SUM(change_to_prices) WHERE status IN review-set` |
| `pillarRag.pipeline.tone` | Pipeline RAG | `red` if deemedRisk > 50, `amber` if > 10 | Auto-acceptance pressure | derived from quotations approaching Cl. 62.3 PM clock |
| `pillarRag.accountability.value` | Accountability value | `<reds>/5` | KPIs in red on the 5-tile process scorecard | rolled up from 5 KPIs |
| `pillarRag.accountability.tone` | Accountability RAG | `red` if reds ≥ 2, `amber` if ≥ 1 | Whether process compliance is breaking | same |

---

## Pillar 02 — Commitment

**Eyebrow:** `MONEY · BASELINE vs FORECAST`
**Headline (computed):** `+<visibleExposure> visible exposure · <visiblePct>% over baseline`
**Primary stat (computed):** `<eac>` — `forecast at completion · <overrunPct> vs baseline`

### Sub-flow stages

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `STAGE_DEFS[1].subStages[0]` | Baseline | `Baseline` | Initial commitment across all contracts — board-approved number | `ts4_contracts → SUM(initial_contract_value)` |
| `STAGE_DEFS[1].subStages[1]` | Implemented | `Implemented` | Implemented variations already absorbed into the master record (Excel: "Implemented Variations (£)") | (current value − initial value); current value derived from latest accepted quotations carried to cost sheet |
| `STAGE_DEFS[1].subStages[2]` | Unimplemented | `Unimplemented` | Unimplemented variations — agreed but not yet carried into the cost sheet (Excel: "Unimplemented Variations (£)") | `ts4_quotations WHERE quotation_decision LIKE '%accept%' AND carried_to_cost_sheet IS NULL/false` |
| `STAGE_DEFS[1].subStages[3]` | Active Quotes | `Active Quotes` | Active quotations awaiting decision | `ts4_quotations WHERE status IN ('Submitted','Assessment','In_Review','Internal_Review','Revised_Quotation')` |
| `STAGE_DEFS[1].subStages[4]` | Open EW Exposure | `Open EW Exposure` | Open cost-bearing EWs likely to escalate into CEs | `ts4_early_warnings WHERE status='Open' AND category IN ('Cost Increase','Change to Scope of Work','Site Conditions - %')` |
| `STAGE_DEFS[1].subStages[5]` | Forecast | `Forecast` | Estimate at completion — projected final cost vs baseline | sum of all five preceding layers |

### Left column — waterfall

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `portfolioWaterfall.baseline` | Baseline | `<initialSum>` | Total committed value at contract award | `ts4_contracts → SUM(initial_contract_value)` |
| `portfolioWaterfall.bookedDrift` | Booked drift | `<currentSum − initialSum>` | Approved variations already in the master record | derived |
| `portfolioWaterfall.lockedUnbooked` | Locked but unbooked | `<sum>` | Accepted quotations not yet carried to cost sheet | `ts4_quotations WHERE quotation_decision LIKE '%accept%' AND carried_to_cost_sheet IS NULL` → `SUM(change_to_prices)` |
| `portfolioWaterfall.pipelineNet` | In pipeline (net) | `<sum>` | Net price-up of active in-review quotations | `ts4_quotations WHERE status IN review-set → SUM(change_to_prices)` |
| `portfolioWaterfall.latentRange` | Latent in risk | `<rough>` | Order-of-magnitude estimate from cost-bearing open EWs (£50k unit estimate) | EW count × £50k unit estimate — needs proper £ on `ts4_early_warnings` |
| `portfolioWaterfall.eac` | Forecast at completion | `<sum of above>` | Projected final cost | derived |
| `portfolioWaterfall.overrunPct` | Overrun % | `<eac/baseline−1>` | % over the baseline | derived |
| `portfolioWaterfall.visibleExposure` | Visible exposure | `<bookedDrift + lockedUnbooked>` | The £-figure most exec packs see today | derived |

### Right column — booked drift by area

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `byArea[*].project` | Project area | `Meltshop / Pickleline / HRP / Programme` | Project area driving the drift | `ts4_contracts → DISTINCT project` |
| `byArea[*].v` | Drift £ in area | `<sum>` | Booked drift £ within that area (current − initial) | `ts4_contracts GROUP BY project → SUM(current_value − initial_contract_value)` |

---

## Pillar 03 — Schedule Position

**Eyebrow:** `TIME · PLAN vs FORECAST DATE`
**Headline (computed):** `<totalDaysRequested> days requested · <contractsAtRisk> contracts carrying slip`
**Primary stat (computed):** `+<totalDaysRequested>d` — `booked <bookedSlipDays>d · pipeline <pipelineSlipDays>d`

### Sub-flow stages

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `STAGE_DEFS[2].subStages[0]` | Completion Date | `Completion Date` | Original completion dates per contract — programme baseline | `ts4_contracts.end_of_contract` |
| `STAGE_DEFS[2].subStages[1]` | Accepted Days | `Accepted Days` | Days agreed via accepted CEs | `ts4_quotations WHERE quotation_decision LIKE '%accept%' → SUM(change_to_days)` |
| `STAGE_DEFS[2].subStages[2]` | Pending Days | `Pending Days` | Days requested in active quotations not yet decided | `ts4_quotations WHERE status IN review-set → SUM(change_to_days)` |
| `STAGE_DEFS[2].subStages[3]` | Past End Date | `Past End Date` | Contracts whose `end_of_contract` is already in the past | `ts4_contracts WHERE end_of_contract < CURRENT_DATE` (the sub-stage detail also surfaces `s.contractsAtRisk` — contracts carrying any accepted+pending day requests — as a secondary number) |
| `STAGE_DEFS[2].subStages[4]` | Critical Path | `Critical Path` | Programme-level days lost — what the exec actually feels | not yet wired — needs programme master schedule integration |
| `STAGE_DEFS[2].subStages[5]` | Forecast | `Forecast` | Projected programme end date with confidence band | not yet wired |

### Left column — top contracts by days slipped

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `schedulePosition.perContract[*].contractor` | Contractor | `<vendor>` | Vendor on the contract | `ts4_contracts.vendor` |
| `schedulePosition.perContract[*].booked` | Booked slip days | `<sum>` | Days locked in via accepted CEs | `ts4_quotations WHERE accepted → SUM(change_to_days) GROUP BY contract_number` |
| `schedulePosition.perContract[*].pipeline` | Pipeline slip days | `<sum>` | Days requested in pending quotations | `ts4_quotations WHERE in-review → SUM(change_to_days) GROUP BY contract_number` |
| `schedulePosition.perContract[*].total` | Total slip days | `<booked + pipeline>` | Combined exposure | derived |

### Right column — schedule totals

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `schedulePosition.totalDaysRequested` | Days requested across CEs | `+3,396d` | Sum of every `change_to_days` value | `ts4_quotations → SUM(change_to_days)` |
| `schedulePosition.bookedSlipDays` | Booked slip (accepted) | `<sum>` | Slip already carried | `ts4_quotations WHERE accepted → SUM(change_to_days)` |
| `schedulePosition.pipelineSlipDays` | Pipeline slip (in review) | `<sum>` | Slip in motion | `ts4_quotations WHERE in-review → SUM(change_to_days)` |
| `schedulePosition.contractsAtRisk` | Contracts carrying slip | `<count>` | How many contracts are non-zero on slip | derived |

---

## Pillar 04 — Early Warnings

**Eyebrow:** `RISK · RAISED → REALISED`
**Headline (computed):** `<open> open · <avgEWAge>d avg age · <mitigatedPct> mitigated`
**Primary stat (computed):** `<unmitigated>` — `open EWs without mitigation · <pct of open>`

### Sub-flow stages

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `STAGE_DEFS[3].subStages[0]` | Raised | `Raised` | EWs raised programme-to-date by role | `ts4_early_warnings → COUNT(*) GROUP BY notification_by` |
| `STAGE_DEFS[3].subStages[1]` | Open | `Open` | Still live (vs closed / submitted) | `ts4_early_warnings WHERE status='Open'` |
| `STAGE_DEFS[3].subStages[2]` | Aging | `Aging` | Open EWs bucketed against the PDF p.16 "Overdue EWs" KPI bands (0-5 / 6-15 / >15 days) | `ts4_early_warnings WHERE status='Open'` bucketed by `no_days_open` |
| `STAGE_DEFS[3].subStages[3]` | Unmitigated | `Unmitigated` | Open EWs with no mitigation populated | `ts4_early_warnings WHERE status='Open' AND (mitigation_measure IS NULL OR TRIM = '')` |
| `STAGE_DEFS[3].subStages[4]` | Category | `Category` | Site Conditions / Programme / Design Maturity dominate | `ts4_early_warnings → COUNT(*) GROUP BY category` |
| `STAGE_DEFS[3].subStages[5]` | Became CEs | `Became CEs` | EWs that translated into NCEs — linkage rate | `ts4_nce ← ts4_early_warnings.nce_number` linkage % |

### Left column — exposure funnel

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `exposureFunnel.raised` | Raised | `884` | All EWs since programme start | `ts4_early_warnings → COUNT(*)` |
| `exposureFunnel.open` | Open | `519` | Still live | `ts4_early_warnings WHERE status='Open'` |
| `exposureFunnel.unmitigated` | Unmitigated | `<open − mitigated>` | Open EWs with no mitigation | `ts4_early_warnings WHERE status='Open' AND mitigation_measure NULL/blank` |
| `exposureFunnel.realisedNCEs` | Realised as NCE | `590` | NCEs in the system | `ts4_nce → COUNT(*)` |
| `exposureFunnel.mitigatedPct` | Mitigation coverage | `<pct>` | Open EWs with a mitigation populated | derived |
| `exposureFunnel.linkageRate` | EW-CE linkage | `<pct>` | Audit-trail strength | `ts4_nce WHERE record_number IN ts4_early_warnings.nce_number` / `COUNT(ts4_nce)` |

### Right column — top categories + top contractors by open

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `categories[*].category` | Category | `Site Conditions - Other / Programme / Design Maturity / Change to Scope of Work / Error by Contractor` | EW category | `ts4_early_warnings.category` |
| `categories[*].count` | Count in category | `304 / 172 / 118 / 105 / 81` | Number of EWs in that bucket | `ts4_early_warnings → COUNT(*) GROUP BY category` |
| `exposureFunnel.topContractorsByOpen[*].name` | Contractor | `<vendor>` | Vendor with open EWs | `ts4_early_warnings.contractor` |
| `exposureFunnel.topContractorsByOpen[*].open` | Open EWs | `<count>` | Open count for that contractor | `ts4_early_warnings WHERE status='Open' GROUP BY contractor → COUNT(*)` |
| `exposureFunnel.topContractorsByOpen[*].mitigated` | Mitigated count | `<count>` | Open EWs with a mitigation populated | same with `mitigation_measure NOT NULL/blank` |
| `exposureFunnel.topContractorsByOpen[*].coverage` | Mitigation coverage % | `<pct>` | mitigated / open | derived |

---

## Pillar 05 — Compensation Events

**Eyebrow:** `COMPENSATION EVENTS · NOTIFIED → IMPLEMENTED`
**Headline (computed):** `<notified> CEs notified · <inReviewNet> in active quotations`
**Primary stat (computed):** `<inReviewNet>` — `<inReview> active quotations awaiting decision`

### Sub-flow stages

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `STAGE_DEFS[4].subStages[0]` | Notified | `Notified` | NCE records — including 60.1(1) "instruction to change Scope" | `ts4_nce → COUNT(*)` |
| `STAGE_DEFS[4].subStages[1]` | Validated | `Validated` | Of those notified, how many were accepted as valid CEs | `ts4_nce WHERE is_compensation_event LIKE '%yes%'` |
| `STAGE_DEFS[4].subStages[2]` | Quoted | `Quoted` | Quotation records and net price-up requested | `ts4_quotations → COUNT(*)` and `SUM(change_to_prices)` |
| `STAGE_DEFS[4].subStages[3]` | In Review | `In Review` | Submitted + Assessment + Revised — money in motion | `ts4_quotations WHERE status IN review-set` |
| `STAGE_DEFS[4].subStages[4]` | Deemed Acceptance | `Deemed Acceptance` | Cl. 62.3 reply window — quotations past or approaching the PM reply bar (21-day proxy collapses the PDF p.11 "2 weeks + Contractor notice + 2 more weeks" mechanism into one bar) | derived: `quotation_date < CURRENT_DATE − 21 AND quotation_decision IS NULL` |
| `STAGE_DEFS[4].subStages[5]` | Implemented | `Implemented` | Accepted CEs now hitting master record | `ts4_quotations WHERE quotation_decision LIKE '%accept%'` |

### Left column — pipeline funnel

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `pipelineFunnel.notified` | CEs notified | `590` | NCE records | `ts4_nce → COUNT(*)` |
| `pipelineFunnel.validated` | Validated as CE | `<count>` | NCEs PM accepted as valid CEs | `ts4_nce WHERE is_compensation_event positive` |
| `pipelineFunnel.quotedTotal` | Quoted | `1151` | Quotation records | `ts4_quotations → COUNT(*)` |
| `pipelineFunnel.quotedNet` | Net price-up | `~£31.5M` | Net of all changes (positives + negatives) | `ts4_quotations → SUM(change_to_prices)` |
| `pipelineFunnel.savings` | Savings | `−£4.2M` | Negative changes | `ts4_quotations → SUM(change_to_prices) WHERE change_to_prices < 0` |
| `pipelineFunnel.inReview` | In review | `<count>` | Active quotations awaiting decision | `ts4_quotations WHERE status IN review-set` |
| `pipelineFunnel.inReviewNet` | In-review net £ | `<sum>` | £ in motion | `ts4_quotations WHERE status IN review-set → SUM(change_to_prices)` |
| `pipelineFunnel.deemedRisk` | Approaching deemed | `<count>` | Items past the Cl. 62.3 reply window (21-day proxy) | derived from KPI helper `approachingDeemedCount` |
| `pipelineFunnel.deemedRiskExposure` | Deemed exposure | `<sum>` | £ in that window | derived from KPI helper `approachingDeemedExposureGBP` |
| `pipelineFunnel.implemented` | Implemented | `<count>` | Accepted CEs | `ts4_quotations WHERE quotation_decision LIKE '%accept%'` |

### Right column — top clauses + deemed-acceptance window

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `topClauses[*].clause` | Cl. 60.1 sub-clause | `60.1(1) / 60.1(3) / 60.1(7) / 60.1(2) / 60.1(12)` | NEC4 sub-clause driving the change | parse leading `60.1(N)` from `ts4_nce.compensation_event_type` |
| `topClauses[*].count` | Count | `284 / 123 / 67 / 31 / 22` | NCE count under that clause | `ts4_nce → COUNT(*) GROUP BY parsed_clause` |
| `topClauses[*].share` | Share % | `<pct>` | Share of all NCEs | derived |
| `pipelineFunnel.deemedRisk` | Items past the Cl. 62.3 reply window | `<count>` | Pre-emptive auto-acceptance signal (21-day proxy) | derived |
| `pipelineFunnel.deemedRiskExposure` | £ in deemed window | `<sum>` | £ value at risk of auto-acceptance | derived |

---

## Pillar 06 — Accountability

**Eyebrow:** `PROCESS · NOTIFY → OWN`
**Headline (computed):** `<reds> of 5 KPIs Red — process cracks visible`
**Primary stat (computed):** `<reds>/5` — `KPIs in red — process compliance`

### Sub-flow stages

| Property path | Label | Dummy value | Business description | DB source hint |
|---|---|---|---|---|
| `STAGE_DEFS[5].subStages[0]` | Notify in Time | `Notify in Time` | EWs with notification_date populated — proxy for raising discipline (no first-aware date is available, so the 8-week bar is not directly measurable) | `ts4_early_warnings → COUNT(notification_date NOT NULL) / COUNT(*)` (proxy) |
| `STAGE_DEFS[5].subStages[1]` | PM Response | `PM Response` | PM response to EWs and CEs within 1 week — target ≥ 95% (PDF p.16 KPI: "PM Response Rate (1 Week) — Target ≥ 95%") | `ts4_nce → COUNT(reply_date ≤ notification_date+7) / COUNT(*)` |
| `STAGE_DEFS[5].subStages[2]` | Risk Meeting | `Risk Meeting` | Risk Reduction Meeting cadence (PDF Page 5) — proxy via mitigation_measure populated on open EWs (target ≥ 90%) | proxy: `ts4_early_warnings WHERE status='Open' AND mitigation_measure NOT NULL/blank → COUNT(*) / open_count` |
| `STAGE_DEFS[5].subStages[3]` | Linkage | `Linkage` | EW-CE linkage rate vs target ≥ 90% — the audit trail strength (PDF "Maintain Traceability") | `ts4_nce WHERE record_number IN ts4_early_warnings.nce_number` / `COUNT(ts4_nce)` |
| `STAGE_DEFS[5].subStages[4]` | EW Age | `EW Age` | Average days from notify → close on EWs (target ≤ 14d — PDF p.16 KPI: "Average EW Age — Target ≤ 14 Days") | `AVG(early_warning_closed − notification_date)` for closed EWs |
| `STAGE_DEFS[5].subStages[5]` | Contract Manager | `Contract Manager` | Each Contract Manager's portfolio rolled up (Excel column name in all three files) | `ts4_contracts → COUNT(*) GROUP BY contract_manager` |

### Left column — 5 KPI tiles

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `accountabilityKPIs[0].id` | `notify` | `notify` | Notify in Time KPI id | n/a |
| `accountabilityKPIs[0].value` | Notify in Time rate | `<pct>` | Share of EWs with notification_date populated (proxy — no first-aware date in schema) | `ts4_early_warnings → COUNT(notification_date NOT NULL) / COUNT(*)` |
| `accountabilityKPIs[0].targetDisplay` | Notify target | `100% with notification_date` | Target text | static |
| `accountabilityKPIs[1].value` | PM Response rate | `<pct>` | CEs replied to within 1 week | `ts4_nce.reply_date − notification_date ≤ 7d` |
| `accountabilityKPIs[1].targetDisplay` | PM Response target | `≥ 95% in 1wk` | PDF p.16 KPI: PM Response Rate (1 Week) | static |
| `accountabilityKPIs[2].value` | Risk Meeting rate | `<pct>` | Mitigation coverage as proxy for Risk Meeting cadence (no meetings table in schema) | `ts4_early_warnings WHERE status='Open' AND mitigation_measure NOT NULL/blank → / open_count` |
| `accountabilityKPIs[2].targetDisplay` | Risk Meeting target | `≥ 90% mitigation cover` | PDF page 16 | static |
| `accountabilityKPIs[3].value` | Linkage rate | `<pct>` | EW-CE linkage strength | derived |
| `accountabilityKPIs[3].targetDisplay` | Linkage target | `≥ 90% EW-CE` | PDF page 16 | static |
| `accountabilityKPIs[4].value` | EW Age | `<days>` | Average days from notify → close on closed EWs (PDF p.16 KPI: Average EW Age) | `AVG(early_warning_closed − notification_date)` |
| `accountabilityKPIs[4].targetDisplay` | EW Age target | `≤ 14d to close` | PDF p.16 "Average EW Age — Target ≤ 14 Days" | static |
| `accountabilityKPIs[*].health` | Tile RAG | `blue / amber / red` | classifyHealth() against target | derived |

### Right column — Contract Manager portfolio leaderboard

> Replaces the prior PM × KPI heatmap. The heatmap had two columns (Notify, Meet) that mapped to fields the schema doesn't really expose — `Notify` was proxied to "EW has any `notification_date`" (≈always true) and `Meet` was proxied to "Open EWs with mitigation text populated", neither of which actually measure the 8-week awareness bar or Risk Reduction Meeting attendance. The leaderboard drops the fakes and shows only fields that come straight out of `ts_*`.

| Property path | Label | Dummy value (live) | Business description | DB source hint |
|---|---|---|---|---|
| `managerLeaderboard[*].pmName` | Manager name | `Richard Chapman / Dave Murray / Gavin Thomas / Lee Puckey / ...` | Contract Manager owning the row | `ts_contracts.contract_manager` |
| `managerLeaderboard[*].contracts` | N (portfolio size) | `8 / 3 / 3 / 2 / 1 ...` | Number of contracts that manager owns | `ts_contracts → COUNT(*) GROUP BY contract_manager` |
| `managerLeaderboard[*].openEWs` | Open EWs | `<count>` | Live early warnings sitting open across that manager's contracts | `ts_early_warnings WHERE status='Open'` joined to that manager's contracts |
| `managerLeaderboard[*].valueAtRiskGBP` | £ at risk | `£0 – £4M+` | Sum of pending quotation `change_to_prices` on that manager's portfolio — money committed to a change that hasn't been formally accepted yet | `ts_quotations → SUM(change_to_prices) WHERE is_active=TRUE AND status IN ('Submitted','Assessment','In_Review','Internal_Review','Revised_Quotation')` joined to that manager's contracts |
| `managerLeaderboard[*].avgCloseDays` | Avg close days | `<days>` or `—` | Average days from EW notification to close on that manager's closed EWs; blank if none closed | `AVG(early_warning_closed − notification_date) WHERE status='Closed'` for that manager's contracts |
| `managerLeaderboard[*].ackRate` | (computed, not shown) | `0.21` | NCE PM acknowledgement rate — kept for future column use | `ts_nce → reply_date − notification_date ≤ 7d` for that manager |
| `managerLeaderboard[*].riskScore` | Risk score (sort key) | `0 – 50+` | Composite ranking signal: `valueAtRisk/100k + openEWs + max(0, avgClose − 14) × 0.5` | derived |
| `managerLeaderboard[*].tone` | Row emphasis | `red/amber/blue` | Visual emphasis only — `red` if openEWs ≥ 5 OR valueAtRisk ≥ £1M, `amber` if openEWs ≥ 2 OR ≥ £200k, else `blue`. All rendered in the single blue scale via opacity. | derived |

---

## Notes on data drift between spec and live dataset

A few small numbers in the original spec drift from `normalized.json` (and therefore from `ts4_*` once live):

| Spec value | Live value | Note |
|---|---|---|
| Meltshop = 18 contracts | 16 | spec was outdated on area split |
| Net price-up = £32.6M | £31.5M | likely EUR→GBP rate difference (`Money` uses 0.85 for EUR) |
| Consultants raised = 413 EWs | `Other` raised = 440 | spec used `Consultants/Contractors/PM`; live taxonomy is `Contractor/PM/Other` |

---

## Autoplay Story Cards

The four parent cards (`STORY_CARDS`) plus the aggregate `OVERVIEW_STORY_CARD` are defined in `frontend/src/components/landing/story/landingStoryCards.ts`. Every numeric and text value below is computed at module load from `normalized.json` — no hardcoded strings. The DB source hints describe the equivalent SQL against the live `ts4_*` tables.

### Number formatting contract

| Formatter | When | Example |
|---|---|---|
| `fmtCurrency(n)` | All £ amounts on the landing screen | `£320.83M`, `£13.77M`, `£758k`, `£0` |
| `fmtCurrencySigned(n)` | Variance / drift values where the sign carries meaning | `+£7.29M`, `−£1.02M` |
| `fmtCount(n)` | Large integer counts (NCEs, EW bin counts) | `590`, `1.5k`, `12.3k` |

`fmtCurrency` is the canonical formatter used by both the story cards and the process-map detail/sub-stage views. The previous `fmtAbsM` (0dp) and `fmtSignedM` (1dp) helpers in `derived.ts` are gone — same number now reads identically wherever it appears.

### Card 01 — Projects

**Headline copy:** computed — `${OVER.contracts} contracts are over budget across ${OVER.projects} projects. Nobody has raised it.` (singular form when `OVER.projects === 1`)

#### Leakage Panel

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `STORY_CARDS.projects.leakage.from.value` | Planned | `£320.83M` | Sum of original contract awards across the four packages — the budget the executive committed when contracts were signed | `ts4_contracts → SUM(initial_contract_value)` |
| `STORY_CARDS.projects.leakage.to.value` | Off-plan | `£31.54M` | Total monetary value of every active variation raised against those contracts — money already in the change pipeline | `ts4_quotations → SUM(change_to_prices) WHERE change_to_prices IS NOT NULL` |
| `STORY_CARDS.projects.leakage.footer` | (footer text) | `Across 4 projects · up to 20 months in` | How many packages and how long the oldest one has been running | `ts4_contracts → COUNT(DISTINCT project), MAX((CURRENT_DATE − MIN(start_of_contract)) / 30.44)` |

#### Visualisation — scatter (Budget By Project)

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `STORY_CARDS.projects.viz.points[i].label` | Project name | `Meltshop`, `Pickleline`, `HRP`, `Programme Cost Management` | The package this row represents | `ts4_contracts.project` (DISTINCT) |
| `STORY_CARDS.projects.viz.points[i].budget` | Budget chip | `£250.69M`, `£52.78M`, `£16.05M`, `£1.31M` | Sum of original awards for all contracts in the package | `ts4_contracts → SUM(initial_contract_value) GROUP BY project` |
| `STORY_CARDS.projects.viz.points[i].xMonths` | Months in | `19`, `13`, `9`, `20` | How long the earliest contract in the package has been live | `ts4_contracts → (CURRENT_DATE − MIN(start_of_contract)) / 30.44 GROUP BY project` |
| `STORY_CARDS.projects.viz.points[i].yBudgetM` | Budget axis | budget / 1e6 | Same as `budget`, expressed in £M for the bar height calc | derived from `budget` |
| `STORY_CARDS.projects.viz.points[i].sizePct` | Bar width % | budget relative to largest project | Visual weight only — not a business number | derived from `budget` |

### Card 02 — Contracts

**Headline copy:** computed — `${pendingCount} active quotations are sitting in review.`

#### Leakage Panel

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `STORY_CARDS.contracts.leakage.from.value` | At risk | `£31.54M` | Same as Card 01's "Off-plan" — total value of all active variations | `ts4_quotations → SUM(change_to_prices) WHERE change_to_prices IS NOT NULL` |
| `STORY_CARDS.contracts.leakage.to.value` | Still Pending | `£13.77M` | Slice of "At risk" still awaiting a contractual decision (any review-flow status) | `ts4_quotations → SUM(change_to_prices) WHERE status IN ('Submitted','Assessment','In_Review','Internal_Review','Revised_Quotation')` |
| `STORY_CARDS.contracts.leakage.footer` | (footer text) | `Across 15 vendors · active quotations` | How many distinct contractors have at least one variation in the pipeline | `ts4_quotations → COUNT(DISTINCT contractor) WHERE change_to_prices IS NOT NULL` |

#### Visualisation — waterfall (Where the £X.XX M Sits)

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `STORY_CARDS.contracts.viz.total.value` | Total exposure | `£31.54M` | Same as the leakage `from.value` — anchors the waterfall total | `ts4_quotations → SUM(change_to_prices) WHERE change_to_prices IS NOT NULL` |
| `STORY_CARDS.contracts.viz.steps[Pending NCEs].value` | Pending NCEs | `£13.77M` | Variations still inside the contractual reply window | `SUM WHERE status IN ('Submitted','Assessment','In_Review','Internal_Review','Revised_Quotation')` |
| `STORY_CARDS.contracts.viz.steps[No-status items].value` | No-status items | `£8.78M` | Variations with non-zero £ impact but no status set — reporting gap, not a decision | `SUM WHERE status IS NULL OR TRIM(status) = ''` |
| `STORY_CARDS.contracts.viz.steps[Accepted variations].value` | Accepted variations | `£8.82M` | Variations the PM has formally approved | `SUM WHERE status IN ('Accepted','Accepted_No_CBS','Acc_with_Instruction')` |
| `STORY_CARDS.contracts.viz.steps[Revised quotations].value` | Revised quotations | `£3.27M` | Quotations the contractor has resubmitted after pushback (subset of the review flow) | `SUM WHERE status = 'Revised_Quotation'` |
| `STORY_CARDS.contracts.viz.steps[Terminated].value` | Terminated | `£157k` | Variations formally killed by contract termination | `SUM WHERE status ILIKE '%terminat%'` |
| `STORY_CARDS.contracts.viz.steps[i].widthPct` | Bar width | step / max(steps) × 100 | Visual proportion within the waterfall | derived from `value` |

### Card 03 — Early Warnings

**Headline copy:** computed — `${EW.openCount} warnings are still open. The oldest is ${STALLED_MONTHS} months old.` (falls back to "X days old" when oldest < 1 month)

#### Leakage Panel

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `STORY_CARDS.ew.leakage.from.value` | Flagged | `£13.77M` | Same as Card 02's "Still Pending" — money in the review flow that the EWs are flagging | `SUM WHERE status IN review-flow statuses` |
| `STORY_CARDS.ew.leakage.to.value` | Stalled >90d | `£8.33M` | Slice of "Flagged" where the formal reply deadline has already passed | `ts4_quotations → SUM(change_to_prices) WHERE status IN review-flow AND quotation_due_date < CURRENT_DATE` |
| `STORY_CARDS.ew.leakage.footer` | (footer text) | `Oldest warning open 473 days` | Headline ageing figure for the oldest unresolved EW | `ts4_early_warnings → MAX(CURRENT_DATE − notification_date) WHERE status = 'Open'` |

#### Visualisation — histogram (Open Warnings By Age)

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `STORY_CARDS.ew.viz.bins[0].value` | 0–14d | `54` | Open EWs raised in the last 2 weeks | `COUNT(*) WHERE status='Open' AND CURRENT_DATE − notification_date < 14` |
| `STORY_CARDS.ew.viz.bins[1].value` | 14–30d | `0` | Open EWs aged 14–30 days | `COUNT(*) WHERE status='Open' AND age BETWEEN 14 AND 29` |
| `STORY_CARDS.ew.viz.bins[2].value` | 30–60d | `46` | Open EWs aged 30–60 days | `COUNT(*) WHERE status='Open' AND age BETWEEN 30 AND 59` |
| `STORY_CARDS.ew.viz.bins[3].value` | 60–90d | `50` | Open EWs aged 60–90 days | `COUNT(*) WHERE status='Open' AND age BETWEEN 60 AND 89` |
| `STORY_CARDS.ew.viz.bins[4].value` | 90–120d | `67` | Open EWs aged 90–120 days | `COUNT(*) WHERE status='Open' AND age BETWEEN 90 AND 119` |
| `STORY_CARDS.ew.viz.bins[5].value` | 120–180d | `103` | Open EWs aged 120–180 days | `COUNT(*) WHERE status='Open' AND age BETWEEN 120 AND 179` |
| `STORY_CARDS.ew.viz.bins[6].value` | 180d+ | `199` | Open EWs older than 6 months — the deepest unresolved exposure | `COUNT(*) WHERE status='Open' AND age >= 180` |
| `STORY_CARDS.ew.viz.bins[i].heightPct` | Bar height | bin / max(bins) × 100 | Visual proportion within the histogram | derived from `value` |
| `STORY_CARDS.ew.viz.bins[i].peak` | Peak flag | `true` on the tallest bin | Drives the "most critical bucket" tone in the chart | derived from `value` |

### Card 04 — NCEs

**Headline copy:** computed — `${NCE.awaitingReply} decisions are still waiting for a reply. ${fmtCurrency(QR.pendingNCEs)} sits behind them.` (or simplified when zero awaiting)

#### Leakage Panel

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `STORY_CARDS.nces.leakage.from.value` | Raised | `£31.54M` | Same as Card 01's "Off-plan" — every variation that has ever been raised | `ts4_quotations → SUM(change_to_prices) WHERE change_to_prices IS NOT NULL` |
| `STORY_CARDS.nces.leakage.to.value` | Accepted So Far | `£8.82M` | Variations the PM has approved — money the budget has actually absorbed | `SUM WHERE status IN ('Accepted','Accepted_No_CBS','Acc_with_Instruction')` |
| `STORY_CARDS.nces.leakage.footer` | (footer text) | `590 NCEs across 14 months` | Total NCEs and the date span between the earliest and latest notification | `ts4_nce → COUNT(*), (MAX(notification_date) − MIN(notification_date)) / 30.44` |

#### Visualisation — waterfall (Top Pending NCEs By Contractor)

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `STORY_CARDS.nces.viz.steps[i].label` | Contractor | top 6 of: Tenova, ASL NG Prep, Churngold, Knights Brown, ASL P Field, Darlow Lloyd | Vendor with the largest pending-review variation total | `GROUP BY contractor` |
| `STORY_CARDS.nces.viz.steps[i].value` | Pending £ | `£4.29M`, `£3.19M`, `£2.06M`, `£1.03M`, `£1.02M`, `£758k` | Total review-flow variation value sitting against that contractor | `SUM(change_to_prices) WHERE status IN review-flow GROUP BY contractor ORDER BY SUM DESC LIMIT 6` |
| `STORY_CARDS.nces.viz.steps[i].widthPct` | Bar width | step / max(top) × 100 | Visual proportion against the largest pending vendor | derived from `value` |

### Overview Card — Pipeline At A Glance

**Headline copy:** computed — `${fmtCurrency(QR.pendingNCEs)} is waiting for a decision. ${fmtCurrency(QR.stalledPastDue)} has been waiting past its due date.`

#### Leakage Panel

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `OVERVIEW_STORY_CARD.leakage.from.value` | Planned | `£320.83M` | Same as Card 01's planned figure | `ts4_contracts → SUM(initial_contract_value)` |
| `OVERVIEW_STORY_CARD.leakage.to.value` | Stalled past due | `£8.33M` | Same as Card 03's stalled figure | `SUM(change_to_prices) WHERE status IN review-flow AND quotation_due_date < CURRENT_DATE` |

#### Visualisation — funnel (Leakage Along The Decision Pipeline)

| Property path | Label | Live value | Business description | DB source hint |
|---|---|---|---|---|
| `OVERVIEW_STORY_CARD.viz.steps[Planned].value` | Planned | `£320.83M` | Original committed budget | `ts4_contracts → SUM(initial_contract_value)` |
| `OVERVIEW_STORY_CARD.viz.steps[At risk].value` | At risk | `£31.54M` | Total active variation value | `ts4_quotations → SUM(change_to_prices) WHERE change_to_prices IS NOT NULL` |
| `OVERVIEW_STORY_CARD.viz.steps[Pending].value` | Pending | `£13.77M` | Variations in the review flow | `SUM WHERE status IN review-flow` |
| `OVERVIEW_STORY_CARD.viz.steps[Stalled past due].value` | Stalled past due | `£8.33M` | Pending variations whose reply deadline has passed | `SUM WHERE status IN review-flow AND quotation_due_date < CURRENT_DATE` |
| `OVERVIEW_STORY_CARD.viz.steps[Accepted].value` | Accepted | `£8.82M` | Variations formally approved by the PM | `SUM WHERE status IN ('Accepted','Accepted_No_CBS','Acc_with_Instruction')` |
| `OVERVIEW_STORY_CARD.viz.steps[i].widthPct` | Bar width | step / Planned × 100 | Visual proportion of every stage relative to the original commitment | derived from `value` |

### Q&A vs landing-screen alignment (open issue)

The autoplay cards now compute their values from `normalized.json` (the same JSON the process-map reads). The Q&A surface (NL2SQL agent) computes against the live `ts4_*` Postgres tables. Two issues remain that this audit did NOT fix:

1. **No `current_value` column.** `ts4_contracts` only stores `initial_contract_value`. The frontend's `currentValue` field is injected during ETL into `normalized.json`. Any Q&A asking for "implementation variance" therefore cannot reproduce the frontend's `bookedDrift = SUM(currentValue − initialValue)` figure (`£7.29M` against the JSON) — the Q&A will compute a different proxy (typically `SUM(change_to_prices) WHERE status='Accepted'`, ≈ `£4.17M`) and disagree.
2. **"Committed" is overloaded.** `kpis.totalCommittedGBP` (`£328.12M`) is the sum of `currentValue`. The story-card "Planned" figure (`£320.83M`) is the sum of `initialValue`. Same word, different definitions, different numbers. Decide which is canonical and align both surfaces.

Resolving either requires a backend change (add `current_value` to the schema, or anchor the agent prompt on a single canonical SQL for variance) — out of scope for the formatting/audit pass.

The pillar code uses live values, not spec values. Update this section if the schema or rate changes.
