Skip to content

Commit d2fd995

Browse files
authored
Merge pull request #2036 from Hack23/copilot/add-pir-status-json-sidecar
[WIP] Add machine-readable PIR status sidecar for cross-cycle inheritance
2 parents 2bf8097 + f239bb0 commit d2fd995

5 files changed

Lines changed: 1646 additions & 2 deletions

File tree

.github/prompts/05-analysis-gate.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,80 @@ if [ -s "$ANALYSIS_DIR/coalition-mathematics.md" ]; then
232232
|| { echo "❌ coalition-mathematics.md: missing seat-count / vote-breakdown table"; FAIL=1; }
233233
fi
234234

235+
# Check 9 — PIR status sidecar (`pir-status.json`)
236+
# A valid pir-status.json must be present after every analysis run so that
237+
# open PIRs can be automatically rolled forward to the next cycle.
238+
# Schema: schemas/pir-status.schema.json (v1.0)
239+
# Roll-forward script: scripts/roll-forward-pirs.ts
240+
PIR_FILE="$ANALYSIS_DIR/pir-status.json"
241+
if [ ! -s "$PIR_FILE" ]; then
242+
echo "❌ pir-status.json missing or empty in $ANALYSIS_DIR — create it per schemas/pir-status.schema.json"
243+
FAIL=1
244+
else
245+
# Structural check: required top-level fields
246+
for PIR_FIELD in schema_version cycle date subfolder pirs generated_at; do
247+
python3 -c "
248+
import json, sys
249+
try:
250+
d = json.load(open('$PIR_FILE'))
251+
sys.exit(0 if '$PIR_FIELD' in d else 1)
252+
except Exception:
253+
sys.exit(1)
254+
" 2>/dev/null || { echo "❌ pir-status.json: missing required field '$PIR_FIELD'"; FAIL=1; }
255+
done
256+
# schema_version must be '1.0'
257+
python3 -c "
258+
import json, sys
259+
try:
260+
d = json.load(open('$PIR_FILE'))
261+
sys.exit(0 if d.get('schema_version') == '1.0' else 1)
262+
except Exception:
263+
sys.exit(1)
264+
" 2>/dev/null || { echo "❌ pir-status.json: schema_version must be '1.0'"; FAIL=1; }
265+
# pirs must be an array
266+
python3 -c "
267+
import json, sys
268+
try:
269+
d = json.load(open('$PIR_FILE'))
270+
sys.exit(0 if isinstance(d.get('pirs'), list) else 1)
271+
except Exception:
272+
sys.exit(1)
273+
" 2>/dev/null || { echo "❌ pir-status.json: 'pirs' field must be a JSON array"; FAIL=1; }
274+
# each PIR (any status) must have valid pir_id, statement, status, confidence;
275+
# answered PIRs must carry answer_summary; non-answered PIRs must not.
276+
python3 -c "
277+
import json, sys, re
278+
try:
279+
d = json.load(open('$PIR_FILE'))
280+
PIR_ID_RE = re.compile(r'^PIR-[A-Za-z0-9]+(-[A-Za-z0-9]+)*$')
281+
VALID_STATUS = {'open','answered','superseded','deferred','cancelled'}
282+
VALID_CONF = {'VERY HIGH','HIGH','MEDIUM','LOW','VERY LOW'}
283+
bad = 0
284+
# Cross-field invariant: subfolder must equal cycle (not enforceable in pure JSON Schema).
285+
if d.get('subfolder') != d.get('cycle'):
286+
print(f'❌ pir-status.json: subfolder={d.get(\"subfolder\")!r} must equal cycle={d.get(\"cycle\")!r}'); bad = 1
287+
for p in d.get('pirs', []):
288+
pid = p.get('pir_id')
289+
if not isinstance(pid, str) or not PIR_ID_RE.match(pid):
290+
print(f'❌ pir-status.json: invalid pir_id format: {pid!r}'); bad = 1
291+
for f in ('statement', 'status', 'confidence'):
292+
if not p.get(f):
293+
print(f'❌ pir-status.json pir={pid!r}: missing required field \"{f}\"'); bad = 1
294+
if p.get('status') not in VALID_STATUS:
295+
print(f'❌ pir-status.json pir={pid!r}: invalid status {p.get(\"status\")!r}'); bad = 1
296+
if p.get('confidence') not in VALID_CONF:
297+
print(f'❌ pir-status.json pir={pid!r}: invalid confidence {p.get(\"confidence\")!r}'); bad = 1
298+
# Conditional: answer_summary required iff status == 'answered'.
299+
if p.get('status') == 'answered' and not p.get('answer_summary'):
300+
print(f'❌ pir-status.json pir={pid!r}: status=answered requires non-empty answer_summary'); bad = 1
301+
if p.get('status') != 'answered' and 'answer_summary' in p:
302+
print(f'❌ pir-status.json pir={pid!r}: status={p.get(\"status\")!r} must not carry answer_summary'); bad = 1
303+
sys.exit(bad)
304+
except Exception as e:
305+
print(f'❌ pir-status.json: parse error: {e}'); sys.exit(1)
306+
" 2>&1 || FAIL=1
307+
fi
308+
235309
[ "$FAIL" -eq 0 ] || exit 1
236310
```
237311

@@ -264,7 +338,7 @@ Non-blocking for `standard` / `deep` runs; **blocking for `comprehensive` / Tier
264338
Inline bash probe — append to the main block after `FAIL=0` bookkeeping completes. Supplementary artifacts have **three independent blocking triggers**, not a single tier-only rule: **aggregation article types** (`weekly-review`, `monthly-review`) require the aggregation artifacts; any run whose **tier** is `comprehensive` (the Tier-C run mode) requires the Tier-C supplementary set; and `cross-run-diff.md` is blocking whenever the workflow has **≥ 2 production runs** of the same article type, including `standard` and `deep` runs. `ARTICLE_TYPE` encodes the workflow family; `ANALYSIS_TIER` (when set) encodes the depth tier (`standard` | `deep` | `comprehensive`); `ANALYSIS_RUN_COUNT` (when set) is the numeric count of runs for the same article-generation cycle (if unset or non-numeric, treated as `1`).
265339

266340
```bash
267-
# Check 9 — supplementary artifacts (blocking for aggregation types, any Tier-C run, and S5 when run-count >= 2)
341+
# Check 10 — supplementary artifacts (blocking for aggregation types, any Tier-C run, and S5 when run-count >= 2)
268342
IS_AGGREGATION=0
269343
IS_TIER_C=0
270344
IS_MULTI_RUN=0

analysis/methodologies/ai-driven-analysis-guide.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ Read every file you produced in Steps 3–5. For each one, **improve every secti
209209
- Add one more named actor (MP, minister, official) to every stakeholder and SWOT entry.
210210
- Add one more dok_id or vote-record citation to every evidence column that has < 2 citations.
211211
- **Tag every key finding to a PIR/EEI** from the catalog in `political-style-guide.md`.
212+
- **Write `pir-status.json`** — every cycle must produce `$ANALYSIS_DIR/pir-status.json` conforming to `schemas/pir-status.schema.json` v1.0 (required fields: `schema_version`, `cycle`, `date`, `subfolder`, `pirs`, `generated_at`). Newly extracted PIRs from `intelligence-assessment.md` default to `status: "open"`; rolled-forward PIRs from a prior cycle preserve their existing status (`open` / `answered` / `superseded` / `deferred` / `cancelled`) and may carry a populated `inherits_from` chain. Open PIRs that are carried forward have their confidence degraded one level (HIGH → MEDIUM, etc.) by `scripts/roll-forward-pirs.ts`; non-open PIRs are preserved unchanged so the historical lineage is never lost. This file is the machine-readable PIR sidecar used for automated roll-forward and CI gate enforcement (Check 9 in `05-analysis-gate.md`).
212213
- Add Statskontoret evidence to every implementation-capacity or agency-burden claim where a relevant public report/page exists.
213214
- Verify every macro/fiscal/monetary/external-sector claim is IMF-first, vintage-tagged when projected, and represented in `economic-data.json` when charted.
214215
- Re-rank the significance scoring if the rewrite reveals a stronger lead.
@@ -579,6 +580,53 @@ Every security-relevant control in Family A maps to **ISO 27001:2022**, **NIST C
579580
| [`political-threat-framework.md`](political-threat-framework.md) | Attack trees + kill chain + threat taxonomy |
580581
| [`political-style-guide.md`](political-style-guide.md) | Writing voice, attribution, evidence density |
581582

583+
### PIR status sidecar — automated roll-forward
584+
585+
Every analysis cycle writes a `pir-status.json` sidecar alongside the 23 required artifacts:
586+
587+
| Item | Detail |
588+
|------|--------|
589+
| **Schema** | `schemas/pir-status.schema.json` v1.0 — JSON Schema 2020-12 |
590+
| **Location** | `analysis/daily/YYYY-MM-DD/{subfolder}/pir-status.json` |
591+
| **Fields** | `schema_version`, `cycle`, `date`, `subfolder`, `generated_at`, `inherited_from`, `pirs[]` |
592+
| **PIR entry fields** | `pir_id` (pattern `PIR-*`), `statement`, `status`, `confidence`, `trigger`, `answer_summary`, `inherits_from[]`, `evidence_refs[]`, `horizon`, `admiralty_grade` |
593+
| **Roll-forward script** | `scripts/roll-forward-pirs.ts` — propagates `open` PIRs from the previous cycle to the current cycle, degrading confidence by one level to flag staleness |
594+
| **CI gate** | Check 9 in `.github/prompts/05-analysis-gate.md` — blocks article generation if `pir-status.json` is absent or structurally invalid |
595+
596+
**How to write `pir-status.json` during analysis (Step 7):**
597+
598+
```json
599+
{
600+
"schema_version": "1.0",
601+
"cycle": "month-ahead",
602+
"date": "2026-04-27",
603+
"subfolder": "month-ahead",
604+
"generated_at": "2026-04-27T10:00:00Z",
605+
"inherited_from": null,
606+
"pirs": [
607+
{
608+
"pir_id": "PIR-1",
609+
"statement": "SD voting discipline on prop. 2025/26:236 (fuel tax)",
610+
"trigger": "May 2026 chamber vote on HD01FiU48",
611+
"status": "open",
612+
"confidence": "HIGH",
613+
"evidence_refs": ["HD01FiU48"],
614+
"horizon": "2026-05-15",
615+
"admiralty_grade": "B2"
616+
}
617+
]
618+
}
619+
```
620+
621+
**Roll-forward usage (next cycle):**
622+
623+
```bash
624+
npx tsx scripts/roll-forward-pirs.ts \
625+
--date 2026-04-28 --cycle month-ahead
626+
```
627+
628+
---
629+
582630
### Templates and platform exemplars
583631

584632
| Document | Purpose |
@@ -591,7 +639,8 @@ Every security-relevant control in Family A maps to **ISO 27001:2022**, **NIST C
591639

592640
**Document Control**
593641
- **Path:** `/analysis/methodologies/ai-driven-analysis-guide.md`
594-
- **Version:** 6.6 — Phase 2–5 alignment (worked examples + narrative-voice + Pass-2 self-audit)
642+
- **Version:** 6.7 — PIR status sidecar (`pir-status.json`) integration
643+
- **Key changes in v6.7:** Added mandatory `pir-status.json` sidecar write step to Pass-2 checklist (Step 7); added PIR status sidecar reference section under Related Documents; added roll-forward usage example (`scripts/roll-forward-pirs.ts`) and schema reference (`schemas/pir-status.schema.json`).
595644
- **Key changes in v6.6:** Step 3 now points at the v1.3 doctype-variant detector (5 extended types: motion-package, fpm, utskottsbetänkande-variants, KU-anmälan, EU-nämnd) and adds Narrative subsection requirement for ≥ L2 per-file artifacts; Step 4 cross-reference-map row links to the 7 atomic edge types in `structural-metadata-methodology.md` v1.3; Step 7 Pass-2 rewrite checklist adds two binding items — Pass-2 Self-Audit Checklist (10 items) and Narrative 6-axis rubric (18/30 floor); DIW section adds worked-example callout to `synthesis-methodology.md` v1.3 (line-by-line scoring + winner/loser rubric) and Sainte-Laguë walkthrough in `electoral-domain-methodology.md` v1.3; Quality Gate Checklist gains rows 11–12.
596645
- **Key changes in v6.5:** source diversity rule integration (political-style-guide.md v3.1)
597646
- **Key changes in v6.4:** Updated Step 1 reading list to reference **Source Diversity Rule** in political-style-guide.md v3.1 (multi-source corroboration by claim priority, conflict resolution, worked scenario); added Source Diversity check to Quality Gate Evidence dimension (P0/P1: ≥3 sources required); added source diversity verification to Pass-2 rewrite checklist; added IMF collection tools to referenced Collection Management Matrix.

schemas/pir-status.schema.json

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://riksdagsmonitor.com/schemas/pir-status.schema.json",
4+
"title": "PIR Status Sidecar",
5+
"description": "Machine-readable Priority Intelligence Requirement (PIR) status sidecar produced by every agentic analysis cycle. Written to `analysis/daily/YYYY-MM-DD/{subfolder}/pir-status.json`. Enables automated PIR roll-forward, cross-cycle gap detection, and CI gate enforcement. Schema v1.0.",
6+
"type": "object",
7+
"additionalProperties": false,
8+
"required": ["schema_version", "cycle", "date", "subfolder", "pirs", "generated_at"],
9+
"properties": {
10+
"schema_version": {
11+
"type": "string",
12+
"description": "Schema version — always '1.0' for this release.",
13+
"enum": ["1.0"]
14+
},
15+
"cycle": {
16+
"type": "string",
17+
"description": "Analysis cycle type slug (matches the subfolder name convention).",
18+
"enum": [
19+
"committeeReports",
20+
"propositions",
21+
"motions",
22+
"interpellations",
23+
"evening-analysis",
24+
"realtime-pulse",
25+
"week-ahead",
26+
"month-ahead",
27+
"weekly-review",
28+
"monthly-review"
29+
]
30+
},
31+
"date": {
32+
"type": "string",
33+
"description": "ISO calendar date of this analysis run (YYYY-MM-DD).",
34+
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
35+
},
36+
"subfolder": {
37+
"type": "string",
38+
"description": "Relative subfolder path under `analysis/daily/YYYY-MM-DD/` — typically equal to `cycle`. Cross-field equality is not enforced by the schema (it would require a verbose `oneOf` per cycle); the CI analysis gate (Check 10 in `.github/prompts/05-analysis-gate.md`) and the `roll-forward-pirs.ts` writer enforce `subfolder === cycle`.",
39+
"minLength": 1
40+
},
41+
"generated_at": {
42+
"type": "string",
43+
"description": "ISO-8601 timestamp of when this sidecar was generated (UTC).",
44+
"format": "date-time"
45+
},
46+
"inherited_from": {
47+
"type": ["string", "null"],
48+
"description": "Source path this file was rolled forward from, e.g. `analysis/daily/2026-04-26/month-ahead/pir-status.json`. Null when this is a freshly authored set (no roll-forward).",
49+
"default": null
50+
},
51+
"pirs": {
52+
"type": "array",
53+
"description": "Ordered list of PIRs for this cycle. May be empty for short-event cycles that did not define standing PIRs.",
54+
"items": { "$ref": "#/$defs/pirEntry" }
55+
}
56+
},
57+
"$defs": {
58+
"pirEntry": {
59+
"type": "object",
60+
"additionalProperties": false,
61+
"required": ["pir_id", "statement", "status", "confidence"],
62+
"properties": {
63+
"pir_id": {
64+
"type": "string",
65+
"description": "Stable local identifier within this cycle, e.g. 'PIR-1', 'PIR-A', 'PIR-FiU-1'. Must be unique within the `pirs` array.",
66+
"pattern": "^PIR-[A-Za-z0-9]+([-][A-Za-z0-9]+)*$"
67+
},
68+
"statement": {
69+
"type": "string",
70+
"description": "Human-readable intelligence requirement statement. Must be specific, falsifiable, and actionable.",
71+
"minLength": 10
72+
},
73+
"trigger": {
74+
"type": "string",
75+
"description": "Observable event or threshold that would answer / close this PIR. Optional but strongly recommended.",
76+
"minLength": 5
77+
},
78+
"status": {
79+
"type": "string",
80+
"description": "Current disposition of this PIR.",
81+
"enum": ["open", "answered", "superseded", "deferred", "cancelled"]
82+
},
83+
"confidence": {
84+
"type": "string",
85+
"description": "ODNI-aligned confidence label on the current status assessment.",
86+
"enum": ["VERY HIGH", "HIGH", "MEDIUM", "LOW", "VERY LOW"]
87+
},
88+
"answer_summary": {
89+
"type": "string",
90+
"description": "Short (≤ 500 chars) summary of the evidence or event that answered this PIR. Populate only when status='answered'.",
91+
"minLength": 1,
92+
"maxLength": 500
93+
},
94+
"inherits_from": {
95+
"type": "array",
96+
"description": "Zero or more `pir_id` references from the previous cycle that this PIR continues or inherits from.",
97+
"items": {
98+
"type": "string",
99+
"pattern": "^PIR-[A-Za-z0-9]+([-][A-Za-z0-9]+)*$"
100+
},
101+
"default": []
102+
},
103+
"evidence_refs": {
104+
"type": "array",
105+
"description": "Riksdag `dok_id` references, primary-source URLs, or named artifacts that provide evidence for the current status assessment. At least one reference is recommended for answered PIRs.",
106+
"items": {
107+
"type": "string",
108+
"minLength": 2
109+
},
110+
"default": []
111+
},
112+
"horizon": {
113+
"type": "string",
114+
"description": "Monitoring horizon for this PIR, expressed as an ISO date (YYYY-MM-DD) or a relative label ('next-session', 'next-week', 'election-day'). Optional.",
115+
"minLength": 4
116+
},
117+
"admiralty_grade": {
118+
"type": "string",
119+
"description": "Admiralty Code grade of the best available source for the current status (STANAG 2022). Source reliability A–F + information credibility 1–6.",
120+
"pattern": "^[A-F][1-6]$"
121+
}
122+
},
123+
"allOf": [
124+
{
125+
"if": { "properties": { "status": { "const": "answered" } }, "required": ["status"] },
126+
"then": { "required": ["answer_summary"] },
127+
"else": { "not": { "required": ["answer_summary"] } }
128+
}
129+
]
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)