@remnic/core 9.3.719 → 9.3.721
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-boundary.d.ts +10 -7
- package/dist/access-boundary.js +37 -34
- package/dist/access-cli.js +71 -66
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +11 -8
- package/dist/access-http.js +44 -41
- package/dist/access-mcp.d.ts +11 -8
- package/dist/access-mcp.js +40 -37
- package/dist/access-operations-batch.js +38 -35
- package/dist/access-operations.d.ts +11 -8
- package/dist/access-operations.js +39 -36
- package/dist/access-schema.d.ts +19 -19
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-CZbnt0t2.d.ts → access-service-Dj3I-VBS.d.ts} +3 -3
- package/dist/access-service.d.ts +11 -8
- package/dist/access-service.js +36 -33
- package/dist/access-surface-catalog.d.ts +10 -7
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +4 -2
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/{auto-sync-24NNS3GU.js → auto-sync-F3KCSQFJ.js} +5 -5
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +11 -8
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +12 -10
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +2 -2
- package/dist/capabilities.d.ts +30 -2
- package/dist/capabilities.js +3 -1
- package/dist/{catalog-CEYlMs9X.d.ts → catalog-F-OEVbRQ.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +15 -13
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.d.ts +4 -0
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-TH7WMHVK.js → chunk-3NN73CM7.js} +2 -2
- package/dist/{chunk-A5TUPZ3N.js → chunk-4SF232E6.js} +60 -30
- package/dist/chunk-4SF232E6.js.map +1 -0
- package/dist/{chunk-DGTDRIUO.js → chunk-4T3BMHKD.js} +2 -2
- package/dist/{chunk-EH27LHML.js → chunk-5O42UKXI.js} +2 -2
- package/dist/{chunk-KRGC4EDX.js → chunk-6FAMMBQ3.js} +2 -2
- package/dist/{chunk-FEB7W5JG.js → chunk-6OIDDKAA.js} +27 -6
- package/dist/chunk-6OIDDKAA.js.map +1 -0
- package/dist/{chunk-67HG7KFE.js → chunk-6YKIYQU7.js} +2 -2
- package/dist/{chunk-LXSG4L55.js → chunk-7CTNUVHZ.js} +2 -2
- package/dist/{chunk-UCJ5CDTK.js → chunk-A5XNBK2D.js} +22 -14
- package/dist/chunk-A5XNBK2D.js.map +1 -0
- package/dist/{chunk-7EEVXW7X.js → chunk-A7TJZLPK.js} +2 -2
- package/dist/{chunk-P5IPFZVH.js → chunk-B7WISIAJ.js} +5 -5
- package/dist/chunk-BCDKBDSY.js +173 -0
- package/dist/chunk-BCDKBDSY.js.map +1 -0
- package/dist/{chunk-JAKYZGV4.js → chunk-BXEOHV7K.js} +3 -3
- package/dist/chunk-C4X4QGIO.js +60 -0
- package/dist/chunk-C4X4QGIO.js.map +1 -0
- package/dist/{chunk-S3OPGGZB.js → chunk-D7EBTQ24.js} +2 -2
- package/dist/{chunk-IO5NQEGZ.js → chunk-DKT3X5C3.js} +2 -2
- package/dist/{chunk-PBH4JUAB.js → chunk-DVRHLN2O.js} +18 -3
- package/dist/chunk-DVRHLN2O.js.map +1 -0
- package/dist/{chunk-S35FX6MU.js → chunk-EBDA5TYA.js} +2 -2
- package/dist/{chunk-2PTBLS2S.js → chunk-EDOQM4E4.js} +2 -2
- package/dist/{chunk-YTZLBBEB.js → chunk-EEJMD2L2.js} +3 -3
- package/dist/{chunk-YGKUAX2B.js → chunk-EHQ5HO5T.js} +11 -11
- package/dist/chunk-EHQ5HO5T.js.map +1 -0
- package/dist/{chunk-JIZGYQYH.js → chunk-EM2LDXJ2.js} +4 -4
- package/dist/{chunk-YYOK4GVF.js → chunk-EVPNWN7Z.js} +10 -10
- package/dist/{chunk-HLIR3RJ7.js → chunk-EXUCUPV2.js} +2 -2
- package/dist/{chunk-F3PVXKHP.js → chunk-GDUOCHCD.js} +27 -21
- package/dist/chunk-GDUOCHCD.js.map +1 -0
- package/dist/{chunk-L5ASZA2F.js → chunk-GJSL4WG2.js} +17 -2
- package/dist/chunk-GJSL4WG2.js.map +1 -0
- package/dist/{chunk-2SXMCQEQ.js → chunk-GZQXOQQ2.js} +3 -3
- package/dist/{chunk-2KAYTPPT.js → chunk-HCVWF657.js} +12 -12
- package/dist/{chunk-N3DUH6OO.js → chunk-ILFSEIRT.js} +1 -1
- package/dist/chunk-ILFSEIRT.js.map +1 -0
- package/dist/{chunk-4KZGLDJJ.js → chunk-J3JPB7ZI.js} +2 -2
- package/dist/{chunk-3JJWNZTT.js → chunk-L2B3EE7B.js} +102 -42
- package/dist/chunk-L2B3EE7B.js.map +1 -0
- package/dist/{chunk-NFRBADNR.js → chunk-LA3HZKES.js} +610 -369
- package/dist/chunk-LA3HZKES.js.map +1 -0
- package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
- package/dist/{chunk-URM3HW2R.js → chunk-LLUGAZ3B.js} +2 -2
- package/dist/{chunk-RVLB4E6N.js → chunk-MDBZTH7N.js} +5 -32
- package/dist/chunk-MDBZTH7N.js.map +1 -0
- package/dist/{chunk-U5H7WHTS.js → chunk-N77ELJ2D.js} +4 -4
- package/dist/{chunk-2VHMVWPU.js → chunk-NXRUUDUF.js} +2 -2
- package/dist/{chunk-DIFU33GU.js → chunk-OHTOPFRF.js} +2 -2
- package/dist/{chunk-74MYJSKB.js → chunk-ORAK3LNZ.js} +2 -2
- package/dist/{chunk-REWQAFSA.js → chunk-PD42EYQN.js} +2 -2
- package/dist/chunk-PHK3HARR.js +32 -0
- package/dist/chunk-PHK3HARR.js.map +1 -0
- package/dist/{chunk-773DPI6T.js → chunk-PKMQK2WY.js} +4 -4
- package/dist/{chunk-72VDBALG.js → chunk-QD6QP5P6.js} +63 -54
- package/dist/chunk-QD6QP5P6.js.map +1 -0
- package/dist/{chunk-CTOQEZSN.js → chunk-QR67FU5G.js} +2 -2
- package/dist/{chunk-AU7Q3LSC.js → chunk-RKNJBZ55.js} +7 -7
- package/dist/{chunk-YI7RZO7Y.js → chunk-SQNED75R.js} +1 -1
- package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
- package/dist/{chunk-IF362TF6.js → chunk-TQT3USCG.js} +2 -2
- package/dist/chunk-TQT3USCG.js.map +1 -0
- package/dist/{chunk-5RHM43BJ.js → chunk-TXQCG6SE.js} +3 -3
- package/dist/{chunk-ANI5DUAE.js → chunk-UIXXE6NI.js} +7 -7
- package/dist/{chunk-2COL35PK.js → chunk-UO5ZHIIH.js} +2 -2
- package/dist/{chunk-OJU73BUX.js → chunk-VCLGUIFM.js} +2 -2
- package/dist/{chunk-5J4OL7TJ.js → chunk-VUA4S7P6.js} +4 -4
- package/dist/{chunk-A66YRSOS.js → chunk-WQYSDPMA.js} +2 -2
- package/dist/chunk-XGTBNVIU.js +79 -0
- package/dist/chunk-XGTBNVIU.js.map +1 -0
- package/dist/{chunk-OZ2KQ7HF.js → chunk-YEE5KJQQ.js} +2 -2
- package/dist/chunk-YN2UN7N3.js +217 -0
- package/dist/chunk-YN2UN7N3.js.map +1 -0
- package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
- package/dist/{chunk-APUJIMDM.js → chunk-Z4RU5VPM.js} +2 -2
- package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
- package/dist/{cli-1orcdXOY.d.ts → cli-P_GaYcCY.d.ts} +3 -3
- package/dist/cli.d.ts +9 -6
- package/dist/cli.js +67 -63
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +12 -10
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +12 -10
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +13 -11
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +12 -10
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +11 -8
- package/dist/extraction-faithfulness.d.ts +8 -2
- package/dist/extraction-faithfulness.js +4 -3
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +5 -4
- package/dist/faithfulness-config.d.ts +55 -0
- package/dist/faithfulness-config.js +11 -0
- package/dist/faithfulness-config.js.map +1 -0
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/fallback-llm.js +2 -2
- package/dist/{forget-U66KPDPY.js → forget-OEE5OELK.js} +2 -2
- package/dist/graph-dashboard-diff.d.ts +4 -0
- package/dist/graph-dashboard-key.d.ts +4 -0
- package/dist/graph-dashboard-parser.d.ts +4 -0
- package/dist/{graph-edge-decay-JLOF3YW4.js → graph-edge-decay-QPRJQ7DL.js} +3 -3
- package/dist/graph-edge-reinforcement.d.ts +4 -0
- package/dist/graph-snapshot.d.ts +7 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.d.ts +7 -1
- package/dist/graph.js +2 -2
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +571 -570
- package/dist/index.js +104 -99
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +4 -4
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +96 -0
- package/dist/local-model-endpoint.js +12 -0
- package/dist/local-model-endpoint.js.map +1 -0
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +12 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +12 -10
- package/dist/maintenance/rebuild-memory-projection.js +13 -11
- package/dist/mcp-memory-inspector-app.d.ts +11 -8
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +22 -20
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +9 -9
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +12 -10
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -2
- package/dist/operator-toolkit.js +32 -28
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +14 -12
- package/dist/{orchestrator-kOxredSr.d.ts → orchestrator-f5xv19kp.d.ts} +178 -81
- package/dist/orchestrator.d.ts +8 -5
- package/dist/orchestrator.js +56 -52
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/provenance.js +2 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +2 -2
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +18 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +5 -3
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +8 -8
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +14 -14
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-B58v3S-a.d.ts → semantic-consolidation-BQm1e1Pn.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +13 -11
- package/dist/semantic-rule-promotion.js +12 -10
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +12 -10
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +45 -1
- package/dist/storage.js +11 -9
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -3
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +1 -1
- package/dist/trust-score-stage.d.ts +178 -0
- package/dist/trust-score-stage.js +18 -0
- package/dist/trust-score-stage.js.map +1 -0
- package/dist/trust-score.d.ts +164 -0
- package/dist/trust-score.js +22 -0
- package/dist/trust-score.js.map +1 -0
- package/dist/{types-ACmDOScJ.d.ts → types-DwO1Ot4n.d.ts} +56 -2
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +12 -10
- package/package.json +2 -2
- package/src/access-service.ts +8 -5
- package/src/capabilities-cluster-b.test.ts +78 -0
- package/src/capabilities.test.ts +11 -1
- package/src/capabilities.ts +58 -0
- package/src/cli/research-status-commands.ts +7 -3
- package/src/cli.ts +11 -6
- package/src/config.ts +60 -30
- package/src/extraction-faithfulness.test.ts +175 -0
- package/src/extraction-faithfulness.ts +53 -1
- package/src/faithfulness-config.test.ts +96 -0
- package/src/faithfulness-config.ts +138 -0
- package/src/graph-snapshot.ts +3 -3
- package/src/graph.ts +8 -7
- package/src/lifecycle/tombstones.test.ts +242 -1
- package/src/lifecycle/tombstones.ts +157 -70
- package/src/local-model-endpoint.test.ts +155 -0
- package/src/local-model-endpoint.ts +189 -0
- package/src/operator-toolkit.ts +24 -7
- package/src/orchestration/tier-migration-coordinator.ts +286 -0
- package/src/orchestrator.ts +331 -222
- package/src/recall-xray.test.ts +62 -0
- package/src/recall-xray.ts +34 -0
- package/src/trust-score-recall-paths.test.ts +157 -0
- package/src/trust-score-stage.test.ts +350 -0
- package/src/trust-score-stage.ts +398 -0
- package/src/trust-score.test.ts +282 -0
- package/src/trust-score.ts +449 -0
- package/src/types.ts +61 -1
- package/dist/chunk-3JJWNZTT.js.map +0 -1
- package/dist/chunk-72VDBALG.js.map +0 -1
- package/dist/chunk-A5TUPZ3N.js.map +0 -1
- package/dist/chunk-F3PVXKHP.js.map +0 -1
- package/dist/chunk-FEB7W5JG.js.map +0 -1
- package/dist/chunk-IF362TF6.js.map +0 -1
- package/dist/chunk-L5ASZA2F.js.map +0 -1
- package/dist/chunk-N3DUH6OO.js.map +0 -1
- package/dist/chunk-NFRBADNR.js.map +0 -1
- package/dist/chunk-PBH4JUAB.js.map +0 -1
- package/dist/chunk-RVLB4E6N.js.map +0 -1
- package/dist/chunk-UCJ5CDTK.js.map +0 -1
- package/dist/chunk-YGKUAX2B.js.map +0 -1
- /package/dist/{auto-sync-24NNS3GU.js.map → auto-sync-F3KCSQFJ.js.map} +0 -0
- /package/dist/{chunk-TH7WMHVK.js.map → chunk-3NN73CM7.js.map} +0 -0
- /package/dist/{chunk-DGTDRIUO.js.map → chunk-4T3BMHKD.js.map} +0 -0
- /package/dist/{chunk-EH27LHML.js.map → chunk-5O42UKXI.js.map} +0 -0
- /package/dist/{chunk-KRGC4EDX.js.map → chunk-6FAMMBQ3.js.map} +0 -0
- /package/dist/{chunk-67HG7KFE.js.map → chunk-6YKIYQU7.js.map} +0 -0
- /package/dist/{chunk-LXSG4L55.js.map → chunk-7CTNUVHZ.js.map} +0 -0
- /package/dist/{chunk-7EEVXW7X.js.map → chunk-A7TJZLPK.js.map} +0 -0
- /package/dist/{chunk-P5IPFZVH.js.map → chunk-B7WISIAJ.js.map} +0 -0
- /package/dist/{chunk-JAKYZGV4.js.map → chunk-BXEOHV7K.js.map} +0 -0
- /package/dist/{chunk-S3OPGGZB.js.map → chunk-D7EBTQ24.js.map} +0 -0
- /package/dist/{chunk-IO5NQEGZ.js.map → chunk-DKT3X5C3.js.map} +0 -0
- /package/dist/{chunk-S35FX6MU.js.map → chunk-EBDA5TYA.js.map} +0 -0
- /package/dist/{chunk-2PTBLS2S.js.map → chunk-EDOQM4E4.js.map} +0 -0
- /package/dist/{chunk-YTZLBBEB.js.map → chunk-EEJMD2L2.js.map} +0 -0
- /package/dist/{chunk-JIZGYQYH.js.map → chunk-EM2LDXJ2.js.map} +0 -0
- /package/dist/{chunk-YYOK4GVF.js.map → chunk-EVPNWN7Z.js.map} +0 -0
- /package/dist/{chunk-HLIR3RJ7.js.map → chunk-EXUCUPV2.js.map} +0 -0
- /package/dist/{chunk-2SXMCQEQ.js.map → chunk-GZQXOQQ2.js.map} +0 -0
- /package/dist/{chunk-2KAYTPPT.js.map → chunk-HCVWF657.js.map} +0 -0
- /package/dist/{chunk-4KZGLDJJ.js.map → chunk-J3JPB7ZI.js.map} +0 -0
- /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
- /package/dist/{chunk-URM3HW2R.js.map → chunk-LLUGAZ3B.js.map} +0 -0
- /package/dist/{chunk-U5H7WHTS.js.map → chunk-N77ELJ2D.js.map} +0 -0
- /package/dist/{chunk-2VHMVWPU.js.map → chunk-NXRUUDUF.js.map} +0 -0
- /package/dist/{chunk-DIFU33GU.js.map → chunk-OHTOPFRF.js.map} +0 -0
- /package/dist/{chunk-74MYJSKB.js.map → chunk-ORAK3LNZ.js.map} +0 -0
- /package/dist/{chunk-REWQAFSA.js.map → chunk-PD42EYQN.js.map} +0 -0
- /package/dist/{chunk-773DPI6T.js.map → chunk-PKMQK2WY.js.map} +0 -0
- /package/dist/{chunk-CTOQEZSN.js.map → chunk-QR67FU5G.js.map} +0 -0
- /package/dist/{chunk-AU7Q3LSC.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-YI7RZO7Y.js.map → chunk-SQNED75R.js.map} +0 -0
- /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
- /package/dist/{chunk-5RHM43BJ.js.map → chunk-TXQCG6SE.js.map} +0 -0
- /package/dist/{chunk-ANI5DUAE.js.map → chunk-UIXXE6NI.js.map} +0 -0
- /package/dist/{chunk-2COL35PK.js.map → chunk-UO5ZHIIH.js.map} +0 -0
- /package/dist/{chunk-OJU73BUX.js.map → chunk-VCLGUIFM.js.map} +0 -0
- /package/dist/{chunk-5J4OL7TJ.js.map → chunk-VUA4S7P6.js.map} +0 -0
- /package/dist/{chunk-A66YRSOS.js.map → chunk-WQYSDPMA.js.map} +0 -0
- /package/dist/{chunk-OZ2KQ7HF.js.map → chunk-YEE5KJQQ.js.map} +0 -0
- /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
- /package/dist/{chunk-APUJIMDM.js.map → chunk-Z4RU5VPM.js.map} +0 -0
- /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
- /package/dist/{forget-U66KPDPY.js.map → forget-OEE5OELK.js.map} +0 -0
- /package/dist/{graph-edge-decay-JLOF3YW4.js.map → graph-edge-decay-QPRJQ7DL.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/extraction-faithfulness.ts"],"sourcesContent":["/**\n * Extraction faithfulness gate — entailment verification of extracted\n * facts against their verified source spans (issue #1576).\n *\n * Part of #1572 (glass-box memory). Depends on #1575 (provenance spans).\n * Improved later by #1585 (fine-tuned local gate model).\n *\n * Unlike the extraction-judge (#376), which gates on fact-worthiness\n * (\"is this worth remembering?\"), this module gates on faithfulness\n * (\"is this fact actually supported by what was said?\"). Hallucinated\n * or mis-paraphrased extractions become durable memories, get recalled\n * with confidence, and poison downstream answers — the highest-severity\n * accuracy failure a memory system can have.\n *\n * Design constraints:\n * - Tagged results only — a backend failure is distinguishable from a\n * genuine verdict (rule 34 / checklist §22: never conflate \"empty\"\n * with \"failed\").\n * - The gate consumes the QUOTE, not the whole conversation — passing\n * full turns re-introduces the hallucination surface you're guarding\n * and blows the latency budget.\n * - No module-level state — batch queues hang off the orchestrator\n * instance (rule 11).\n * - Byte-identical pre-feature pipeline when mode is \"off\" (rule 39).\n * - Graceful degradation — a backend failure never blocks writes\n * (checklist §4); the fact proceeds with verdict \"unchecked\".\n * - Never mutate mw_*\\/trust fields — faithfulness is its own\n * frontmatter island consumed by #1577.\n */\n\nimport { createHash } from \"node:crypto\";\n\nimport { log } from \"./logger.js\";\nimport type { PluginConfig, MemoryFrontmatter, FaithfulnessFrontmatter } from \"./types.js\";\nimport type { LocalLlmClient } from \"./local-llm.js\";\nimport { type FallbackLlmClient, gatewayTaskChainOptions } from \"./fallback-llm.js\";\nimport { extractJsonCandidates } from \"./json-extract.js\";\n\n// Re-export for callers importing from this module.\nexport type { FaithfulnessFrontmatter } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * The three entailment verdicts. \"unchecked\" is NOT a member of this union —\n * it lives only in the frontmatter to mark facts that bypassed the gate\n * (backend failure, no span, mode=off). A real verdict is always one of\n * these three.\n */\nexport type FaithfulnessVerdict = \"entailed\" | \"contradicted\" | \"unsupported\";\n\n/**\n * Failure codes for tagged-error results. Each is distinguishable so callers\n * and telemetry can tell \"no span available\" from \"the backend was down\".\n */\nexport type FaithfulnessFailureCode =\n | \"no_span\"\n | \"backend_unavailable\"\n | \"malformed_output\"\n | \"timeout\";\n\n/**\n * Tagged result — success carries the verdict + model + optional rationale;\n * failure carries only the error code. Never conflate the two (rule 34).\n */\nexport type FaithfulnessResult =\n | { ok: true; verdict: FaithfulnessVerdict; model: string; rationale?: string }\n | { ok: false; error: { code: FaithfulnessFailureCode } };\n\n/**\n * Input for a single faithfulness check. The quote is the verified span from\n * #1575; context is optional surrounding turn text (bounded by\n * faithfulnessContextChars).\n */\nexport interface FaithfulnessCheckInput {\n factText: string;\n quote: string;\n context?: string;\n}\n\n/**\n * Shape persisted to frontmatter (issue #1576). Serialized as a single JSON\n * line so it round-trips through the existing YAML parser without special\n * handling.\n *\n * `verdict: \"unchecked\"` marks a fact that entered the gate but could not be\n * evaluated (backend failure, timeout). It is distinct from the field being\n * absent (which means the gate was off entirely or the fact predates #1576).\n */\n\n// ---------------------------------------------------------------------------\n// Sealed prompt (hash lets #1573-style caching key on it)\n// ---------------------------------------------------------------------------\n\nconst FAITHFULNESS_SYSTEM_PROMPT = `You are a faithfulness verifier for a memory system. Given a QUOTE from the source conversation and an extracted FACT, determine whether the FACT is supported by the QUOTE.\n\nRules:\n- \"entailed\" — the FACT directly follows from the QUOTE (paraphrase is OK).\n- \"contradicted\" — the FACT asserts something the QUOTE directly negates.\n- \"unsupported\" — the FACT introduces information not present in the QUOTE.\n\nAnswer with a JSON array. Each element: {\"index\": <int>, \"verdict\": \"entailed\"|\"contradicted\"|\"unsupported\", \"rationale\": \"<one sentence>\"}.\n\nExamples:\nQUOTE: \"I've been using Vim for about ten years now.\"\nFACT: \"The user has used Vim for approximately a decade.\"\n{\"index\": 0, \"verdict\": \"entailed\", \"rationale\": \"Paraphrase of the same duration.\"}\n\nQUOTE: \"I've been using Vim for about ten years now.\"\nFACT: \"The user prefers Emacs.\"\n{\"index\": 0, \"verdict\": \"unsupported\", \"rationale\": \"QUOTE mentions Vim, not Emacs.\"}\n\nQUOTE: \"I stopped drinking coffee last month.\"\nFACT: \"The user drinks coffee regularly.\"\n{\"index\": 0, \"verdict\": \"contradicted\", \"rationale\": \"QUOTE says they stopped; FACT claims the opposite.\"}`;\n\n/**\n * SHA-256 hash of the sealed system prompt. Cache keys and prompt-version\n * telemetry include this so a prompt change is detectable (#1573-style\n * caching can invalidate stale entries).\n */\nexport const FAITHFULNESS_PROMPT_HASH = createHash(\"sha256\")\n .update(FAITHFULNESS_SYSTEM_PROMPT)\n .digest(\"hex\");\n\n// ---------------------------------------------------------------------------\n// Prompt construction\n// ---------------------------------------------------------------------------\n\n/**\n * Build the user-prompt payload for a batch of (fact, quote) pairs.\n * Each pair is numbered so the LLM's response can be correlated back.\n *\n * Context (when provided) is included after the quote, clearly delimited,\n * so the model knows it is supplementary — not the primary evidence.\n */\nfunction buildBatchPrompt(inputs: FaithfulnessCheckInput[], contextChars: number): string {\n const parts: string[] = [];\n for (let i = 0; i < inputs.length; i++) {\n const inp = inputs[i];\n if (!inp) continue;\n // The QUOTE is the primary entailment evidence and is already bounded\n // upstream (provenance.maxQuoteChars / locateFactQuote). Send it whole\n // so the verifier never judges entailment on a truncated span. Only the\n // supplementary CONTEXT is bounded by contextChars (the config's \"window\n // around the quote\" semantics — cursor review).\n parts.push(`--- Item ${i} ---\nQUOTE: \"${inp.quote}\"`);\n if (inp.context && inp.context.trim().length > 0) {\n parts.push(`CONTEXT: \"${inp.context.slice(0, contextChars)}\"`);\n }\n parts.push(`FACT: \"${inp.factText}\"\n\nRespond with the JSON array entry for index ${i}.`);\n }\n parts.push(`\\nRespond now with a single JSON array covering indexes 0–${inputs.length - 1}.`);\n return parts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Response parsing\n// ---------------------------------------------------------------------------\n\nconst VALID_VERDICTS = new Set<string>([\"entailed\", \"contradicted\", \"unsupported\"]);\n\n/**\n * Parsed LLM response entry — validated shape.\n */\ninterface ParsedFaithfulnessEntry {\n index: number;\n verdict: FaithfulnessVerdict;\n rationale?: string;\n}\n\n/**\n * Parse the LLM response into a map of index → entry. Returns null when the\n * response is unparseable (rules 13/18: malformed output → tagged error,\n * never a crash).\n *\n * Accepts both a bare JSON array and a JSON array embedded in surrounding\n * text (the `extractJsonCandidates` helper handles fenced blocks and\n * code-block-wrapped JSON).\n */\nexport function parseFaithfulnessResponse(\n raw: string,\n expectedCount: number,\n): Map<number, ParsedFaithfulnessEntry> | null {\n if (!raw || typeof raw !== \"string\" || raw.trim().length === 0) {\n return null;\n }\n\n const candidates = extractJsonCandidates(raw);\n for (const candidate of candidates) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n continue;\n }\n const map = parseEntries(parsed, expectedCount);\n if (map) return map;\n }\n\n // Last resort: the model may have returned bare objects without array\n // wrapping — try parsing the whole thing as a single entry.\n try {\n const single = JSON.parse(raw.trim());\n const map = parseEntries([single], expectedCount);\n if (map) return map;\n } catch {\n // fall through\n }\n\n return null;\n}\n\nfunction parseEntries(\n data: unknown,\n expectedCount: number,\n): Map<number, ParsedFaithfulnessEntry> | null {\n if (!Array.isArray(data)) {\n // Accept object-wrapped arrays — {\"results\":[...]}, {\"verdicts\":[...]},\n // {\"entries\":[...]}, {\"facts\":[...]} — a common JSON-object prompt shape the\n // extraction-judge parser already accepts. Rejecting them marked the whole\n // batch malformed_output; in enforce mode those facts then wrote as ACTIVE\n // (gate bypass — codex Ob4RO).\n if (data && typeof data === \"object\") {\n for (const key of [\"results\", \"verdicts\", \"entries\", \"facts\"]) {\n const inner = (data as Record<string, unknown>)[key];\n if (Array.isArray(inner)) return parseEntries(inner, expectedCount);\n }\n }\n return null;\n }\n const map = new Map<number, ParsedFaithfulnessEntry>();\n for (const entry of data) {\n if (!entry || typeof entry !== \"object\") continue;\n const obj = entry as Record<string, unknown>;\n const idx = typeof obj.index === \"number\" ? obj.index : undefined;\n if (idx === undefined || idx < 0 || idx >= expectedCount) continue;\n const verdictRaw = typeof obj.verdict === \"string\" ? obj.verdict : undefined;\n if (!verdictRaw || !VALID_VERDICTS.has(verdictRaw)) continue;\n const rationale =\n typeof obj.rationale === \"string\" && obj.rationale.trim().length > 0\n ? obj.rationale.trim().slice(0, 500)\n : undefined;\n map.set(idx, {\n index: idx,\n verdict: verdictRaw as FaithfulnessVerdict,\n rationale,\n });\n }\n return map.size > 0 ? map : null;\n}\n\n// ---------------------------------------------------------------------------\n// LLM call helper\n// ---------------------------------------------------------------------------\n\ninterface LlmCallResult {\n content: string | null;\n modelUsed: string | null;\n}\n\n/**\n * Call the model routing chain (local → fallback) with the faithfulness\n * classification prompt. Mirrors `callJudgeLlm` in extraction-judge.ts so\n * the same routing, model-override, and gateway-chain logic applies.\n *\n * Returns the raw content string and the model that produced it, or\n * `{ content: null, modelUsed: null }` when every backend is unavailable.\n */\nasync function callFaithfulnessLlm(\n systemPrompt: string,\n userPrompt: string,\n config: PluginConfig,\n localLlm: LocalLlmClient | null,\n fallbackLlm: FallbackLlmClient | null,\n timeoutMs: number,\n signal?: AbortSignal,\n): Promise<LlmCallResult> {\n const messages: Array<{ role: \"system\" | \"user\"; content: string }> = [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: userPrompt },\n ];\n\n const modelOverride = config.extractionFaithfulnessModel || undefined;\n\n // Skip the local backend when (a) modelSource is \"gateway\", or (b) a\n // faithfulness model override is set. The local client always sends\n // config.localLlmModel and silently ignores options.model, so a local\n // success would run the wrong model and prevent the override from ever\n // reaching the gateway. Routing straight to the gateway honors the\n // override (codex review PRRT_kwDORJXyws6ObYQ8).\n const skipLocal = config.modelSource === \"gateway\" || Boolean(modelOverride);\n const gatewayChain = gatewayTaskChainOptions(config);\n\n let modelUsed: string | null = null;\n\n // Try local LLM first (only when no override routes the call to gateway).\n // The local client uses its OWN per-attempt AbortController keyed on\n // `timeoutMs` (it does not read options.signal — see LocalLlmChatCompletionOptions),\n // so the batch `signal` is not forwarded here; `timeoutMs` bounds each local\n // attempt instead. The batch AbortController still governs the fallback path,\n // which DOES honor the signal. (Matches extraction-judge.ts, which also passes\n // no signal to the local client.)\n if (localLlm && !skipLocal) {\n try {\n const result = await callLocalLlm(localLlm, messages, {\n temperature: 0.1,\n maxTokens: 2048,\n responseFormat: { type: \"json_object\" },\n timeoutMs,\n operation: \"extraction-faithfulness\",\n });\n if (result.content) {\n return { content: result.content, modelUsed: result.modelUsed ?? \"local\" };\n }\n } catch (err) {\n log.debug(\n `extraction-faithfulness: local LLM failed, trying fallback: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // Try fallback LLM\n if (fallbackLlm) {\n try {\n const result = await fallbackLlm.chatCompletion(\n messages as Array<{ role: \"system\" | \"user\" | \"assistant\"; content: string }>,\n {\n temperature: 0.1,\n maxTokens: 2048,\n timeoutMs,\n ...(modelOverride ? { model: modelOverride } : {}),\n ...gatewayChain,\n ...(signal ? { signal } : {}),\n },\n );\n if (result?.content) {\n return { content: result.content, modelUsed: result.modelUsed ?? \"fallback\" };\n }\n } catch (err) {\n log.debug(\n `extraction-faithfulness: fallback LLM failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n return { content: null, modelUsed: null };\n}\n\n/**\n * Call the local LLM's chatCompletion through a narrow, typed interface.\n * LocalLlmClient.chatCompletion accepts an options bag that includes an\n * `operation` discriminator; we forward it so the client can route\n * appropriately.\n */\ninterface LocalLlmCallOptions {\n temperature: number;\n maxTokens: number;\n responseFormat?: { type: string };\n timeoutMs: number;\n operation: string;\n}\n\ninterface LocalLlmCallResponse {\n content: string | null;\n modelUsed?: string;\n}\n\nasync function callLocalLlm(\n client: LocalLlmClient,\n messages: Array<{ role: \"system\" | \"user\"; content: string }>,\n options: LocalLlmCallOptions,\n): Promise<LocalLlmCallResponse> {\n // LocalLlmClient.chatCompletion returns { content: string } — we use a\n // typed call signature so no `any` leaks. The local client does not\n // expose modelUsed, so we return \"local\" as the model identifier.\n const result = await client.chatCompletion(messages, options);\n return {\n content: result?.content ?? null,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Core batch check\n// ---------------------------------------------------------------------------\n\n/**\n * Result of a batch faithfulness check — per-input results plus timing.\n */\nexport interface FaithfulnessBatchResult {\n results: FaithfulnessResult[];\n /** Wall-clock duration of the LLM call (0 when backend was not called). */\n elapsedMs: number;\n}\n\n/**\n * Evaluate a batch of (fact, quote) pairs for faithfulness.\n *\n * - Calls the LLM once with all pairs (bounded by `inputs.length`).\n * - On timeout → every input gets `{ ok: false, code: \"timeout\" }`.\n * - On backend unavailable → `{ ok: false, code: \"backend_unavailable\" }`.\n * - On malformed output → `{ ok: false, code: \"malformed_output\" }`.\n * - Inputs with empty quotes get `{ ok: false, code: \"no_span\" }` and are\n * NOT sent to the LLM.\n *\n * No module-level state: the caller (orchestrator) owns any caches.\n */\nexport async function checkFaithfulnessBatch(\n inputs: FaithfulnessCheckInput[],\n config: PluginConfig,\n localLlm: LocalLlmClient | null,\n fallbackLlm: FallbackLlmClient | null,\n): Promise<FaithfulnessBatchResult> {\n const timeoutMs =\n typeof config.extractionFaithfulnessTimeoutMs === \"number\" &&\n Number.isFinite(config.extractionFaithfulnessTimeoutMs) &&\n config.extractionFaithfulnessTimeoutMs > 0\n ? config.extractionFaithfulnessTimeoutMs\n : 8000;\n\n const contextChars =\n typeof config.extractionFaithfulnessContextChars === \"number\" &&\n Number.isFinite(config.extractionFaithfulnessContextChars) &&\n config.extractionFaithfulnessContextChars > 0\n ? config.extractionFaithfulnessContextChars\n : 400;\n\n // Partition: inputs with a real quote vs. inputs without one.\n const results: FaithfulnessResult[] = new Array(inputs.length);\n const checkableIndices: number[] = [];\n const checkableInputs: FaithfulnessCheckInput[] = [];\n\n for (let i = 0; i < inputs.length; i++) {\n const inp = inputs[i];\n if (!inp || !inp.quote || inp.quote.trim().length === 0) {\n results[i] = { ok: false, error: { code: \"no_span\" } };\n } else if (!inp.factText || inp.factText.trim().length === 0) {\n results[i] = { ok: false, error: { code: \"no_span\" } };\n } else {\n checkableIndices.push(i);\n checkableInputs.push(inp);\n }\n }\n\n if (checkableInputs.length === 0) {\n return { results, elapsedMs: 0 };\n }\n\n const userPrompt = buildBatchPrompt(checkableInputs, contextChars);\n const startedAt = Date.now();\n\n let timedOut = false;\n const controller = new AbortController();\n // Race the LLM call against the budget so the batch fails open at\n // `timeoutMs` regardless of which backend is in flight. The local backend\n // ignores the batch AbortSignal (it aborts each attempt via its own\n // controller keyed on `timeoutMs`), so awaiting callFaithfulnessLlm directly\n // could block past the budget on a slow/retrying local verifier. The timer\n // both aborts the fallback (which honors the signal) and resolves the race\n // so the batch returns promptly; the in-flight local call aborts on its own\n // per-attempt timeoutMs. (codex review PRRT_kwDORJXyws6ObgMJ.)\n // Manual deferred instead of Promise.withResolvers (ES2024) — plugin-openclaw's\n // standalone tsconfig targets ES2022 lib and this module is reachable from its\n // type graph, so withResolvers would TS2550 there.\n let resolveTimeout!: (value: true) => void;\n const racedTimeout = new Promise<true>((resolve) => {\n resolveTimeout = resolve;\n });\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n resolveTimeout(true);\n }, timeoutMs);\n\n try {\n const callPromise = callFaithfulnessLlm(\n FAITHFULNESS_SYSTEM_PROMPT,\n userPrompt,\n config,\n localLlm,\n fallbackLlm,\n timeoutMs,\n controller.signal,\n );\n const settled = await Promise.race([\n callPromise.then(\n (r) => ({ done: true as const, result: r }),\n // An unexpected throw is treated as \"no content\" (backend_unavailable)\n // so the race never rejects and the outer catch is the final safety net.\n () => ({ done: true as const, result: { content: null, modelUsed: null } }),\n ),\n racedTimeout.then(() => ({ done: false as const })),\n ]);\n const elapsedMs = Date.now() - startedAt;\n\n if (!settled.done) {\n // The budget elapsed before any backend returned content. Fail open as\n // timeout; the orphaned callPromise resolves/rejects harmlessly (the\n // fallback was aborted; the local client aborts on its own timeoutMs).\n for (const idx of checkableIndices) {\n results[idx] = { ok: false, error: { code: \"timeout\" } };\n }\n return { results, elapsedMs };\n }\n\n const llmResult = settled.result;\n\n // Cursor review: if the LLM returned usable content, use it even when\n // the abort timer raced — a response that lands just as the timer fires\n // has real verdicts. Only fall back to timeout errors when there is no\n // content to parse (the call genuinely did not complete in time).\n if (timedOut && !llmResult.content) {\n for (const idx of checkableIndices) {\n results[idx] = { ok: false, error: { code: \"timeout\" } };\n }\n return { results, elapsedMs };\n }\n\n if (!llmResult.content) {\n for (const idx of checkableIndices) {\n results[idx] = { ok: false, error: { code: \"backend_unavailable\" } };\n }\n return { results, elapsedMs };\n }\n\n const parsed = parseFaithfulnessResponse(llmResult.content, checkableInputs.length);\n if (!parsed) {\n for (const idx of checkableIndices) {\n results[idx] = { ok: false, error: { code: \"malformed_output\" } };\n }\n return { results, elapsedMs };\n }\n\n for (let j = 0; j < checkableIndices.length; j++) {\n const inputIdx = checkableIndices[j];\n if (inputIdx === undefined) continue;\n const entry = parsed.get(j);\n if (entry) {\n results[inputIdx] = {\n ok: true,\n verdict: entry.verdict,\n model: llmResult.modelUsed ?? \"unknown\",\n ...(entry.rationale ? { rationale: entry.rationale } : {}),\n };\n } else {\n // LLM response was missing this index\n results[inputIdx] = { ok: false, error: { code: \"malformed_output\" } };\n }\n }\n\n return { results, elapsedMs };\n } catch (err) {\n const elapsedMs = Date.now() - startedAt;\n if (timedOut || isAbortError(err)) {\n for (const idx of checkableIndices) {\n results[idx] = { ok: false, error: { code: \"timeout\" } };\n }\n return { results, elapsedMs };\n }\n log.warn(\n `extraction-faithfulness: batch check threw unexpectedly: ${err instanceof Error ? err.message : String(err)}`,\n );\n for (const idx of checkableIndices) {\n results[idx] = { ok: false, error: { code: \"backend_unavailable\" } };\n }\n return { results, elapsedMs };\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction isAbortError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const maybe = err as { name?: string; message?: string };\n return (\n maybe.name === \"AbortError\" ||\n maybe.message === \"This operation was aborted\" ||\n maybe.message === \"The operation was aborted\"\n );\n}\n\n\n// ---------------------------------------------------------------------------\n// Extraction-path orchestration helpers (issue #1576)\n//\n// These wrap checkFaithfulnessBatch + verdict application so the orchestrator\n// holds only thin delegation (ground rule 4: god files gain thin wiring only).\n// The core algorithm lives above; these are call-site glue.\n// ---------------------------------------------------------------------------\n\n/**\n * Telemetry counters for the faithfulness gate. Hang off the orchestrator\n * instance (rule 11: no module-level state) and surface via console_state so\n * `remnic doctor` renders the verdict distribution.\n */\nexport interface FaithfulnessGateCounters {\n entailed: number;\n contradicted: number;\n unsupported: number;\n unchecked: number;\n skippedNoSpan: number;\n}\n\n/**\n * Create a fresh zeroed counters object. Callers store it on the orchestrator\n * instance and pass it by reference to the gate helpers, which mutate it.\n */\nexport function createFaithfulnessCounters(): FaithfulnessGateCounters {\n return { entailed: 0, contradicted: 0, unsupported: 0, unchecked: 0, skippedNoSpan: 0 };\n}\n\n/**\n * Structural slice of an extracted fact that the gate reads. Kept loose so\n * this pure module does not depend on the full `ExtractedFact` type.\n */\nexport interface FaithfulnessGateFact {\n content: string;\n sources?: { quote?: string }[];\n}\n\n/**\n * Run the faithfulness batch over a list of extracted facts and return a map\n * keyed by the ORIGINAL fact index → result. Facts without a usable verified\n * source span are omitted from the batch (they are tagged `skipped_no_span`\n * at apply time, never gated — don't punish legacy data). Updates `counters`\n * for console_state telemetry.\n *\n * Fail-open: any pipeline error is caught, logged, and an empty map returned\n * so the caller records `unchecked`/`skipped_no_span` and proceeds — a gate\n * outage must never block memory writes (checklist §4).\n */\n// Common English stopwords — filtered before overlap scoring so function\n// words (the, a, is, ...) don't dilute the signal between a fact and its\n// source sentence.\nconst STOPWORDS = new Set([\n \"the\",\"a\",\"an\",\"and\",\"or\",\"but\",\"is\",\"are\",\"was\",\"were\",\"be\",\"been\",\"being\",\n \"to\",\"of\",\"in\",\"on\",\"at\",\"for\",\"with\",\"from\",\"by\",\"as\",\"it\",\"its\",\"this\",\"that\",\n \"these\",\"those\",\"i\",\"you\",\"he\",\"she\",\"we\",\"they\",\"my\",\"your\",\"his\",\"her\",\"our\",\"their\",\n \"has\",\"have\",\"had\",\"do\",\"does\",\"did\",\"will\",\"would\",\"can\",\"could\",\"should\",\"not\",\"no\",\n \"s\",\"very\",\"really\",\"just\",\"so\",\"than\",\"then\",\"there\",\"here\",\"about\",\"into\",\"over\",\"under\",\n]);\n\n/**\n * Crude stemmer: strip common suffixes so \"prefers\"/\"prefer\",\n * \"using\"/\"use\", \"started\"/\"start\" collapse to one token. Not a real\n * stemmer — just enough to raise recall for the interim locator (#1575 will\n * replace this with NLI-verified spans).\n */\nfunction crudeStem(word: string): string {\n if (word.length > 5 && word.endsWith(\"ing\")) return word.slice(0, -3);\n if (word.length > 4 && word.endsWith(\"ed\")) return word.slice(0, -2);\n if (word.length > 3 && (word.endsWith(\"s\")) && !word.endsWith(\"ss\")) return word.slice(0, -1);\n return word;\n}\n\n/**\n * Tokenize text into a lowercase, stopword-filtered, crudely-stemmed token\n * set for overlap scoring.\n */\nfunction tokenize(text: string): Set<string> {\n const tokens = new Set<string>();\n for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {\n if (raw.length <= 1) continue;\n if (STOPWORDS.has(raw)) continue;\n tokens.add(crudeStem(raw));\n }\n return tokens;\n}\n\n/**\n * Overlap coefficient: |A ∩ B| / min(|A|, |B|). Robust for paraphrase\n * matching where a short fact paraphrases a longer source sentence — a short\n * fact fully supported by a long sentence scores high, unrelated text scores 0.\n */\nfunction overlapCoefficient(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 || b.size === 0) return 0;\n let intersection = 0;\n const [small, large] = a.size <= b.size ? [a, b] : [b, a];\n for (const t of small) if (large.has(t)) intersection++;\n return intersection / small.size;\n}\n\n/**\n * Minimum overlap for a located quote to be trusted as the fact's source span.\n * Below this the fact is treated as having no located span (skipped_no_span)\n * rather than judged against an unrelated sentence.\n */\nconst LOCATE_QUOTE_MIN_OVERLAP = 0.3;\n\n/**\n * Locate the best-matching verbatim span (sentence) from the source turn text\n * for an extracted fact, by token overlap. Returns the quote when a confident\n * match is found, `undefined` otherwise (the gate then records\n * skipped_no_span — never judged against an unrelated span).\n *\n * This is the interim source-text locator that makes the gate functional on\n * real extraction output (where #1575 has not yet attached per-fact\n * `sources`). It produces a genuine located quote per fact so the gate\n * consumes a span, not the whole conversation (issue #1576 design constraint).\n * #1575's NLI-verified per-fact locator will replace this when it lands.\n */\n/**\n * Extract a bounded window of `sourceText` centered on the located `quote`,\n * bounded by `contextChars` (the config's \"window around the quote\" semantics).\n *\n * Used by the fallback-locator path in `runFaithfulnessGateBatch` so the\n * verifier sees surrounding turn text — without it,\n * `extractionFaithfulnessContextChars` is effectively ignored in the\n * production path (codex review PRRT_kwDORJXyws6OblI1). Returns undefined when\n * the quote is absent from sourceText (e.g. it was truncated by maxQuoteChars).\n */\nexport function extractContextWindow(\n sourceText: string,\n quote: string,\n contextChars: number,\n /**\n * Character offset of `quote` within `sourceText` (from `locateFactQuote`).\n * When the quote string appears more than once — e.g. repeated anaphoric\n * lines after different entities — pass the matched occurrence's offset so\n * the context window centers on the actual match, not the first occurrence\n * (issue #1633, codex PRRT_kwDORJXyws6ObzwI). Defaults to the first\n * occurrence via indexOf for backward compatibility.\n */\n matchedOffset?: number,\n): string | undefined {\n if (!sourceText || !quote || !(contextChars > 0)) return undefined;\n let idx: number;\n if (matchedOffset !== undefined && matchedOffset >= 0) {\n const at = sourceText.indexOf(quote, matchedOffset);\n // Fall back to the first occurrence if the matched offset is stale (e.g.\n // the quote was bounded and is not a literal substring at that offset).\n idx = at >= 0 ? at : sourceText.indexOf(quote);\n } else {\n idx = sourceText.indexOf(quote);\n }\n if (idx < 0) return undefined;\n const quoteEnd = idx + quote.length;\n const center = Math.floor((idx + quoteEnd) / 2);\n const half = Math.floor(contextChars / 2);\n let start = Math.max(0, center - half);\n const end = Math.min(sourceText.length, start + contextChars);\n // Re-anchor start so the window uses the full budget when end clamped.\n start = Math.max(0, end - contextChars);\n const window = sourceText.slice(start, end).trim();\n return window.length > 0 ? window : undefined;\n}\n\n/**\n * A located fallback quote plus the offset it begins at in the source text.\n * `offset` lets `extractContextWindow` disambiguate repeated occurrences of\n * the same quote string (issue #1633).\n */\nexport interface LocatedQuote {\n /** Verbatim source span, centered on the matched terms when truncated. */\n quote: string;\n /** Character offset of `quote` within sourceText. */\n offset: number;\n}\n\ninterface SourceCandidate {\n /** Trimmed candidate text. */\n text: string;\n /** Character offset of `text` within the source string. */\n start: number;\n}\n\n/**\n * Split source text into candidate spans (sentences, then line segments as a\n * fallback for transcripts without sentence punctuation), tracking each\n * candidate's start offset so repeated occurrences can be disambiguated\n * (issue #1633).\n */\nfunction splitSourceCandidates(sourceText: string): SourceCandidate[] {\n const candidates: SourceCandidate[] = [];\n const re = /(?<=[.!?])\\s+|\\n+/g;\n let lastEnd = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(sourceText)) !== null) {\n pushCandidate(candidates, sourceText, lastEnd, m.index);\n lastEnd = re.lastIndex;\n }\n pushCandidate(candidates, sourceText, lastEnd, sourceText.length);\n return candidates;\n}\n\nfunction pushCandidate(\n out: SourceCandidate[],\n source: string,\n begin: number,\n end: number,\n): void {\n const seg = source.slice(begin, end);\n const leadingWS = seg.length - seg.trimStart().length;\n const trimmed = seg.trim();\n if (trimmed.length > 0) {\n out.push({ text: trimmed, start: begin + leadingWS });\n }\n}\n\n/**\n * Locate the start offsets of fact-token matches inside a candidate. Used by\n * `locateFactQuote` to build a bounded window that is guaranteed to contain\n * real evidence (issue #1633, codex PRRT_kwDORJXyws6Obrwe / PRRT_kwDORJXyws6Oce-O).\n */\nfunction locateMatchedTokens(factTokens: Set<string>, candidate: string): number[] {\n const lower = candidate.toLowerCase();\n const wordRe = /[a-z0-9]+/g;\n const positions: number[] = [];\n let m: RegExpExecArray | null;\n while ((m = wordRe.exec(lower)) !== null) {\n const word = m[0];\n if (word.length <= 1) continue;\n if (STOPWORDS.has(word)) continue;\n if (factTokens.has(crudeStem(word))) {\n positions.push(m.index);\n }\n }\n return positions;\n}\n\n/**\n * Build a bounded window of `maxChars` from `text` that contains the densest\n * cluster of matched-token positions. Slides a maxChars-wide window anchored\n * at each matched token and keeps the one that captures the most matches\n * (tiebreak: earliest anchor). This guarantees the window includes real\n * evidence even when the matched span is wider than `maxChars` (e.g. fact\n * tokens at both ends of a long sentence with filler between). Centering on\n * the densest cluster's midpoint then uses any leftover budget as leading and\n * trailing context. Returns the window text and its start offset within `text`\n * so callers can translate the in-candidate offset back to a source-text offset.\n */\nfunction boundedWindow(\n text: string,\n matchedPositions: number[],\n maxChars: number,\n): { text: string; start: number } {\n if (matchedPositions.length === 0) {\n // Defensive: overlap scoring accepted the candidate but no individual\n // token matched (e.g. all matches were stopwords). Fall back to prefix.\n const end = Math.min(text.length, maxChars);\n return { text: text.slice(0, end), start: 0 };\n }\n // matchedPositions is sorted ascending (the regex scans left-to-right), so a\n // two-pointer sliding window finds the densest maxChars-wide cluster in O(n)\n // instead of O(n^2). This matters when a long unpunctuated candidate carries\n // many repeats of a fact token (pasted logs, minified text) — thousands of\n // positions would otherwise stall extraction before the LLM call (codex\n // PRRT_kwDORJXyws6Ocih3).\n let bestAnchor = matchedPositions[0]!;\n let bestCount = 0;\n let bestLast = bestAnchor;\n let j = 0;\n for (let i = 0; i < matchedPositions.length; i++) {\n const anchor = matchedPositions[i]!;\n const winEnd = anchor + maxChars;\n if (j < i) j = i;\n while (j + 1 < matchedPositions.length && matchedPositions[j + 1]! < winEnd) {\n j++;\n }\n const count = j - i + 1;\n if (count > bestCount) {\n bestCount = count;\n bestAnchor = anchor;\n bestLast = matchedPositions[j]!;\n }\n }\n // The densest cluster [bestAnchor, bestLast] fits within maxChars by\n // construction; center a maxChars window on its midpoint, clamped to text.\n const center = Math.floor((bestAnchor + bestLast) / 2);\n const half = Math.floor(maxChars / 2);\n let start = Math.max(0, center - half);\n const end = Math.min(text.length, start + maxChars);\n // Re-anchor start so the window uses the full budget when end clamped.\n start = Math.max(0, end - maxChars);\n return { text: text.slice(start, end), start };\n}\n\nexport function locateFactQuote(\n factText: string,\n sourceText: string,\n maxQuoteChars = 600,\n): LocatedQuote | undefined {\n if (!factText || !sourceText) return undefined;\n const factTokens = tokenize(factText);\n if (factTokens.size === 0) return undefined;\n const candidates = splitSourceCandidates(sourceText);\n if (candidates.length === 0) return undefined;\n // Pick the best-overlap candidate. Tiebreak equal own-overlap scores by a\n // \"context\" score that includes the immediately PRECEDING candidate's text,\n // so a repeated anaphoric line (\"It launched in March\") resolves to the\n // occurrence whose neighbor names the fact's entity (issue #1633, codex\n // PRRT_kwDORJXyws6Oce-S). Without this tiebreak the first occurrence wins\n // even when the second is the one the fact refers to.\n let best: { candidate: SourceCandidate; score: number; contextScore: number } | null = null;\n for (let i = 0; i < candidates.length; i++) {\n const candidate = candidates[i]!;\n const score = overlapCoefficient(factTokens, tokenize(candidate.text));\n const prevText = i > 0 ? candidates[i - 1]!.text : \"\";\n const contextText = prevText ? `${prevText} ${candidate.text}` : candidate.text;\n const contextScore = overlapCoefficient(factTokens, tokenize(contextText));\n if (\n !best ||\n score > best.score ||\n (score === best.score && contextScore > best.contextScore)\n ) {\n best = { candidate, score, contextScore };\n }\n }\n if (!best || best.score < LOCATE_QUOTE_MIN_OVERLAP) return undefined;\n const { text, start } = best.candidate;\n if (text.length <= maxQuoteChars) {\n return { quote: text, offset: start };\n }\n // Build a bounded window around the densest cluster of matched terms instead\n // of returning the first maxQuoteChars prefix (issue #1633). Returning the\n // prefix drops supporting words that fall after char ~600, routing\n // actually-entailed facts to pending_review in enforce mode.\n const matched = locateMatchedTokens(factTokens, text);\n const win = boundedWindow(text, matched, maxQuoteChars);\n return { quote: win.text, offset: start + win.start };\n}\nexport async function runFaithfulnessGateBatch(\n facts: readonly FaithfulnessGateFact[],\n mode: \"shadow\" | \"enforce\",\n config: PluginConfig,\n localLlm: LocalLlmClient | null,\n fallbackLlm: FallbackLlmClient | null,\n counters: FaithfulnessGateCounters,\n /**\n * The verbatim source turn text the facts were extracted from. When a fact\n * has no #1575 `sources`, locateFactQuote finds a fallback span here so the\n * gate runs on real extraction output. May be empty (replay/import paths\n * with no source turns) — facts then get skipped_no_span.\n */\n sourceText = \"\",\n): Promise<Map<number, FaithfulnessResult> | null> {\n // Phase 1 — build the checkable inputs. This is pure (no LLM, no throw):\n // locateFactQuote and the source/span selection never reject. Keeping it\n // outside the try lets the catch walk the same inputs to tag a pipeline\n // failure as \"unchecked\" rather than dropping it on the floor.\n const inputs: { factIndex: number; input: FaithfulnessCheckInput }[] = [];\n for (let fi = 0; fi < facts.length; fi++) {\n const f = facts[fi];\n if (!f || typeof f.content !== \"string\" || !f.content.trim()) continue;\n // Prefer #1575 verified spans; fall back to a located quote from the\n // source turn text so the gate runs even before per-fact sources are\n // attached. Without either, the fact is skipped_no_span (never gated).\n // A composite fact may be supported by multiple adjacent spans — collect\n // every valid source quote so the verifier sees the full evidence, not\n // just sources[0] (codex review PRRT_kwDORJXyws6ObYQ_).\n const sources = Array.isArray(f.sources) ? f.sources : [];\n const sourceQuotes = sources\n .map((s) => (s && typeof s.quote === \"string\" ? s.quote.trim() : \"\"))\n .filter((q) => q.length > 0);\n const usingFallbackLocator = sourceQuotes.length === 0;\n let quote: string;\n let matchedOffset: number | undefined;\n if (sourceQuotes.length > 0) {\n quote = sourceQuotes.join(\"\\n\");\n } else {\n // Fallback locator returns the matched span AND its offset so the\n // context window centers on the occurrence that actually matched the\n // fact, not the first indexOf hit (issue #1633).\n const located = locateFactQuote(f.content, sourceText);\n if (!located) continue; // no located span — applyFaithfulnessVerdict tags skipped_no_span\n quote = located.quote;\n matchedOffset = located.offset;\n }\n // Pass source context into the verifier so extractionFaithfulnessContextChars\n // actually applies in the fallback-locator path (codex P2\n // PRRT_kwDORJXyws6OblI1). #1575 verified spans already carry full evidence,\n // so context is only synthesized for the fallback locator.\n const fallbackContext =\n usingFallbackLocator\n ? extractContextWindow(\n sourceText,\n quote,\n config.extractionFaithfulnessContextChars,\n matchedOffset,\n )\n : undefined;\n inputs.push({\n factIndex: fi,\n input: {\n factText: f.content,\n quote,\n ...(fallbackContext ? { context: fallbackContext } : {}),\n },\n });\n }\n const resultsByFactIndex = new Map<number, FaithfulnessResult>();\n if (inputs.length === 0) return resultsByFactIndex;\n try {\n const batch = await checkFaithfulnessBatch(\n inputs.map((x) => x.input),\n config,\n localLlm,\n fallbackLlm,\n );\n for (let j = 0; j < inputs.length; j++) {\n const entry = inputs[j];\n if (entry) resultsByFactIndex.set(entry.factIndex, batch.results[j]!);\n }\n // Verdict-distribution counters are bumped in applyFaithfulnessVerdict\n // (at the per-fact apply point), NOT here, so console_state reflects\n // facts that actually reached verdict application — not facts later\n // dropped by dedup, importance, or judge gates (cursor review).\n log.info(\n `extraction-faithfulness[${mode}]: ${inputs.length} facts checked, ${batch.elapsedMs}ms`,\n );\n } catch (err) {\n // Fail-open: a pipeline error never blocks writes (checklist §4). Tag each\n // checkable fact as backend_unavailable so applyFaithfulnessVerdict records\n // \"unchecked\" (issue spec: backend failure → unchecked). Returning null\n // here would make a shadow/enforce batch failure indistinguishable from\n // gate-off, losing the telemetry signal (cursor review). Facts with no\n // located span stay absent → skipped_no_span at apply time.\n log.warn(\n `extraction-faithfulness: pipeline error, tagging ${inputs.length} checkable facts as unchecked (fail-open): ${err instanceof Error ? err.message : String(err)}`,\n );\n for (const { factIndex } of inputs) {\n resultsByFactIndex.set(factIndex, { ok: false, error: { code: \"backend_unavailable\" } });\n }\n }\n return resultsByFactIndex;\n}\n\n/**\n * Apply a pre-computed faithfulness verdict to a single fact, producing the\n * frontmatter record + an optional enforce-mode `pending_review` status.\n *\n * - `resultsByFactIndex` null (gate off) → nothing (rule 39: byte-identical).\n * - fact has no entry (no verified span) → `skipped_no_span`, never gated.\n * - backend failure (`ok: false`) → `unchecked`, fact proceeds (checklist §4).\n * - enforce + unsupported/contradicted → `pending_review` (memory persists,\n * enters the review queue, never silently dropped).\n *\n * Mutates `counters.skippedNoSpan` for facts without a span.\n */\nexport function applyFaithfulnessVerdict(\n resultsByFactIndex: Map<number, FaithfulnessResult> | null,\n factLoopIndex: number,\n mode: \"off\" | \"shadow\" | \"enforce\",\n factContent: string,\n counters: FaithfulnessGateCounters,\n): {\n faithfulness: FaithfulnessFrontmatter | undefined;\n enforceStatus: \"pending_review\" | undefined;\n} {\n if (!resultsByFactIndex) {\n return { faithfulness: undefined, enforceStatus: undefined };\n }\n const result = resultsByFactIndex.get(factLoopIndex);\n if (!result) {\n // No result for this fact index — it had no verified source span.\n counters.skippedNoSpan++;\n return {\n faithfulness: { verdict: \"skipped_no_span\", at: new Date().toISOString() },\n enforceStatus: undefined,\n };\n }\n if (result.ok) {\n if (result.verdict === \"entailed\") counters.entailed++;\n else if (result.verdict === \"contradicted\") counters.contradicted++;\n else if (result.verdict === \"unsupported\") counters.unsupported++;\n const fm: FaithfulnessFrontmatter = {\n verdict: result.verdict,\n ...(result.model ? { model: result.model } : {}),\n ...(result.rationale ? { rationale: result.rationale } : {}),\n at: new Date().toISOString(),\n };\n let enforceStatus: \"pending_review\" | undefined;\n if (\n mode === \"enforce\" &&\n (result.verdict === \"unsupported\" || result.verdict === \"contradicted\")\n ) {\n enforceStatus = \"pending_review\";\n log.info(\n `extraction-faithfulness[enforce]: routing \"${factContent.slice(0, 60)}…\" to pending_review (verdict=${result.verdict})`,\n );\n }\n return { faithfulness: fm, enforceStatus };\n }\n // Backend failure — record as unchecked, fact proceeds (graceful degradation).\n counters.unchecked++;\n return {\n faithfulness: { verdict: \"unchecked\", at: new Date().toISOString() },\n enforceStatus: undefined,\n };\n}\n// ---------------------------------------------------------------------------\n// Frontmatter serialization (single-line JSON, like provenance sources)\n// ---------------------------------------------------------------------------\n\n/**\n * Canonical key order for the serialized `faithfulness` frontmatter field.\n * Deterministic emission (rule 38).\n */\nconst FAITHFULNESS_KEY_ORDER = [\"verdict\", \"model\", \"rationale\", \"at\"] as const;\n\n/**\n * Serialize the `faithfulness` frontmatter field as a single JSON line,\n * appended to `lines`. Called from `serializeFrontmatter` in storage.ts.\n *\n * Contract: when `fm.faithfulness` is absent, no line is emitted — the\n * memory round-trips byte-identical to pre-#1576 behavior (rule 39).\n */\nexport function serializeFaithfulnessFields(fm: MemoryFrontmatter, lines: string[]): void {\n if (!fm.faithfulness) return;\n const fm2 = fm.faithfulness;\n const canonical: Record<string, unknown> = {};\n for (const key of FAITHFULNESS_KEY_ORDER) {\n const val = fm2[key];\n if (val !== undefined && val !== null && val !== \"\") {\n canonical[key] = val;\n }\n }\n // verdict is always present on a valid FaithfulnessFrontmatter\n if (!canonical.verdict) return;\n lines.push(`faithfulness: ${JSON.stringify(canonical)}`);\n}\n\n/**\n * Parse the `faithfulness` frontmatter line from its single-line JSON form.\n * Returns `undefined` for missing, blank, or corrupt values so a malformed\n * frontmatter never poisons downstream readers (rule 34 spirit).\n */\nexport function parseFaithfulnessField(\n raw: string | undefined,\n): FaithfulnessFrontmatter | undefined {\n if (!raw || typeof raw !== \"string\" || raw.trim().length === 0) return undefined;\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return undefined;\n const obj = parsed as Record<string, unknown>;\n const verdictRaw = typeof obj.verdict === \"string\" ? obj.verdict : undefined;\n if (!verdictRaw) return undefined;\n const validVerdicts = new Set([\n \"entailed\",\n \"contradicted\",\n \"unsupported\",\n \"unchecked\",\n \"skipped_no_span\",\n ]);\n if (!validVerdicts.has(verdictRaw)) return undefined;\n const result: FaithfulnessFrontmatter = {\n verdict: verdictRaw as FaithfulnessFrontmatter[\"verdict\"],\n };\n if (typeof obj.model === \"string\" && obj.model.length > 0) {\n result.model = obj.model;\n }\n if (typeof obj.rationale === \"string\" && obj.rationale.length > 0) {\n result.rationale = obj.rationale.slice(0, 500);\n }\n if (typeof obj.at === \"string\" && obj.at.length > 0) {\n result.at = obj.at;\n }\n return result;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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","names":["end"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/graph-snapshot.ts"],"sourcesContent":["/**\n * Graph snapshot — read-only view of the multi-graph memory adjacency\n * (issue #691 PR 2/5).\n *\n * Composes the JSONL edge store from `graph.ts` with optional node-metadata\n * loading so HTTP / MCP / future CLI surfaces can return a consistent\n * `{ nodes, edges, generatedAt }` shape suitable for the admin pane scaffold\n * shipped in PR 1/5.\n *\n * All inputs are validated and clamped at this layer so the access surface\n * (which is responsible for HTTP/MCP-flavored errors) can keep the wiring\n * thin. The module is pure async I/O — no global state, no caches.\n */\n\nimport type { GraphEdge, GraphType } from \"./graph.js\";\nimport { readAllEdges } from \"./graph.js\";\nimport { readEdgeConfidence } from \"./graph-edge-reinforcement.js\";\nimport * as path from \"path\";\n\n/** Default `limit` when the caller omits it. */\nexport const GRAPH_SNAPSHOT_DEFAULT_LIMIT = 500;\n\n/** Hard upper bound on `limit` to keep responses sized for the admin pane. */\nexport const GRAPH_SNAPSHOT_MAX_LIMIT = 5000;\n\n/** Categories accepted by the `categories` filter; matches `MemoryFile.frontmatter.category`. */\nexport type GraphSnapshotCategory = string;\n\nexport interface GraphSnapshotNode {\n /** Stable identifier — relative memory path (matches `GraphEdge.from` / `to`). */\n id: string;\n /** Short human label. Falls back to the basename when no metadata is available. */\n label: string;\n /** Memory category (e.g. `fact`, `decision`, `entity`). `\"unknown\"` when metadata is missing. */\n kind: string;\n /** Aggregate edge confidence touching this node, used for sizing in the admin pane. */\n score: number;\n /** Most recent edge timestamp (ISO) touching this node — best-effort recency signal. */\n lastUpdated: string | null;\n}\n\nexport interface GraphSnapshotEdge {\n source: string;\n target: string;\n kind: GraphType;\n /** Edge confidence in [0, 1]; legacy edges without confidence return 1.0. */\n confidence: number;\n}\n\nexport interface GraphSnapshotResponse {\n nodes: GraphSnapshotNode[];\n edges: GraphSnapshotEdge[];\n generatedAt: string;\n}\n\nexport interface GraphSnapshotRequest {\n /** Max number of edges to include (after filtering). Default 500, max 5000. */\n limit?: number;\n /** Inclusive lower bound on edge `ts` (ISO string). Edges older than this are dropped. */\n since?: string;\n /** When set, restrict the snapshot to the focus node and its direct neighbors. */\n focusNodeId?: string;\n /**\n * Optional category allow-list. Edges are kept only when both endpoints\n * resolve to nodes whose category falls in this set; nodes are kept only\n * when their category matches. When omitted, no category filter is applied.\n */\n categories?: GraphSnapshotCategory[];\n}\n\n/**\n * Loader contract: given a relative memory path, return `{ category, label,\n * updated }` or `null` when the memory cannot be resolved. Implementations\n * typically wrap `StorageManager.readMemoryByPath`; the snapshot module stays\n * agnostic so tests can pass a synchronous in-memory map.\n */\nexport type GraphSnapshotNodeLoader = (\n relPath: string,\n) => Promise<GraphSnapshotNodeMetadata | null>;\n\nexport interface GraphSnapshotNodeMetadata {\n /** Memory category, e.g. `fact` / `decision` / `entity`. */\n category: string;\n /** Display label — usually the memory id or the entity name. */\n label: string;\n /** ISO `updated` timestamp from frontmatter, when available. */\n updated?: string;\n}\n\n/**\n * Coerce a caller-supplied limit into the `[1, GRAPH_SNAPSHOT_MAX_LIMIT]` range.\n *\n * - `undefined` / `null` → `GRAPH_SNAPSHOT_DEFAULT_LIMIT`.\n * - non-finite or non-integer → throws `Error` (callers translate to 400).\n * - `<= 0` → throws.\n * - `> GRAPH_SNAPSHOT_MAX_LIMIT` → clamped to the max (no error — admin\n * panel callers send liberal limits).\n */\nexport function normalizeGraphSnapshotLimit(raw: unknown): number {\n if (raw === undefined || raw === null) return GRAPH_SNAPSHOT_DEFAULT_LIMIT;\n if (typeof raw !== \"number\" || !Number.isFinite(raw) || !Number.isInteger(raw)) {\n throw new Error(\"graphSnapshot: limit must be a positive integer\");\n }\n if (raw <= 0) {\n throw new Error(\"graphSnapshot: limit must be a positive integer\");\n }\n if (raw > GRAPH_SNAPSHOT_MAX_LIMIT) return GRAPH_SNAPSHOT_MAX_LIMIT;\n return raw;\n}\n\n/**\n * Validate and parse an ISO timestamp into a millisecond epoch. Throws\n * when the input cannot be parsed; callers translate to a 400. `undefined`\n * input yields `undefined`.\n */\nexport function parseGraphSnapshotSince(raw: string | undefined): number | undefined {\n if (raw === undefined || raw === null || raw === \"\") return undefined;\n const ms = Date.parse(raw);\n if (!Number.isFinite(ms)) {\n throw new Error(\"graphSnapshot: since must be a parseable ISO timestamp\");\n }\n return ms;\n}\n\n/**\n * Build a `GraphSnapshotResponse` from the raw edge JSONL on disk plus an\n * optional node-metadata loader.\n *\n * The function is split into pure subroutines so HTTP / MCP / tests can\n * exercise the same logic without spinning up a full `StorageManager`.\n */\nexport async function buildGraphSnapshot(opts: {\n memoryDir: string;\n graphConfig: {\n entityGraphEnabled: boolean;\n timeGraphEnabled: boolean;\n causalGraphEnabled: boolean;\n };\n request: GraphSnapshotRequest;\n loadNode: GraphSnapshotNodeLoader;\n /** Override clock for deterministic tests. */\n now?: () => Date;\n}): Promise<GraphSnapshotResponse> {\n const limit = normalizeGraphSnapshotLimit(opts.request.limit);\n const sinceMs = parseGraphSnapshotSince(opts.request.since);\n const focusNodeId = opts.request.focusNodeId?.trim();\n const categoryFilter = normalizeCategoryFilter(opts.request.categories);\n\n const allEdges = await readAllEdges(opts.memoryDir, opts.graphConfig);\n\n // Time-window filter (CLAUDE.md rule 35 — half-open `[since, +∞)`; equality\n // is treated as inclusive because callers expect a single-point pin to\n // surface edges stamped exactly at that instant).\n const timeFiltered = sinceMs === undefined\n ? allEdges\n : allEdges.filter((edge) => {\n const ts = Date.parse(edge.ts);\n return Number.isFinite(ts) && ts >= sinceMs;\n });\n\n // Focus-node neighborhood filter — restrict to edges incident on the node.\n const focusFiltered = focusNodeId\n ? timeFiltered.filter((edge) => edge.from === focusNodeId || edge.to === focusNodeId)\n : timeFiltered;\n\n // Sort newest-first so the limit window keeps the most recent edges. `ts`\n // is an ISO string, so a string comparison agrees with chronological order\n // for any well-formed timestamp; we still parse to avoid surprising\n // ordering when timezones drift, falling back to lexicographic compare on\n // tie / parse failure for stable ordering (CLAUDE.md rule 19).\n const sortedEdges = [...focusFiltered].sort((a, b) => {\n const aMs = Date.parse(a.ts);\n const bMs = Date.parse(b.ts);\n if (Number.isFinite(aMs) && Number.isFinite(bMs) && aMs !== bMs) {\n return bMs - aMs;\n }\n if (a.ts !== b.ts) return a.ts < b.ts ? 1 : -1;\n // Stable secondary key: from/to/type so identical-ts edges have a\n // deterministic order across runs.\n const aKey = `${a.from}|${a.to}|${a.type}`;\n const bKey = `${b.from}|${b.to}|${b.type}`;\n if (aKey === bKey) return 0;\n return aKey < bKey ? -1 : 1;\n });\n\n // Streaming category + limit pass. Metadata is loaded lazily per\n // endpoint with a memoization cache (`metadata`) so we never re-read the\n // same memory file twice, never read more than `2 × limit` files, and\n // never silently drop matching edges because a static cap fired before\n // the category filter saw them — the bug Cursor flagged on PR #734\n // when the pre-limit edge set referenced more than `2 × MAX_LIMIT`\n // unique nodes.\n const metadata = new Map<string, GraphSnapshotNodeMetadata | null>();\n const trimmedEdges: GraphEdge[] = [];\n\n const ensureMetadata = async (relPath: string): Promise<GraphSnapshotNodeMetadata | null> => {\n if (metadata.has(relPath)) return metadata.get(relPath) ?? null;\n let resolved: GraphSnapshotNodeMetadata | null = null;\n try {\n resolved = await opts.loadNode(relPath);\n } catch {\n // Loader errors are best-effort — leave the path absent so it\n // falls through to the basename label / \"unknown\" category.\n resolved = null;\n }\n metadata.set(relPath, resolved);\n return resolved;\n };\n\n for (const edge of sortedEdges) {\n if (trimmedEdges.length >= limit) break;\n if (categoryFilter !== null) {\n const fromMeta = await ensureMetadata(edge.from);\n const toMeta = await ensureMetadata(edge.to);\n if (!fromMeta || !toMeta) continue;\n if (\n !categoryFilter.has(fromMeta.category)\n || !categoryFilter.has(toMeta.category)\n ) {\n continue;\n }\n } else {\n // Even without a category filter we still want labels / kinds for\n // the surfaced nodes, so warm the metadata cache for this edge.\n // The cache is bounded by `2 × limit` because we stop iterating\n // once `trimmedEdges` reaches `limit`.\n await ensureMetadata(edge.from);\n await ensureMetadata(edge.to);\n }\n trimmedEdges.push(edge);\n }\n\n const resolvedMetadata = new Map<string, GraphSnapshotNodeMetadata>();\n for (const [key, value] of metadata) {\n if (value) resolvedMetadata.set(key, value);\n }\n\n const nodes = buildNodeIndex(trimmedEdges, resolvedMetadata);\n const edges: GraphSnapshotEdge[] = trimmedEdges.map((edge) => ({\n source: edge.from,\n target: edge.to,\n kind: edge.type,\n confidence: readEdgeConfidence(edge),\n }));\n\n const generatedAt = (opts.now ? opts.now() : new Date()).toISOString();\n return { nodes, edges, generatedAt };\n}\n\nfunction normalizeCategoryFilter(\n raw: GraphSnapshotCategory[] | undefined,\n): Set<string> | null {\n if (raw === undefined || raw === null) return null;\n if (!Array.isArray(raw)) {\n throw new Error(\"graphSnapshot: categories must be an array of strings\");\n }\n const cleaned = new Set<string>();\n for (const value of raw) {\n if (typeof value !== \"string\") {\n throw new Error(\"graphSnapshot: categories must be an array of strings\");\n }\n const trimmed = value.trim();\n if (trimmed.length > 0) cleaned.add(trimmed);\n }\n // Empty allow-list → reject everything. We surface this as a 400 because\n // the admin pane should never send `categories=` with no values.\n if (cleaned.size === 0) {\n throw new Error(\"graphSnapshot: categories must contain at least one non-empty value\");\n }\n return cleaned;\n}\n\nfunction buildNodeIndex(\n edges: GraphEdge[],\n metadata: Map<string, GraphSnapshotNodeMetadata>,\n): GraphSnapshotNode[] {\n type Aggregate = {\n score: number;\n lastUpdatedMs: number | null;\n lastUpdatedIso: string | null;\n };\n const aggregates = new Map<string, Aggregate>();\n\n function bump(id: string, edge: GraphEdge): void {\n const current = aggregates.get(id) ?? {\n score: 0,\n lastUpdatedMs: null,\n lastUpdatedIso: null,\n };\n current.score += readEdgeConfidence(edge);\n const ms = Date.parse(edge.ts);\n if (Number.isFinite(ms) && (current.lastUpdatedMs === null || ms > current.lastUpdatedMs)) {\n current.lastUpdatedMs = ms;\n current.lastUpdatedIso = edge.ts;\n }\n aggregates.set(id, current);\n }\n\n for (const edge of edges) {\n bump(edge.from, edge);\n bump(edge.to, edge);\n }\n\n // Stable order: descending score, then ascending id for determinism.\n const ids = Array.from(aggregates.keys()).sort((a, b) => {\n const aScore = aggregates.get(a)?.score ?? 0;\n const bScore = aggregates.get(b)?.score ?? 0;\n if (aScore !== bScore) return bScore - aScore;\n if (a === b) return 0;\n return a < b ? -1 : 1;\n });\n\n return ids.map((id) => {\n const meta = metadata.get(id);\n const aggregate = aggregates.get(id);\n return {\n id,\n label: meta?.label ?? defaultLabelFromPath(id),\n kind: meta?.category ?? \"unknown\",\n score: Number((aggregate?.score ?? 0).toFixed(4)),\n lastUpdated: aggregate?.lastUpdatedIso ?? null,\n };\n });\n}\n\nfunction defaultLabelFromPath(relPath: string): string {\n const base = path.basename(relPath, path.extname(relPath));\n return base.length > 0 ? base : relPath;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recall-xray.ts"],"sourcesContent":["/**\n * Recall X-ray snapshot schema (issue #570, PR 1).\n *\n * The X-ray surface is the unified, per-result attribution document that\n * merges the tier that served each memory, its score decomposition, any\n * graph path, an audit entry id, and the exact filter/eligibility ladder\n * that either admitted or rejected each candidate. This file defines the\n * schema and an in-memory builder + builder-state helper.\n *\n * Scope for PR 1 (this slice):\n * - Types only + pure builder functions (no IO, no rendering).\n * - Orchestrator plumbing captures a snapshot when the caller passes\n * `xrayCapture: true`. No behavior change when the flag is absent.\n * - NO new public surfaces here — CLI/HTTP/MCP land in later slices.\n *\n * The shared renderer lands in PR 2 at `recall-xray-renderer.ts`. Do not\n * fork formatting logic into other surfaces; extend the renderer.\n */\n\nimport { randomUUID } from \"node:crypto\";\n\nimport {\n normalizeRetrievedMemoryProvenance,\n type RetrievedMemoryProvenance,\n} from \"./memory-provenance.js\";\nimport type { RecallDisclosure, RecallTierExplain } from \"./types.js\";\nimport { isRecallDisclosure } from \"./types.js\";\n\n/**\n * Estimate token cost of a payload at the rough ~4 chars/token English\n * heuristic. Non-negative integer; returns 0 for empty / null input.\n * Used by recall surfaces to attach `estimatedTokens` to X-ray results\n * (issue #677 PR 3/4). Identical to the private heuristic in\n * `chunking.ts`; kept self-contained here so X-ray callers don't pull\n * in chunking internals.\n */\nexport function estimateRecallTokens(text: string | null | undefined): number {\n if (typeof text !== \"string\" || text.length === 0) return 0;\n return Math.ceil(text.length / 4);\n}\n\n/**\n * Aggregated per-disclosure token spend summary, computed by the\n * renderer from a snapshot's results. Non-negative integers.\n */\nexport interface RecallXrayDisclosureSummary {\n chunk: { count: number; estimatedTokens: number };\n section: { count: number; estimatedTokens: number };\n raw: { count: number; estimatedTokens: number };\n /** Number of results without a recorded disclosure level. */\n unspecified: { count: number; estimatedTokens: number };\n}\n\n/**\n * Which retrieval source produced a given result. This is the X-ray\n * tier ladder called out in issue #570 and is *distinct* from the\n * `RetrievalTier` enum (which describes issue #518's tier-explain\n * block). Keeping the sets separate lets the two observability\n * surfaces evolve without conflating their vocabularies:\n *\n * - `RetrievalTier` — direct-answer eligibility ladder.\n * - `RecallXrayServedBy` — which source materialized each result.\n */\nexport type RecallXrayServedBy =\n | \"direct-answer\"\n | \"hybrid\"\n | \"graph\"\n | \"recent-scan\"\n | \"procedural\"\n | \"review-context\"\n | \"lcm-file-parts\"\n | \"lcm-tool-parts\";\n\nexport const RECALL_XRAY_SERVED_BY_VALUES: readonly RecallXrayServedBy[] = [\n \"direct-answer\",\n \"hybrid\",\n \"graph\",\n \"recent-scan\",\n \"procedural\",\n \"review-context\",\n \"lcm-file-parts\",\n \"lcm-tool-parts\",\n] as const;\n\nexport function isRecallXrayServedBy(\n value: unknown,\n): value is RecallXrayServedBy {\n return (\n typeof value === \"string\" &&\n (RECALL_XRAY_SERVED_BY_VALUES as readonly string[]).includes(value)\n );\n}\n\n/**\n * Score decomposition for a single X-ray result.\n *\n * All fields are optional because different tiers populate different\n * terms: `hybrid` reports vector + bm25 + mmr penalty, `direct-answer`\n * reports importance + tier prior, etc. The only guaranteed field is\n * `final`, which is the post-combination score used for ordering.\n */\nexport interface RecallXrayScoreDecomposition {\n vector?: number;\n bm25?: number;\n importance?: number;\n mmrPenalty?: number;\n tierPrior?: number;\n /** Additive boost from `reinforcement_count` frontmatter (issue #687 PR 3/4). */\n reinforcementBoost?: number;\n final: number;\n}\n\n/**\n * Per-result breakdown inside an X-ray snapshot.\n */\nexport interface RecallXrayResult {\n memoryId: string;\n path: string;\n servedBy: RecallXrayServedBy;\n scoreDecomposition: RecallXrayScoreDecomposition;\n graphPath?: string[];\n /**\n * Issue #681 PR 3/3 — per-edge confidence values aligned with\n * `graphPath`. When present, `graphEdgeConfidences[i]` is the\n * confidence of the edge between `graphPath[i]` and `graphPath[i+1]`,\n * so the array length is one less than `graphPath`. Legacy edges\n * without a recorded confidence render as `1.0`. Operators use this\n * to attribute floor-pruning and PageRank ranking decisions back to\n * specific edges. The renderer drops the line when the array is\n * empty or absent so legacy snapshots round-trip cleanly.\n */\n graphEdgeConfidences?: number[];\n auditEntryId?: string;\n /** Human-readable list of filters the candidate *passed*. */\n admittedBy: string[];\n /**\n * First filter that *would have* rejected the candidate, or undefined\n * when the candidate was admitted without a rejection trace. When\n * present, `admittedBy` may still contain filters the candidate passed\n * before the rejecting gate; consumers should render both.\n */\n rejectedBy?: string;\n /**\n * Disclosure depth used to render this result's payload (issue #677\n * PR 3/4). Mirrors the per-result disclosure already exposed in the\n * recall response so X-ray consumers can attribute token spend to\n * the depth that produced it.\n */\n disclosure?: RecallDisclosure;\n /**\n * Estimated token cost of the rendered payload at the chosen\n * disclosure depth. Non-negative integer. Computed by callers via\n * `estimateRecallTokens(text)`; the renderer aggregates these into\n * a per-disclosure summary so operators can see where their budget\n * went.\n */\n estimatedTokens?: number;\n /**\n * Free-form tags from the memory's YAML frontmatter (issue #689 PR 3/3).\n * Populated by the X-ray capture path when the caller passes a `tags`\n * filter so per-result tags are available alongside the filter trace\n * in `snapshot.filters`. Also populated without a filter when the\n * orchestrator decorates results via `xrayCapture: true` so all X-ray\n * consumers can inspect memory labels without a separate storage read.\n * Absent (not `[]`) when the frontmatter has no tags or the memory\n * could not be read.\n */\n tags?: string[];\n /**\n * Per-result memory provenance and safety context. Populated by recall\n * capture when the memory frontmatter was already loaded by retrieval.\n */\n provenance?: RetrievedMemoryProvenance;\n /**\n * Claim-level provenance span (issue #1575 PR 3). The first source excerpt\n * backing this memory, surfaced so X-ray consumers can see the literal\n * utterance each fact derives from alongside retrieval provenance. Absent\n * when the memory carries no source spans (legacy or `provenance: \"none\"`).\n */\n sourceSpan?: {\n /** Verbatim quote excerpt (capped at write time by `provenance.maxQuoteChars`). */\n quote: string;\n /** ISO timestamp of the source turn. */\n observedAt: string;\n /** Coarse strength tag from the frontmatter. */\n provenance: \"verified\" | \"unverified\" | \"none\";\n };\n}\n\n/**\n * Trace entry for a filter the orchestrator evaluated during recall.\n * Captures the name of the filter, how many candidates it saw, and how\n * many it let through. Used by X-ray consumers to render the filter\n * ladder above the per-result breakdown.\n */\nexport interface RecallFilterTrace {\n name: string;\n considered: number;\n admitted: number;\n /** Optional human-readable reason for any rejections. */\n reason?: string;\n}\n\n/**\n * Peer-profile injection annotation (issue #679 completion).\n *\n * When `peerProfileRecallEnabled` is true and a peer is registered for\n * the session, the orchestrator injects a `## Peer Profile` section into\n * the recall context. This annotation records which peer was injected and\n * how many profile fields were included so operators can correlate\n * retrieval quality with peer-context enrichment.\n *\n * `null` means no peer profile was injected (peer not registered, feature\n * disabled, or peer has no profile fields).\n */\nexport interface RecallXrayPeerProfileInjection {\n /** The peer id whose profile was injected. */\n peerId: string;\n /**\n * Number of profile fields included after the `peerProfileRecallMaxFields`\n * cap was applied. Zero means the profile existed but had no fields.\n */\n fieldsInjected: number;\n}\n\n/**\n * The unified X-ray snapshot. CLI, HTTP, and MCP surfaces all render\n * this same shape through the shared renderer (CLAUDE.md rule 22).\n */\nexport interface RecallXraySnapshot {\n /** Stable v1 tag so downstream consumers can version-gate their parsers. */\n schemaVersion: \"1\";\n query: string;\n /** UUID minted per capture; unique across snapshots within a process. */\n snapshotId: string;\n /** Epoch milliseconds the snapshot was captured. */\n capturedAt: number;\n /**\n * Tier-explain block from issue #518, carried verbatim when present.\n * `null` means direct-answer tier did not run (disabled, or another\n * tier served the query).\n */\n tierExplain: RecallTierExplain | null;\n results: RecallXrayResult[];\n filters: RecallFilterTrace[];\n /**\n * Character budget accounting for the final assembled recall payload.\n * `used` is the rendered-context length; `chars` is the cap. Both are\n * non-negative integers in `[0, 2**31)`.\n */\n budget: { chars: number; used: number };\n /** Optional session-scope fields carried for downstream filtering. */\n sessionKey?: string;\n namespace?: string;\n traceId?: string;\n /**\n * Peer-profile injection metadata (issue #679 completion).\n * Non-null when `peerProfileRecallEnabled` is true and a peer profile\n * was successfully injected into this recall's context. `null` (or\n * absent) means no peer profile was injected.\n */\n peerProfileInjection?: RecallXrayPeerProfileInjection | null;\n}\n\n// ─── Builder ──────────────────────────────────────────────────────────────\n\nexport interface BuildXraySnapshotInput {\n query: string;\n tierExplain?: RecallTierExplain | null;\n results?: RecallXrayResult[];\n filters?: RecallFilterTrace[];\n budget?: { chars?: number; used?: number };\n sessionKey?: string;\n namespace?: string;\n traceId?: string;\n /** Peer-profile injection metadata (issue #679 completion). */\n peerProfileInjection?: RecallXrayPeerProfileInjection | null;\n /** Optional injected timestamp for deterministic tests. */\n now?: () => number;\n /** Optional injected id generator for deterministic tests. */\n snapshotIdGenerator?: () => string;\n}\n\n/**\n * Build a `RecallXraySnapshot` from explicit input fields. Pure\n * function; safe to call from anywhere. All array/object inputs are\n * shallow-copied so caller mutation after build cannot tear the\n * returned snapshot.\n */\nexport function buildXraySnapshot(\n input: BuildXraySnapshotInput,\n): RecallXraySnapshot {\n const now = input.now ?? Date.now;\n const snapshotIdGenerator = input.snapshotIdGenerator ?? randomUUID;\n\n const results = Array.isArray(input.results)\n ? input.results.map(cloneResult)\n : [];\n const filters = Array.isArray(input.filters)\n ? input.filters.map(cloneFilter)\n : [];\n\n const budgetChars = nonNegativeInt(input.budget?.chars);\n const budgetUsed = nonNegativeInt(input.budget?.used);\n\n const tierExplain =\n input.tierExplain && typeof input.tierExplain === \"object\"\n ? cloneTierExplain(input.tierExplain)\n : null;\n\n // Peer-profile injection annotation (issue #679 completion).\n // Deep-copy via structuredClone so the caller can't mutate the snapshot\n // after build. Accept null explicitly (no injection) or a valid object.\n let peerProfileInjection: RecallXrayPeerProfileInjection | null | undefined;\n if (input.peerProfileInjection && typeof input.peerProfileInjection === \"object\") {\n const raw = input.peerProfileInjection;\n const peerId = nonEmptyString(raw.peerId);\n if (peerId !== undefined) {\n peerProfileInjection = {\n peerId,\n fieldsInjected: nonNegativeInt(raw.fieldsInjected),\n };\n }\n } else if (input.peerProfileInjection === null) {\n peerProfileInjection = null;\n }\n\n const out: RecallXraySnapshot = {\n schemaVersion: \"1\",\n query: typeof input.query === \"string\" ? input.query : \"\",\n snapshotId: snapshotIdGenerator(),\n capturedAt: now(),\n tierExplain,\n results,\n filters,\n budget: { chars: budgetChars, used: budgetUsed },\n sessionKey: nonEmptyString(input.sessionKey),\n namespace: nonEmptyString(input.namespace),\n traceId: nonEmptyString(input.traceId),\n };\n if (peerProfileInjection !== undefined) {\n out.peerProfileInjection = peerProfileInjection;\n }\n return out;\n}\n\n/**\n * Mutable builder used by the orchestrator to accumulate X-ray fields\n * as recall progresses. Call `build()` to get the finalized\n * immutable-ish snapshot. All inputs are validated at insert time so\n * a malformed entry cannot poison the snapshot later.\n */\nexport class RecallXrayBuilder {\n private readonly query: string;\n private readonly sessionKey: string | undefined;\n private namespace: string | undefined;\n private traceId: string | undefined;\n private tierExplain: RecallTierExplain | null = null;\n private readonly results: RecallXrayResult[] = [];\n private readonly filters: RecallFilterTrace[] = [];\n private budgetChars = 0;\n private budgetUsed = 0;\n private peerProfileInjection: RecallXrayPeerProfileInjection | null | undefined;\n\n constructor(opts: {\n query: string;\n sessionKey?: string;\n namespace?: string;\n traceId?: string;\n }) {\n this.query = typeof opts.query === \"string\" ? opts.query : \"\";\n this.sessionKey = nonEmptyString(opts.sessionKey);\n this.namespace = nonEmptyString(opts.namespace);\n this.traceId = nonEmptyString(opts.traceId);\n }\n\n setNamespace(namespace: string | undefined): void {\n this.namespace = nonEmptyString(namespace);\n }\n\n setTraceId(traceId: string | undefined): void {\n this.traceId = nonEmptyString(traceId);\n }\n\n setTierExplain(tierExplain: RecallTierExplain | null | undefined): void {\n this.tierExplain =\n tierExplain && typeof tierExplain === \"object\"\n ? cloneTierExplain(tierExplain)\n : null;\n }\n\n setBudget(budget: { chars?: number; used?: number }): void {\n this.budgetChars = nonNegativeInt(budget.chars);\n this.budgetUsed = nonNegativeInt(budget.used);\n }\n\n /**\n * Record peer-profile injection metadata for this recall snapshot\n * (issue #679 completion). Pass `null` to explicitly record that no\n * injection happened (feature enabled but no peer / no profile fields).\n */\n setPeerProfileInjection(\n injection: RecallXrayPeerProfileInjection | null,\n ): void {\n this.peerProfileInjection = injection;\n }\n\n recordResult(result: RecallXrayResult): void {\n this.results.push(cloneResult(result));\n }\n\n recordFilter(filter: RecallFilterTrace): void {\n this.filters.push(cloneFilter(filter));\n }\n\n build(opts: {\n now?: () => number;\n snapshotIdGenerator?: () => string;\n } = {}): RecallXraySnapshot {\n return buildXraySnapshot({\n query: this.query,\n tierExplain: this.tierExplain,\n results: this.results,\n filters: this.filters,\n budget: { chars: this.budgetChars, used: this.budgetUsed },\n sessionKey: this.sessionKey,\n namespace: this.namespace,\n traceId: this.traceId,\n peerProfileInjection: this.peerProfileInjection,\n now: opts.now,\n snapshotIdGenerator: opts.snapshotIdGenerator,\n });\n }\n}\n\n// ─── Internals ────────────────────────────────────────────────────────────\n\nfunction cloneResult(result: RecallXrayResult): RecallXrayResult {\n if (!result || typeof result !== \"object\") {\n throw new TypeError(\"RecallXrayResult must be an object\");\n }\n if (!isRecallXrayServedBy(result.servedBy)) {\n throw new TypeError(\n `RecallXrayResult.servedBy must be one of ${RECALL_XRAY_SERVED_BY_VALUES.join(\n \", \",\n )}; got ${JSON.stringify(result.servedBy)}`,\n );\n }\n const memoryId = typeof result.memoryId === \"string\" ? result.memoryId : \"\";\n const path = typeof result.path === \"string\" ? result.path : \"\";\n const admittedBy = Array.isArray(result.admittedBy)\n ? result.admittedBy.filter((x): x is string => typeof x === \"string\")\n : [];\n const graphPath = Array.isArray(result.graphPath)\n ? result.graphPath.filter((x): x is string => typeof x === \"string\")\n : undefined;\n // Issue #681 PR 3/3 — per-edge confidences alongside graph path.\n // Each entry is clamped into [0, 1]; the array is rejected wholesale\n // when alignment cannot be verified so downstream surfaces can rely\n // on `graphEdgeConfidences[i]` describing the edge between\n // `graphPath[i]` and `graphPath[i+1]`.\n //\n // Cursor review (#735): the input array length MUST match\n // `graphPath.length - 1` *before* any per-element filtering. The\n // earlier implementation skipped non-finite entries via `continue`\n // and then length-checked the cleaned array — that would silently\n // shift surviving values to earlier positions. Example: input\n // `[0.5, NaN, 0.7]` for a 3-edge path would collapse to\n // `[0.5, 0.7]`, length-check would pass against `expected = 2`, and\n // the renderer would mis-attribute `0.7` to edge B→C when it really\n // came from edge C→D. Reject on either size mismatch or any\n // non-finite entry so misalignment is impossible.\n let graphEdgeConfidences: number[] | undefined;\n if (Array.isArray(result.graphEdgeConfidences) && graphPath && graphPath.length > 1) {\n const expected = graphPath.length - 1;\n const raw = result.graphEdgeConfidences;\n if (raw.length === expected) {\n const cleaned: number[] = [];\n let allFinite = true;\n for (const value of raw) {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n allFinite = false;\n break;\n }\n cleaned.push(Math.min(1, Math.max(0, value)));\n }\n if (allFinite) {\n graphEdgeConfidences = cleaned;\n }\n }\n }\n const auditEntryId = nonEmptyString(result.auditEntryId);\n const rejectedBy = nonEmptyString(result.rejectedBy);\n const scoreDecomposition = cloneScoreDecomposition(result.scoreDecomposition);\n const out: RecallXrayResult = {\n memoryId,\n path,\n servedBy: result.servedBy,\n scoreDecomposition,\n admittedBy,\n };\n if (graphPath !== undefined) out.graphPath = graphPath;\n if (graphEdgeConfidences !== undefined) {\n out.graphEdgeConfidences = graphEdgeConfidences;\n }\n if (auditEntryId !== undefined) out.auditEntryId = auditEntryId;\n if (rejectedBy !== undefined) out.rejectedBy = rejectedBy;\n // Disclosure + token telemetry (issue #677 PR 3/4). Only attach when\n // present and well-formed; unknown disclosure values are dropped so a\n // bad caller can't poison downstream renderers. Uses the shared\n // `isRecallDisclosure` guard so adding a fourth disclosure level\n // requires touching only `types.ts`.\n if (isRecallDisclosure(result.disclosure)) {\n out.disclosure = result.disclosure;\n }\n if (\n typeof result.estimatedTokens === \"number\" &&\n Number.isFinite(result.estimatedTokens) &&\n result.estimatedTokens >= 0\n ) {\n out.estimatedTokens = Math.floor(result.estimatedTokens);\n }\n // Tags from frontmatter (issue #689 PR 3/3). Normalize identically to\n // the recall-surface path: trim and drop empty strings so consumers can\n // compare tags directly without a secondary normalization step.\n if (Array.isArray(result.tags)) {\n const cleanedTags = result.tags\n .filter((t): t is string => typeof t === \"string\")\n .map((t) => t.trim())\n .filter((t) => t.length > 0);\n if (cleanedTags.length > 0) {\n out.tags = cleanedTags;\n }\n }\n const provenance = normalizeRetrievedMemoryProvenance(result.provenance);\n if (provenance !== undefined) {\n out.provenance = provenance;\n }\n // Claim-level provenance span (#1575 PR 3). Deep-copy the three scalar\n // fields so the snapshot returned by getLastXraySnapshot is independent\n // of the caller's object (matches the structuredClone contract).\n const ss = result.sourceSpan;\n if (\n ss &&\n typeof ss.quote === \"string\" &&\n typeof ss.observedAt === \"string\" &&\n (ss.provenance === \"verified\" ||\n ss.provenance === \"unverified\" ||\n ss.provenance === \"none\")\n ) {\n out.sourceSpan = {\n quote: ss.quote,\n observedAt: ss.observedAt,\n provenance: ss.provenance,\n };\n }\n return out;\n}\n\n/**\n * Summarize per-disclosure token spend across an X-ray snapshot's\n * results. Pure helper — used by the markdown renderer to print a\n * \"per-disclosure token spend\" line and exposed for tests / surfaces.\n */\nexport function summarizeDisclosureTokens(\n results: ReadonlyArray<RecallXrayResult>,\n): RecallXrayDisclosureSummary {\n const summary: RecallXrayDisclosureSummary = {\n chunk: { count: 0, estimatedTokens: 0 },\n section: { count: 0, estimatedTokens: 0 },\n raw: { count: 0, estimatedTokens: 0 },\n unspecified: { count: 0, estimatedTokens: 0 },\n };\n for (const result of results) {\n const tokens =\n typeof result.estimatedTokens === \"number\" &&\n Number.isFinite(result.estimatedTokens) &&\n result.estimatedTokens >= 0\n ? Math.floor(result.estimatedTokens)\n : 0;\n const bucket = isRecallDisclosure(result.disclosure)\n ? result.disclosure\n : \"unspecified\";\n summary[bucket].count += 1;\n summary[bucket].estimatedTokens += tokens;\n }\n return summary;\n}\n\nfunction cloneFilter(filter: RecallFilterTrace): RecallFilterTrace {\n if (!filter || typeof filter !== \"object\") {\n throw new TypeError(\"RecallFilterTrace must be an object\");\n }\n const out: RecallFilterTrace = {\n name: typeof filter.name === \"string\" ? filter.name : \"\",\n considered: nonNegativeInt(filter.considered),\n admitted: nonNegativeInt(filter.admitted),\n };\n const reason = nonEmptyString(filter.reason);\n if (reason !== undefined) out.reason = reason;\n return out;\n}\n\nfunction cloneScoreDecomposition(\n value: RecallXrayScoreDecomposition | undefined,\n): RecallXrayScoreDecomposition {\n if (!value || typeof value !== \"object\") {\n return { final: 0 };\n }\n const out: RecallXrayScoreDecomposition = {\n final: finiteNumber(value.final) ?? 0,\n };\n const vector = finiteNumber(value.vector);\n if (vector !== undefined) out.vector = vector;\n const bm25 = finiteNumber(value.bm25);\n if (bm25 !== undefined) out.bm25 = bm25;\n const importance = finiteNumber(value.importance);\n if (importance !== undefined) out.importance = importance;\n const mmrPenalty = finiteNumber(value.mmrPenalty);\n if (mmrPenalty !== undefined) out.mmrPenalty = mmrPenalty;\n const tierPrior = finiteNumber(value.tierPrior);\n if (tierPrior !== undefined) out.tierPrior = tierPrior;\n const reinforcementBoost = finiteNumber(value.reinforcementBoost);\n if (reinforcementBoost !== undefined && reinforcementBoost > 0) {\n out.reinforcementBoost = reinforcementBoost;\n }\n return out;\n}\n\nfunction cloneTierExplain(tierExplain: RecallTierExplain): RecallTierExplain {\n // Use structuredClone so future RecallTierExplain additions do not\n // silently share references through hand-enumerated fields. The\n // payload is JSON-shaped.\n return structuredClone(tierExplain);\n}\n\nfunction nonNegativeInt(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return 0;\n if (value <= 0) return 0;\n return Math.floor(value);\n}\n\nfunction finiteNumber(value: unknown): number | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return undefined;\n return value;\n}\n\nfunction nonEmptyString(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n if (trimmed.length === 0) return undefined;\n return trimmed;\n}\n"],"mappings":"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","names":[]}
|