From 3137e9964ec34abe4b594b624250e0762b5f6898 Mon Sep 17 00:00:00 2001 From: BrianCLong <6404035+BrianCLong@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:05:32 -0700 Subject: [PATCH] fix: align narrative state outputs with schema --- .../policies/narrative_ci/determinism.rego | 27 ++++ .../fixtures/determinism_fail.json | 5 + .../fixtures/determinism_pass.json | 5 + .../fixtures/traceability_fail.json | 6 + .../fixtures/traceability_pass.json | 10 ++ .../policies/narrative_ci/traceability.rego | 27 ++++ .github/workflows/narrative-ci.yml | 42 +++++ docs/roadmap/STATUS.json | 12 +- evidence/index.json | 10 +- intelgraph/pipelines/narrative_ci/README.md | 34 ++++ .../narrative_ci/config/defaults.yml | 26 ++++ intelgraph/pipelines/narrative_ci/lib/hash.ts | 5 + intelgraph/pipelines/narrative_ci/lib/ids.ts | 11 ++ .../pipelines/narrative_ci/lib/json_stable.ts | 28 ++++ .../narrative_ci/lib/schema_validate.ts | 74 +++++++++ .../narrative_ci/steps/30_score_seeding.ts | 23 +++ .../narrative_ci/steps/31_score_handoff.ts | 23 +++ .../steps/32_score_compression.ts | 23 +++ .../narrative_ci/steps/40_state_machine.ts | 24 +++ .../narrative_ci/steps/50_bundle_evidence.ts | 87 +++++++++++ intelgraph/schema/narrative.graph.yml | 147 ++++++++++++++++++ schemas/narrative/narrative_graph.schema.json | 42 +++++ .../narrative/narrative_metrics.schema.json | 19 +++ schemas/narrative/narrative_state.schema.json | 26 ++++ schemas/narrative/narrative_tests.schema.json | 24 +++ .../narrative/provenance_receipt.schema.json | 65 ++++++++ 26 files changed, 821 insertions(+), 4 deletions(-) create mode 100644 .github/policies/narrative_ci/determinism.rego create mode 100644 .github/policies/narrative_ci/fixtures/determinism_fail.json create mode 100644 .github/policies/narrative_ci/fixtures/determinism_pass.json create mode 100644 .github/policies/narrative_ci/fixtures/traceability_fail.json create mode 100644 .github/policies/narrative_ci/fixtures/traceability_pass.json create mode 100644 .github/policies/narrative_ci/traceability.rego create mode 100644 .github/workflows/narrative-ci.yml create mode 100644 intelgraph/pipelines/narrative_ci/README.md create mode 100644 intelgraph/pipelines/narrative_ci/config/defaults.yml create mode 100644 intelgraph/pipelines/narrative_ci/lib/hash.ts create mode 100644 intelgraph/pipelines/narrative_ci/lib/ids.ts create mode 100644 intelgraph/pipelines/narrative_ci/lib/json_stable.ts create mode 100644 intelgraph/pipelines/narrative_ci/lib/schema_validate.ts create mode 100644 intelgraph/pipelines/narrative_ci/steps/30_score_seeding.ts create mode 100644 intelgraph/pipelines/narrative_ci/steps/31_score_handoff.ts create mode 100644 intelgraph/pipelines/narrative_ci/steps/32_score_compression.ts create mode 100644 intelgraph/pipelines/narrative_ci/steps/40_state_machine.ts create mode 100644 intelgraph/pipelines/narrative_ci/steps/50_bundle_evidence.ts create mode 100644 intelgraph/schema/narrative.graph.yml create mode 100644 schemas/narrative/narrative_graph.schema.json create mode 100644 schemas/narrative/narrative_metrics.schema.json create mode 100644 schemas/narrative/narrative_state.schema.json create mode 100644 schemas/narrative/narrative_tests.schema.json create mode 100644 schemas/narrative/provenance_receipt.schema.json diff --git a/.github/policies/narrative_ci/determinism.rego b/.github/policies/narrative_ci/determinism.rego new file mode 100644 index 00000000000..7eb9e2af3d3 --- /dev/null +++ b/.github/policies/narrative_ci/determinism.rego @@ -0,0 +1,27 @@ +package narrative_ci.determinism + +import future.keywords.in + +default deny = [] + +forbidden_keys := {"ts", "timestamp", "created_at", "updated_at"} + +deny[msg] { + payload := input.payloads[_] + walk(payload, [path, value]) + is_object(value) + some key + key := object.keys(value)[_] + key in forbidden_keys + msg := sprintf("forbidden timestamp field %s at %v", [key, path]) +} + +test_determinism_pass { + input := data.fixtures.determinism_pass + count(deny with input as input) == 0 +} + +test_determinism_fail { + input := data.fixtures.determinism_fail + count(deny with input as input) > 0 +} 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..1e0a560035f --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/determinism_fail.json @@ -0,0 +1,5 @@ +{ + "payloads": [ + { "run_id": "fixture-run", "created_at": "2026-01-01T00: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..ac1d848c296 --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/determinism_pass.json @@ -0,0 +1,5 @@ +{ + "payloads": [ + { "run_id": "fixture-run", "values": [1, 2, 3] } + ] +} 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..67556e5f1cc --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/traceability_fail.json @@ -0,0 +1,6 @@ +{ + "inferred_nodes": [ + { "id": "claim-1", "type": "Claim" } + ], + "provenance_edges": [] +} 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..fc4408f0651 --- /dev/null +++ b/.github/policies/narrative_ci/fixtures/traceability_pass.json @@ -0,0 +1,10 @@ +{ + "inferred_nodes": [ + { "id": "claim-1", "type": "Claim" }, + { "id": "frame-1", "type": "Frame" } + ], + "provenance_edges": [ + { "from_type": "Artifact", "to": "claim-1" }, + { "from_type": "Artifact", "to": "frame-1" } + ] +} diff --git a/.github/policies/narrative_ci/traceability.rego b/.github/policies/narrative_ci/traceability.rego new file mode 100644 index 00000000000..7836b32fcdf --- /dev/null +++ b/.github/policies/narrative_ci/traceability.rego @@ -0,0 +1,27 @@ +package narrative_ci.traceability + +import future.keywords.in + +default deny = [] + +deny[msg] { + node := input.inferred_nodes[_] + not has_provenance(node) + msg := sprintf("missing provenance for %s:%s", [node.type, node.id]) +} + +has_provenance(node) { + edge := input.provenance_edges[_] + edge.to == node.id + edge.from_type == "Artifact" +} + +test_traceability_pass { + input := data.fixtures.traceability_pass + count(deny with input as input) == 0 +} + +test_traceability_fail { + input := data.fixtures.traceability_fail + count(deny with input as input) > 0 +} diff --git a/.github/workflows/narrative-ci.yml b/.github/workflows/narrative-ci.yml new file mode 100644 index 00000000000..fee80e2dc5e --- /dev/null +++ b/.github/workflows/narrative-ci.yml @@ -0,0 +1,42 @@ +name: narrative-ci +on: + workflow_dispatch: {} + pull_request: + paths: + - "intelgraph/pipelines/narrative_ci/**" + - "intelgraph/schema/**" + - "schemas/narrative/**" + - ".github/policies/narrative_ci/**" + schedule: + - cron: "17 3 * * *" + +jobs: + verify: + runs-on: ubuntu-latest + permissions: + contents: read + env: + NARRATIVE_CI_ENABLED: "false" + steps: + - uses: actions/checkout@v4 + - name: Setup OPA + uses: open-policy-agent/setup-opa@950f159a49aa91f9323f36f1de81c7f6b5de9576 + - name: Run fixture pipeline + run: | + npx tsx intelgraph/pipelines/narrative_ci/steps/50_bundle_evidence.ts --fixture + - name: Validate schemas + run: | + npx tsx intelgraph/pipelines/narrative_ci/lib/schema_validate.ts out schemas/narrative + - name: OPA policy tests + run: | + opa test .github/policies/narrative_ci -v + - name: Determinism gate (no timestamps) + run: | + ! rg -n '"ts"|"timestamp"|"created_at"|"updated_at"' out || (echo "timestamp-like fields found in deterministic outputs" && exit 1) + - name: Upload evidence + uses: actions/upload-artifact@v4 + with: + name: narrative-ci-evidence + path: | + evidence/** + out/** diff --git a/docs/roadmap/STATUS.json b/docs/roadmap/STATUS.json index 281d973af71..eb8195f5a75 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-10T12:00:00Z", + "revision_note": "Refined narrative CI lane-1 scaffold with state schema and deterministic output path.", "initiatives": [ { "id": "adenhq-hive-subsumption-lane1", @@ -193,6 +193,12 @@ "status": "in_progress", "owner": "codex", "notes": "Documented CI/CD high-signal deltas with enforced action register and evidence targets." + }, + { + "id": "narrative-ci-lane1", + "status": "in_progress", + "owner": "codex", + "notes": "Scaffolded narrative CI lane-1 pipeline, schemas, and policy gates with deterministic evidence bundling." } ], "summary": { @@ -200,7 +206,7 @@ "partial": 2, "incomplete": 0, "not_started": 5, - "total": 17, + "total": 21, "ga_blockers": [] } } diff --git a/evidence/index.json b/evidence/index.json index 4b113aa9959..3169da5437d 100644 --- a/evidence/index.json +++ b/evidence/index.json @@ -400,6 +400,14 @@ "metrics": "evidence/EVD-INTSUM-2026-THREAT-HORIZON-001/metrics.json", "stamp": "evidence/EVD-INTSUM-2026-THREAT-HORIZON-001/stamp.json" } + }, + { + "evidence_id": "EVD-NARRATIVE-CI-METRICS-001", + "files": { + "report": "evidence/EVD-NARRATIVE-CI-METRICS-001/report.json", + "metrics": "evidence/EVD-NARRATIVE-CI-METRICS-001/metrics.json", + "stamp": "evidence/EVD-NARRATIVE-CI-METRICS-001/stamp.json" + } } ] -} \ No newline at end of file +} diff --git a/intelgraph/pipelines/narrative_ci/README.md b/intelgraph/pipelines/narrative_ci/README.md new file mode 100644 index 00000000000..d39530f5967 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/README.md @@ -0,0 +1,34 @@ +# Narrative CI Pipeline + +This pipeline scaffolds deterministic narrative scoring, state transitions, and evidence +bundling for Summit/IntelGraph. It is intentionally fixture-first and uses stable JSON +serialization so outputs are identical across runs with the same inputs. + +## Local run (fixture mode) + +```bash +npx tsx intelgraph/pipelines/narrative_ci/steps/50_bundle_evidence.ts --fixture +``` + +## Validate outputs + +```bash +npx tsx intelgraph/pipelines/narrative_ci/lib/schema_validate.ts out schemas/narrative +``` + +## Evidence outputs + +Each run emits: + +- `out/metrics/*.json` deterministic payloads +- `out/narratives/fixture/state_transitions.json` +- `evidence/EVD-NARRATIVE-CI-METRICS-001/{report.json,metrics.json,stamp.json}` +- `evidence/index.json` updates tracked by `50_bundle_evidence.ts` + +## Tuning thresholds + +Edit `config/defaults.yml` only. All emitted evidence includes a config hash. + +## Rollback + +Set `NARRATIVE_CI_ENABLED=false` to disable the workflow and block graph writes. diff --git a/intelgraph/pipelines/narrative_ci/config/defaults.yml b/intelgraph/pipelines/narrative_ci/config/defaults.yml new file mode 100644 index 00000000000..f6bd7394e01 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/config/defaults.yml @@ -0,0 +1,26 @@ +version: 1 +thresholds: + seeding_density_high: 0.65 + handoff_score_high: 0.7 + compression_ratio_high: 0.6 + contested_counterclaim_rate: 0.4 + dormant_volume_runs: 4 + reactivation_similarity: 0.8 +register_shift: + dictionary: + - "it is hereby" + - "pursuant to" + - "hereby" + - "therefore" + - "compliance" +tier_jump_scores: + fringe_to_mainstream: 1.0 + adjacent: 0.5 + same: 0.1 +citation_circularity: + denylist: + - "lowcred.example" + - "mirror.example" + allowlist: + - "gov.example" + - "ngo.example" diff --git a/intelgraph/pipelines/narrative_ci/lib/hash.ts b/intelgraph/pipelines/narrative_ci/lib/hash.ts new file mode 100644 index 00000000000..a1782e42abf --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/lib/hash.ts @@ -0,0 +1,5 @@ +import { createHash } from 'node:crypto'; + +export function sha256(value: string | Buffer): string { + return createHash('sha256').update(value).digest('hex'); +} diff --git a/intelgraph/pipelines/narrative_ci/lib/ids.ts b/intelgraph/pipelines/narrative_ci/lib/ids.ts new file mode 100644 index 00000000000..a19431980ab --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/lib/ids.ts @@ -0,0 +1,11 @@ +import { createHash } from 'node:crypto'; + +export const ITEM_SLUG = 'NARRATIVE-CI'; + +export function buildEvidenceId(area: string, sequence: string): string { + return `EVD-${ITEM_SLUG}-${area}-${sequence}`; +} + +export function stableHash(input: string): string { + return createHash('sha256').update(input).digest('hex'); +} 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..b01baee6af2 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/lib/json_stable.ts @@ -0,0 +1,28 @@ +type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; + +function sortValue(value: JsonValue): JsonValue { + if (Array.isArray(value)) { + return value.map(sortValue); + } + + if (value && typeof value === 'object') { + const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b)); + const sorted: { [key: string]: JsonValue } = {}; + for (const [key, entryValue] of entries) { + sorted[key] = sortValue(entryValue as JsonValue); + } + return sorted; + } + + return value; +} + +export function stableStringify(value: JsonValue): string { + return JSON.stringify(sortValue(value)); +} diff --git a/intelgraph/pipelines/narrative_ci/lib/schema_validate.ts b/intelgraph/pipelines/narrative_ci/lib/schema_validate.ts new file mode 100644 index 00000000000..142ea21ae4d --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/lib/schema_validate.ts @@ -0,0 +1,74 @@ +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; + +const [outDir, schemaDir] = process.argv.slice(2); + +if (!outDir || !schemaDir) { + console.error('Usage: schema_validate.ts '); + process.exit(1); +} + +const ajv = new Ajv({ allErrors: true, strict: false }); +addFormats(ajv); + +async function loadSchemas(directory: string) { + const files = await readdir(directory); + const schemas = await Promise.all( + files + .filter((file) => file.endsWith('.json')) + .map(async (file) => { + const schemaPath = path.join(directory, file); + const contents = await readFile(schemaPath, 'utf-8'); + return { name: path.basename(file, '.json'), schema: JSON.parse(contents) }; + }), + ); + + for (const entry of schemas) { + if (entry.schema.$id) { + ajv.addSchema(entry.schema); + } + } + + return schemas; +} + +async function validateOutputs( + directory: string, + schemas: Array<{ name: string; schema: unknown }>, +) { + const files = await readdir(directory, { withFileTypes: true }); + for (const file of files) { + const fullPath = path.join(directory, file.name); + if (file.isDirectory()) { + await validateOutputs(fullPath, schemas); + continue; + } + if (!file.name.endsWith('.json')) { + continue; + } + const payload = JSON.parse(await readFile(fullPath, 'utf-8')); + const matching = schemas.find((entry) => { + const schemaObj = entry.schema as { ['x-targets']?: string[] }; + if (schemaObj && Array.isArray(schemaObj['x-targets'])) { + return schemaObj['x-targets'].some((target) => fullPath.endsWith(target)); + } + return fullPath.includes(entry.name); + }); + if (!matching) { + continue; + } + const validate = ajv.compile(matching.schema); + const valid = validate(payload); + if (!valid) { + console.error(`Schema validation failed for ${fullPath}`); + console.error(validate.errors); + process.exit(1); + } + } +} + +const schemas = await loadSchemas(schemaDir); +await validateOutputs(outDir, schemas); +console.log('Schema validation succeeded'); diff --git a/intelgraph/pipelines/narrative_ci/steps/30_score_seeding.ts b/intelgraph/pipelines/narrative_ci/steps/30_score_seeding.ts new file mode 100644 index 00000000000..80e9b98b97c --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/30_score_seeding.ts @@ -0,0 +1,23 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { sha256 } from '../lib/hash.js'; +import { stableStringify } from '../lib/json_stable.js'; + +const outDir = path.resolve('out/metrics'); +await mkdir(outDir, { recursive: true }); + +const configPath = path.resolve('intelgraph/pipelines/narrative_ci/config/defaults.yml'); +const configContents = await readFile(configPath, 'utf-8'); +const payload = { + run_id: 'fixture-run', + config_hash: sha256(configContents), + scores: [], +}; + +await writeFile( + path.join(outDir, 'seeding_density.json'), + `${stableStringify(payload)}\n`, + 'utf-8', +); + +console.log('Seeding density scores written'); 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..06cbdecf28e --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/31_score_handoff.ts @@ -0,0 +1,23 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { sha256 } from '../lib/hash.js'; +import { stableStringify } from '../lib/json_stable.js'; + +const outDir = path.resolve('out/metrics'); +await mkdir(outDir, { recursive: true }); + +const configPath = path.resolve('intelgraph/pipelines/narrative_ci/config/defaults.yml'); +const configContents = await readFile(configPath, 'utf-8'); +const payload = { + run_id: 'fixture-run', + config_hash: sha256(configContents), + handoff_candidates: [], +}; + +await writeFile( + path.join(outDir, 'handoff_candidates.json'), + `${stableStringify(payload)}\n`, + 'utf-8', +); + +console.log('Handoff candidates written'); diff --git a/intelgraph/pipelines/narrative_ci/steps/32_score_compression.ts b/intelgraph/pipelines/narrative_ci/steps/32_score_compression.ts new file mode 100644 index 00000000000..93c80723622 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/32_score_compression.ts @@ -0,0 +1,23 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { sha256 } from '../lib/hash.js'; +import { stableStringify } from '../lib/json_stable.js'; + +const outDir = path.resolve('out/metrics'); +await mkdir(outDir, { recursive: true }); + +const configPath = path.resolve('intelgraph/pipelines/narrative_ci/config/defaults.yml'); +const configContents = await readFile(configPath, 'utf-8'); +const payload = { + run_id: 'fixture-run', + config_hash: sha256(configContents), + compression_ratios: [], +}; + +await writeFile( + path.join(outDir, 'compression_ratio.json'), + `${stableStringify(payload)}\n`, + 'utf-8', +); + +console.log('Compression ratios written'); 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..8c1bba59a35 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/40_state_machine.ts @@ -0,0 +1,24 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { sha256 } from '../lib/hash.js'; +import { stableStringify } from '../lib/json_stable.js'; + +const outDir = path.resolve('out/narratives/fixture'); +await mkdir(outDir, { recursive: true }); + +const configPath = path.resolve('intelgraph/pipelines/narrative_ci/config/defaults.yml'); +const configContents = await readFile(configPath, 'utf-8'); + +const payload = { + run_id: 'fixture-run', + config_hash: sha256(configContents), + transitions: [], +}; + +await writeFile( + path.join(outDir, 'state_transitions.json'), + `${stableStringify(payload)}\n`, + 'utf-8', +); + +console.log('Narrative state transitions written'); 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..235ac33bb69 --- /dev/null +++ b/intelgraph/pipelines/narrative_ci/steps/50_bundle_evidence.ts @@ -0,0 +1,87 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { buildEvidenceId } from '../lib/ids.js'; +import { sha256 } from '../lib/hash.js'; +import { stableStringify } from '../lib/json_stable.js'; + +const args = new Set(process.argv.slice(2)); +const fixtureMode = args.has('--fixture'); + +if (fixtureMode) { + await import('./30_score_seeding.js'); + await import('./31_score_handoff.js'); + await import('./32_score_compression.js'); + await import('./40_state_machine.js'); +} + +const evidenceId = buildEvidenceId('METRICS', '001'); +const evidenceDir = path.resolve('evidence', evidenceId); +await mkdir(evidenceDir, { recursive: true }); + +const configPath = path.resolve('intelgraph/pipelines/narrative_ci/config/defaults.yml'); +const configContents = await readFile(configPath, 'utf-8'); +const configHash = sha256(configContents); + +const report = { + evidence_id: evidenceId, + run_id: 'fixture-run', + config_hash: configHash, + outputs: { + seeding_density: 'out/metrics/seeding_density.json', + handoff_candidates: 'out/metrics/handoff_candidates.json', + compression_ratio: 'out/metrics/compression_ratio.json', + state_transitions: 'out/narratives/fixture/state_transitions.json', + }, +}; + +const metrics = { + run_id: 'fixture-run', + totals: { + narratives_scored: 0, + handoff_candidates: 0, + }, +}; + +const stamp = { + generated_at: new Date().toISOString(), + run_id: 'fixture-run', +}; + +await writeFile( + path.join(evidenceDir, 'report.json'), + `${stableStringify(report)}\n`, + 'utf-8', +); +await writeFile( + path.join(evidenceDir, 'metrics.json'), + `${stableStringify(metrics)}\n`, + 'utf-8', +); +await writeFile( + path.join(evidenceDir, 'stamp.json'), + `${JSON.stringify(stamp, null, 2)}\n`, + 'utf-8', +); + +const indexPath = path.resolve('evidence/index.json'); +const indexContents = await readFile(indexPath, 'utf-8'); +const index = JSON.parse(indexContents) as { + version: number; + items: Array<{ evidence_id: string; files: Record }>; +}; + +const existing = index.items.some((item) => item.evidence_id === evidenceId); +if (!existing) { + index.items.push({ + evidence_id: evidenceId, + files: { + report: `evidence/${evidenceId}/report.json`, + metrics: `evidence/${evidenceId}/metrics.json`, + stamp: `evidence/${evidenceId}/stamp.json`, + }, + }); +} + +await writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf-8'); + +console.log(`Evidence bundle written: ${evidenceId}`); diff --git a/intelgraph/schema/narrative.graph.yml b/intelgraph/schema/narrative.graph.yml new file mode 100644 index 00000000000..0fe8265acc9 --- /dev/null +++ b/intelgraph/schema/narrative.graph.yml @@ -0,0 +1,147 @@ +version: 1 +nodes: + Artifact: + key: artifact_id + props: + - artifact_id + - kind + - uri + - content_sha256 + - published_at + - lang + - outlet_tier + Actor: + key: actor_id + props: + - actor_id + - kind + - handle + - org + - credibility_tier + Community: + key: community_id + props: + - community_id + - method + - cohesion + - size_est + Event: + key: event_id + props: + - event_id + - kind + - occurred_at + - external_ref + Narrative: + key: narrative_id + props: + - narrative_id + - title + - state + - created_run_id + Frame: + key: frame_id + props: + - frame_id + - label + - stance + - blame_target + Claim: + key: claim_id + props: + - claim_id + - text_norm + - verifiability + - confidence + Assumption: + key: assumption_id + props: + - assumption_id + - text_norm + - stability + - confidence + Handoff: + key: handoff_id + props: + - handoff_id + - from_tier + - to_tier + - score + - triggered_run_id + ProvenanceReceipt: + key: receipt_id + props: + - receipt_id + - target_type + - target_id + - method + - model + - confidence +edges: + PUBLISHED: + from: Actor + to: Artifact + props: + - published_at + AMPLIFIED: + from: Community + to: Artifact + props: + - weight + MAKES: + from: Artifact + to: Claim + props: + - extract_confidence + EXPRESSES: + from: Artifact + to: Frame + props: + - extract_confidence + SUPPORTED_BY: + from: Frame + to: Assumption + props: + - support_weight + COMPOSED_OF: + from: Narrative + to: Frame + props: + - weight + TRIGGERED_BY: + from: Narrative + to: Event + props: + - weight + HANDOFF_TO: + from: Narrative + to: Handoff + props: [] + DISPUTED_BY: + from: Claim + to: Claim + props: + - stance + BROKEN_BY: + from: Assumption + to: Claim + props: + - mode + HAS_PROVENANCE: + from: ProvenanceReceipt + to: Claim + props: [] + HAS_PROVENANCE_ASSUMPTION: + from: ProvenanceReceipt + to: Assumption + props: [] + HAS_PROVENANCE_FRAME: + from: ProvenanceReceipt + to: Frame + props: [] + HAS_PROVENANCE_NARRATIVE: + from: ProvenanceReceipt + to: Narrative + props: [] +provenance: + receipt_node: ProvenanceReceipt diff --git a/schemas/narrative/narrative_graph.schema.json b/schemas/narrative/narrative_graph.schema.json new file mode 100644 index 00000000000..fe22820849e --- /dev/null +++ b/schemas/narrative/narrative_graph.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "narrative_graph", + "type": "object", + "required": ["version", "nodes", "edges"], + "properties": { + "version": { "type": "integer", "minimum": 1 }, + "nodes": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["key", "props"], + "properties": { + "key": { "type": "string" }, + "props": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + } + }, + "edges": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["from", "to", "props"], + "properties": { + "from": { "type": "string" }, + "to": { "type": "string" }, + "props": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + } + }, + "provenance": { + "type": "object", + "properties": { + "receipt_node": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/schemas/narrative/narrative_metrics.schema.json b/schemas/narrative/narrative_metrics.schema.json new file mode 100644 index 00000000000..7865fbdb23d --- /dev/null +++ b/schemas/narrative/narrative_metrics.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "narrative_metrics", + "x-targets": [ + "seeding_density.json", + "handoff_candidates.json", + "compression_ratio.json" + ], + "type": "object", + "required": ["run_id", "config_hash"], + "properties": { + "run_id": { "type": "string" }, + "config_hash": { "type": "string" }, + "scores": { "type": "array" }, + "handoff_candidates": { "type": "array" }, + "compression_ratios": { "type": "array" } + }, + "additionalProperties": false +} diff --git a/schemas/narrative/narrative_state.schema.json b/schemas/narrative/narrative_state.schema.json new file mode 100644 index 00000000000..ce5fdd94745 --- /dev/null +++ b/schemas/narrative/narrative_state.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "narrative_state", + "x-targets": ["state_transitions.json"], + "type": "object", + "required": ["run_id", "config_hash", "transitions"], + "properties": { + "run_id": { "type": "string" }, + "config_hash": { "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" }, + "score_basis": { "type": "object" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/schemas/narrative/narrative_tests.schema.json b/schemas/narrative/narrative_tests.schema.json new file mode 100644 index 00000000000..e85b3cb4035 --- /dev/null +++ b/schemas/narrative/narrative_tests.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "narrative_tests", + "x-targets": ["results.json"], + "type": "object", + "required": ["suite_id", "run_id", "results"], + "properties": { + "suite_id": { "type": "string" }, + "run_id": { "type": "string" }, + "results": { + "type": "array", + "items": { + "type": "object", + "required": ["test_id", "status"], + "properties": { + "test_id": { "type": "string" }, + "status": { "type": "string", "enum": ["pass", "fail", "skipped"] } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/schemas/narrative/provenance_receipt.schema.json b/schemas/narrative/provenance_receipt.schema.json new file mode 100644 index 00000000000..6904b1d7bf4 --- /dev/null +++ b/schemas/narrative/provenance_receipt.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "provenance_receipt", + "type": "object", + "required": ["receipt_id", "target", "sources", "method", "model", "confidence"], + "properties": { + "receipt_id": { "type": "string" }, + "target": { + "type": "object", + "required": ["type", "id"], + "properties": { + "type": { "type": "string" }, + "id": { "type": "string" } + }, + "additionalProperties": false + }, + "sources": { + "type": "array", + "items": { + "type": "object", + "required": ["artifact_id", "content_sha256"], + "properties": { + "artifact_id": { "type": "string" }, + "content_sha256": { "type": "string" }, + "spans": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start": { "type": "integer" }, + "end": { "type": "integer" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "method": { + "type": "object", + "required": ["pipeline_step", "transform_chain"], + "properties": { + "pipeline_step": { "type": "string" }, + "transform_chain": { + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false + }, + "model": { + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" } + }, + "additionalProperties": false + }, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, + "rationale_template_id": { "type": "string" } + }, + "additionalProperties": false +}