Skip to content

[Look-Ahead §5] scenario-forecast long-horizon-mode + forward-indicators multi-horizon decay#1593

Merged
pethers merged 9 commits into
mainfrom
copilot/update-scenario-forecast-long-horizon
May 1, 2026
Merged

[Look-Ahead §5] scenario-forecast long-horizon-mode + forward-indicators multi-horizon decay#1593
pethers merged 9 commits into
mainfrom
copilot/update-scenario-forecast-long-horizon

Conversation

Copilot AI commented May 1, 2026

Copy link
Copy Markdown
Contributor

scenario-forecast.md lacked any mechanism to enforce the deeper scenario set required for term/election-cycle runs; forward-indicators.md had no horizon-tagging contract, leaving indicators active beyond their predictive shelf-life.

Template changes

  • scenario-forecast.md — YAML frontmatter long-horizon-mode: false flag (moved to byte 0 so the aggregator strips it correctly) + new §0 section specifying the override contract for term-outlook and election-cycle article types:

    • ≥ 6 scenarios (replaces the 3–5 standard cap)
    • Mandatory EP-election outcome branches (centre-right / centre-left / fragmented)
    • Mandatory regime-change branch (unconditional in long-horizon mode)
    • ≥ 2 wildcard/black-swan branches
    • Per-scenario WEP bands drawn from forward-projection-methodology.md §3 (single source of truth — not duplicated here)
    • Anti-patterns table updated: "5+ scenarios" rule explicitly exempted in long-horizon mode
    • Activation signals section accurately describes enforcement: Stage-C gates on manifest.articleType, not registry fields
    • §0 wording clarifies that Stage C enforces the gate regardless of the frontmatter flag for enforced article types
  • forward-indicators.md — New §Multi-Horizon Decay Table mapping each indicator class to the horizons where it retains signal () vs. has decayed (), across T+7d / 30d / 90d / 12m / term-end / EP-election. Indicator card template gains a **Horizons:** field (8 structure fields total) so monitoring plans can retire indicators at their decay point. Canonical WEP numbers referenced to forward-projection-methodology.md §3, not duplicated.

Enforcement

reference-quality-thresholds.json — new structuralRequirements.longHorizonScenarioGate:

"longHorizonScenarioGate": {
  "articleTypes": ["term-outlook", "election-cycle"],
  "minScenarios": 6,
  "artifact": "intelligence/scenario-forecast.md"
}

validate-analysis-completeness.js — new functions wired into main():

  • getScenarioCountableContent(content) — strips everything after the ## … Worked example H2 so template examples don't satisfy the gate
  • countScenarios(content) — counts ### Scenario [A-Za-z0-9]+(?:-[A-Za-z0-9]+)*[:—] headings in the countable region; supports hyphenated IDs (e.g. A-24) while still excluding underscore-containing identifiers to prevent false positives
  • validateLongHorizonScenarioGate(runDir, rules) — emits hard-RED long-horizon-scenario-count:N<6 when the gate fires; throws long-horizon-scenario-gate:invalid-config when the config targets the current articleType but artifact/minScenarios are missing or invalid (no silent fallback defaults); includes path-traversal guard rejecting absolute paths and .. segments to prevent reading outside runDir
  • WEP_BAND_RE updated to include "About even" so artifacts using the canonical forward-projection-methodology.md §3 label are not incorrectly flagged with wep:missing

Prompt update

11-forward-projection.md §5 expanded with the full long-horizon requirements list, mandatory (unconditional) regime-change branch, and explicit cross-links to scenario-forecast.md §0 and forward-indicators.md §decay-table. No double source of truth: both templates point at forward-projection-methodology.md §3 for WEP bands.

… decay table to forward-indicators.md

- analysis/templates/scenario-forecast.md: Add YAML frontmatter `long-horizon-mode: false` flag,
  §0 Long-Horizon Mode section documenting ≥6 scenarios/EP-election branches/WEP bands,
  updated anti-patterns table (exempts long-horizon from 5+ cap), updated Stage-C signals,
  cross-references pointing at forward-projection-methodology.md §3 as single WEP source of truth
- analysis/templates/forward-indicators.md: Add §Multi-Horizon Decay Table showing decay at
  7d/30d/90d/12m/term-end/EP-election; per-indicator horizon tags in card template; quality
  checklist update; cross-references to forward-projection-methodology.md §3 and scenario-forecast.md §0
- analysis/methodologies/reference-quality-thresholds.json: Add longHorizonScenarioGate config
  in structuralRequirements (articleTypes: term-outlook/election-cycle, minScenarios: 6)
- scripts/validate-analysis-completeness.js: Add countScenarios() + validateLongHorizonScenarioGate()
  that reads longHorizonScenarioGate from thresholds JSON and enforces ≥6 scenario headings;
  buildRules() extended to carry longHorizonScenarioGate; gate called from main()
- .github/prompts/11-forward-projection.md: Expand §5 long-horizon hint with full requirements
  list; add cross-links to scenario-forecast.md §0 and forward-indicators.md §decay-table;
  §6 cross-references updated with new links; forward-projection-methodology.md §3 cited as
  canonical WEP source (no double source of truth)
- test/unit/validate-analysis-completeness.test.js: Add 6 new tests for the long-horizon
  scenario-count gate (RED on &lt;6 scenarios for term-outlook/election-cycle, GREEN on ≥6,
  no-op for breaking/absent-gate)

Agent-Logs-Url: https://github.com/Hack23/euparliamentmonitor/sessions/236bef8a-7870-4023-9dc8-01ae4b1e25cf

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
@github-actions github-actions Bot added documentation Documentation updates infrastructure CI/CD and build infrastructure testing Testing improvements test-coverage-impact Impacts test coverage needs-tests Needs test coverage scripts Scripts and automation labels May 1, 2026
…ON config fields in longHorizonScenarioGate

- countScenarios() regex: [\w]+ → [A-Za-z0-9]+ to avoid matching underscore-containing headings
- buildRules() longHorizonScenarioGate: strict field validation on artifact+minScenarios; a malformed config now returns null (no gate) rather than silently applying defaults that could drift from the documented requirements

Agent-Logs-Url: https://github.com/Hack23/euparliamentmonitor/sessions/236bef8a-7870-4023-9dc8-01ae4b1e25cf

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copilot AI changed the title [WIP] Update scenario-forecast.md for long-horizon mode [Look-Ahead §5] scenario-forecast long-horizon-mode + forward-indicators multi-horizon decay May 1, 2026
Copilot finished work on behalf of pethers May 1, 2026 14:33
Copilot AI requested a review from pethers May 1, 2026 14:33
@pethers pethers marked this pull request as ready for review May 1, 2026 14:34
Copilot AI review requested due to automatic review settings May 1, 2026 14:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds long-horizon enforcement and horizon-aware indicator retirement rules to the forward-projection analysis artifacts, tightening Stage-C completeness for term/election-cycle runs.

Changes:

  • Introduces a Stage-C “long-horizon scenario count” structural gate (≥6 scenarios) driven by reference-quality-thresholds.json.
  • Expands scenario-forecast.md with a long-horizon-mode contract and updates forward-indicators.md with a multi-horizon decay table + per-indicator horizon tagging.
  • Updates the forward-projection prompt and adds unit tests covering the new validator gate behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
scripts/validate-analysis-completeness.js Adds scenario heading counter + long-horizon scenario-count gate wired into Stage C.
analysis/methodologies/reference-quality-thresholds.json Adds structuralRequirements.longHorizonScenarioGate configuration.
test/unit/validate-analysis-completeness.test.js Adds unit tests for the new long-horizon scenario-count gate.
analysis/templates/scenario-forecast.md Adds long-horizon-mode specification and updated scenario requirements.
analysis/templates/forward-indicators.md Adds multi-horizon decay table and Horizons tagging contract for indicators.
.github/prompts/11-forward-projection.md Updates Stage-B prompt with long-horizon requirements and cross-links.

Comment on lines +740 to +751
// default them; a malformed config should not bypass the gate.
if (
typeof lhGateCfg.artifact === 'string' &&
lhGateCfg.artifact.length > 0 &&
typeof lhGateCfg.minScenarios === 'number' &&
lhGateCfg.minScenarios > 0
) {
longHorizonScenarioGate = {
artifact: lhGateCfg.artifact,
minScenarios: lhGateCfg.minScenarios,
};
}

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildRules() claims a malformed structuralRequirements.longHorizonScenarioGate “should not bypass the gate”, but if articleType is targeted and artifact/minScenarios are missing/invalid you currently just leave longHorizonScenarioGate = null, which silently disables enforcement. Consider turning this into a hard Stage-C failure (e.g., emit a RED issue like long-horizon-scenario-gate:invalid-config) when the config is present for the current articleType but invalid, and add a unit test for that case.

Suggested change
// default them; a malformed config should not bypass the gate.
if (
typeof lhGateCfg.artifact === 'string' &&
lhGateCfg.artifact.length > 0 &&
typeof lhGateCfg.minScenarios === 'number' &&
lhGateCfg.minScenarios > 0
) {
longHorizonScenarioGate = {
artifact: lhGateCfg.artifact,
minScenarios: lhGateCfg.minScenarios,
};
}
// default them; a malformed config must fail Stage C instead of bypassing
// the gate for targeted article types.
const hasValidArtifact =
typeof lhGateCfg.artifact === 'string' && lhGateCfg.artifact.trim().length > 0;
const hasValidMinScenarios =
typeof lhGateCfg.minScenarios === 'number' && lhGateCfg.minScenarios > 0;
if (!hasValidArtifact || !hasValidMinScenarios) {
throw new Error(
`long-horizon-scenario-gate:invalid-config articleType=${articleType} ` +
'structuralRequirements.longHorizonScenarioGate must define a non-empty ' +
'`artifact` and a positive numeric `minScenarios` for targeted article types.'
);
}
longHorizonScenarioGate = {
artifact: lhGateCfg.artifact.trim(),
minScenarios: lhGateCfg.minScenarios,
};

Copilot uses AI. Check for mistakes.
Comment thread analysis/templates/scenario-forecast.md Outdated
Comment on lines +1 to +7
<!-- SPDX-FileCopyrightText: 2024-2026 Hack23 AB -->
<!-- SPDX-License-Identifier: Apache-2.0 -->

---
long-horizon-mode: false
---

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The YAML frontmatter is not at byte/line 0 because it’s preceded by the SPDX HTML comments. The aggregator’s stripFrontMatter() only removes frontmatter when the document starts with ---, so this block will leak into aggregated/published HTML. Move the frontmatter to the very top (before the SPDX comments) or remove it and express the flag in-body instead.

Suggested change
<!-- SPDX-FileCopyrightText: 2024-2026 Hack23 AB -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
---
long-horizon-mode: false
---
---
long-horizon-mode: false
---
<!-- SPDX-FileCopyrightText: 2024-2026 Hack23 AB -->
<!-- SPDX-License-Identifier: Apache-2.0 -->

Copilot uses AI. Check for mistakes.
"articleTypes": ["term-outlook", "election-cycle"],
"minScenarios": 6,
"artifact": "intelligence/scenario-forecast.md",
"note": "Enforced by scripts/validate-analysis-completeness.js countScenarios(). Counts headings matching /^###\\s+Scenario\\s+\\d+[:—]/m in the artifact. Activated for articleTypes listed above (electoralOverlay===true OR scenarioMaxHorizonMonths>=36 in article-horizons registry)."

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The longHorizonScenarioGate.note regex description doesn’t match the implementation: countScenarios() accepts alphanumeric IDs (A–Z, 0–9) and uses a global multiline match, but the note says it only counts \d+ and implies a single match. Please update the note so it accurately reflects the validator’s actual heading pattern.

Suggested change
"note": "Enforced by scripts/validate-analysis-completeness.js countScenarios(). Counts headings matching /^###\\s+Scenario\\s+\\d+[:—]/m in the artifact. Activated for articleTypes listed above (electoralOverlay===true OR scenarioMaxHorizonMonths>=36 in article-horizons registry)."
"note": "Enforced by scripts/validate-analysis-completeness.js countScenarios(). Counts all headings matching /^###\\s+Scenario\\s+[A-Z0-9]+[:—]/gm in the artifact. Activated for articleTypes listed above (electoralOverlay===true OR scenarioMaxHorizonMonths>=36 in article-horizons registry)."

Copilot uses AI. Check for mistakes.
Comment on lines +292 to +294
- [ ] Each indicator has all 7 structure fields (including `**Horizons:**` tag)
- [ ] Multi-Horizon Decay Table present in §Multi-Horizon Decay Table
- [ ] Every indicator tagged with its active horizons (7d / 30d / 90d / 12m / term-end / EP-election)

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template now adds a required **Horizons:** field, but the checklist still says “all 7 structure fields (including **Horizons:**)”. With Horizons added, the card template appears to have 8 fields (Category, Horizons, Description, Current State, Thresholds, Monitoring Method, Scenario Linkage, Historical Context). Update the checklist (and any earlier “7 fields” wording) so the required field count is consistent with the template.

Suggested change
- [ ] Each indicator has all 7 structure fields (including `**Horizons:**` tag)
- [ ] Multi-Horizon Decay Table present in §Multi-Horizon Decay Table
- [ ] Every indicator tagged with its active horizons (7d / 30d / 90d / 12m / term-end / EP-election)
- [ ] Each indicator has all 8 structure fields (including required `**Horizons:**`)
- [ ] Multi-Horizon Decay Table present in §Multi-Horizon Decay Table
- [ ] Every indicator has the `**Horizons:**` field populated with its active horizons (7d / 30d / 90d / 12m / term-end / EP-election)

Copilot uses AI. Check for mistakes.

- **≥ 6 scenarios** are mandatory (enforced by Stage-C validator on `term-outlook`/`election-cycle`).
- Mandatory **EP-election outcome branch** with three sub-paths: centre-right majority, centre-left majority, fragmented coalition.
- At least **1 regime-change branch** when any tripwire from §2 is armed.

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bullet makes the regime-change branch conditional (“when any tripwire from §2 is armed”), but scenario-forecast.md §0 defines a regime-change branch as mandatory in long-horizon mode. Align the prompt wording with the template/spec so agents don’t follow conflicting requirements.

Suggested change
- At least **1 regime-change branch** when any tripwire from §2 is armed.
- Mandatory **regime-change branch**; use tripwires from §2 to define and monitor the branch.

Copilot uses AI. Check for mistakes.
@pethers

pethers commented May 1, 2026

Copy link
Copy Markdown
Member

@copilot apply changes based on the comments in this thread

…g error, note accuracy, 8-field count, mandatory regime-change branch

- analysis/templates/scenario-forecast.md: Move YAML frontmatter to byte 0
  (before SPDX comments) so cleanArtifact() strips it correctly
- scripts/validate-analysis-completeness.js: buildRules() now throws
  'long-horizon-scenario-gate:invalid-config' when the gate targets the
  current articleType but artifact/minScenarios fields are missing/invalid;
  main() catches and exits 1 with clean message + RED gate line
- analysis/methodologies/reference-quality-thresholds.json: Fix note regex
  to /^###\s+Scenario\s+[A-Z0-9]+[:—]/gm (matching the actual countScenarios
  implementation)
- analysis/templates/forward-indicators.md: Update checklist from "7 structure
  fields" to "8 structure fields" (Horizons is the 8th); clarify wording
- .github/prompts/11-forward-projection.md: Make regime-change branch
  unconditionally mandatory in long-horizon mode (align with scenario-forecast.md §0)
- test/unit/validate-analysis-completeness.test.js: Add 2 new tests for the
  invalid-config error path (missing artifact, zero minScenarios)

Agent-Logs-Url: https://github.com/Hack23/euparliamentmonitor/sessions/083fc432-56db-42b7-bdf5-3ea1a304d21b

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>

Copilot AI commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

All five review suggestions applied in commit 6df6f00:

  1. Frontmatter position — moved ---\nlong-horizon-mode: false\n--- to byte 0 (before SPDX comments) so cleanArtifact() strips it correctly.
  2. Invalid-config errorbuildRules() now throws long-horizon-scenario-gate:invalid-config when the gate targets the current articleType but artifact/minScenarios are missing or invalid; main() catches it, emits a RED gate line, and exits 1. Two new unit tests cover the missing-artifact and zero-minScenarios cases.
  3. Note regex accuracy — updated reference-quality-thresholds.json note to /^###\s+Scenario\s+[A-Z0-9]+[:—]/gm matching the actual countScenarios() implementation.
  4. 8-field checklistforward-indicators.md checklist updated from "7 structure fields" to "8 structure fields" with the **Horizons:** field explicitly listed.
  5. Mandatory regime-change branch11-forward-projection.md §5 now reads "Mandatory regime-change branch; use tripwires from §2 to define and monitor the branch." (unconditional, aligned with scenario-forecast.md §0).

Copilot finished work on behalf of pethers May 1, 2026 14:45
@pethers pethers requested a review from Copilot May 1, 2026 14:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread analysis/templates/scenario-forecast.md Outdated
Comment on lines +18 to +19
**`long-horizon-mode`** is an optional frontmatter flag. Set it to `true` when writing a `term-outlook` or `election-cycle` analysis run — or any run where the workflow's registry entry carries `electoralOverlay === true` OR `scenarioMaxHorizonMonths >= 36`. When unset or `false`, standard behaviour (3–5 scenarios, 7/30/90-day horizon) is preserved.

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text implies Stage C activation is based on the article-horizons registry fields (electoralOverlay / scenarioMaxHorizonMonths), but the validator code actually only gates by manifest.articleType (via structuralRequirements.longHorizonScenarioGate.articleTypes). Please reword this section to reflect the actual enforcement mechanism to avoid misleading authors.

Copilot uses AI. Check for mistakes.
Comment thread analysis/templates/scenario-forecast.md Outdated

### Activation signals (checked by Stage-C validator)

The validator enforces the ≥ 6 scenario floor when the manifest's `articleType` is one of: **`term-outlook`**, **`election-cycle`** (i.e. any type whose registry entry has `electoralOverlay === true` OR `scenarioMaxHorizonMonths >= 36`).

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph states the validator enforces the ≥6 scenario floor for term-outlook/election-cycle “i.e.” any type with electoralOverlay === true OR scenarioMaxHorizonMonths >= 36. The validator doesn’t inspect those registry fields, so the “i.e.” equivalence is inaccurate. Suggest changing to: enforced when manifest.articleType is one of the configured types; the registry fields are the rationale for which types are listed.

Suggested change
The validator enforces the ≥ 6 scenario floor when the manifest's `articleType` is one of: **`term-outlook`**, **`election-cycle`** (i.e. any type whose registry entry has `electoralOverlay === true` OR `scenarioMaxHorizonMonths >= 36`).
The validator enforces the ≥ 6 scenario floor when the manifest's `articleType` is one of the configured types: **`term-outlook`**, **`election-cycle`**. Registry fields such as `electoralOverlay === true` and `scenarioMaxHorizonMonths >= 36` explain why those types are included, but are not the fields the validator directly inspects.

Copilot uses AI. Check for mistakes.
Comment thread analysis/templates/scenario-forecast.md Outdated
| **Scenarios Developed** | `[REQUIRED: count 3-5]` |
| **Horizon** | `[REQUIRED: 7 days / 30 days / 90 days / 12m / term-end / EP-election]` |
| **Scenarios Developed** | `[REQUIRED: count 3-5 (standard) or ≥6 (long-horizon)]` |
| **Long-Horizon Mode** | `[REQUIRED when electoralOverlay=true or scenarioMaxHorizonMonths≥36: true / false]` |

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata row marks Long-Horizon Mode as required “when electoralOverlay=true or scenarioMaxHorizonMonths≥36”, but Stage C currently doesn’t validate this flag (it only checks manifest.articleType). To prevent false expectations, either align the wording with the real validator behavior or add a validator check for the frontmatter flag if that’s intended to become enforceable.

Copilot uses AI. Check for mistakes.
Comment on lines +292 to +293
- [ ] Each indicator has all 8 structure fields (including required `**Horizons:**` tag)
- [ ] Multi-Horizon Decay Table present in §Multi-Horizon Decay Table

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checklist now asserts “8 structure fields” due to the new **Horizons:** tag, but earlier in the template Section 2 still says “≥10 indicators with all 7 structure fields”. Please update the earlier requirement text and/or the master table header guidance so the template is internally consistent about the expected field count.

Copilot uses AI. Check for mistakes.
@pethers

pethers commented May 1, 2026

Copy link
Copy Markdown
Member

@copilot apply changes based on the comments in this thread

…r; fix forward-indicators.md field count

- scenario-forecast.md §0 §18: Remove registry-field claim from flag description —
  long-horizon-mode is for term-outlook/election-cycle runs only, no registry field
  mention that implied validator inspects electoralOverlay/scenarioMaxHorizonMonths
- scenario-forecast.md §0 §33: Replace inaccurate 'i.e.' equivalence with accurate
  statement: validator gates on manifest.articleType; registry fields explain why
  those types are listed but are not directly inspected
- scenario-forecast.md §60: Update metadata row label from
  'when electoralOverlay=true or scenarioMaxHorizonMonths>=36' to
  'for term-outlook and election-cycle articleTypes' (matches actual gate behavior)
- forward-indicators.md §Section2: Update '7 structure fields' to '8 structure fields'
  for internal consistency with the checklist updated in the previous commit

Agent-Logs-Url: https://github.com/Hack23/euparliamentmonitor/sessions/8866c0c4-3722-4bad-a71e-571df41f1f1a

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>

Copilot AI commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

All four review comments addressed in commit 9b63272:

  1. §0 flag description (line 18) — removed the registry-field clause; description now simply states the flag applies to term-outlook and election-cycle runs.
  2. Activation signals paragraph (line 33) — replaced the inaccurate "i.e." equivalence with the suggested wording: validator gates on manifest.articleType; registry fields (electoralOverlay, scenarioMaxHorizonMonths) explain why those types are listed but are not directly inspected.
  3. Metadata row (line 60) — label changed from "when electoralOverlay=true or scenarioMaxHorizonMonths≥36" to "for term-outlook and election-cycle articleTypes" to match the actual gate behavior.
  4. Section 2 field count (forward-indicators.md line 88) — updated from "all 7 structure fields" to "all 8 structure fields" for internal consistency with the checklist.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment on lines +673 to +678
* and letter variants used in worked examples). Only alphanumeric identifiers
* are matched to avoid false positives from underscore-containing headings.
*/
function countScenarios(content) {
// Match "### Scenario 1:" or "### Scenario A —" (digit or letter identifier only)
const re = /^###\s+Scenario\s+[A-Za-z0-9]+\s*[:—]/gm;

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

countScenarios() only counts headings where the scenario identifier is strictly [A-Za-z0-9]+. In existing artifacts, scenario IDs sometimes contain hyphens (e.g. ### Scenario A-24 — ... in analysis/daily runs), which this regex will not count and could cause false REDs for long-horizon runs if that style is used. Consider allowing hyphens in the identifier (while still excluding underscores) or explicitly documenting/enforcing that hyphens are not permitted in scenario IDs for gated artifacts.

Suggested change
* and letter variants used in worked examples). Only alphanumeric identifiers
* are matched to avoid false positives from underscore-containing headings.
*/
function countScenarios(content) {
// Match "### Scenario 1:" or "### Scenario A —" (digit or letter identifier only)
const re = /^###\s+Scenario\s+[A-Za-z0-9]+\s*[:]/gm;
* and letter variants used in worked examples), and also supports
* hyphen-separated alphanumeric identifiers such as `A-24`. Underscore-
* containing identifiers are still excluded to avoid false positives.
*/
function countScenarios(content) {
// Match "### Scenario 1:", "### Scenario A —", or "### Scenario A-24 —"
// while rejecting underscore-containing identifiers.
const re = /^###\s+Scenario\s+[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\s*[:]/gm;

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +27
| **Minimum scenario count** | ≥ 3 | **≥ 6** |
| **EP-election outcome branch** | Optional | **Mandatory** (centre-right / centre-left / fragmented coalition) |
| **Per-scenario WEP confidence band** | Required | Required — drawn from the decay table in [`forward-projection-methodology.md §3`](../methodologies/forward-projection-methodology.md#3-wep-decay-table) |
| **Regime-change branch** | When any tripwire fires | **Mandatory** (at least 1 dedicated branch) |

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This template pushes authors to use WEP bands from forward-projection-methodology.md §3, which includes the label “About even”. The Stage-C validator’s WEP_BAND_RE currently matches “Roughly Even”/“Even Chance” but not “About even”, so scenario-forecast artifacts that follow the canonical table can be incorrectly flagged with wep:missing. To keep the “single source of truth” contract, the validator’s WEP matcher should accept “About even” (or the canonical label set should be made consistent).

Copilot uses AI. Check for mistakes.
@pethers

pethers commented May 1, 2026

Copy link
Copy Markdown
Member

@copilot apply changes based on the comments in this thread

…even'

- validate-analysis-completeness.js countScenarios(): update regex from
  /^###\s+Scenario\s+[A-Za-z0-9]+\s*[:—]/gm  to
  /^###\s+Scenario\s+[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\s*[:—]/gm
  so hyphenated scenario IDs (e.g. A-24) are counted correctly; underscore
  identifiers still excluded to prevent false positives
- validate-analysis-completeness.js WEP_BAND_RE: add 'About even' so artifacts
  following the canonical forward-projection-methodology.md §3 table are not
  incorrectly flagged wep:missing
- reference-quality-thresholds.json longHorizonScenarioGate.note: update regex
  description to reflect hyphen support; remove inaccurate registry-field clause
- test: add 'counts hyphenated scenario IDs (e.g. A-24) correctly' test (57 pass)

Agent-Logs-Url: https://github.com/Hack23/euparliamentmonitor/sessions/396c22b9-fda6-4b27-a92a-d63b67fa355f

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>

Copilot AI commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

Both issues fixed in commit bd6c381:

  1. countScenarios() hyphenated IDs — regex updated to /^###\s+Scenario\s+[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\s*[:—]/gm so IDs like A-24 are counted; underscores still excluded. New unit test 'counts hyphenated scenario IDs (e.g. A-24) correctly' added and passing.

  2. WEP_BAND_RE missing "About even""About even" added to the matcher so artifacts using the canonical forward-projection-methodology.md §3 label are no longer incorrectly flagged with wep:missing.

reference-quality-thresholds.json note also updated to reflect the hyphen-supporting pattern.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Comment on lines +693 to +700
function validateLongHorizonScenarioGate(runDir, rules) {
if (!rules.longHorizonScenarioGate) return null;
const { artifact, minScenarios } = rules.longHorizonScenarioGate;
const absPath = path.join(runDir, artifact);
if (!fs.existsSync(absPath)) return null; // already caught as missing artifact
const content = fs.readFileSync(absPath, 'utf8');
const count = countScenarios(content);
if (count >= minScenarios) return null; // gate passes

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateLongHorizonScenarioGate() builds absPath via path.join(runDir, artifact). If artifact is absolute (or contains .. segments), path.join can escape runDir and read arbitrary files on the runner. Even though the config is repo-controlled, it’s safer to enforce that artifact is a normalized relative path under runDir (e.g., reject path.isAbsolute(artifact) and verify path.resolve(runDir, artifact) starts with the resolved runDir prefix).

Copilot uses AI. Check for mistakes.
Comment on lines +671 to +681
* Count the number of scenario headings in a scenario-forecast artifact.
* Matches `### Scenario N:` and `### Scenario N —` patterns (both numeric
* and letter variants used in worked examples), and also supports
* hyphen-separated alphanumeric identifiers such as `A-24`. Underscore-
* containing identifiers are still excluded to avoid false positives.
*/
function countScenarios(content) {
// Match "### Scenario 1:", "### Scenario A —", or "### Scenario A-24 —"
// while rejecting underscore-containing identifiers.
const re = /^###\s+Scenario\s+[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\s*[:—]/gm;
const matches = content.match(re);

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

countScenarios() currently counts every matching ### Scenario ... heading in the entire file. Since analysis/templates/scenario-forecast.md contains a worked example with ### Scenario A/B/C — ... headings, an analysis run can meet the ≥6-scenario gate by keeping the worked example plus only writing 3 real scenarios. To make the gate enforce the intended scenario set, consider excluding the worked-example section (e.g., only count headings before the "## 🛠️ Worked example" H2, or ignore matches that occur after that header).

Suggested change
* Count the number of scenario headings in a scenario-forecast artifact.
* Matches `### Scenario N:` and `### Scenario N —` patterns (both numeric
* and letter variants used in worked examples), and also supports
* hyphen-separated alphanumeric identifiers such as `A-24`. Underscore-
* containing identifiers are still excluded to avoid false positives.
*/
function countScenarios(content) {
// Match "### Scenario 1:", "### Scenario A —", or "### Scenario A-24 —"
// while rejecting underscore-containing identifiers.
const re = /^###\s+Scenario\s+[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\s*[:]/gm;
const matches = content.match(re);
* Return the portion of a scenario-forecast artifact that should contribute
* to the scenario-count gate.
*
* Worked examples in the template may contain `### Scenario ...` headings
* that must not satisfy the minimum authored-scenarios requirement, so
* everything from the first `## ... Worked example` H2 onwards is excluded.
*/
function getScenarioCountableContent(content) {
const workedExampleHeader = /^##\s+.*Worked example\b.*$/im;
const match = workedExampleHeader.exec(content);
if (!match || typeof match.index !== 'number') {
return content;
}
return content.slice(0, match.index);
}
/**
* Count the number of scenario headings in a scenario-forecast artifact.
* Matches authored `### Scenario N:` and `### Scenario N —` patterns and
* also supports hyphen-separated alphanumeric identifiers such as `A-24`.
* Underscore-containing identifiers are still excluded to avoid false
* positives. Headings in the worked-example section are ignored.
*/
function countScenarios(content) {
const countableContent = getScenarioCountableContent(content);
// Match "### Scenario 1:", "### Scenario A —", or "### Scenario A-24 —"
// while rejecting underscore-containing identifiers.
const re = /^###\s+Scenario\s+[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\s*[:]/gm;
const matches = countableContent.match(re);

Copilot uses AI. Check for mistakes.
Comment thread analysis/templates/scenario-forecast.md Outdated
Comment on lines +18 to +20
**`long-horizon-mode`** is an optional frontmatter flag. Set it to `true` when writing a `term-outlook` or `election-cycle` analysis run. When unset or `false`, standard behaviour (3–5 scenarios, 7/30/90-day horizon) is preserved.

### When `long-horizon-mode: true` is set the following requirements override the standard rules:

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section says long-horizon-mode is optional and that when it’s unset/false, standard behavior is preserved. However, the new Stage-C gate is enforced based on manifest.articleType (term-outlook/election-cycle) regardless of this frontmatter flag. Consider rewording to avoid implying that a term-outlook/election-cycle run can stay in “standard mode” by leaving the flag false, since Stage C will still require ≥6 scenarios.

Suggested change
**`long-horizon-mode`** is an optional frontmatter flag. Set it to `true` when writing a `term-outlook` or `election-cycle` analysis run. When unset or `false`, standard behaviour (3–5 scenarios, 7/30/90-day horizon) is preserved.
### When `long-horizon-mode: true` is set the following requirements override the standard rules:
**`long-horizon-mode`** is an optional frontmatter flag that marks a run as long-horizon in the template. Set it to `true` when writing a `term-outlook` or `election-cycle` analysis run. However, for validator-enforced long-horizon article types — **`term-outlook`** and **`election-cycle`** — Stage C still applies the long-horizon rules from `manifest.articleType` even if this flag is unset or `false`. Standard behaviour (3–5 scenarios, 7/30/90-day horizon) is preserved only for article types outside that enforced set.
### For long-horizon runs — whether triggered by `long-horizon-mode: true` or by validator-enforced `manifest.articleType` the following requirements override the standard rules:

Copilot uses AI. Check for mistakes.
Comment thread analysis/templates/scenario-forecast.md Outdated
- §6 monitoring plan with at least 3 dated decision points
- Confidence label per scenario (🟢/🟡/🔴) with rationale

**Long-horizon mode (`long-horizon-mode: true`, activated when `electoralOverlay === true` OR `scenarioMaxHorizonMonths >= 36`):**

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parenthetical here claims long-horizon mode is “activated when electoralOverlay === true OR scenarioMaxHorizonMonths >= 36”, but the validator you added enforces the scenario-count gate based on manifest.articleType only (and does not inspect registry fields). To keep the template accurate and avoid confusing editors, update this activation description to match the actual enforcement trigger.

Suggested change
**Long-horizon mode (`long-horizon-mode: true`, activated when `electoralOverlay === true` OR `scenarioMaxHorizonMonths >= 36`):**
**Long-horizon mode (`long-horizon-mode: true`, with Stage-C scenario-count enforcement for `term-outlook` or `election-cycle` article types):**

Copilot uses AI. Check for mistakes.
},
"structuralRequirements": {
"note": "Structural contracts enforced by scripts/validate-analysis-completeness.js. mermaidRequired = artifacts that MUST contain at least one Mermaid block (in addition to the directory-based default for intelligence/, classification/, risk-scoring/, threat-assessment/). readerBlockRequired = artifacts that MUST contain a Reader Briefing / 'For Citizens' / 'Plain Language' section. sourceDiversityRequired = artifacts that MUST cite at least one EP MCP tool reference OR carry an evidence table (markdown table with a Source / Evidence / Reference header column, e.g. a 'Data Sources & Provenance' block); the validator accepts either signal. requiredSections = artifact -> array of H2 substrings that MUST appear (case-insensitive substring match). Added 2026-04 alongside template v2.0 enrichment of impact-matrix, forces-analysis, actor-mapping, political-capital-risk, actor-threat-profiles, legislative-disruption, legislative-velocity-risk, consequence-trees.",
"note": "Structural contracts enforced by scripts/validate-analysis-completeness.js. mermaidRequired = artifacts that MUST contain at least one Mermaid block (in addition to the directory-based default for intelligence/, classification/, risk-scoring/, threat-assessment/). readerBlockRequired = artifacts that MUST contain a Reader Briefing / 'For Citizens' / 'Plain Language' section. sourceDiversityRequired = artifacts that MUST cite at least one EP MCP tool reference OR carry an evidence table (markdown table with a Source / Evidence / Reference header column, e.g. a 'Data Sources & Provenance' block); the validator accepts either signal. requiredSections = artifact -> array of H2 substrings that MUST appear (case-insensitive substring match). Added 2026-04 alongside template v2.0 enrichment of impact-matrix, forces-analysis, actor-mapping, political-capital-risk, actor-threat-profiles, legislative-disruption, legislative-velocity-risk, consequence-trees. longHorizonScenarioGate = article types whose intelligence/scenario-forecast.md MUST contain >= 6 scenario headings (### Scenario N pattern); activated when electoralOverlay===true or scenarioMaxHorizonMonths>=36. Added 2026-05 alongside scenario-forecast.md template v1.3.",

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structuralRequirements note says the long-horizon scenario gate is “activated when electoralOverlay===true or scenarioMaxHorizonMonths>=36”, but the implementation in scripts/validate-analysis-completeness.js activates it when manifest.articleType is in longHorizonScenarioGate.articleTypes. Suggest updating this note to reflect the actual activation criteria (manifest articleType), or readers may assume registry fields affect enforcement.

Suggested change
"note": "Structural contracts enforced by scripts/validate-analysis-completeness.js. mermaidRequired = artifacts that MUST contain at least one Mermaid block (in addition to the directory-based default for intelligence/, classification/, risk-scoring/, threat-assessment/). readerBlockRequired = artifacts that MUST contain a Reader Briefing / 'For Citizens' / 'Plain Language' section. sourceDiversityRequired = artifacts that MUST cite at least one EP MCP tool reference OR carry an evidence table (markdown table with a Source / Evidence / Reference header column, e.g. a 'Data Sources & Provenance' block); the validator accepts either signal. requiredSections = artifact -> array of H2 substrings that MUST appear (case-insensitive substring match). Added 2026-04 alongside template v2.0 enrichment of impact-matrix, forces-analysis, actor-mapping, political-capital-risk, actor-threat-profiles, legislative-disruption, legislative-velocity-risk, consequence-trees. longHorizonScenarioGate = article types whose intelligence/scenario-forecast.md MUST contain >= 6 scenario headings (### Scenario N pattern); activated when electoralOverlay===true or scenarioMaxHorizonMonths>=36. Added 2026-05 alongside scenario-forecast.md template v1.3.",
"note": "Structural contracts enforced by scripts/validate-analysis-completeness.js. mermaidRequired = artifacts that MUST contain at least one Mermaid block (in addition to the directory-based default for intelligence/, classification/, risk-scoring/, threat-assessment/). readerBlockRequired = artifacts that MUST contain a Reader Briefing / 'For Citizens' / 'Plain Language' section. sourceDiversityRequired = artifacts that MUST cite at least one EP MCP tool reference OR carry an evidence table (markdown table with a Source / Evidence / Reference header column, e.g. a 'Data Sources & Provenance' block); the validator accepts either signal. requiredSections = artifact -> array of H2 substrings that MUST appear (case-insensitive substring match). Added 2026-04 alongside template v2.0 enrichment of impact-matrix, forces-analysis, actor-mapping, political-capital-risk, actor-threat-profiles, legislative-disruption, legislative-velocity-risk, consequence-trees. longHorizonScenarioGate = article types whose intelligence/scenario-forecast.md MUST contain >= 6 scenario headings (### Scenario N pattern); activated for manifest.articleType values listed in longHorizonScenarioGate.articleTypes. Added 2026-05 alongside scenario-forecast.md template v1.3.",

Copilot uses AI. Check for mistakes.
@pethers

pethers commented May 1, 2026

Copy link
Copy Markdown
Member

@copilot apply changes based on the comments in this thread

…racy

1. validateLongHorizonScenarioGate() rejects absolute paths and ".."
   segments in artifact config to prevent reading outside runDir.
2. countScenarios() now excludes headings after "## ... Worked example"
   H2 so template examples don't satisfy the gate.
3. scenario-forecast.md §0 wording clarifies that Stage C enforces
   based on manifest.articleType regardless of the frontmatter flag.
4. scenario-forecast.md line 323 parenthetical updated to reference
   Stage-C enforcement by articleType (not registry fields).
5. reference-quality-thresholds.json structuralRequirements.note updated
   to say "activated for manifest.articleType values" (not registry fields).
6. Two new unit tests: worked-example exclusion + path-traversal guard.

Agent-Logs-Url: https://github.com/Hack23/euparliamentmonitor/sessions/ac45082f-e2d6-40a8-8fae-f2e76b78421d

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>

Copilot AI commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

All five review comments addressed in commit f1acdf9:

  1. Path-traversal guardvalidateLongHorizonScenarioGate() now rejects absolute paths and .. segments, verifying the resolved path stays under runDir.
  2. Worked-example exclusion — new getScenarioCountableContent() strips everything after ## … Worked example H2 before counting scenarios, preventing template examples from satisfying the gate.
  3. §0 frontmatter flag wording (line 18-20) — reworded to clarify Stage C enforces via manifest.articleType regardless of the long-horizon-mode flag.
  4. Line 323 activation description — parenthetical updated to "with Stage-C scenario-count enforcement for term-outlook or election-cycle article types".
  5. reference-quality-thresholds.json note — updated to "activated for manifest.articleType values listed in longHorizonScenarioGate.articleTypes".

Two new unit tests cover the worked-example exclusion and path-traversal rejection (59 tests passing).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

'utf8',
);
const result = runHere();
expect(result.code).toBe(1);

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path-traversal test currently accepts an unhandled exception (stack trace) as a valid failure mode. Once the validator is updated to handle long-horizon gate config errors gracefully, this test should also assert that stdout still contains a STAGE_C_GATE: RED line (so downstream tooling keeps working even on invalid config).

Suggested change
expect(result.code).toBe(1);
expect(result.code).toBe(1);
expect(result.stdout).toContain('STAGE_C_GATE: RED');

Copilot uses AI. Check for mistakes.
Comment on lines +931 to +933
if (longHorizonScenarioResult) {
mergeForwardRegistryResult(results, longHorizonScenarioResult);
}

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mergeForwardRegistryResult is now used to merge non-forward-registry synthetic results (the long-horizon scenario gate). Renaming this helper to something artifact-agnostic (and updating call sites) would reduce confusion for future maintainers.

Copilot uses AI. Check for mistakes.
Comment on lines +930 to +932
const longHorizonScenarioResult = validateLongHorizonScenarioGate(runDir, rules);
if (longHorizonScenarioResult) {
mergeForwardRegistryResult(results, longHorizonScenarioResult);

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateLongHorizonScenarioGate can throw (e.g. path traversal / resolves outside runDir), but this call site doesn't catch those errors. That will terminate the validator with an unhandled exception and can prevent emitting the expected STAGE_C_GATE line on stdout, breaking CI/parsers. Wrap this gate invocation in a try/catch (or validate/throw earlier inside buildRules so main’s existing catch handles it) and convert failures into a normal RED result + gate line output.

Suggested change
const longHorizonScenarioResult = validateLongHorizonScenarioGate(runDir, rules);
if (longHorizonScenarioResult) {
mergeForwardRegistryResult(results, longHorizonScenarioResult);
try {
const longHorizonScenarioResult = validateLongHorizonScenarioGate(runDir, rules);
if (longHorizonScenarioResult) {
mergeForwardRegistryResult(results, longHorizonScenarioResult);
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
mergeForwardRegistryResult(results, {
ok: false,
ruleId: 'long-horizon-scenario-gate',
relativePath: 'scenario-forecast.md',
message: `Long-horizon scenario gate validation failed: ${message}`,
});

Copilot uses AI. Check for mistakes.
@pethers

pethers commented May 1, 2026

Copy link
Copy Markdown
Member

@copilot apply changes based on the comments in this thread

…ix test assertion

1. Renamed mergeForwardRegistryResult → mergeSyntheticResult since it now
   handles non-forward-registry synthetic results (long-horizon gate).
2. validateLongHorizonScenarioGate() call site wrapped in try/catch so
   thrown errors (path traversal, resolve outside runDir) produce a normal
   RED result + STAGE_C_GATE line on stdout instead of unhandled exception.
3. Path-traversal test updated to assert STAGE_C_GATE: RED on stdout.

Agent-Logs-Url: https://github.com/Hack23/euparliamentmonitor/sessions/993d1a98-525b-437e-8e11-dba43cd79ed6

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>

Copilot AI commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

All three review comments addressed in commit 7eb8442:

  1. Renamed helpermergeForwardRegistryResultmergeSyntheticResult since it now handles non-forward-registry results (long-horizon gate).
  2. try/catch wrappervalidateLongHorizonScenarioGate() call site now catches thrown errors (path traversal, resolve outside runDir) and converts them to a normal RED result with STAGE_C_GATE: RED on stdout instead of an unhandled exception.
  3. Path-traversal test — assertion updated to expect(result.stdout).toContain('STAGE_C_GATE: RED') so downstream CI parsers are verified.

All 59 unit tests passing.

Copilot finished work on behalf of pethers May 1, 2026 16:13
@pethers pethers merged commit fc4d4df into main May 1, 2026
14 checks passed
@pethers pethers deleted the copilot/update-scenario-forecast-long-horizon branch May 1, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation updates infrastructure CI/CD and build infrastructure needs-tests Needs test coverage scripts Scripts and automation test-coverage-impact Impacts test coverage testing Testing improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Look-Ahead §5] Update scenario-forecast.md (long-horizon-mode) and forward-indicators.md (multi-horizon decay)

3 participants