# N.T.O.I.D. — method note **National Threat Observation and Intelligence Dashboards.** Country-level public boards that track how close one nation is to a serious security crisis. This is the national layer beside [WW3 Watch](../) (the global board). It is not a world-war scoreboard, it has no countdown, and it does not predict dates. Everything on a board is either a **fact** (something that happened, with a primary source), an **official claim** (what an authority says, attributed to that authority), or **analysis** (our reading, labelled as ours). Attribution of an incident is marked **open** until an authority confirms it. If a bullet cannot carry a named source and a URL, it is not on the board. ## 1. Indicator levels Each board carries about 8–12 **precursor indicators**. Each indicator has a level on a five-step scale: | Level | Name | Meaning | |---|---|---| | 0 | quiet | Nothing notable in the sources this period. | | 1 | low | Background activity; authorities describe it as routine or contained. | | 2 | elevated | Authorities flag a rising or persistent threat, or a confirmed incident with limited impact. | | 3 | high | Authorities describe a serious, present threat, or confirmed incidents with national-scale impact. | | 4 | severe | Authorities state a crisis is under way or imminent, or confirmed incidents have broken a national function. | Rules for setting a level: - The level follows **what the primary sources say**, not what we fear. An official threat assessment with a published scale (e.g. the Dutch DTN 1–5) maps onto this scale in the indicator's `level_basis` line so a reader can check the mapping. - An indicator exists only in a **domain where evidence exists**. Domains: `cyber-critical-infrastructure`, `hybrid-influence`, `military-territorial`, `energy-resources`, `economy-supply-chains`, `social-cohesion-unrest`, `diplomatic-alignment`, `legal-emergency-powers`. An empty domain is simply absent — we do not pad it. - Every indicator carries `level`, `previous_level`, `last_change` (date the level last changed), `moved_this_update`, and 2–4 sourced bullets. `moved_this_update` is true only when `level != previous_level`, and then `last_change` equals the board's update date. The validator enforces this, so an indicator cannot "move" quietly. - A first assessment has `previous_level: null` and does not count as movement. ## 2. Composite score (0–100) The composite is computed, never typed in: ``` domain_mean_d = weighted average of level_i over the indicators in domain d # 0..4 mean_part = average(domain_mean_d over the domains present) / 4 # 0..1 worst_part = max(level_i) / 4 # 0..1 composite = round(100 × (0.7 × mean_part + 0.3 × worst_part)) ``` - 70 % average, 30 % worst indicator, so one severe indicator cannot be averaged away and one loud indicator cannot carry the whole board. - The average is taken **per domain first, then across domains**, so a board with five cyber indicators and one of everything else is not a cyber board with decoration. A domain counts once however many indicators it carries. - `weight` is optional, default 1, allowed 0.5–2. Weights are set once per indicator and explained in `weight_basis`; they are not a tuning knob. - A board with no indicators has no composite and no watch level (`unrated`). That is what an empty template shows. ## 3. Watch level (V → I) | Watch level | Name | Composite band | Floor condition | |---|---|---|---| | V | quiet | 0–19 | — | | IV | guarded | 20–39 | — | | III | elevated | 40–59 | — | | II | high | 60–79 | at least one indicator at level 3 or 4 | | I | severe | 80–100 | at least one indicator at level 4 | The exact band numbers live in `schema/levels.json`; the validator and the dashboard read that file, and a test checks this table matches it. **Rules for when the watch level changes** 1. The computed level is the band the composite falls in, capped by the floor conditions (no indicator at 3 → at most III; no indicator at 4 → at most II). 2. The watch level **changes only in an update where at least one indicator level changed**. If the composite crosses a band boundary purely because an indicator was added, removed or re-weighted, the level is **held** for that update with `watch_level.hold: {reason}` and moves at the next update where an indicator moves. A hold lasts one update at most. 3. A change of watch level is always an **alert** (section 5) and always gets a line in the board's `changelog` saying which indicators moved. 4. The level never moves more than one step per update. If the computed level is two or more steps away, the board moves one step, records `hold` with the reason, and moves again at the next update. Big jumps are a sign of a re-scoring, and re-scorings are explained, not hidden. ## 4. Hazard band and story stage (optional) Two optional national-scale labels, set by the editor and sourced like everything else. They answer different questions and are never derived from the composite. **Hazard band** — how bad a break would be for this country, given its dependencies: `low`, `moderate`, `high`, `severe`, `extreme`. The `basis` text names the dependencies the sources describe (energy imports, digital providers, chokepoints, alliance obligations). **Story stage** — where the story actually is right now: | Stage | Meaning | |---|---| | quiet | No live confrontation; routine posture. | | pressure | Sustained below-threshold pressure (hybrid, cyber, economic) documented by authorities. | | confrontation | Open, acknowledged confrontation short of a break (sanctions, expulsions, sustained sabotage campaign, mobilisation steps). | | crisis | Authorities have declared a crisis or invoked emergency measures; essential services at risk. | | break | A national function has failed or the country is party to an armed conflict on its territory or by alliance obligation. | | recovery | Post-break stabilisation. | **Do not invent movement.** Both labels carry `since` (date) and `previous`. They change only when a sourced event moves them, and each change is an alert. ## 5. Alerts An update produces an alert only for: - an indicator whose `level` changed; - a change of hazard band or story stage; - a change of watch level (which, by rule 3.2, implies an indicator moved). The composite moving by a few points is **not** an alert. The dashboard's alert strip is computed from those three conditions and nothing else. ## 6. Sources and wording - Preferred: national coordinators and CERTs (NCTV, NCSC), defence and interior ministries, central banks, IAEA, IMO/UKMTO, courts and gazettes, Reuters/AP/AFP, national broadcasters. Social posts are tips to go and verify, never a source on the board. - Each bullet: `kind` (`fact` / `official-claim` / `analysis`), `attribution` (`confirmed` / `open` / `not-applicable`), and a `source` with `name`, `url`, `date`. - Tone: calm, adult, newspaper. What changed, why it matters, what a household might sensibly prepare — in the words of the public-preparedness authority of that country where one exists, with its URL. - Nothing private: no credentials, IPs, server paths, or internal infrastructure anywhere in this repository. `tests/test_hygiene.py` greps for it. ## 7. Time `updated_at` is an ISO-8601 timestamp with the Europe/Amsterdam offset (`+01:00` or `+02:00`). The dashboard prints it as "Last updated 8 November 2026, 10:30 (Europe/Amsterdam)". Dates inside bullets are the source's publication dates. ## 8. Files | Path | What | |---|---| | `schema/country-board.schema.json` | JSON Schema (2020-12) for one board. | | `schema/levels.json` | Indicator scale, watch-level bands, stage and hazard enums — the single source for validator and dashboard. | | `data/.json` | One board per country (ISO 3166-1 alpha-2, lower-case; `uk` is used instead of `gb` on purpose) or per region (`balt` = Estonia, Latvia, Lithuania, listed in `country.members`). `status: template` boards are empty by design. | | `tools/validate.py` | Dependency-free validator: structure, enums, URL on every bullet, movement consistency, composite and watch level recomputed and compared, no-secrets grep. | | `tools/build.py` | Validates every board, then inlines all boards, `schema/levels.json` and `schema/hosts.json` into `src/index.html` → `dist/index.html` (single self-contained hub file), copies `data/`, `schema/`, this note and the checklist into `dist/`, and writes one single-board docroot per country host under `dist/hosts//`. | | `CHECKLIST.md` | Daily update checklist. | | `tests/` | Plain-script tests; run each one, check rc. |