diff --git a/.github/policies/narrative_ci/determinism.rego b/.github/policies/narrative_ci/determinism.rego new file mode 100644 index 00000000000..ce4b1c6020a --- /dev/null +++ b/.github/policies/narrative_ci/determinism.rego @@ -0,0 +1,31 @@ +package narrative_ci.determinism + +default allow = false + +deny_keys := {"ts", "timestamp", "created_at", "updated_at", "time", "datetime"} + +allow { + not has_bad_key(input.payload) +} + +has_bad_key(x) { + is_object(x) + some k + x[k] + deny_keys[k] +} + +has_bad_key(x) { + is_object(x) + some k + has_bad_key(x[k]) +} + +has_bad_key(x) { + is_array(x) + some i + has_bad_key(x[i]) +} + +is_object(x) { type_name(x) == "object" } +is_array(x) { type_name(x) == "array" } diff --git a/.github/policies/narrative_ci/fixtures/determinism_fail.json b/.github/policies/narrative_ci/fixtures/determinism_fail.json new file mode 100644 index 00000000000..89ea63ccef9 --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/determinism_fail.json @@ -0,0 +1 @@ +{ "payload": { "ok": 1, "ts": "2026-02-07T00:00:00Z" } } diff --git a/.github/policies/narrative_ci/fixtures/determinism_pass.json b/.github/policies/narrative_ci/fixtures/determinism_pass.json new file mode 100644 index 00000000000..3446d318a84 --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/determinism_pass.json @@ -0,0 +1 @@ +{ "payload": { "ok": 1, "note": "no timestamps here" } } diff --git a/.github/policies/narrative_ci/fixtures/tier_taxonomy_fail.json b/.github/policies/narrative_ci/fixtures/tier_taxonomy_fail.json new file mode 100644 index 00000000000..a710854035f --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/tier_taxonomy_fail.json @@ -0,0 +1,4 @@ +{ + "allowed_tiers": ["fringe", "mainstream"], + "handoff_candidates": [{ "to_tier": "govt_cert" }] +} diff --git a/.github/policies/narrative_ci/fixtures/tier_taxonomy_pass.json b/.github/policies/narrative_ci/fixtures/tier_taxonomy_pass.json new file mode 100644 index 00000000000..2515e8a3fee --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/tier_taxonomy_pass.json @@ -0,0 +1,4 @@ +{ + "allowed_tiers": ["fringe", "mainstream", "govt_cert"], + "handoff_candidates": [{ "to_tier": "govt_cert" }] +} diff --git a/.github/policies/narrative_ci/fixtures/traceability_fail.json b/.github/policies/narrative_ci/fixtures/traceability_fail.json new file mode 100644 index 00000000000..6d302c6b090 --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/traceability_fail.json @@ -0,0 +1,4 @@ +{ + "inferred_nodes": [{ "type": "Claim", "id": "clm_missing_receipt" }], + "receipts": [] +} diff --git a/.github/policies/narrative_ci/fixtures/traceability_pass.json b/.github/policies/narrative_ci/fixtures/traceability_pass.json new file mode 100644 index 00000000000..376e1029b61 --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/traceability_pass.json @@ -0,0 +1,9 @@ +{ + "inferred_nodes": [{ "type": "Claim", "id": "clm_ok" }], + "receipts": [ + { + "target": { "type": "Claim", "id": "clm_ok" }, + "sources": [{ "artifact_id": "art_x", "content_sha256": "sha256:abc" }] + } + ] +} diff --git a/.github/policies/narrative_ci/tier_taxonomy.rego b/.github/policies/narrative_ci/tier_taxonomy.rego new file mode 100644 index 00000000000..293d119347a --- /dev/null +++ b/.github/policies/narrative_ci/tier_taxonomy.rego @@ -0,0 +1,19 @@ +package narrative_ci.tier_taxonomy + +default allow = false + +allow { + allowed := { t | t := input.allowed_tiers[_] } + every_candidate_tier_allowed(allowed) +} + +every_candidate_tier_allowed(allowed) { + cands := input.handoff_candidates + not exists_disallowed(cands, allowed) +} + +exists_disallowed(cands, allowed) { + some i + t := cands[i].to_tier + not allowed[t] +} diff --git a/.github/policies/narrative_ci/traceability.rego b/.github/policies/narrative_ci/traceability.rego new file mode 100644 index 00000000000..923987e0da8 --- /dev/null +++ b/.github/policies/narrative_ci/traceability.rego @@ -0,0 +1,28 @@ +package narrative_ci.traceability + +default allow = false + +allow { + every_inferred_has_receipt + every_receipt_has_artifact +} + +every_inferred_has_receipt { + inferred := input.inferred_nodes + receipts := { r.target.id | r := input.receipts[_] } + not exists_missing(inferred, receipts) +} + +exists_missing(inferred, receipts) { + some i + inferred[i].id != "" + not receipts[inferred[i].id] +} + +every_receipt_has_artifact { + some r + r := input.receipts[_] + count(r.sources) > 0 + r.sources[0].artifact_id != "" + r.sources[0].content_sha256 != "" +} diff --git a/.github/workflows/narrative-ci-verify.yml b/.github/workflows/narrative-ci-verify.yml new file mode 100644 index 00000000000..12edc47a1ff --- /dev/null +++ b/.github/workflows/narrative-ci-verify.yml @@ -0,0 +1,55 @@ +name: narrative-ci-verify + +on: + pull_request: + paths: + - "intelgraph/schema/**" + - "intelgraph/pipelines/narrative_ci/**" + - "schemas/narrative/**" + - "fixtures/feb07_2026/**" + - ".github/policies/narrative_ci/**" + - ".github/workflows/narrative-ci-verify.yml" + workflow_dispatch: {} + +jobs: + verify: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Install OPA + run: | + set -euo pipefail + curl -sSfL -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static + chmod +x opa + sudo mv opa /usr/local/bin/opa + + - name: OPA fixtures + run: | + set -euo pipefail + opa eval -d .github/policies/narrative_ci/traceability.rego -i .github/policies/narrative_ci/fixtures/traceability_pass.json "data.narrative_ci.traceability.allow" | rg "true" + ! opa eval -d .github/policies/narrative_ci/traceability.rego -i .github/policies/narrative_ci/fixtures/traceability_fail.json "data.narrative_ci.traceability.allow" | rg "true" + + opa eval -d .github/policies/narrative_ci/determinism.rego -i .github/policies/narrative_ci/fixtures/determinism_pass.json "data.narrative_ci.determinism.allow" | rg "true" + ! opa eval -d .github/policies/narrative_ci/determinism.rego -i .github/policies/narrative_ci/fixtures/determinism_fail.json "data.narrative_ci.determinism.allow" | rg "true" + + opa eval -d .github/policies/narrative_ci/tier_taxonomy.rego -i .github/policies/narrative_ci/fixtures/tier_taxonomy_pass.json "data.narrative_ci.tier_taxonomy.allow" | rg "true" + ! opa eval -d .github/policies/narrative_ci/tier_taxonomy.rego -i .github/policies/narrative_ci/fixtures/tier_taxonomy_fail.json "data.narrative_ci.tier_taxonomy.allow" | rg "true" + + - name: Determinism linter (out/**) + run: | + set -euo pipefail + if [ -d out ]; then + ! rg -n "\"(ts|timestamp|created_at|updated_at|datetime|time)\"" out || (echo "timestamp-like keys found in deterministic outputs" && exit 1) + fi + + - name: Upload fixtures + policies (debug) + uses: actions/upload-artifact@v4 + with: + name: narrative-ci-fixtures + path: | + fixtures/feb07_2026/** + .github/policies/narrative_ci/** diff --git a/docs/roadmap/STATUS.json b/docs/roadmap/STATUS.json index 281d973af71..4b7e13c945c 100644 --- a/docs/roadmap/STATUS.json +++ b/docs/roadmap/STATUS.json @@ -1,6 +1,6 @@ { - "last_updated": "2026-02-07T00:00:00Z", - "revision_note": "Added Summit PR Stack Sequencer skill scaffolding.", + "last_updated": "2026-02-07T12:00:00Z", + "revision_note": "Added narrative CI lane-1 fixtures + gates initiative.", "initiatives": [ { "id": "adenhq-hive-subsumption-lane1", @@ -8,6 +8,12 @@ "owner": "codex", "notes": "Scaffold adenhq/hive subsumption bundle, required check mapping, and evidence-first lane-1 posture." }, + { + "id": "narrative-ci-lane1-fixtures", + "status": "in_progress", + "owner": "codex", + "notes": "Narrative CI contracts, fixtures, deterministic pipeline steps, and OPA gates for Feb 7, 2026 update." + }, { "id": "B", "name": "Federation + Ingestion Mesh", @@ -200,7 +206,7 @@ "partial": 2, "incomplete": 0, "not_started": 5, - "total": 17, + "total": 18, "ga_blockers": [] } } diff --git a/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/metrics.json b/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/metrics.json new file mode 100644 index 00000000000..52c51fa743f --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/metrics.json @@ -0,0 +1,4 @@ +{ + "evidence_id": "EVD-SITUPDATE-2026-02-07-DELTA-001", + "metrics": { "fixture": true } +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/report.json b/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/report.json new file mode 100644 index 00000000000..70741233e27 --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/report.json @@ -0,0 +1,6 @@ +{ + "evidence_id": "EVD-SITUPDATE-2026-02-07-DELTA-001", + "subject": { "type": "narrative_ci_fixture", "name": "SITUPDATE-2026-02-07" }, + "result": "pass", + "artifacts": [] +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/stamp.json b/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/stamp.json new file mode 100644 index 00000000000..f1bf14f5e6d --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/stamp.json @@ -0,0 +1,3 @@ +{ + "ts_utc": "2026-02-07T00:00:00Z" +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/metrics.json b/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/metrics.json new file mode 100644 index 00000000000..c8d24816ba0 --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/metrics.json @@ -0,0 +1,4 @@ +{ + "evidence_id": "EVD-SITUPDATE-2026-02-07-GATES-001", + "metrics": { "fixture": true } +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/report.json b/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/report.json new file mode 100644 index 00000000000..962a61edd75 --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/report.json @@ -0,0 +1,6 @@ +{ + "evidence_id": "EVD-SITUPDATE-2026-02-07-GATES-001", + "subject": { "type": "narrative_ci_fixture", "name": "SITUPDATE-2026-02-07" }, + "result": "pass", + "artifacts": [] +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/stamp.json b/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/stamp.json new file mode 100644 index 00000000000..f1bf14f5e6d --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-GATES-001/stamp.json @@ -0,0 +1,3 @@ +{ + "ts_utc": "2026-02-07T00:00:00Z" +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/metrics.json b/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/metrics.json new file mode 100644 index 00000000000..d438464391c --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/metrics.json @@ -0,0 +1,4 @@ +{ + "evidence_id": "EVD-SITUPDATE-2026-02-07-HANDOFF-001", + "metrics": { "fixture": true } +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/report.json b/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/report.json new file mode 100644 index 00000000000..b33286d179e --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/report.json @@ -0,0 +1,6 @@ +{ + "evidence_id": "EVD-SITUPDATE-2026-02-07-HANDOFF-001", + "subject": { "type": "narrative_ci_fixture", "name": "SITUPDATE-2026-02-07" }, + "result": "pass", + "artifacts": [] +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/stamp.json b/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/stamp.json new file mode 100644 index 00000000000..f1bf14f5e6d --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/stamp.json @@ -0,0 +1,3 @@ +{ + "ts_utc": "2026-02-07T00:00:00Z" +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/metrics.json b/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/metrics.json new file mode 100644 index 00000000000..01f0857dbc4 --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/metrics.json @@ -0,0 +1,4 @@ +{ + "evidence_id": "EVD-SITUPDATE-2026-02-07-STATE-001", + "metrics": { "fixture": true } +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/report.json b/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/report.json new file mode 100644 index 00000000000..e3e3940911e --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/report.json @@ -0,0 +1,6 @@ +{ + "evidence_id": "EVD-SITUPDATE-2026-02-07-STATE-001", + "subject": { "type": "narrative_ci_fixture", "name": "SITUPDATE-2026-02-07" }, + "result": "pass", + "artifacts": [] +} diff --git a/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/stamp.json b/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/stamp.json new file mode 100644 index 00000000000..f1bf14f5e6d --- /dev/null +++ b/evidence/EVD-SITUPDATE-2026-02-07-STATE-001/stamp.json @@ -0,0 +1,3 @@ +{ + "ts_utc": "2026-02-07T00:00:00Z" +} diff --git a/evidence/index.json b/evidence/index.json index 4b113aa9959..cebaa749836 100644 --- a/evidence/index.json +++ b/evidence/index.json @@ -400,6 +400,38 @@ "metrics": "evidence/EVD-INTSUM-2026-THREAT-HORIZON-001/metrics.json", "stamp": "evidence/EVD-INTSUM-2026-THREAT-HORIZON-001/stamp.json" } + }, + { + "evidence_id": "EVD-SITUPDATE-2026-02-07-DELTA-001", + "files": { + "report": "evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/report.json", + "metrics": "evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/metrics.json", + "stamp": "evidence/EVD-SITUPDATE-2026-02-07-DELTA-001/stamp.json" + } + }, + { + "evidence_id": "EVD-SITUPDATE-2026-02-07-HANDOFF-001", + "files": { + "report": "evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/report.json", + "metrics": "evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/metrics.json", + "stamp": "evidence/EVD-SITUPDATE-2026-02-07-HANDOFF-001/stamp.json" + } + }, + { + "evidence_id": "EVD-SITUPDATE-2026-02-07-STATE-001", + "files": { + "report": "evidence/EVD-SITUPDATE-2026-02-07-STATE-001/report.json", + "metrics": "evidence/EVD-SITUPDATE-2026-02-07-STATE-001/metrics.json", + "stamp": "evidence/EVD-SITUPDATE-2026-02-07-STATE-001/stamp.json" + } + }, + { + "evidence_id": "EVD-SITUPDATE-2026-02-07-GATES-001", + "files": { + "report": "evidence/EVD-SITUPDATE-2026-02-07-GATES-001/report.json", + "metrics": "evidence/EVD-SITUPDATE-2026-02-07-GATES-001/metrics.json", + "stamp": "evidence/EVD-SITUPDATE-2026-02-07-GATES-001/stamp.json" + } } ] -} \ No newline at end of file +} diff --git a/fixtures/feb07_2026/artifact_index.json b/fixtures/feb07_2026/artifact_index.json new file mode 100644 index 00000000000..04fbb189dd3 --- /dev/null +++ b/fixtures/feb07_2026/artifact_index.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "artifacts": [ + { + "artifact_id": "art_reuters_alekseyev_2026_02_07", + "kind": "news_report", + "uri": "https://www.reuters.com/world/two-suspects-attempted-killing-russian-general-will-soon-be-interrogated-2026-02-07/", + "content_sha256": "sha256:REPLACE_WITH_REAL_HASH_IF_AVAILABLE", + "outlet": "Reuters", + "outlet_tier": "mainstream", + "lang": "en" + }, + { + "artifact_id": "art_guardian_alekseyev_2026_02_06", + "kind": "news_report", + "uri": "https://www.theguardian.com/world/2026/feb/06/russia-military-general-vladimir-alekseyev", + "content_sha256": "sha256:REPLACE_WITH_REAL_HASH_IF_AVAILABLE", + "outlet": "The Guardian", + "outlet_tier": "mainstream", + "lang": "en" + }, + { + "artifact_id": "art_unit42_shadow_campaigns", + "kind": "vendor_intel", + "uri": "https://unit42.paloaltonetworks.com/shadow-campaigns-uncovering-global-espionage/", + "content_sha256": "sha256:REPLACE_WITH_REAL_HASH_IF_AVAILABLE", + "outlet": "Palo Alto Unit 42", + "outlet_tier": "vendor_intel", + "lang": "en" + }, + { + "artifact_id": "art_bleepingcomputer_signal_germany", + "kind": "security_news", + "uri": "https://www.bleepingcomputer.com/news/security/germany-warns-of-signal-account-hijacking-targeting-senior-figures/", + "content_sha256": "sha256:REPLACE_WITH_REAL_HASH_IF_AVAILABLE", + "outlet": "BleepingComputer", + "outlet_tier": "mainstream", + "lang": "en" + } + ] +} diff --git a/fixtures/feb07_2026/current_snapshot.json b/fixtures/feb07_2026/current_snapshot.json new file mode 100644 index 00000000000..a3cf52af3e5 --- /dev/null +++ b/fixtures/feb07_2026/current_snapshot.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "run_id": "run_0002", + "narratives": [ + { + "narrative_id": "narr_alekseyev_hit", + "title": "Alekseyev assassination attempt", + "state": "Contested" + }, + { + "narrative_id": "narr_shadow_campaigns", + "title": "Shadow Campaigns global espionage", + "state": "Normalized" + }, + { + "narrative_id": "narr_signal_phishing", + "title": "Signal account hijacking via fake support", + "state": "Seeded" + } + ], + "claims": [ + { + "claim_id": "clm_alekseyev_regained_consciousness", + "claim_hash": "hash:cur_regained_consciousness", + "text_norm": "Alekseyev regained consciousness after surgery following the assassination attempt.", + "verifiability": 0.7, + "confidence": 0.6, + "supporting_artifact_ids": ["art_reuters_alekseyev_2026_02_07"] + }, + { + "claim_id": "clm_shadow_campaigns_37_155", + "claim_hash": "hash:cur_shadow_37_155", + "text_norm": "Shadow Campaigns compromised targets across 37 countries and conducted reconnaissance across 155 countries.", + "verifiability": 0.9, + "confidence": 0.8, + "supporting_artifact_ids": ["art_unit42_shadow_campaigns"] + }, + { + "claim_id": "clm_signal_hijacking_fake_support", + "claim_hash": "hash:cur_signal_fake_support", + "text_norm": "Targets are being phished via fake Signal support chats to hijack Signal accounts.", + "verifiability": 0.85, + "confidence": 0.75, + "supporting_artifact_ids": ["art_bleepingcomputer_signal_germany"] + } + ] +} diff --git a/fixtures/feb07_2026/previous_snapshot.json b/fixtures/feb07_2026/previous_snapshot.json new file mode 100644 index 00000000000..259de1b7b05 --- /dev/null +++ b/fixtures/feb07_2026/previous_snapshot.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "run_id": "run_0001", + "narratives": [ + { + "narrative_id": "narr_alekseyev_hit", + "title": "Alekseyev assassination attempt", + "state": "Seeded" + } + ], + "claims": [ + { + "claim_id": "clm_alekseyev_critical_condition", + "claim_hash": "hash:prev_critical_condition", + "text_norm": "Alekseyev is in critical condition after a shooting in Moscow.", + "verifiability": 0.8, + "confidence": 0.7, + "supporting_artifact_ids": ["art_guardian_alekseyev_2026_02_06"] + } + ] +} diff --git a/fixtures/feb07_2026/provenance_receipts.json b/fixtures/feb07_2026/provenance_receipts.json new file mode 100644 index 00000000000..c17fa0ab856 --- /dev/null +++ b/fixtures/feb07_2026/provenance_receipts.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "receipts": [ + { + "receipt_id": "rcpt_clm_alekseyev_regained_consciousness", + "target": { "type": "Claim", "id": "clm_alekseyev_regained_consciousness" }, + "sources": [ + { + "artifact_id": "art_reuters_alekseyev_2026_02_07", + "content_sha256": "sha256:REPLACE_WITH_REAL_HASH_IF_AVAILABLE" + } + ], + "method": { "pipeline_step": "fixture_extract", "transform_chain": ["normalize_text_v1"] }, + "model": { "name": "rules_v1", "version": "1.0.0" }, + "confidence": 0.6 + } + ] +} diff --git a/intelgraph/pipelines/narrative_ci/README.md b/intelgraph/pipelines/narrative_ci/README.md new file mode 100644 index 00000000000..7d39fa522a2 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/README.md @@ -0,0 +1,43 @@ +# Narrative CI (Lane 1 Fixtures) + +This pipeline scaffolds deterministic narrative governance outputs for the Feb 7, 2026 situation update. +It is fixtures-first and gate-complete: adapters can replace the fixture inputs without changing the +output contracts, evidence layout, or OPA gates. + +## Inputs (fixtures in this PR) + +- `fixtures/feb07_2026/artifact_index.json` +- `fixtures/feb07_2026/previous_snapshot.json` +- `fixtures/feb07_2026/current_snapshot.json` +- `fixtures/feb07_2026/provenance_receipts.json` +- `intelgraph/pipelines/narrative_ci/config/tiers.yml` +- `intelgraph/pipelines/narrative_ci/config/lexicons.yml` +- `intelgraph/pipelines/narrative_ci/config/thresholds.yml` + +## Deterministic outputs + +- `out/delta/.json` +- `out/handoff/.json` +- `out/state/.json` +- `out/early_warning/.json` +- `out/run_manifest/.json` + +## Evidence layout + +- `evidence//report.json` +- `evidence//metrics.json` +- `evidence//stamp.json` +- `evidence/index.json` + +## OPA gates + +- Traceability +- Determinism +- Tier taxonomy + +## Notes + +- Deterministic payloads must never include timestamp-like keys. Time is allowed only in + `stamp.json`. +- Adapters will swap in real artifact stores and handoff/state resolvers while preserving + the same payload shapes. diff --git a/intelgraph/pipelines/narrative_ci/config/lexicons.yml b/intelgraph/pipelines/narrative_ci/config/lexicons.yml new file mode 100644 index 00000000000..2c7bd569821 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/config/lexicons.yml @@ -0,0 +1,8 @@ +version: 1 +register_markers: + hedging: ["may", "might", "suggests", "raising questions", "possible", "alleged"] + legalistic: ["pursuant", "hereby", "whereas", "statute", "jurisdiction", "liable"] +implied_reference_markers: + - "you already know" + - "as we said" + - "everyone knows" diff --git a/intelgraph/pipelines/narrative_ci/config/thresholds.yml b/intelgraph/pipelines/narrative_ci/config/thresholds.yml new file mode 100644 index 00000000000..de7d7be50f2 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/config/thresholds.yml @@ -0,0 +1,8 @@ +version: 1 +handoff: + alert_score: 0.70 +state_machine: + institutionalize_handoff_score: 0.80 + contest_min_counterclaims: 2 +delta: + max_items: 200 diff --git a/intelgraph/pipelines/narrative_ci/config/tiers.yml b/intelgraph/pipelines/narrative_ci/config/tiers.yml new file mode 100644 index 00000000000..3fa3fb057ef --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/config/tiers.yml @@ -0,0 +1,21 @@ +version: 1 +tiers: + fringe: + rank: 10 + examples: ["forums", "anonymous channels"] + influencer: + rank: 20 + examples: ["large personal accounts"] + newsletter: + rank: 30 + examples: ["substack-like"] + mainstream: + rank: 40 + examples: ["major outlets"] + vendor_intel: + rank: 50 + examples: ["security vendor threat intel"] + govt_cert: + rank: 60 + examples: ["national cybersecurity agencies"] +allowed_tier_labels: ["fringe", "influencer", "newsletter", "mainstream", "vendor_intel", "govt_cert"] diff --git a/intelgraph/pipelines/narrative_ci/lib/hash.ts b/intelgraph/pipelines/narrative_ci/lib/hash.ts new file mode 100644 index 00000000000..64f91e29055 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/lib/hash.ts @@ -0,0 +1,5 @@ +import { createHash } from "crypto"; + +export function sha256Hex(input: string): string { + return createHash("sha256").update(input, "utf8").digest("hex"); +} diff --git a/intelgraph/pipelines/narrative_ci/lib/io.ts b/intelgraph/pipelines/narrative_ci/lib/io.ts new file mode 100644 index 00000000000..f2c8da16e60 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/lib/io.ts @@ -0,0 +1,35 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { load } from "js-yaml"; +import { stableStringify } from "./json_stable"; + +export function readJson(path: string): T { + return JSON.parse(readFileSync(path, "utf8")) as T; +} + +export function readYaml(path: string): T { + return load(readFileSync(path, "utf8")) as T; +} + +export function readConfig(path: string): T { + if (path.endsWith(".yml") || path.endsWith(".yaml")) { + return readYaml(path); + } + return readJson(path); +} + +export function writeJsonDeterministic(path: string, obj: unknown) { + const dir = path.split("/").slice(0, -1).join("/"); + if (dir && !existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + writeFileSync(path, `${stableStringify(obj)}\n`, "utf8"); +} + +export function writeStampJson(path: string) { + const dir = path.split("/").slice(0, -1).join("/"); + if (dir && !existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + // timestamps allowed ONLY here + writeFileSync(path, JSON.stringify({ ts_utc: new Date().toISOString() }) + "\n", "utf8"); +} diff --git a/intelgraph/pipelines/narrative_ci/lib/json_stable.ts b/intelgraph/pipelines/narrative_ci/lib/json_stable.ts new file mode 100644 index 00000000000..4e50a60ffd2 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/lib/json_stable.ts @@ -0,0 +1,18 @@ +// Deterministic JSON stringify: keys sorted, no whitespace variance. +export function stableStringify(value: unknown): string { + return JSON.stringify(sortKeys(value)); +} + +function sortKeys(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(sortKeys); + } + if (value && typeof value === "object") { + const out: Record = {}; + for (const key of Object.keys(value as Record).sort()) { + out[key] = sortKeys((value as Record)[key]); + } + return out; + } + return value; +} diff --git a/intelgraph/pipelines/narrative_ci/lib/schema_validate.stub.ts b/intelgraph/pipelines/narrative_ci/lib/schema_validate.stub.ts new file mode 100644 index 00000000000..833a0f831f5 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/lib/schema_validate.stub.ts @@ -0,0 +1,8 @@ +export type SchemaValidationResult = { + ok: boolean; + errors: string[]; +}; + +export function validateAgainstSchema(_payload: unknown, _schemaPath: string): SchemaValidationResult { + return { ok: true, errors: [] }; +} diff --git a/intelgraph/pipelines/narrative_ci/steps/31_score_handoff.ts b/intelgraph/pipelines/narrative_ci/steps/31_score_handoff.ts new file mode 100644 index 00000000000..6de47ee3904 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/31_score_handoff.ts @@ -0,0 +1,109 @@ +import { readConfig, readJson, writeJsonDeterministic } from "../lib/io"; +import { sha256Hex } from "../lib/hash"; +import { stableStringify } from "../lib/json_stable"; + +type Artifact = { artifact_id: string; outlet_tier: string; uri: string; outlet: string }; + +type Snapshot = { + version: number; + run_id: string; + narratives: { narrative_id: string; title: string; state: string }[]; + claims: { + claim_id: string; + claim_hash: string; + text_norm: string; + supporting_artifact_ids: string[]; + }[]; +}; + +type TiersCfg = { + tiers: Record; + allowed_tier_labels: string[]; +}; + +type LexCfg = { + register_markers: { hedging: string[]; legalistic: string[] }; +}; + +function countMarkers(text: string, markers: string[]): number { + const t = text.toLowerCase(); + let count = 0; + for (const marker of markers) { + if (t.includes(marker.toLowerCase())) { + count += 1; + } + } + return count; +} + +export function main() { + const artifacts = readJson<{ artifacts: Artifact[] }>( + "fixtures/feb07_2026/artifact_index.json", + ).artifacts; + const tiers = readConfig("intelgraph/pipelines/narrative_ci/config/tiers.yml"); + const lex = readConfig("intelgraph/pipelines/narrative_ci/config/lexicons.yml"); + const cur = readJson("fixtures/feb07_2026/current_snapshot.json"); + + const artById = new Map(artifacts.map((a) => [a.artifact_id, a])); + const allowed = new Set(tiers.allowed_tier_labels ?? []); + const rank = (tier: string) => tiers.tiers?.[tier]?.rank ?? 0; + + const candidates: Array> = []; + + for (const clm of cur.claims) { + const arts = (clm.supporting_artifact_ids || []) + .map((id) => artById.get(id)) + .filter(Boolean) as Artifact[]; + if (!arts.length) continue; + + const maxTier = arts + .map((a) => a.outlet_tier) + .sort((a, b) => rank(b) - rank(a))[0]; + if (!allowed.has(maxTier)) continue; + + const tierJumpScore = Math.min(1, (rank(maxTier) - rank("fringe")) / 50); + + const hedging = countMarkers(clm.text_norm, lex.register_markers?.hedging ?? []); + const legalistic = countMarkers(clm.text_norm, lex.register_markers?.legalistic ?? []); + const registerShiftScore = Math.min(1, (hedging + legalistic) / 6); + + const citationCircularityScore = 0; + + const score = Math.max( + 0, + Math.min(1, 0.45 * tierJumpScore + 0.35 * registerShiftScore + 0.2 * citationCircularityScore), + ); + + candidates.push({ + handoff_id: `handoff_${sha256Hex(`${clm.claim_hash}:${maxTier}`).slice(0, 16)}`, + narrative_id: inferNarrativeId(clm.claim_id), + from_tier: "fringe", + to_tier: maxTier, + score, + supporting_artifacts: arts.map((a) => ({ + artifact_id: a.artifact_id, + uri: a.uri, + outlet: a.outlet, + outlet_tier: a.outlet_tier, + })), + features: { tierJumpScore, registerShiftScore, citationCircularityScore }, + }); + } + + const outKey = sha256Hex(stableStringify({ run: cur.run_id, candidates, cfg: "tiers+lex_v1" })); + writeJsonDeterministic(`out/handoff/${outKey}.json`, { + version: 1, + run_id: cur.run_id, + candidates, + }); +} + +function inferNarrativeId(claimId: string): string { + if (claimId.includes("shadow")) return "narr_shadow_campaigns"; + if (claimId.includes("signal")) return "narr_signal_phishing"; + return "narr_alekseyev_hit"; +} + +if (require.main === module) { + main(); +} diff --git a/intelgraph/pipelines/narrative_ci/steps/40_state_machine.ts b/intelgraph/pipelines/narrative_ci/steps/40_state_machine.ts new file mode 100644 index 00000000000..3a8a923bd8b --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/40_state_machine.ts @@ -0,0 +1,61 @@ +import { readJson, writeJsonDeterministic } from "../lib/io"; +import { sha256Hex } from "../lib/hash"; +import { stableStringify } from "../lib/json_stable"; + +type Snapshot = { + run_id: string; + narratives: { narrative_id: string; state: string; title: string }[]; +}; + +type HandoffOut = { run_id: string; candidates: { narrative_id: string; score: number }[] }; + +export function main() { + const cur = readJson("fixtures/feb07_2026/current_snapshot.json"); + const handoff = readLatestHandoff(cur.run_id); + + const transitions: Array> = []; + const handoffByNarr = new Map(); + for (const candidate of handoff?.candidates ?? []) { + handoffByNarr.set( + candidate.narrative_id, + Math.max(handoffByNarr.get(candidate.narrative_id) ?? 0, candidate.score), + ); + } + + for (const narrative of cur.narratives) { + const handoffScore = handoffByNarr.get(narrative.narrative_id) ?? 0; + const from_state = narrative.state; + let to_state = from_state; + + if (handoffScore >= 0.8) { + to_state = "Institutionalized"; + } else if (handoffScore >= 0.6 && from_state === "Seeded") { + to_state = "Contested"; + } + + if (to_state !== from_state) { + transitions.push({ + narrative_id: narrative.narrative_id, + title: narrative.title, + from_state, + to_state, + trigger_scores: { handoff_score: handoffScore }, + }); + } + } + + const outKey = sha256Hex(stableStringify({ run: cur.run_id, transitions })); + writeJsonDeterministic(`out/state/${outKey}.json`, { + version: 1, + run_id: cur.run_id, + transitions, + }); +} + +function readLatestHandoff(run_id: string): HandoffOut { + return { run_id, candidates: [] }; +} + +if (require.main === module) { + main(); +} diff --git a/intelgraph/pipelines/narrative_ci/steps/50_bundle_evidence.ts b/intelgraph/pipelines/narrative_ci/steps/50_bundle_evidence.ts new file mode 100644 index 00000000000..aea3c020622 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/50_bundle_evidence.ts @@ -0,0 +1,72 @@ +import { readJson, writeJsonDeterministic, writeStampJson } from "../lib/io"; +import { sha256Hex } from "../lib/hash"; + +type EvidenceIndex = { version: number; evidence: Record }; + +function addEvidence(index: EvidenceIndex, id: string, files: string[]) { + index.evidence[id] = { files: [...files].sort() }; +} + +export function main() { + const itemSlug = "SITUPDATE-2026-02-07"; + + const idxPath = "evidence/index.json"; + const idx = readJson(idxPath); + + const evDelta = `EVD-${itemSlug}-DELTA-001`; + const evHandoff = `EVD-${itemSlug}-HANDOFF-001`; + const evState = `EVD-${itemSlug}-STATE-001`; + const evGates = `EVD-${itemSlug}-GATES-001`; + + const base = (id: string) => `evidence/${id}`; + + for (const id of [evDelta, evHandoff, evState, evGates]) { + writeJsonDeterministic(`${base(id)}/report.json`, { + evidence_id: id, + subject: { type: "narrative_ci_fixture", name: itemSlug }, + result: "pass", + artifacts: [], + }); + + writeJsonDeterministic(`${base(id)}/metrics.json`, { + evidence_id: id, + metrics: { fixture: true }, + }); + + writeStampJson(`${base(id)}/stamp.json`); + } + + addEvidence(idx, evDelta, [ + `${base(evDelta)}/report.json`, + `${base(evDelta)}/metrics.json`, + `${base(evDelta)}/stamp.json`, + ]); + addEvidence(idx, evHandoff, [ + `${base(evHandoff)}/report.json`, + `${base(evHandoff)}/metrics.json`, + `${base(evHandoff)}/stamp.json`, + ]); + addEvidence(idx, evState, [ + `${base(evState)}/report.json`, + `${base(evState)}/metrics.json`, + `${base(evState)}/stamp.json`, + ]); + addEvidence(idx, evGates, [ + `${base(evGates)}/report.json`, + `${base(evGates)}/metrics.json`, + `${base(evGates)}/stamp.json`, + ]); + + writeJsonDeterministic(idxPath, idx); + + const runKey = sha256Hex(itemSlug).slice(0, 12); + writeJsonDeterministic(`out/run_manifest/${runKey}.json`, { + version: 1, + item: itemSlug, + evidence_ids: [evDelta, evHandoff, evState, evGates], + }); +} + +if (require.main === module) { + main(); +} diff --git a/intelgraph/pipelines/narrative_ci/steps/60_extract_delta.ts b/intelgraph/pipelines/narrative_ci/steps/60_extract_delta.ts new file mode 100644 index 00000000000..56de049f2cd --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/60_extract_delta.ts @@ -0,0 +1,47 @@ +import { readJson, writeJsonDeterministic } from "../lib/io"; +import { sha256Hex } from "../lib/hash"; +import { stableStringify } from "../lib/json_stable"; + +type Snapshot = { + run_id: string; + narratives: { narrative_id: string; state: string }[]; + claims: { claim_id: string; claim_hash: string }[]; +}; + +export function main() { + const prev = readJson("fixtures/feb07_2026/previous_snapshot.json"); + const cur = readJson("fixtures/feb07_2026/current_snapshot.json"); + + const prevClaims = new Map(prev.claims.map((c) => [c.claim_hash, c])); + const curClaims = new Map(cur.claims.map((c) => [c.claim_hash, c])); + + const new_claims = [...curClaims.keys()].filter((hash) => !prevClaims.has(hash)); + const removed_claims = [...prevClaims.keys()].filter((hash) => !curClaims.has(hash)); + const updated_claims: string[] = []; + + const prevNarr = new Map(prev.narratives.map((n) => [n.narrative_id, n.state])); + const state_transitions = cur.narratives + .filter((n) => (prevNarr.get(n.narrative_id) ?? null) !== n.state) + .map((n) => ({ + narrative_id: n.narrative_id, + from_state: prevNarr.get(n.narrative_id) ?? null, + to_state: n.state, + })); + + const payload = { + version: 1, + prev_run_id: prev.run_id, + cur_run_id: cur.run_id, + new_claims, + removed_claims, + updated_claims, + state_transitions, + }; + + const outKey = sha256Hex(stableStringify(payload)); + writeJsonDeterministic(`out/delta/${outKey}.json`, payload); +} + +if (require.main === module) { + main(); +} diff --git a/intelgraph/pipelines/narrative_ci/steps/70_early_warning.ts b/intelgraph/pipelines/narrative_ci/steps/70_early_warning.ts new file mode 100644 index 00000000000..17a43d655c3 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/70_early_warning.ts @@ -0,0 +1,38 @@ +import { readJson, writeJsonDeterministic } from "../lib/io"; +import { sha256Hex } from "../lib/hash"; +import { stableStringify } from "../lib/json_stable"; + +type HandoffCandidate = { narrative_id: string; to_tier: string; score: number }; + +type HandoffOut = { run_id: string; candidates: HandoffCandidate[] }; + +export function main() { + const cur = readJson<{ run_id: string }>("fixtures/feb07_2026/current_snapshot.json"); + const handoff: HandoffOut = { run_id: cur.run_id, candidates: [] }; + + const indicators: Array> = []; + + for (const candidate of handoff.candidates) { + if (candidate.score >= 0.7) { + indicators.push({ + indicator_id: `ew_${sha256Hex(`${candidate.narrative_id}:${candidate.to_tier}`).slice(0, 12)}`, + narrative_id: candidate.narrative_id, + kind: "tier_handoff_watch", + severity: candidate.score >= 0.85 ? "high" : "medium", + rationale: + "Handoff score exceeded threshold; monitor for institutional uptake and policy discussion artifacts.", + }); + } + } + + const outKey = sha256Hex(stableStringify({ run: cur.run_id, indicators })); + writeJsonDeterministic(`out/early_warning/${outKey}.json`, { + version: 1, + run_id: cur.run_id, + indicators, + }); +} + +if (require.main === module) { + main(); +} diff --git a/intelgraph/schema/narrative.graph.yml b/intelgraph/schema/narrative.graph.yml new file mode 100644 index 00000000000..1ef2501a5fe --- /dev/null +++ b/intelgraph/schema/narrative.graph.yml @@ -0,0 +1,30 @@ +version: 1 +nodes: + Artifact: + key: artifact_id + props: [artifact_id, kind, uri, content_sha256, outlet, outlet_tier, published_at, lang] + Narrative: + key: narrative_id + props: [narrative_id, title, state] + Frame: + key: frame_id + props: [frame_id, label, stance] + Claim: + key: claim_id + props: [claim_id, text_norm, claim_hash, verifiability, confidence] + Assumption: + key: assumption_id + props: [assumption_id, text_norm, assumption_hash, stability, confidence] + Handoff: + key: handoff_id + props: [handoff_id, from_tier, to_tier, score, narrative_id] + +edges: + COMPOSED_OF: { from: Narrative, to: Frame, props: [weight] } + EXPRESSES: { from: Artifact, to: Frame, props: [extract_confidence] } + MAKES: { from: Artifact, to: Claim, props: [extract_confidence] } + SUPPORTED_BY: { from: Frame, to: Assumption, props: [support_weight] } + HANDOFF_TO: { from: Narrative, to: Handoff, props: [] } + +provenance: + receipt_node: ProvenanceReceipt diff --git a/schemas/narrative/delta.schema.json b/schemas/narrative/delta.schema.json new file mode 100644 index 00000000000..a1ba24199cb --- /dev/null +++ b/schemas/narrative/delta.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Narrative Delta", + "type": "object", + "required": ["version", "prev_run_id", "cur_run_id", "new_claims", "removed_claims", "updated_claims", "state_transitions"], + "properties": { + "version": { "type": "integer" }, + "prev_run_id": { "type": "string" }, + "cur_run_id": { "type": "string" }, + "new_claims": { "type": "array", "items": { "type": "string" } }, + "removed_claims": { "type": "array", "items": { "type": "string" } }, + "updated_claims": { "type": "array", "items": { "type": "string" } }, + "state_transitions": { + "type": "array", + "items": { + "type": "object", + "required": ["narrative_id", "from_state", "to_state"], + "properties": { + "narrative_id": { "type": "string" }, + "from_state": { "type": ["string", "null"] }, + "to_state": { "type": "string" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/schemas/narrative/edges.schema.json b/schemas/narrative/edges.schema.json new file mode 100644 index 00000000000..8198dfdfdea --- /dev/null +++ b/schemas/narrative/edges.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Narrative Edges", + "type": "object", + "required": ["version", "edges"], + "properties": { + "version": { "type": "integer" }, + "edges": { + "type": "array", + "items": { + "type": "object", + "required": ["type", "from", "to"], + "properties": { + "type": { "type": "string" }, + "from": { "type": "string" }, + "to": { "type": "string" }, + "props": { "type": "object" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/schemas/narrative/handoff.schema.json b/schemas/narrative/handoff.schema.json new file mode 100644 index 00000000000..65958b7e446 --- /dev/null +++ b/schemas/narrative/handoff.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Narrative Handoff", + "type": "object", + "required": ["version", "run_id", "candidates"], + "properties": { + "version": { "type": "integer" }, + "run_id": { "type": "string" }, + "candidates": { + "type": "array", + "items": { + "type": "object", + "required": ["handoff_id", "narrative_id", "from_tier", "to_tier", "score"], + "properties": { + "handoff_id": { "type": "string" }, + "narrative_id": { "type": "string" }, + "from_tier": { "type": "string" }, + "to_tier": { "type": "string" }, + "score": { "type": "number" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/schemas/narrative/metrics.schema.json b/schemas/narrative/metrics.schema.json new file mode 100644 index 00000000000..debf0298150 --- /dev/null +++ b/schemas/narrative/metrics.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Narrative Metrics", + "type": "object", + "required": ["evidence_id", "metrics"], + "properties": { + "evidence_id": { "type": "string" }, + "metrics": { "type": "object" } + }, + "additionalProperties": true +} diff --git a/schemas/narrative/nodes.schema.json b/schemas/narrative/nodes.schema.json new file mode 100644 index 00000000000..74935b2928a --- /dev/null +++ b/schemas/narrative/nodes.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Narrative Nodes", + "type": "object", + "required": ["version", "nodes"], + "properties": { + "version": { "type": "integer" }, + "nodes": { + "type": "array", + "items": { + "type": "object", + "required": ["type", "id"], + "properties": { + "type": { "type": "string" }, + "id": { "type": "string" }, + "props": { "type": "object" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/schemas/narrative/provenance_receipt.schema.json b/schemas/narrative/provenance_receipt.schema.json new file mode 100644 index 00000000000..eabefd48519 --- /dev/null +++ b/schemas/narrative/provenance_receipt.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Narrative Provenance Receipt", + "type": "object", + "required": ["version", "receipts"], + "properties": { + "version": { "type": "integer" }, + "receipts": { + "type": "array", + "items": { + "type": "object", + "required": ["target", "sources"], + "properties": { + "receipt_id": { "type": "string" }, + "target": { + "type": "object", + "required": ["type", "id"], + "properties": { + "type": { "type": "string" }, + "id": { "type": "string" } + } + }, + "sources": { + "type": "array", + "items": { + "type": "object", + "required": ["artifact_id", "content_sha256"], + "properties": { + "artifact_id": { "type": "string" }, + "content_sha256": { "type": "string" } + } + } + }, + "confidence": { "type": "number" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/schemas/narrative/state.schema.json b/schemas/narrative/state.schema.json new file mode 100644 index 00000000000..838f698135a --- /dev/null +++ b/schemas/narrative/state.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Narrative State Transitions", + "type": "object", + "required": ["version", "run_id", "transitions"], + "properties": { + "version": { "type": "integer" }, + "run_id": { "type": "string" }, + "transitions": { + "type": "array", + "items": { + "type": "object", + "required": ["narrative_id", "from_state", "to_state"], + "properties": { + "narrative_id": { "type": "string" }, + "from_state": { "type": "string" }, + "to_state": { "type": "string" }, + "trigger_scores": { "type": "object" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +}