Summary
The knowledge graph silently drops the entire risk layer (node_type='risk') for every current-format session. The risk-aggregator now emits structured review-outputs/risk-summary.json, but neither the persistence layer nor the KG Phase 7 parser was updated to match — so risk-summary never reaches the reports table, and even when it does it fails to parse. Confirmed 0 risk nodes on the two most recent completed sessions, vs 22–23 on May sessions that used the older Markdown narrative.
risk-summary also feeds executive-summary synthesis (risk-aggregator.js:26 → consumedBy: ['memo-executive-summary-writer']) and is listed as a KG CRITICAL_REPORT gate (hookDBBridge.js:1359), so the impact is broader than the graph alone.
Two independent root causes (both must be fixed)
Bug #1 — Persistence gap: risk-summary.json never becomes a report
Producer writes JSON (risk-aggregator.js:25 → outputFiles: ['risk-summary.json', 'risk-aggregator-state.json']), but both persistence paths store Markdown only:
- Live hook —
src/utils/hookDBBridge.js:1444-1447:
if (tool_name === 'Write' && filePath.includes('/reports/')) {
if (filePath.endsWith('.md')) { // <-- .json never persisted as a report
await persistReport(pool, sessionCache, input, result);
}
- Backfill —
scripts/backfill-local-to-db.mjs walkMarkdown() collects .md only.
Meanwhile hookDBBridge.js:1359 waits for risk-summary as a CRITICAL_REPORT (6 retries), times out, records missing_reports: ['risk-summary'], and builds the KG with no risk input.
Bug #2 — Parser schema drift: even when persisted, the JSON doesn't parse
src/utils/knowledgeGraph/kgPhases6to8.js:380 (Phase 7 risk extraction):
const categories = parsed.risk_categories || parsed.categories || [];
The current producer keys its array as exposure_by_category (neither risk_categories nor categories), and exposures are strings ("weighted_exposure": "$433.75M", "exposure_low"/"exposure_high") rather than the numeric p50/p10/p90 fields the synth block reads (lines 391-394). Result: categories = [], fall through to the Markdown regex (Path B, no **bold** in JSON) → 0 risk nodes.
Why it regressed (Cardinal worked, June doesn't)
May sessions emitted risk-summary-narrative.md (Markdown) → hit the .md persist path and Phase 7 Path B regex. The producer was later switched to structured risk-summary.json without updating the persistence filter or the JSON parser.
Blast radius (queried against prod super_legal)
| Session |
risk nodes |
has risk report |
failing bug |
| 2026-06-16-1781644875 |
0 |
no |
#1 (+#2) |
| 2026-06-08-1780888014 |
0 |
no |
#1 (+#2) |
| 2026-05-27-1779903178 |
0 |
yes |
#2 alone |
| 2026-05-22-1779484021 |
23 |
yes |
— (Markdown narrative) |
| 2026-05-20-1779247022 |
22 |
yes |
— (Markdown narrative) |
The 2026-05-27 row is the isolated proof of Bug #2: the report was present but still yielded 0 risk nodes. June sessions fail earlier at Bug #1.
Suggested solutions
Fix #1 — persist risk-summary.json as a report (report_type='review', report_key='risk-summary'):
- Live hook
hookDBBridge.js:~1445: persist risk-summary.json (and, conservatively, scope to that filename rather than all /reports/*.json to avoid pulling in state sidecars). extractReportKey already strips .json (hookDBBridge.js:173), so key derivation is in place.
- Backfill
scripts/backfill-local-to-db.mjs: have walkMarkdown (or a sibling pass) also ingest review-outputs/risk-summary.json. Note the existing exclude set already skips *-state.json; only the structured deliverable should be ingested as a report.
Fix #2 — make Phase 7 accept the current schema (kgPhases6to8.js:380):
- Add
parsed.exposure_by_category to the category source list.
- Parse string exposure fields: read
weighted_exposure / exposure_low / exposure_high and normalize $433.75M / $2.33B → numeric before building the synth block; map string severity and probability (e.g. "8% fail") defensively.
- Unit-test with a fixture of the real
risk-summary.json shape (exposure_by_category[].findings[]) — see reports/2026-06-16-1781644875/review-outputs/risk-summary.json for a canonical example (16 findings, 9 HIGH).
Hardening (recommended):
- Add a contract test asserting the risk-aggregator output schema matches what Phase 7 / executive-summary synthesis consume (this is producer↔consumer drift; pin it).
- Consider widening the alias resolver if the producer key changes again (
kgHelpers.js:180 already aliases risk → ['risk-summary', 'risk-narrative', 'risk-assessment', 'risk-summary-narrative']).
Remediation for affected past sessions
Once both fixes land, re-ingest risk-summary.json for affected sessions and re-run the KG build (upsert) to backfill the missing risk nodes (and any downstream re-synthesis). At minimum: 2026-06-16, 2026-06-08, 2026-05-27.
Notes
- Surfaced while recovering an unpersisted session (
2026-06-16-1781644875, Fox/Roku). Diagnosis is read-only against prod; no fix applied yet pending triage.
Summary
The knowledge graph silently drops the entire risk layer (
node_type='risk') for every current-format session. The risk-aggregator now emits structuredreview-outputs/risk-summary.json, but neither the persistence layer nor the KG Phase 7 parser was updated to match — sorisk-summarynever reaches thereportstable, and even when it does it fails to parse. Confirmed0risk nodes on the two most recent completed sessions, vs 22–23 on May sessions that used the older Markdown narrative.risk-summaryalso feeds executive-summary synthesis (risk-aggregator.js:26→consumedBy: ['memo-executive-summary-writer']) and is listed as a KG CRITICAL_REPORT gate (hookDBBridge.js:1359), so the impact is broader than the graph alone.Two independent root causes (both must be fixed)
Bug #1 — Persistence gap:
risk-summary.jsonnever becomes a reportProducer writes JSON (
risk-aggregator.js:25→outputFiles: ['risk-summary.json', 'risk-aggregator-state.json']), but both persistence paths store Markdown only:src/utils/hookDBBridge.js:1444-1447:scripts/backfill-local-to-db.mjswalkMarkdown()collects.mdonly.Meanwhile
hookDBBridge.js:1359waits forrisk-summaryas a CRITICAL_REPORT (6 retries), times out, recordsmissing_reports: ['risk-summary'], and builds the KG with no risk input.Bug #2 — Parser schema drift: even when persisted, the JSON doesn't parse
src/utils/knowledgeGraph/kgPhases6to8.js:380(Phase 7 risk extraction):The current producer keys its array as
exposure_by_category(neitherrisk_categoriesnorcategories), and exposures are strings ("weighted_exposure": "$433.75M","exposure_low"/"exposure_high") rather than the numericp50/p10/p90fields the synth block reads (lines 391-394). Result:categories = [], fall through to the Markdown regex (Path B, no**bold**in JSON) → 0 risk nodes.Why it regressed (Cardinal worked, June doesn't)
May sessions emitted
risk-summary-narrative.md(Markdown) → hit the.mdpersist path and Phase 7 Path B regex. The producer was later switched to structuredrisk-summary.jsonwithout updating the persistence filter or the JSON parser.Blast radius (queried against prod
super_legal)The 2026-05-27 row is the isolated proof of Bug #2: the report was present but still yielded 0 risk nodes. June sessions fail earlier at Bug #1.
Suggested solutions
Fix #1 — persist
risk-summary.jsonas a report (report_type='review',report_key='risk-summary'):hookDBBridge.js:~1445: persistrisk-summary.json(and, conservatively, scope to that filename rather than all/reports/*.jsonto avoid pulling in state sidecars).extractReportKeyalready strips.json(hookDBBridge.js:173), so key derivation is in place.scripts/backfill-local-to-db.mjs: havewalkMarkdown(or a sibling pass) also ingestreview-outputs/risk-summary.json. Note the existing exclude set already skips*-state.json; only the structured deliverable should be ingested as a report.Fix #2 — make Phase 7 accept the current schema (
kgPhases6to8.js:380):parsed.exposure_by_categoryto the category source list.weighted_exposure/exposure_low/exposure_highand normalize$433.75M/$2.33B→ numeric before building the synth block; map stringseverityandprobability(e.g."8% fail") defensively.risk-summary.jsonshape (exposure_by_category[].findings[]) — seereports/2026-06-16-1781644875/review-outputs/risk-summary.jsonfor a canonical example (16 findings, 9 HIGH).Hardening (recommended):
kgHelpers.js:180already aliasesrisk→['risk-summary', 'risk-narrative', 'risk-assessment', 'risk-summary-narrative']).Remediation for affected past sessions
Once both fixes land, re-ingest
risk-summary.jsonfor affected sessions and re-run the KG build (upsert) to backfill the missingrisknodes (and any downstream re-synthesis). At minimum: 2026-06-16, 2026-06-08, 2026-05-27.Notes
2026-06-16-1781644875, Fox/Roku). Diagnosis is read-only against prod; no fix applied yet pending triage.