[Look-Ahead §5] scenario-forecast long-horizon-mode + forward-indicators multi-horizon decay#1593
Conversation
… 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 <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>
…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>
There was a problem hiding this comment.
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.mdwith a long-horizon-mode contract and updatesforward-indicators.mdwith 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. |
| // 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, | ||
| }; | ||
| } |
There was a problem hiding this comment.
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.
| // 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, | |
| }; |
| <!-- SPDX-FileCopyrightText: 2024-2026 Hack23 AB --> | ||
| <!-- SPDX-License-Identifier: Apache-2.0 --> | ||
|
|
||
| --- | ||
| long-horizon-mode: false | ||
| --- | ||
|
|
There was a problem hiding this comment.
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.
| <!-- 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 --> |
| "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)." |
There was a problem hiding this comment.
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.
| "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)." |
| - [ ] 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) |
There was a problem hiding this comment.
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.
| - [ ] 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) |
|
|
||
| - **≥ 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. |
There was a problem hiding this comment.
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.
| - 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 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>
All five review suggestions applied in commit
|
| **`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. | ||
|
|
There was a problem hiding this comment.
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.
|
|
||
| ### 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`). |
There was a problem hiding this comment.
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.
| 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. |
| | **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]` | |
There was a problem hiding this comment.
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.
| - [ ] Each indicator has all 8 structure fields (including required `**Horizons:**` tag) | ||
| - [ ] Multi-Horizon Decay Table present in §Multi-Horizon Decay Table |
There was a problem hiding this comment.
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 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>
All four review comments addressed in commit
|
| * 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; |
There was a problem hiding this comment.
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.
| * 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; |
| | **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) | |
There was a problem hiding this comment.
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 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>
Both issues fixed in commit
|
| 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 |
There was a problem hiding this comment.
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).
| * 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); |
There was a problem hiding this comment.
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).
| * 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); |
| **`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: |
There was a problem hiding this comment.
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.
| **`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: |
| - §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`):** |
There was a problem hiding this comment.
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.
| **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):** |
| }, | ||
| "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.", |
There was a problem hiding this comment.
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.
| "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 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>
All five review comments addressed in commit
Two new unit tests cover the worked-example exclusion and path-traversal rejection (59 tests passing). |
| 'utf8', | ||
| ); | ||
| const result = runHere(); | ||
| expect(result.code).toBe(1); |
There was a problem hiding this comment.
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).
| expect(result.code).toBe(1); | |
| expect(result.code).toBe(1); | |
| expect(result.stdout).toContain('STAGE_C_GATE: RED'); |
| if (longHorizonScenarioResult) { | ||
| mergeForwardRegistryResult(results, longHorizonScenarioResult); | ||
| } |
There was a problem hiding this comment.
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.
| const longHorizonScenarioResult = validateLongHorizonScenarioGate(runDir, rules); | ||
| if (longHorizonScenarioResult) { | ||
| mergeForwardRegistryResult(results, longHorizonScenarioResult); |
There was a problem hiding this comment.
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.
| 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 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>
All three review comments addressed in commit
All 59 unit tests passing. |
scenario-forecast.mdlacked any mechanism to enforce the deeper scenario set required for term/election-cycle runs;forward-indicators.mdhad no horizon-tagging contract, leaving indicators active beyond their predictive shelf-life.Template changes
scenario-forecast.md— YAML frontmatterlong-horizon-mode: falseflag (moved to byte 0 so the aggregator strips it correctly) + new §0 section specifying the override contract forterm-outlookandelection-cyclearticle types:forward-projection-methodology.md §3(single source of truth — not duplicated here)manifest.articleType, not registry fieldsforward-indicators.md— New§Multi-Horizon Decay Tablemapping 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 toforward-projection-methodology.md §3, not duplicated.Enforcement
reference-quality-thresholds.json— newstructuralRequirements.longHorizonScenarioGate:validate-analysis-completeness.js— new functions wired intomain():getScenarioCountableContent(content)— strips everything after the## … Worked exampleH2 so template examples don't satisfy the gatecountScenarios(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 positivesvalidateLongHorizonScenarioGate(runDir, rules)— emits hard-REDlong-horizon-scenario-count:N<6when the gate fires; throwslong-horizon-scenario-gate:invalid-configwhen the config targets the currentarticleTypebutartifact/minScenariosare missing or invalid (no silent fallback defaults); includes path-traversal guard rejecting absolute paths and..segments to prevent reading outsiderunDirWEP_BAND_REupdated to include"About even"so artifacts using the canonicalforward-projection-methodology.md §3label are not incorrectly flagged withwep:missingPrompt update
11-forward-projection.md§5 expanded with the full long-horizon requirements list, mandatory (unconditional) regime-change branch, and explicit cross-links toscenario-forecast.md §0andforward-indicators.md §decay-table. No double source of truth: both templates point atforward-projection-methodology.md §3for WEP bands.