@remnic/core 9.3.725 → 9.3.727
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 +5 -5
- package/dist/access-boundary.js +28 -27
- package/dist/access-cli.js +65 -63
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +20 -5
- package/dist/access-http.js +34 -33
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +31 -30
- package/dist/access-operations-batch.js +29 -28
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +30 -29
- package/dist/{access-service-B2PlPtxh.d.ts → access-service-Bgpjxo4f.d.ts} +36 -4
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +27 -26
- package/dist/access-surface-catalog.d.ts +5 -5
- 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 +3 -3
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +9 -8
- 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/capabilities.d.ts +1 -1
- package/dist/{catalog-CzUb-GyD.d.ts → catalog-bMczlO2G.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +10 -9
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-KGZF2VO3.js → chunk-27QQC7ZX.js} +80 -19
- package/dist/{chunk-KGZF2VO3.js.map → chunk-27QQC7ZX.js.map} +1 -1
- package/dist/{chunk-HCVWF657.js → chunk-2KAYTPPT.js} +12 -12
- package/dist/{chunk-4SF232E6.js → chunk-36SEDFAU.js} +19 -7
- package/dist/chunk-36SEDFAU.js.map +1 -0
- package/dist/{chunk-ZK2L72VN.js → chunk-37436GML.js} +2 -2
- package/dist/{chunk-SC2YAWRF.js → chunk-3TSTGI6F.js} +2 -2
- package/dist/{chunk-FACTVMXQ.js → chunk-4H4F3BFL.js} +2 -2
- package/dist/{chunk-WAFDHKES.js → chunk-4NOI577P.js} +2 -2
- package/dist/{chunk-YQMPUYHE.js → chunk-4NR2PDZ5.js} +2 -2
- package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
- package/dist/{chunk-FN6QP7JQ.js → chunk-6PY5YJAV.js} +100 -14
- package/dist/{chunk-FN6QP7JQ.js.map → chunk-6PY5YJAV.js.map} +1 -1
- package/dist/{chunk-PKMQK2WY.js → chunk-773DPI6T.js} +4 -4
- package/dist/{chunk-4R3CGCI5.js → chunk-7RLCCNHU.js} +15 -12
- package/dist/chunk-7RLCCNHU.js.map +1 -0
- package/dist/{chunk-XGTBNVIU.js → chunk-AAD2ZS6P.js} +11 -1
- package/dist/chunk-AAD2ZS6P.js.map +1 -0
- package/dist/{chunk-Z3FGC3QE.js → chunk-AQLOP5F4.js} +139 -30
- package/dist/chunk-AQLOP5F4.js.map +1 -0
- package/dist/{chunk-FXCIOLRB.js → chunk-ASKXIAHD.js} +2 -2
- package/dist/{chunk-5OPYYPJN.js → chunk-CBWI22JN.js} +2 -2
- package/dist/{chunk-ZOHMPFOB.js → chunk-CKFLLAIS.js} +3 -3
- package/dist/{chunk-GZQXOQQ2.js → chunk-CSRXQ6NB.js} +5 -5
- package/dist/{chunk-QR67FU5G.js → chunk-CTOQEZSN.js} +2 -2
- package/dist/{chunk-ZUDM75KG.js → chunk-DCWIQFNA.js} +4 -4
- package/dist/{chunk-6CWGS2LY.js → chunk-DKJCRGWM.js} +3 -3
- package/dist/{chunk-JJJCVCCQ.js → chunk-DMQWV6SQ.js} +2 -2
- package/dist/{chunk-F3XZN24Z.js → chunk-DRUDC2UX.js} +2 -2
- package/dist/{chunk-4WZ3J7M6.js → chunk-EY6MDMHK.js} +2 -2
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-C4X4QGIO.js → chunk-G6NKALRN.js} +5 -2
- package/dist/chunk-G6NKALRN.js.map +1 -0
- package/dist/{chunk-6SQQOJ4U.js → chunk-GKRLPHPU.js} +2 -2
- package/dist/{chunk-D7EBTQ24.js → chunk-HOHVIVDR.js} +2 -2
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-UBHVAGO7.js → chunk-JYLFZWUX.js} +2 -2
- package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
- package/dist/chunk-LTHVM4XN.js +109 -0
- package/dist/chunk-LTHVM4XN.js.map +1 -0
- package/dist/{chunk-WM7DNKBD.js → chunk-M4V2JVSZ.js} +2 -2
- package/dist/{chunk-I2HL5M6T.js → chunk-MJIHJ7OG.js} +52 -52
- package/dist/{chunk-J334N6QV.js → chunk-MLKWJOAJ.js} +2 -2
- package/dist/{chunk-C6BWLYW5.js → chunk-N3VNUM4Z.js} +171 -79
- package/dist/chunk-N3VNUM4Z.js.map +1 -0
- package/dist/{chunk-DF3I6JGP.js → chunk-NSA5ZRUL.js} +15 -15
- package/dist/{chunk-VI5SGGXB.js → chunk-OEEW4KJA.js} +4 -4
- package/dist/{chunk-7AG54AF3.js → chunk-PUR2CKX2.js} +1 -1
- package/dist/chunk-PUR2CKX2.js.map +1 -0
- package/dist/{chunk-NT36GU6X.js → chunk-R7IYU5DZ.js} +2 -2
- package/dist/{chunk-TT5U3UDO.js → chunk-SNKPUJHR.js} +4 -4
- package/dist/{chunk-T55EGZSJ.js → chunk-UVPL565C.js} +2 -2
- package/dist/chunk-W5H6ZRPT.js +14 -0
- package/dist/chunk-W5H6ZRPT.js.map +1 -0
- package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
- package/dist/{chunk-KLFYKC3K.js → chunk-XPHVAOBT.js} +195 -74
- package/dist/chunk-XPHVAOBT.js.map +1 -0
- package/dist/{chunk-H7HUQIY7.js → chunk-Y4YD5TYW.js} +3 -3
- package/dist/{chunk-MDBZTH7N.js → chunk-YKBHOBJK.js} +44 -10
- package/dist/chunk-YKBHOBJK.js.map +1 -0
- package/dist/{chunk-YUYPWJD3.js → chunk-YOLZS7NY.js} +2 -2
- package/dist/{chunk-6OIDDKAA.js → chunk-YTU7WKYN.js} +35 -9
- package/dist/chunk-YTU7WKYN.js.map +1 -0
- package/dist/{cli-IOlDHSBT.d.ts → cli-BLME420h.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +60 -59
- package/dist/compat/codex-cli-fallback.d.ts +1 -0
- package/dist/compat/codex-cli-fallback.js +22 -0
- package/dist/compat/codex-cli-fallback.js.map +1 -0
- package/dist/compat/codex-thread-key.d.ts +1 -0
- package/dist/compat/codex-thread-key.js +18 -0
- package/dist/compat/codex-thread-key.js.map +1 -0
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +9 -8
- 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 +3 -3
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +9 -8
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +9 -8
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/content-hash.d.ts +14 -0
- package/dist/content-hash.js +10 -0
- package/dist/content-hash.js.map +1 -0
- 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/dashboard-runtime.js +2 -2
- 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 +9 -8
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-faithfulness.js +2 -2
- 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-redaction-rules.d.ts +33 -0
- package/dist/extraction-redaction-rules.js +14 -0
- package/dist/extraction-redaction-rules.js.map +1 -0
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +3 -3
- package/dist/faithfulness-config.d.ts +1 -0
- package/dist/faithfulness-config.js +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{forget-OEE5OELK.js → forget-WYLR4ZFV.js} +2 -2
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +107 -105
- 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 +9 -4
- package/dist/local-model-endpoint.js +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +9 -8
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -8
- package/dist/maintenance/rebuild-memory-projection.js +10 -9
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- 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 +19 -18
- 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 +9 -8
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +28 -27
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +11 -10
- package/dist/{orchestrator-C_OMSIMZ.d.ts → orchestrator-DqPAz8ot.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +51 -49
- 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 +1 -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-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 +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +4 -4
- 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/schemas.d.ts +22 -22
- 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-B8LiYtvx.d.ts → semantic-consolidation-Cl15ksZy.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +10 -9
- package/dist/semantic-rule-promotion.js +9 -8
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +9 -8
- 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 +3 -3
- package/dist/storage.js +9 -8
- package/dist/summarizer.d.ts +1 -1
- 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/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-DmTaqob2.d.ts → types-ChQb9D6s.d.ts} +8 -0
- 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 +9 -8
- package/package.json +26 -2
- package/src/access-http.ts +68 -1
- package/src/admin/admin-surfaces.test.ts +289 -0
- package/src/admin/admin-surfaces.ts +138 -7
- package/src/bitemporal-dedup-backfill.test.ts +294 -0
- package/src/chat/chat-engine.test.ts +66 -1
- package/src/chat/chat-http.test.ts +119 -1
- package/src/chat/chat-http.ts +252 -97
- package/src/chat/chat-session.ts +143 -7
- package/src/compat/codex-cli-fallback.ts +23 -0
- package/src/compat/codex-subpath-aliases.test.ts +93 -0
- package/src/compat/codex-thread-key.ts +23 -0
- package/src/config.test.ts +50 -0
- package/src/config.ts +21 -4
- package/src/content-hash.ts +24 -0
- package/src/correction/correction-access-wiring.ts +104 -19
- package/src/correction/correction-contract.ts +68 -10
- package/src/correction/correction-executor.test.ts +112 -2
- package/src/correction/correction-executor.ts +65 -5
- package/src/correction/correction-planner.test.ts +47 -0
- package/src/correction/correction-planner.ts +40 -1
- package/src/correction/correction-storage-integration.test.ts +183 -0
- package/src/extraction-faithfulness.test.ts +240 -0
- package/src/extraction-faithfulness.ts +58 -6
- package/src/extraction-redaction-rules.test.ts +213 -0
- package/src/extraction-redaction-rules.ts +208 -0
- package/src/faithfulness-config.ts +16 -1
- package/src/local-model-endpoint.ts +24 -3
- package/src/orchestrator.ts +252 -9
- package/src/provenance-extraction.test.ts +266 -0
- package/src/provenance.ts +66 -16
- package/src/storage.ts +6 -10
- package/src/types.ts +8 -0
- package/dist/chunk-4R3CGCI5.js.map +0 -1
- package/dist/chunk-4SF232E6.js.map +0 -1
- package/dist/chunk-6OIDDKAA.js.map +0 -1
- package/dist/chunk-7AG54AF3.js.map +0 -1
- package/dist/chunk-C4X4QGIO.js.map +0 -1
- package/dist/chunk-C6BWLYW5.js.map +0 -1
- package/dist/chunk-KLFYKC3K.js.map +0 -1
- package/dist/chunk-MDBZTH7N.js.map +0 -1
- package/dist/chunk-XGTBNVIU.js.map +0 -1
- package/dist/chunk-Z3FGC3QE.js.map +0 -1
- /package/dist/{chunk-HCVWF657.js.map → chunk-2KAYTPPT.js.map} +0 -0
- /package/dist/{chunk-ZK2L72VN.js.map → chunk-37436GML.js.map} +0 -0
- /package/dist/{chunk-SC2YAWRF.js.map → chunk-3TSTGI6F.js.map} +0 -0
- /package/dist/{chunk-FACTVMXQ.js.map → chunk-4H4F3BFL.js.map} +0 -0
- /package/dist/{chunk-WAFDHKES.js.map → chunk-4NOI577P.js.map} +0 -0
- /package/dist/{chunk-YQMPUYHE.js.map → chunk-4NR2PDZ5.js.map} +0 -0
- /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
- /package/dist/{chunk-PKMQK2WY.js.map → chunk-773DPI6T.js.map} +0 -0
- /package/dist/{chunk-FXCIOLRB.js.map → chunk-ASKXIAHD.js.map} +0 -0
- /package/dist/{chunk-5OPYYPJN.js.map → chunk-CBWI22JN.js.map} +0 -0
- /package/dist/{chunk-ZOHMPFOB.js.map → chunk-CKFLLAIS.js.map} +0 -0
- /package/dist/{chunk-GZQXOQQ2.js.map → chunk-CSRXQ6NB.js.map} +0 -0
- /package/dist/{chunk-QR67FU5G.js.map → chunk-CTOQEZSN.js.map} +0 -0
- /package/dist/{chunk-ZUDM75KG.js.map → chunk-DCWIQFNA.js.map} +0 -0
- /package/dist/{chunk-6CWGS2LY.js.map → chunk-DKJCRGWM.js.map} +0 -0
- /package/dist/{chunk-JJJCVCCQ.js.map → chunk-DMQWV6SQ.js.map} +0 -0
- /package/dist/{chunk-F3XZN24Z.js.map → chunk-DRUDC2UX.js.map} +0 -0
- /package/dist/{chunk-4WZ3J7M6.js.map → chunk-EY6MDMHK.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-6SQQOJ4U.js.map → chunk-GKRLPHPU.js.map} +0 -0
- /package/dist/{chunk-D7EBTQ24.js.map → chunk-HOHVIVDR.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-UBHVAGO7.js.map → chunk-JYLFZWUX.js.map} +0 -0
- /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
- /package/dist/{chunk-WM7DNKBD.js.map → chunk-M4V2JVSZ.js.map} +0 -0
- /package/dist/{chunk-I2HL5M6T.js.map → chunk-MJIHJ7OG.js.map} +0 -0
- /package/dist/{chunk-J334N6QV.js.map → chunk-MLKWJOAJ.js.map} +0 -0
- /package/dist/{chunk-DF3I6JGP.js.map → chunk-NSA5ZRUL.js.map} +0 -0
- /package/dist/{chunk-VI5SGGXB.js.map → chunk-OEEW4KJA.js.map} +0 -0
- /package/dist/{chunk-NT36GU6X.js.map → chunk-R7IYU5DZ.js.map} +0 -0
- /package/dist/{chunk-TT5U3UDO.js.map → chunk-SNKPUJHR.js.map} +0 -0
- /package/dist/{chunk-T55EGZSJ.js.map → chunk-UVPL565C.js.map} +0 -0
- /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
- /package/dist/{chunk-H7HUQIY7.js.map → chunk-Y4YD5TYW.js.map} +0 -0
- /package/dist/{chunk-YUYPWJD3.js.map → chunk-YOLZS7NY.js.map} +0 -0
- /package/dist/{forget-OEE5OELK.js.map → forget-WYLR4ZFV.js.map} +0 -0
|
@@ -26,6 +26,7 @@ import type { FaithfulnessResult } from "./extraction-faithfulness.js";
|
|
|
26
26
|
import type { FallbackLlmClient } from "./fallback-llm.js";
|
|
27
27
|
import type { LocalLlmClient } from "./local-llm.js";
|
|
28
28
|
import type { MemoryFrontmatter } from "./types.js";
|
|
29
|
+
import { callOpenAiCompatibleChat } from "./local-model-endpoint.js";
|
|
29
30
|
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
31
32
|
// Helpers — stub LLM clients
|
|
@@ -1429,3 +1430,242 @@ test("checkFaithfulnessBatch: local-endpoint failure does NOT leak the local mod
|
|
|
1429
1430
|
);
|
|
1430
1431
|
assert.equal(result.results[0]?.ok, true);
|
|
1431
1432
|
});
|
|
1433
|
+
|
|
1434
|
+
// ---------------------------------------------------------------------------
|
|
1435
|
+
// Issue #1700 nits #5 + #6: probe signal forwarding + local parse-fallback
|
|
1436
|
+
// ---------------------------------------------------------------------------
|
|
1437
|
+
|
|
1438
|
+
test("callOpenAiCompatibleChat: a pre-aborted batch signal aborts the probe immediately, not after timeoutMs (#1700 nit #5)", async () => {
|
|
1439
|
+
// The batch signal is ALREADY aborted when the probe starts. With nit #5 the
|
|
1440
|
+
// probe forwards it and aborts instantly; without it the probe would wait for
|
|
1441
|
+
// its own timeoutMs (5000ms here). Assert the probe returns null fast.
|
|
1442
|
+
const endpoint = { baseUrl: "http://localhost:11434/v1", model: "remnic-faithfulness-gate-v1" };
|
|
1443
|
+
const batchController = new AbortController();
|
|
1444
|
+
batchController.abort();
|
|
1445
|
+
const spy = ((_url: string, init: RequestInit) =>
|
|
1446
|
+
new Promise<Response>((_resolve, reject) => {
|
|
1447
|
+
const sig = init.signal;
|
|
1448
|
+
if (!sig) return reject(new Error("no signal"));
|
|
1449
|
+
if (sig.aborted) return reject(new Error("aborted"));
|
|
1450
|
+
sig.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
1451
|
+
})) as unknown as typeof fetch;
|
|
1452
|
+
const start = Date.now();
|
|
1453
|
+
const result = await callOpenAiCompatibleChat(
|
|
1454
|
+
endpoint,
|
|
1455
|
+
[{ role: "user", content: "hi" }],
|
|
1456
|
+
{ timeoutMs: 5000, signal: batchController.signal },
|
|
1457
|
+
spy,
|
|
1458
|
+
);
|
|
1459
|
+
const elapsed = Date.now() - start;
|
|
1460
|
+
assert.equal(result, null, "a pre-aborted signal must fail the probe closed (null)");
|
|
1461
|
+
assert.ok(
|
|
1462
|
+
elapsed < 1000,
|
|
1463
|
+
`probe must abort immediately on a pre-aborted signal (${elapsed}ms), not wait timeoutMs`,
|
|
1464
|
+
);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
test("checkFaithfulnessBatch: default surfaces malformed_output for a 200-with-garbage local response (#1700 nit #6)", async () => {
|
|
1468
|
+
// extractionFaithfulnessLocalParseFallback unset (default false): a local
|
|
1469
|
+
// endpoint returning 200 with non-verdict JSON surfaces malformed_output so a
|
|
1470
|
+
// misconfigured endpoint is visible. The configured chain must NOT run.
|
|
1471
|
+
const inputs = [{ factText: "Fact", quote: "Quote" }];
|
|
1472
|
+
const config = parseConfig({
|
|
1473
|
+
extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
|
|
1474
|
+
extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
|
|
1475
|
+
extractionFaithfulnessTimeoutMs: 5000,
|
|
1476
|
+
});
|
|
1477
|
+
const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
|
|
1478
|
+
const { fetch: garbageFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
|
|
1479
|
+
model: "remnic-faithfulness-gate-v1",
|
|
1480
|
+
choices: [{ message: { content: "this is not a verdict array" } }],
|
|
1481
|
+
}));
|
|
1482
|
+
const result = await checkFaithfulnessBatch(
|
|
1483
|
+
inputs,
|
|
1484
|
+
config,
|
|
1485
|
+
null,
|
|
1486
|
+
stubFallbackLlm(JSON.stringify([{ index: 0, verdict: "entailed" }]), fallbackCalls),
|
|
1487
|
+
garbageFetch,
|
|
1488
|
+
);
|
|
1489
|
+
assert.equal(fallbackCalls.length, 0, "chain must NOT run when flag is off — garbage surfaces loudly");
|
|
1490
|
+
assert.equal(result.results[0]?.ok, false);
|
|
1491
|
+
if (!result.results[0]?.ok) {
|
|
1492
|
+
assert.equal(result.results[0].error.code, "malformed_output");
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
test("checkFaithfulnessBatch: extractionFaithfulnessLocalParseFallback falls back to the chain on local parse failure (#1700 nit #6)", async () => {
|
|
1497
|
+
// Flag ON: the same 200-with-garbage local response now falls through to the
|
|
1498
|
+
// configured chain instead of surfacing malformed_output (resilient fallback).
|
|
1499
|
+
const inputs = [{ factText: "Fact", quote: "Quote" }];
|
|
1500
|
+
const config = parseConfig({
|
|
1501
|
+
extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
|
|
1502
|
+
extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
|
|
1503
|
+
extractionFaithfulnessTimeoutMs: 5000,
|
|
1504
|
+
extractionFaithfulnessLocalParseFallback: true,
|
|
1505
|
+
});
|
|
1506
|
+
const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
|
|
1507
|
+
const { fetch: garbageFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
|
|
1508
|
+
model: "remnic-faithfulness-gate-v1",
|
|
1509
|
+
choices: [{ message: { content: "this is not a verdict array" } }],
|
|
1510
|
+
}));
|
|
1511
|
+
const result = await checkFaithfulnessBatch(
|
|
1512
|
+
inputs,
|
|
1513
|
+
config,
|
|
1514
|
+
null,
|
|
1515
|
+
stubFallbackLlm(JSON.stringify([{ index: 0, verdict: "entailed" }]), fallbackCalls),
|
|
1516
|
+
garbageFetch,
|
|
1517
|
+
);
|
|
1518
|
+
assert.equal(
|
|
1519
|
+
fallbackCalls.length,
|
|
1520
|
+
1,
|
|
1521
|
+
"chain MUST run when flag is on and the local response is unparseable",
|
|
1522
|
+
);
|
|
1523
|
+
assert.equal(result.results[0]?.ok, true);
|
|
1524
|
+
if (result.results[0]?.ok) {
|
|
1525
|
+
assert.equal(result.results[0].verdict, "entailed", "fallback chain verdict is used");
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
test("checkFaithfulnessBatch: extractionFaithfulnessLocalParseFallback does NOT fall back when the local response is valid (#1700 nit #6 regression)", async () => {
|
|
1530
|
+
// Flag ON but the local response IS a valid verdict array → use it, do not
|
|
1531
|
+
// call the chain. Guards against the flag over-triggering on good responses.
|
|
1532
|
+
const inputs = [{ factText: "Fact", quote: "Quote" }];
|
|
1533
|
+
const config = parseConfig({
|
|
1534
|
+
extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
|
|
1535
|
+
extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
|
|
1536
|
+
extractionFaithfulnessTimeoutMs: 5000,
|
|
1537
|
+
extractionFaithfulnessLocalParseFallback: true,
|
|
1538
|
+
});
|
|
1539
|
+
const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
|
|
1540
|
+
const { fetch: goodFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
|
|
1541
|
+
model: "remnic-faithfulness-gate-v1",
|
|
1542
|
+
choices: [{ message: { content: JSON.stringify([{ index: 0, verdict: "contradicted" }]) } }],
|
|
1543
|
+
}));
|
|
1544
|
+
const result = await checkFaithfulnessBatch(
|
|
1545
|
+
inputs,
|
|
1546
|
+
config,
|
|
1547
|
+
null,
|
|
1548
|
+
stubFallbackLlm(JSON.stringify([{ index: 0, verdict: "entailed" }]), fallbackCalls),
|
|
1549
|
+
goodFetch,
|
|
1550
|
+
);
|
|
1551
|
+
assert.equal(fallbackCalls.length, 0, "chain must NOT run when the local response is valid, even with the flag on");
|
|
1552
|
+
assert.equal(result.results[0]?.ok, true);
|
|
1553
|
+
if (result.results[0]?.ok) {
|
|
1554
|
+
assert.equal(result.results[0].verdict, "contradicted", "valid local verdict is used");
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
test("checkFaithfulnessBatch: extractionFaithfulnessLocalParseFallback falls back when the local response is a PARTIAL verdict array (codex P2 PRRT_kwDORJXyws6O6zwZ)", async () => {
|
|
1559
|
+
// Flag ON: a local endpoint that returns a valid-but-INCOMPLETE verdict
|
|
1560
|
+
// array (1 of N entries) must fall through to the configured chain, NOT be
|
|
1561
|
+
// accepted as-is. parseFaithfulnessResponse is truthy as soon as ONE entry
|
|
1562
|
+
// is valid, so the previous guard accepted the partial response and the
|
|
1563
|
+
// missing indexes surfaced as malformed_output downstream -- defeating the
|
|
1564
|
+
// resilient fallback. Accept the local response only when it covers every
|
|
1565
|
+
// expected index.
|
|
1566
|
+
const inputs = [
|
|
1567
|
+
{ factText: "Fact A", quote: "Quote A" },
|
|
1568
|
+
{ factText: "Fact B", quote: "Quote B" },
|
|
1569
|
+
];
|
|
1570
|
+
const config = parseConfig({
|
|
1571
|
+
extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
|
|
1572
|
+
extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
|
|
1573
|
+
extractionFaithfulnessTimeoutMs: 5000,
|
|
1574
|
+
extractionFaithfulnessLocalParseFallback: true,
|
|
1575
|
+
});
|
|
1576
|
+
const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
|
|
1577
|
+
// Local endpoint returns only index 0 -- index 1 is missing (partial).
|
|
1578
|
+
const { fetch: partialFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
|
|
1579
|
+
model: "remnic-faithfulness-gate-v1",
|
|
1580
|
+
choices: [{ message: { content: JSON.stringify([{ index: 0, verdict: "contradicted" }]) } }],
|
|
1581
|
+
}));
|
|
1582
|
+
const result = await checkFaithfulnessBatch(
|
|
1583
|
+
inputs,
|
|
1584
|
+
config,
|
|
1585
|
+
null,
|
|
1586
|
+
stubFallbackLlm(
|
|
1587
|
+
JSON.stringify([
|
|
1588
|
+
{ index: 0, verdict: "entailed" },
|
|
1589
|
+
{ index: 1, verdict: "unsupported" },
|
|
1590
|
+
]),
|
|
1591
|
+
fallbackCalls,
|
|
1592
|
+
),
|
|
1593
|
+
partialFetch,
|
|
1594
|
+
);
|
|
1595
|
+
assert.equal(
|
|
1596
|
+
fallbackCalls.length,
|
|
1597
|
+
1,
|
|
1598
|
+
"a partial local verdict array must fall through to the configured chain",
|
|
1599
|
+
);
|
|
1600
|
+
// The chain's complete verdict array is used for both indexes.
|
|
1601
|
+
assert.equal(result.results[0]?.ok && result.results[0].verdict, "entailed");
|
|
1602
|
+
assert.equal(result.results[1]?.ok && result.results[1].verdict, "unsupported");
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
test("checkFaithfulnessBatch: extractionFaithfulnessLocalParseFallback falls back when local indexes are fractional/non-contiguous (codex P2 PRRT_kwDORJXyws6O7PfY)", async () => {
|
|
1606
|
+
// Flag ON: a malformed local response with expectedCount distinct but
|
|
1607
|
+
// NON-INTEGER indexes (e.g. 0 and 0.5 for a two-item batch) must NOT be
|
|
1608
|
+
// accepted. parseFaithfulnessResponse used to admit any numeric index, so a
|
|
1609
|
+
// size-only check passed while index 1 stayed missing -> malformed_output.
|
|
1610
|
+
// parseEntries now rejects non-integer indexes, and the gate requires every
|
|
1611
|
+
// integer key 0..N-1 before accepting the local response.
|
|
1612
|
+
const inputs = [
|
|
1613
|
+
{ factText: "Fact A", quote: "Quote A" },
|
|
1614
|
+
{ factText: "Fact B", quote: "Quote B" },
|
|
1615
|
+
];
|
|
1616
|
+
const config = parseConfig({
|
|
1617
|
+
extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
|
|
1618
|
+
extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
|
|
1619
|
+
extractionFaithfulnessTimeoutMs: 5000,
|
|
1620
|
+
extractionFaithfulnessLocalParseFallback: true,
|
|
1621
|
+
});
|
|
1622
|
+
const fallbackCalls: Array<{ messages: unknown; options: unknown }> = [];
|
|
1623
|
+
// Local endpoint returns indexes 0 and 0.5 -- two distinct keys, but index 1
|
|
1624
|
+
// is missing (fractional index is invalid).
|
|
1625
|
+
const { fetch: fracFetch } = fakeFetchFor("http://localhost:11434/v1", () => ({
|
|
1626
|
+
model: "remnic-faithfulness-gate-v1",
|
|
1627
|
+
choices: [{ message: { content: JSON.stringify([
|
|
1628
|
+
{ index: 0, verdict: "contradicted" },
|
|
1629
|
+
{ index: 0.5, verdict: "entailed" },
|
|
1630
|
+
]) } }],
|
|
1631
|
+
}));
|
|
1632
|
+
const result = await checkFaithfulnessBatch(
|
|
1633
|
+
inputs,
|
|
1634
|
+
config,
|
|
1635
|
+
null,
|
|
1636
|
+
stubFallbackLlm(
|
|
1637
|
+
JSON.stringify([
|
|
1638
|
+
{ index: 0, verdict: "entailed" },
|
|
1639
|
+
{ index: 1, verdict: "unsupported" },
|
|
1640
|
+
]),
|
|
1641
|
+
fallbackCalls,
|
|
1642
|
+
),
|
|
1643
|
+
fracFetch,
|
|
1644
|
+
);
|
|
1645
|
+
assert.equal(
|
|
1646
|
+
fallbackCalls.length,
|
|
1647
|
+
1,
|
|
1648
|
+
"a fractional/non-contiguous local response must fall through to the chain",
|
|
1649
|
+
);
|
|
1650
|
+
assert.equal(result.results[0]?.ok && result.results[0].verdict, "entailed");
|
|
1651
|
+
assert.equal(result.results[1]?.ok && result.results[1].verdict, "unsupported");
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
test('parseConfig: extractionFaithfulnessLocalParseFallback coerces CLI string "true" (#1700 review — coerceBool parity)', () => {
|
|
1655
|
+
// A CLI override reaches the parser as the string "true"; === true would
|
|
1656
|
+
// silently leave the flag disabled. coerceBool must turn it on (gotcha 36).
|
|
1657
|
+
const onByString = parseConfig({
|
|
1658
|
+
extractionFaithfulnessLocalParseFallback: "true",
|
|
1659
|
+
});
|
|
1660
|
+
assert.equal(onByString.extractionFaithfulnessLocalParseFallback, true);
|
|
1661
|
+
const onByBool = parseConfig({
|
|
1662
|
+
extractionFaithfulnessLocalParseFallback: true,
|
|
1663
|
+
});
|
|
1664
|
+
assert.equal(onByBool.extractionFaithfulnessLocalParseFallback, true);
|
|
1665
|
+
const offDefault = parseConfig({});
|
|
1666
|
+
assert.equal(offDefault.extractionFaithfulnessLocalParseFallback, false);
|
|
1667
|
+
const offByString = parseConfig({
|
|
1668
|
+
extractionFaithfulnessLocalParseFallback: "false",
|
|
1669
|
+
});
|
|
1670
|
+
assert.equal(offByString.extractionFaithfulnessLocalParseFallback, false);
|
|
1671
|
+
});
|
|
@@ -220,6 +220,27 @@ export function parseFaithfulnessResponse(
|
|
|
220
220
|
return null;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* True when `map` contains an entry for every integer index in
|
|
225
|
+
* [0, expectedCount). The local parse-fallback gate uses this to accept a
|
|
226
|
+
* local model-lab response only when it is COMPLETE: size alone is
|
|
227
|
+
* insufficient because a malformed response with duplicate/fractional indexes
|
|
228
|
+
* can yield expectedCount distinct map keys without covering index 0..N-1
|
|
229
|
+
* (codex P2 PRRT_kwDORJXyws6O7PfY). parseEntries now rejects non-integer
|
|
230
|
+
* indexes so the two checks agree, but the explicit coverage loop documents
|
|
231
|
+
* the invariant and stays robust to any future parser relaxation.
|
|
232
|
+
*/
|
|
233
|
+
function coversAllIndexes(
|
|
234
|
+
map: Map<number, ParsedFaithfulnessEntry>,
|
|
235
|
+
expectedCount: number,
|
|
236
|
+
): boolean {
|
|
237
|
+
if (map.size < expectedCount) return false;
|
|
238
|
+
for (let i = 0; i < expectedCount; i++) {
|
|
239
|
+
if (!map.has(i)) return false;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
223
244
|
function parseEntries(
|
|
224
245
|
data: unknown,
|
|
225
246
|
expectedCount: number,
|
|
@@ -243,7 +264,7 @@ function parseEntries(
|
|
|
243
264
|
if (!entry || typeof entry !== "object") continue;
|
|
244
265
|
const obj = entry as Record<string, unknown>;
|
|
245
266
|
const idx = typeof obj.index === "number" ? obj.index : undefined;
|
|
246
|
-
if (idx === undefined || idx < 0 || idx >= expectedCount) continue;
|
|
267
|
+
if (idx === undefined || !Number.isInteger(idx) || idx < 0 || idx >= expectedCount) continue;
|
|
247
268
|
const verdictRaw = typeof obj.verdict === "string" ? obj.verdict : undefined;
|
|
248
269
|
if (!verdictRaw || !VALID_VERDICTS.has(verdictRaw)) continue;
|
|
249
270
|
const rationale =
|
|
@@ -279,6 +300,7 @@ interface LlmCallResult {
|
|
|
279
300
|
async function callFaithfulnessLlm(
|
|
280
301
|
systemPrompt: string,
|
|
281
302
|
userPrompt: string,
|
|
303
|
+
expectedCount: number,
|
|
282
304
|
config: PluginConfig,
|
|
283
305
|
localLlm: LocalLlmClient | null,
|
|
284
306
|
fallbackLlm: FallbackLlmClient | null,
|
|
@@ -311,15 +333,44 @@ async function callFaithfulnessLlm(
|
|
|
311
333
|
const result = await callOpenAiCompatibleChat(
|
|
312
334
|
localEndpoint,
|
|
313
335
|
messages,
|
|
314
|
-
{
|
|
336
|
+
{
|
|
337
|
+
temperature: 0.1,
|
|
338
|
+
maxTokens: 2048,
|
|
339
|
+
responseFormatJson: true,
|
|
340
|
+
timeoutMs: probeBudgetMs,
|
|
341
|
+
// Forward the batch signal so the probe aborts the instant the batch
|
|
342
|
+
// budget elapses, not after probeBudgetMs (issue #1700 nit #5).
|
|
343
|
+
...(signal ? { signal } : {}),
|
|
344
|
+
},
|
|
315
345
|
fetchImpl,
|
|
316
346
|
);
|
|
317
347
|
if (result?.content) {
|
|
318
|
-
|
|
348
|
+
// Issue #1700 nit #6: pre-validate the local endpoint response shape.
|
|
349
|
+
// Default (extractionFaithfulnessLocalParseFallback=false) returns the
|
|
350
|
+
// content as-is -- a 200-with-garbage local response surfaces
|
|
351
|
+
// malformed_output, alerting the operator to a misconfigured endpoint.
|
|
352
|
+
// When the operator opts into resilient fallback, an unparseable OR
|
|
353
|
+
// PARTIAL local response falls through to the configured chain instead
|
|
354
|
+
// of surfacing. parseFaithfulnessResponse is truthy as soon as ONE entry
|
|
355
|
+
// is valid, so accept the local response only when the parsed verdict
|
|
356
|
+
// map covers every expected index -- otherwise the missing indexes would
|
|
357
|
+
// surface as malformed_output downstream, defeating the resilient
|
|
358
|
+
// fallback (codex P2 PRRT_kwDORJXyws6O6zwZ).
|
|
359
|
+
if (!config.extractionFaithfulnessLocalParseFallback) {
|
|
360
|
+
return { content: result.content, modelUsed: result.modelUsed };
|
|
361
|
+
}
|
|
362
|
+
const parsedLocal = parseFaithfulnessResponse(result.content, expectedCount);
|
|
363
|
+
if (parsedLocal && coversAllIndexes(parsedLocal, expectedCount)) {
|
|
364
|
+
return { content: result.content, modelUsed: result.modelUsed };
|
|
365
|
+
}
|
|
366
|
+
log.debug(
|
|
367
|
+
"extraction-faithfulness: local endpoint returned incomplete/unparseable output; falling back to configured chain",
|
|
368
|
+
);
|
|
369
|
+
} else {
|
|
370
|
+
log.debug(
|
|
371
|
+
"extraction-faithfulness: local model-lab endpoint unavailable, trying configured chain",
|
|
372
|
+
);
|
|
319
373
|
}
|
|
320
|
-
log.debug(
|
|
321
|
-
"extraction-faithfulness: local model-lab endpoint unavailable, trying configured chain",
|
|
322
|
-
);
|
|
323
374
|
}
|
|
324
375
|
|
|
325
376
|
// extractionFaithfulnessModel is the LOCAL served model's name (e.g.
|
|
@@ -532,6 +583,7 @@ export async function checkFaithfulnessBatch(
|
|
|
532
583
|
const callPromise = callFaithfulnessLlm(
|
|
533
584
|
FAITHFULNESS_SYSTEM_PROMPT,
|
|
534
585
|
userPrompt,
|
|
586
|
+
checkableInputs.length,
|
|
535
587
|
config,
|
|
536
588
|
localLlm,
|
|
537
589
|
fallbackLlm,
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extraction-redaction-rules.test.ts — issue #1669 (#1580 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* Proves the extraction-layer redaction-rule helper reads persisted
|
|
5
|
+
* `redaction_rule` patterns and withholds matching content. The orchestrator
|
|
6
|
+
* consults these rules before the storage write chokepoint so a never_store
|
|
7
|
+
* correction actually blocks future extraction of matching content.
|
|
8
|
+
*/
|
|
9
|
+
import { strict as assert } from "node:assert";
|
|
10
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import {
|
|
15
|
+
REDACTION_RULES_SUBDIR,
|
|
16
|
+
compileRedactionPattern,
|
|
17
|
+
contentMatchesRedactionRules,
|
|
18
|
+
loadRedactionRules,
|
|
19
|
+
} from "./extraction-redaction-rules.js";
|
|
20
|
+
import { validateRedactionPattern } from "./correction/correction-contract.js";
|
|
21
|
+
|
|
22
|
+
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
23
|
+
const dir = await mkdtemp(path.join(tmpdir(), "remnic-redact-"));
|
|
24
|
+
try {
|
|
25
|
+
return await fn(dir);
|
|
26
|
+
} finally {
|
|
27
|
+
await rm(dir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function writeRule(stateDir: string, pattern: string): Promise<void> {
|
|
32
|
+
const dir = path.join(stateDir, REDACTION_RULES_SUBDIR);
|
|
33
|
+
await mkdir(dir, { recursive: true });
|
|
34
|
+
const slug = pattern.replace(/[^a-zA-Z0-9]+/g, "-").slice(0, 64) || "rule";
|
|
35
|
+
await writeFile(
|
|
36
|
+
path.join(dir, `${slug}.json`),
|
|
37
|
+
`${JSON.stringify({ pattern, namespace: "default", createdAt: "2026-07-06T00:00:00Z" })}\n`,
|
|
38
|
+
"utf-8",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("compileRedactionPattern: literal pattern matches by substring", () => {
|
|
43
|
+
const rule = compileRedactionPattern("secret-token-123");
|
|
44
|
+
assert.ok(rule.matcher("the secret-token-123 leaked here"));
|
|
45
|
+
assert.ok(!rule.matcher("nothing here"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("compileRedactionPattern: regex-like pattern compiles to a RegExp", () => {
|
|
49
|
+
// Only /.../-wrapped patterns compile as RegExp (review thread P1).
|
|
50
|
+
const rule = compileRedactionPattern("/secret-token-\\d+/");
|
|
51
|
+
assert.ok(rule.matcher("found secret-token-999 in logs"));
|
|
52
|
+
assert.ok(!rule.matcher("found secret-token-abc in logs"));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("compileRedactionPattern (P1): unwrapped pattern with metacharacters is literal", () => {
|
|
56
|
+
// abc+def must match the LITERAL string, not the regex abccccdef.
|
|
57
|
+
const rule = compileRedactionPattern("abc+def");
|
|
58
|
+
assert.ok(rule.matcher("the value is abc+def here"),
|
|
59
|
+
"unwrapped metachar pattern must match the literal substring");
|
|
60
|
+
assert.ok(!rule.matcher("abccccdef"),
|
|
61
|
+
"unwrapped metachar pattern must NOT be compiled as a regex");
|
|
62
|
+
// An API-token-shaped literal like foo(bar) also works.
|
|
63
|
+
const rule2 = compileRedactionPattern("foo(bar)");
|
|
64
|
+
assert.ok(rule2.matcher("contains foo(bar) text"));
|
|
65
|
+
assert.ok(!rule2.matcher("contains foobar text"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("compileRedactionPattern: /wrapped/ regex strips delimiters", () => {
|
|
69
|
+
const rule = compileRedactionPattern("/internal-only-[a-z]+/");
|
|
70
|
+
assert.ok(rule.matcher("this is internal-only-foo data"));
|
|
71
|
+
assert.ok(!rule.matcher("this is public data"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("compileRedactionPattern: malformed /wrapped/ regex falls back to literal", () => {
|
|
75
|
+
// /.../-wrapped with an unbalanced bracket — RegExp() throws, falls back
|
|
76
|
+
// to literal substring on the BODY (without delimiters).
|
|
77
|
+
const rule = compileRedactionPattern("/[unclosed/");
|
|
78
|
+
assert.ok(rule.matcher("contains [unclosed literally"));
|
|
79
|
+
assert.ok(!rule.matcher("no match"));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("loadRedactionRules: empty when the dir does not exist (cold install)", async () => {
|
|
83
|
+
await withTempDir(async (dir) => {
|
|
84
|
+
const rules = await loadRedactionRules(dir);
|
|
85
|
+
assert.equal(rules.length, 0);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("loadRedactionRules: reads persisted literal + regex patterns", async () => {
|
|
90
|
+
await withTempDir(async (dir) => {
|
|
91
|
+
await writeRule(dir, "secret-token-123");
|
|
92
|
+
await writeRule(dir, "/api-key-[a-f0-9]{8}/");
|
|
93
|
+
const rules = await loadRedactionRules(dir);
|
|
94
|
+
assert.equal(rules.length, 2);
|
|
95
|
+
assert.ok(contentMatchesRedactionRules("leaked secret-token-123 here", rules));
|
|
96
|
+
assert.ok(contentMatchesRedactionRules("api-key-deadbeef was used", rules));
|
|
97
|
+
assert.ok(!contentMatchesRedactionRules("clean content", rules));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("loadRedactionRules: skips a corrupt rule file without failing the pass", async () => {
|
|
102
|
+
await withTempDir(async (dir) => {
|
|
103
|
+
await writeRule(dir, "good-pattern");
|
|
104
|
+
const dir2 = path.join(dir, REDACTION_RULES_SUBDIR);
|
|
105
|
+
// A non-JSON file masquerading as a rule.
|
|
106
|
+
await writeFile(path.join(dir2, "broken.json"), "{not valid json", "utf-8");
|
|
107
|
+
// A rule file missing the `pattern` field.
|
|
108
|
+
await writeFile(path.join(dir2, "nopattern.json"), JSON.stringify({ foo: "bar" }), "utf-8");
|
|
109
|
+
const rules = await loadRedactionRules(dir);
|
|
110
|
+
assert.equal(rules.length, 1, "only the well-formed rule compiles");
|
|
111
|
+
assert.ok(contentMatchesRedactionRules("good-pattern here", rules));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("contentMatchesRedactionRules: empty rule set never matches", () => {
|
|
116
|
+
assert.equal(contentMatchesRedactionRules("anything", []), false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("regression (#1669): a persisted redaction rule blocks matching content", async () => {
|
|
120
|
+
await withTempDir(async (dir) => {
|
|
121
|
+
// Simulate a `redaction_rule` correction that persisted a never-store pattern.
|
|
122
|
+
await writeRule(dir, "do-not-store-this-exact-phrase");
|
|
123
|
+
const rules = await loadRedactionRules(dir);
|
|
124
|
+
// Content matching the persisted pattern is withheld.
|
|
125
|
+
assert.ok(
|
|
126
|
+
contentMatchesRedactionRules("a fact containing do-not-store-this-exact-phrase inside", rules),
|
|
127
|
+
"matching content must be withheld",
|
|
128
|
+
);
|
|
129
|
+
// Non-matching content passes through.
|
|
130
|
+
assert.ok(
|
|
131
|
+
!contentMatchesRedactionRules("a perfectly fine durable fact", rules),
|
|
132
|
+
"non-matching content must pass",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("#1669 thread #3: catastrophic-backtracking regex falls back to literal (ReDoS guard)", () => {
|
|
138
|
+
// A pattern like (a+)+ is a classic ReDoS shape. It must NOT compile as a
|
|
139
|
+
// RegExp — it would hang persistExtraction on a near-miss fact. Instead it
|
|
140
|
+
// falls back to literal substring match.
|
|
141
|
+
const rule = compileRedactionPattern("(a+)+");
|
|
142
|
+
// The literal fallback matches when the exact string "(a+)+" appears:
|
|
143
|
+
assert.equal(rule.matcher("a fact with (a+)+ inside"), true,
|
|
144
|
+
"literal fallback must still match the pattern as a substring");
|
|
145
|
+
// A long near-miss string that would cause exponential backtracking on the
|
|
146
|
+
// regex must return quickly (literal match, no regex evaluation):
|
|
147
|
+
const nearMiss = "a".repeat(100) + "!";
|
|
148
|
+
assert.equal(rule.matcher(nearMiss), false,
|
|
149
|
+
"near-miss must not hang — literal fallback, not regex");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("#1669 thread #3: safe regex pattern still compiles as RegExp", () => {
|
|
153
|
+
// A benign anchored pattern compiles normally.
|
|
154
|
+
const rule = compileRedactionPattern("/^secret-key-[a-f0-9]+$/");
|
|
155
|
+
assert.equal(rule.matcher("secret-key-deadbeef"), true);
|
|
156
|
+
assert.equal(rule.matcher("not-a-secret"), false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("#1669 cursor thread: overly broad /wrapped/ regex falls back to literal (not compiled)", () => {
|
|
160
|
+
// A hand-edited rule file with /.*/ must NOT compile — it would match every
|
|
161
|
+
// fact and withhold all extraction. isSafeRegex now rejects overly-broad
|
|
162
|
+
// bodies, so the rule falls back to literal substring on the body.
|
|
163
|
+
const rule = compileRedactionPattern("/.*/");
|
|
164
|
+
assert.ok(!rule.matcher("any fact content"),
|
|
165
|
+
"overly broad regex must not match arbitrary content");
|
|
166
|
+
assert.ok(rule.matcher("contains .* literally"),
|
|
167
|
+
"literal fallback on the body must still match the substring .*");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("#1669 thread #3 (first-line guard): validateRedactionPattern rejects catastrophic-backtracking shapes", () => {
|
|
171
|
+
// Classic ReDoS shapes must be rejected at apply time — not just fall back
|
|
172
|
+
// to literal at extraction time. Each is a nested quantifier or overlapping
|
|
173
|
+
// alternation that would hang on a near-miss fact.
|
|
174
|
+
const pathological = ["/(a+)+/", "/(a*)*/", "/(a?)+/", "/(a|a)+/"];
|
|
175
|
+
for (const p of pathological) {
|
|
176
|
+
assert.throws(
|
|
177
|
+
() => validateRedactionPattern(p),
|
|
178
|
+
{ message: /unsafe/ },
|
|
179
|
+
`validateRedactionPattern must reject catastrophic pattern: ${p}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
// The overly-broad check still fires for /.../-wrapped .* / .+ patterns.
|
|
183
|
+
assert.throws(() => validateRedactionPattern("/.*/"));
|
|
184
|
+
assert.throws(() => validateRedactionPattern("/.+/"));
|
|
185
|
+
// A near-miss string that would cause exponential backtracking on the
|
|
186
|
+
// regex never reaches extraction — the plan is rejected at apply time.
|
|
187
|
+
const nearMiss = "a".repeat(100) + "!";
|
|
188
|
+
assert.ok(nearMiss.length === 101);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("#1669 thread #3 (first-line guard): valid patterns still pass", () => {
|
|
192
|
+
// Bounded literal + benign anchored regex must NOT be rejected.
|
|
193
|
+
assert.equal(validateRedactionPattern("secret-token-\\d+"), "secret-token-\\d+");
|
|
194
|
+
assert.equal(validateRedactionPattern("(a|b)+"), "(a|b)+");
|
|
195
|
+
assert.equal(validateRedactionPattern("/^secret-key-[a-f0-9]+$/"), "/^secret-key-[a-f0-9]+$/");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("#1669 P2: validateRedactionPattern rejects zero-width regex", () => {
|
|
199
|
+
// Empty body or regex that matches the empty string would withhold every fact.
|
|
200
|
+
assert.throws(() => validateRedactionPattern("//"));
|
|
201
|
+
assert.throws(() => validateRedactionPattern("/a?/"));
|
|
202
|
+
assert.throws(() => validateRedactionPattern("/a*/"));
|
|
203
|
+
assert.throws(() => validateRedactionPattern("/(a|)/"));
|
|
204
|
+
// A bounded non-zero-width regex still passes.
|
|
205
|
+
assert.equal(validateRedactionPattern("/^secret-[a-f]+$/"), "/^secret-[a-f]+$/");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("#1669 P2: compileRedactionPattern empty body never matches", () => {
|
|
209
|
+
// A hand-edited // rule file must not withhold all extraction.
|
|
210
|
+
const rule = compileRedactionPattern("//");
|
|
211
|
+
assert.equal(rule.matcher("anything"), false);
|
|
212
|
+
assert.equal(rule.matcher(""), false);
|
|
213
|
+
});
|