nlm-memory 0.19.0 → 0.20.1
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/README.md +17 -10
- package/dist/cli/digest.js +21 -0
- package/dist/cli/digest.js.map +1 -1
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +49 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/nlm.d.ts +13 -1
- package/dist/cli/nlm.js +766 -55
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/actions/overlay.d.ts +20 -6
- package/dist/core/actions/overlay.js +32 -18
- package/dist/core/actions/overlay.js.map +1 -1
- package/dist/core/adapters/claude-code.js +6 -0
- package/dist/core/adapters/claude-code.js.map +1 -1
- package/dist/core/classifier/hierarchical-classify.d.ts +6 -2
- package/dist/core/classifier/hierarchical-classify.js +10 -6
- package/dist/core/classifier/hierarchical-classify.js.map +1 -1
- package/dist/core/classifier/prompt.js +3 -3
- package/dist/core/digest/compose.d.ts +10 -0
- package/dist/core/digest/compose.js +7 -1
- package/dist/core/digest/compose.js.map +1 -1
- package/dist/core/embedding/embed-backfill.d.ts +13 -2
- package/dist/core/embedding/embed-backfill.js +181 -43
- package/dist/core/embedding/embed-backfill.js.map +1 -1
- package/dist/core/embedding/embedding-config.d.ts +17 -0
- package/dist/core/embedding/embedding-config.js +12 -0
- package/dist/core/embedding/embedding-config.js.map +1 -0
- package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
- package/dist/core/embedding/pg-embed-backfill.js +264 -0
- package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
- package/dist/core/entities/dedup-suggest.d.ts +25 -0
- package/dist/core/entities/dedup-suggest.js +107 -0
- package/dist/core/entities/dedup-suggest.js.map +1 -0
- package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
- package/dist/core/eval/classifier-fixture-eval.js +217 -0
- package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
- package/dist/core/eval/extraction-scoring.d.ts +63 -0
- package/dist/core/eval/extraction-scoring.js +79 -0
- package/dist/core/eval/extraction-scoring.js.map +1 -0
- package/dist/core/eval/run-eval.d.ts +40 -0
- package/dist/core/eval/run-eval.js +34 -0
- package/dist/core/eval/run-eval.js.map +1 -0
- package/dist/core/exemplars/capture-from-session.d.ts +1 -0
- package/dist/core/exemplars/capture-from-session.js +6 -2
- package/dist/core/exemplars/capture-from-session.js.map +1 -1
- package/dist/core/exemplars/diff-parse.d.ts +23 -0
- package/dist/core/exemplars/diff-parse.js +73 -0
- package/dist/core/exemplars/diff-parse.js.map +1 -0
- package/dist/core/exemplars/embed-backfill.d.ts +36 -0
- package/dist/core/exemplars/embed-backfill.js +85 -0
- package/dist/core/exemplars/embed-backfill.js.map +1 -0
- package/dist/core/exemplars/embed-text.d.ts +15 -0
- package/dist/core/exemplars/embed-text.js +18 -0
- package/dist/core/exemplars/embed-text.js.map +1 -0
- package/dist/core/exemplars/extract-exemplar.js +2 -65
- package/dist/core/exemplars/extract-exemplar.js.map +1 -1
- package/dist/core/exemplars/recall-code.js +4 -0
- package/dist/core/exemplars/recall-code.js.map +1 -1
- package/dist/core/exemplars/warm-embedder.d.ts +14 -0
- package/dist/core/exemplars/warm-embedder.js +18 -0
- package/dist/core/exemplars/warm-embedder.js.map +1 -0
- package/dist/core/health/corpus-state.d.ts +8 -0
- package/dist/core/health/corpus-state.js +11 -0
- package/dist/core/health/corpus-state.js.map +1 -0
- package/dist/core/health/embed-inflight.d.ts +9 -0
- package/dist/core/health/embed-inflight.js +30 -0
- package/dist/core/health/embed-inflight.js.map +1 -0
- package/dist/core/health/embedding-lane-state.d.ts +6 -0
- package/dist/core/health/embedding-lane-state.js +18 -0
- package/dist/core/health/embedding-lane-state.js.map +1 -0
- package/dist/core/health/warmup-state.d.ts +12 -0
- package/dist/core/health/warmup-state.js +12 -0
- package/dist/core/health/warmup-state.js.map +1 -0
- package/dist/core/hook/gate.d.ts +13 -6
- package/dist/core/hook/gate.js +47 -6
- package/dist/core/hook/gate.js.map +1 -1
- package/dist/core/hook/hook-log.d.ts +5 -0
- package/dist/core/hook/hook-log.js.map +1 -1
- package/dist/core/hook/memo.d.ts +12 -0
- package/dist/core/hook/memo.js +41 -1
- package/dist/core/hook/memo.js.map +1 -1
- package/dist/core/hook/pointer-block.d.ts +1 -0
- package/dist/core/hook/pointer-block.js +23 -2
- package/dist/core/hook/pointer-block.js.map +1 -1
- package/dist/core/hook/query-extract.d.ts +2 -1
- package/dist/core/hook/query-extract.js +10 -1
- package/dist/core/hook/query-extract.js.map +1 -1
- package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
- package/dist/core/hook/resolve-conversation-by-query.js +98 -0
- package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
- package/dist/core/hook/select.d.ts +8 -0
- package/dist/core/hook/select.js +9 -2
- package/dist/core/hook/select.js.map +1 -1
- package/dist/core/hook/transcript.js +16 -2
- package/dist/core/hook/transcript.js.map +1 -1
- package/dist/core/ingest/ingest-session.d.ts +5 -0
- package/dist/core/ingest/ingest-session.js +3 -0
- package/dist/core/ingest/ingest-session.js.map +1 -1
- package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
- package/dist/core/ingest/reclassify-oversized.js +3 -0
- package/dist/core/ingest/reclassify-oversized.js.map +1 -1
- package/dist/core/ingest/reprocess.d.ts +96 -0
- package/dist/core/ingest/reprocess.js +275 -0
- package/dist/core/ingest/reprocess.js.map +1 -0
- package/dist/core/integrity/check-invariants.d.ts +6 -1
- package/dist/core/integrity/check-invariants.js +209 -3
- package/dist/core/integrity/check-invariants.js.map +1 -1
- package/dist/core/metrics/corpus-stats.d.ts +49 -0
- package/dist/core/metrics/corpus-stats.js +111 -0
- package/dist/core/metrics/corpus-stats.js.map +1 -0
- package/dist/core/metrics/re-derivation.d.ts +48 -0
- package/dist/core/metrics/re-derivation.js +87 -0
- package/dist/core/metrics/re-derivation.js.map +1 -0
- package/dist/core/recall/citation-log.d.ts +8 -0
- package/dist/core/recall/citation-log.js +17 -0
- package/dist/core/recall/citation-log.js.map +1 -1
- package/dist/core/recall/query-intent.d.ts +7 -0
- package/dist/core/recall/query-intent.js +25 -0
- package/dist/core/recall/query-intent.js.map +1 -0
- package/dist/core/recall/query-log.d.ts +4 -0
- package/dist/core/recall/query-log.js +4 -0
- package/dist/core/recall/query-log.js.map +1 -1
- package/dist/core/recall/recall-service.d.ts +37 -1
- package/dist/core/recall/recall-service.js +106 -63
- package/dist/core/recall/recall-service.js.map +1 -1
- package/dist/core/recall/related-exemplars.d.ts +1 -0
- package/dist/core/recall/related-exemplars.js +4 -1
- package/dist/core/recall/related-exemplars.js.map +1 -1
- package/dist/core/recall/related-facts.js +2 -2
- package/dist/core/recall/related-facts.js.map +1 -1
- package/dist/core/recall/reranker.d.ts +2 -1
- package/dist/core/recall/reranker.js +3 -3
- package/dist/core/recall/reranker.js.map +1 -1
- package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
- package/dist/core/recall-facts/fact-query-log.js +2 -0
- package/dist/core/recall-facts/fact-query-log.js.map +1 -1
- package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
- package/dist/core/recall-facts/fact-recall-service.js +21 -8
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scan-once.d.ts +6 -6
- package/dist/core/scheduler/scan-once.js +13 -49
- package/dist/core/scheduler/scan-once.js.map +1 -1
- package/dist/core/scheduler/scheduler.d.ts +10 -0
- package/dist/core/scheduler/scheduler.js +80 -28
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/signals/code-signal.d.ts +49 -0
- package/dist/core/signals/code-signal.js +53 -0
- package/dist/core/signals/code-signal.js.map +1 -0
- package/dist/core/storage/db-restore.js +3 -1
- package/dist/core/storage/db-restore.js.map +1 -1
- package/dist/core/storage/fact-batch.d.ts +9 -0
- package/dist/core/storage/fact-batch.js +14 -0
- package/dist/core/storage/fact-batch.js.map +1 -0
- package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
- package/dist/core/storage/pg-code-exemplar-store.js +7 -0
- package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
- package/dist/core/storage/pg-embedding-config.d.ts +29 -0
- package/dist/core/storage/pg-embedding-config.js +53 -0
- package/dist/core/storage/pg-embedding-config.js.map +1 -0
- package/dist/core/storage/pg-entity-store.d.ts +12 -0
- package/dist/core/storage/pg-entity-store.js +79 -0
- package/dist/core/storage/pg-entity-store.js.map +1 -0
- package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
- package/dist/core/storage/pg-fact-ingest.js +34 -0
- package/dist/core/storage/pg-fact-ingest.js.map +1 -0
- package/dist/core/storage/pg-fact-store.d.ts +3 -0
- package/dist/core/storage/pg-fact-store.js +26 -30
- package/dist/core/storage/pg-fact-store.js.map +1 -1
- package/dist/core/storage/pg-migrate.d.ts +18 -0
- package/dist/core/storage/pg-migrate.js +73 -0
- package/dist/core/storage/pg-migrate.js.map +1 -0
- package/dist/core/storage/pg-session-store.d.ts +9 -0
- package/dist/core/storage/pg-session-store.js +204 -76
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/pg-storage.d.ts +9 -7
- package/dist/core/storage/pg-storage.js +20 -50
- package/dist/core/storage/pg-storage.js.map +1 -1
- package/dist/core/storage/pg-workstream-store.d.ts +24 -0
- package/dist/core/storage/pg-workstream-store.js +96 -0
- package/dist/core/storage/pg-workstream-store.js.map +1 -0
- package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js +11 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
- package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
- package/dist/core/storage/sqlite-embedding-config.js +27 -0
- package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
- package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
- package/dist/core/storage/sqlite-entity-store.js +96 -0
- package/dist/core/storage/sqlite-entity-store.js.map +1 -0
- package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
- package/dist/core/storage/sqlite-fact-store.js +76 -25
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +20 -5
- package/dist/core/storage/sqlite-session-store.js +183 -68
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +8 -8
- package/dist/core/storage/sqlite-storage.js +21 -32
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/sqlite-workstream-store.d.ts +24 -0
- package/dist/core/storage/sqlite-workstream-store.js +98 -0
- package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
- package/dist/core/util/with-timeout.d.ts +5 -0
- package/dist/core/util/with-timeout.js +20 -0
- package/dist/core/util/with-timeout.js.map +1 -0
- package/dist/core/work-digest/active-spans.d.ts +8 -0
- package/dist/core/work-digest/active-spans.js +29 -0
- package/dist/core/work-digest/active-spans.js.map +1 -0
- package/dist/core/work-digest/attribute.d.ts +15 -0
- package/dist/core/work-digest/attribute.js +76 -0
- package/dist/core/work-digest/attribute.js.map +1 -0
- package/dist/core/work-digest/build-work-digest.d.ts +12 -0
- package/dist/core/work-digest/build-work-digest.js +69 -0
- package/dist/core/work-digest/build-work-digest.js.map +1 -0
- package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
- package/dist/core/work-digest/compose-work-digest.js +52 -0
- package/dist/core/work-digest/compose-work-digest.js.map +1 -0
- package/dist/core/work-digest/merge-active.d.ts +10 -0
- package/dist/core/work-digest/merge-active.js +22 -0
- package/dist/core/work-digest/merge-active.js.map +1 -0
- package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
- package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
- package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
- package/dist/core/work-digest/topics.d.ts +18 -0
- package/dist/core/work-digest/topics.js +26 -0
- package/dist/core/work-digest/topics.js.map +1 -0
- package/dist/core/work-digest/types.d.ts +45 -0
- package/dist/core/work-digest/types.js +2 -0
- package/dist/core/work-digest/types.js.map +1 -0
- package/dist/core/workstream/bind.d.ts +25 -0
- package/dist/core/workstream/bind.js +23 -0
- package/dist/core/workstream/bind.js.map +1 -0
- package/dist/core/workstream/compose-recall.d.ts +2 -0
- package/dist/core/workstream/compose-recall.js +39 -0
- package/dist/core/workstream/compose-recall.js.map +1 -0
- package/dist/core/workstream/merge-suggest.d.ts +19 -0
- package/dist/core/workstream/merge-suggest.js +61 -0
- package/dist/core/workstream/merge-suggest.js.map +1 -0
- package/dist/core/workstream/model.d.ts +20 -0
- package/dist/core/workstream/model.js +9 -0
- package/dist/core/workstream/model.js.map +1 -0
- package/dist/core/workstream/name-match.d.ts +8 -0
- package/dist/core/workstream/name-match.js +18 -0
- package/dist/core/workstream/name-match.js.map +1 -0
- package/dist/core/workstream/resolve.d.ts +10 -0
- package/dist/core/workstream/resolve.js +21 -0
- package/dist/core/workstream/resolve.js.map +1 -0
- package/dist/core/workstream/rollup.d.ts +12 -0
- package/dist/core/workstream/rollup.js +17 -0
- package/dist/core/workstream/rollup.js.map +1 -0
- package/dist/core/workstream/work-topics.d.ts +10 -0
- package/dist/core/workstream/work-topics.js +55 -0
- package/dist/core/workstream/work-topics.js.map +1 -0
- package/dist/hook/hook-helpers.d.ts +4 -0
- package/dist/hook/hook-helpers.js +36 -0
- package/dist/hook/hook-helpers.js.map +1 -0
- package/dist/hook/pre-compact-hook.d.ts +23 -0
- package/dist/hook/pre-compact-hook.js +66 -0
- package/dist/hook/pre-compact-hook.js.map +1 -0
- package/dist/hook/prompt-recall-hook.d.ts +37 -0
- package/dist/hook/prompt-recall-hook.js +120 -20
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/recall-gate.d.ts +29 -0
- package/dist/hook/recall-gate.js +50 -0
- package/dist/hook/recall-gate.js.map +1 -0
- package/dist/hook/recall-over-http.d.ts +1 -1
- package/dist/hook/recall-over-http.js +11 -11
- package/dist/hook/recall-over-http.js.map +1 -1
- package/dist/hook/recent-context.d.ts +25 -0
- package/dist/hook/recent-context.js +104 -0
- package/dist/hook/recent-context.js.map +1 -0
- package/dist/hook/score-floor.d.ts +27 -0
- package/dist/hook/score-floor.js +42 -0
- package/dist/hook/score-floor.js.map +1 -0
- package/dist/hook/session-end-hook.js +8 -29
- package/dist/hook/session-end-hook.js.map +1 -1
- package/dist/hook/session-start-hook.js +24 -59
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +22 -59
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/hook/subagent-start-hook.d.ts +28 -0
- package/dist/hook/subagent-start-hook.js +81 -0
- package/dist/hook/subagent-start-hook.js.map +1 -0
- package/dist/http/app.js +28 -8
- package/dist/http/app.js.map +1 -1
- package/dist/install/setup.js +2 -0
- package/dist/install/setup.js.map +1 -1
- package/dist/llm/build-classifier.d.ts +22 -0
- package/dist/llm/build-classifier.js +42 -0
- package/dist/llm/build-classifier.js.map +1 -0
- package/dist/llm/build-embedder.d.ts +11 -0
- package/dist/llm/build-embedder.js +42 -0
- package/dist/llm/build-embedder.js.map +1 -0
- package/dist/llm/bundled-embedder-client.d.ts +37 -0
- package/dist/llm/bundled-embedder-client.js +90 -0
- package/dist/llm/bundled-embedder-client.js.map +1 -0
- package/dist/llm/classifier-box.d.ts +13 -5
- package/dist/llm/classifier-box.js +28 -6
- package/dist/llm/classifier-box.js.map +1 -1
- package/dist/llm/classifier-egress.d.ts +18 -0
- package/dist/llm/classifier-egress.js +59 -0
- package/dist/llm/classifier-egress.js.map +1 -0
- package/dist/llm/client-shared.d.ts +4 -0
- package/dist/llm/client-shared.js +39 -0
- package/dist/llm/client-shared.js.map +1 -0
- package/dist/llm/deepseek-client.d.ts +20 -0
- package/dist/llm/deepseek-client.js +59 -45
- package/dist/llm/deepseek-client.js.map +1 -1
- package/dist/llm/embedder-info.d.ts +15 -0
- package/dist/llm/embedder-info.js +27 -0
- package/dist/llm/embedder-info.js.map +1 -0
- package/dist/llm/model-quirks.d.ts +4 -0
- package/dist/llm/model-quirks.js +7 -0
- package/dist/llm/model-quirks.js.map +1 -0
- package/dist/llm/naming.d.ts +5 -0
- package/dist/llm/naming.js +20 -0
- package/dist/llm/naming.js.map +1 -0
- package/dist/llm/ollama-client.d.ts +7 -8
- package/dist/llm/ollama-client.js +59 -51
- package/dist/llm/ollama-client.js.map +1 -1
- package/dist/llm/ollama-code-embedder.d.ts +8 -3
- package/dist/llm/ollama-code-embedder.js +14 -24
- package/dist/llm/ollama-code-embedder.js.map +1 -1
- package/dist/llm/openai-code-embedder-client.d.ts +25 -0
- package/dist/llm/openai-code-embedder-client.js +52 -0
- package/dist/llm/openai-code-embedder-client.js.map +1 -0
- package/dist/llm/openai-embedder-client.d.ts +41 -0
- package/dist/llm/openai-embedder-client.js +83 -0
- package/dist/llm/openai-embedder-client.js.map +1 -0
- package/dist/mcp/server.d.ts +47 -2
- package/dist/mcp/server.js +303 -18
- package/dist/mcp/server.js.map +1 -1
- package/dist/ports/code-embedder.d.ts +6 -1
- package/dist/ports/code-exemplar-store.d.ts +5 -0
- package/dist/ports/entity-store.d.ts +35 -0
- package/dist/ports/entity-store.js +7 -0
- package/dist/ports/entity-store.js.map +1 -0
- package/dist/ports/fact-store.d.ts +0 -0
- package/dist/ports/fact-store.js.map +1 -1
- package/dist/ports/llm-client.d.ts +9 -1
- package/dist/ports/session-store.d.ts +12 -0
- package/dist/ports/storage.d.ts +7 -30
- package/dist/ports/storage.js +3 -7
- package/dist/ports/storage.js.map +1 -1
- package/dist/ports/transcript-adapter.d.ts +2 -0
- package/dist/ports/workstream-store.d.ts +21 -0
- package/dist/ports/workstream-store.js +2 -0
- package/dist/ports/workstream-store.js.map +1 -0
- package/dist/shared/types.d.ts +23 -2
- package/fixtures/classifier-gold/README.md +94 -0
- package/fixtures/classifier-gold/reference.json +318 -0
- package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
- package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
- package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
- package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
- package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
- package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
- package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
- package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
- package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
- package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
- package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
- package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
- package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
- package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
- package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
- package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
- package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
- package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
- package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
- package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
- package/migrations/000_initial_schema.sql +1 -1
- package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
- package/migrations/025_workstreams.sql +30 -0
- package/migrations/026_embedding_config.sql +12 -0
- package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
- package/migrations/028_classifier_provenance.sql +5 -0
- package/migrations/pg/001_initial.sql +34 -0
- package/migrations/pg/019_split_replaces.sql +16 -7
- package/migrations/pg/025_workstreams.sql +34 -0
- package/migrations/pg/026_embedding_config.sql +12 -0
- package/migrations/pg/028_classifier_provenance.sql +5 -0
- package/migrations/pg/029_entity_variants.sql +9 -0
- package/nlm/index.js +395 -37
- package/package.json +12 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/prompt-recall-hook.mjs +402 -41
- package/plugin/scripts/session-start-hook.mjs +286 -60
- package/plugin/scripts/stop-hook.mjs +85 -65
- package/plugin-hermes-agent/__init__.py +2 -1
- package/templates/agent-contract/README.md +24 -0
- package/templates/agent-contract/claude-code.md +40 -0
- package/templates/agent-contract/generic.md +38 -0
- package/dist/core/embedding/embed-normalize.d.ts +0 -28
- package/dist/core/embedding/embed-normalize.js +0 -98
- package/dist/core/embedding/embed-normalize.js.map +0 -1
- package/dist/core/storage/pg-tx-context.d.ts +0 -52
- package/dist/core/storage/pg-tx-context.js +0 -149
- package/dist/core/storage/pg-tx-context.js.map +0 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pg-embed-backfill -- re-embed every session in the Postgres store into
|
|
3
|
+
* session_embedding_chunks. Mirrors reembedCorpus from embed-backfill.ts
|
|
4
|
+
* but drives a pg Pool instead of a SQLite file.
|
|
5
|
+
*
|
|
6
|
+
* Resumable via a JSON state file at $NLM_EMBED_STATE (default
|
|
7
|
+
* ~/.nlm/embed_reembed_pg.state). Same SAVE_EVERY = 25 checkpoint cadence.
|
|
8
|
+
*
|
|
9
|
+
* When the probed dim/model/provider differs from the stored embedding_config
|
|
10
|
+
* row, both vector tables are rebuilt: rows cleared, the embedding column
|
|
11
|
+
* altered to the new dimension, and indexes recreated. Facts (superseded_by
|
|
12
|
+
* IS NULL AND retired_at IS NULL) are reembedded after the session loop.
|
|
13
|
+
* Same-config reruns skip the rebuild.
|
|
14
|
+
*
|
|
15
|
+
* CRITICAL: the final config write is an awaited pool.query() INSERT ON
|
|
16
|
+
* CONFLICT -- never routed through PgEmbeddingConfigStore.upsertLane, which
|
|
17
|
+
* is fire-and-forget and can be lost when pool.end() follows.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { Pool } from "pg";
|
|
23
|
+
import { LLMUnreachableError } from "../../ports/llm-client.js";
|
|
24
|
+
import { chunkSessionText } from "../embedding/chunk-body.js";
|
|
25
|
+
const DEFAULT_STATE_PATH = join(homedir(), ".nlm", "embed_reembed_pg.state");
|
|
26
|
+
const SAVE_EVERY = 25;
|
|
27
|
+
function loadStateData(path) {
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return { done: new Set() };
|
|
30
|
+
try {
|
|
31
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
return { done: new Set(raw.done ?? []), ...(raw.config ? { config: raw.config } : {}) };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { done: new Set() };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function saveState(path, done, config) {
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
+
const data = { done: [...done].sort() };
|
|
41
|
+
if (config)
|
|
42
|
+
data.config = config;
|
|
43
|
+
writeFileSync(path, JSON.stringify(data));
|
|
44
|
+
}
|
|
45
|
+
export async function reembedCorpusPg(opts) {
|
|
46
|
+
const statePath = opts.statePath ?? DEFAULT_STATE_PATH;
|
|
47
|
+
const pool = new Pool({ connectionString: opts.pgUrl });
|
|
48
|
+
try {
|
|
49
|
+
const tableCheck = await pool.query("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'embedding_config') AS exists");
|
|
50
|
+
if (!tableCheck.rows[0]?.exists) {
|
|
51
|
+
throw new Error("embedding_config table not found; run `nlm migrate` to apply pending migrations");
|
|
52
|
+
}
|
|
53
|
+
const probeResult = await opts.embedder.embed("nlm dimension probe", "query");
|
|
54
|
+
const dim = probeResult.vector.length;
|
|
55
|
+
const model = probeResult.model;
|
|
56
|
+
const provider = opts.embedderProvider ?? process.env["NLM_EMBED_PROVIDER"] ?? "ollama";
|
|
57
|
+
const cfgRows = await pool.query("SELECT provider, model, dim FROM embedding_config WHERE lane = 'prose'");
|
|
58
|
+
const storedConfig = cfgRows.rows[0] ?? null;
|
|
59
|
+
const stateData = loadStateData(statePath);
|
|
60
|
+
let configMatch = false;
|
|
61
|
+
let needsRebuild = false;
|
|
62
|
+
if (storedConfig !== null) {
|
|
63
|
+
configMatch =
|
|
64
|
+
storedConfig.provider === provider &&
|
|
65
|
+
storedConfig.model === model &&
|
|
66
|
+
storedConfig.dim === dim;
|
|
67
|
+
if (!configMatch) {
|
|
68
|
+
const stateConfigMatches = stateData.config !== undefined &&
|
|
69
|
+
stateData.config.provider === provider &&
|
|
70
|
+
stateData.config.model === model &&
|
|
71
|
+
stateData.config.dim === dim;
|
|
72
|
+
if (!stateConfigMatches) {
|
|
73
|
+
needsRebuild = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Resume state: stored config differs from runtime, but the state file already
|
|
78
|
+
// carries the new config (tables were rebuilt in a prior interrupted run).
|
|
79
|
+
// No column alter is needed, but facts must still be reembedded to finish the run.
|
|
80
|
+
const isResumeState = storedConfig !== null && !configMatch && !needsRebuild;
|
|
81
|
+
if (opts.dryRun) {
|
|
82
|
+
if (needsRebuild) {
|
|
83
|
+
const stored = storedConfig;
|
|
84
|
+
process.stderr.write(`[pg-embed-backfill] dim mismatch: stored ${stored.provider}/${stored.model}@${stored.dim},` +
|
|
85
|
+
` runtime ${provider}/${model}@${dim}.` +
|
|
86
|
+
` Would ALTER session_embedding_chunks and fact_embeddings to vector(${dim}).\n`);
|
|
87
|
+
}
|
|
88
|
+
else if (isResumeState) {
|
|
89
|
+
const stored = storedConfig;
|
|
90
|
+
process.stderr.write(`[pg-embed-backfill] resume state: interrupted rebuild detected.` +
|
|
91
|
+
` Stored config ${stored.provider}/${stored.model}@${stored.dim}` +
|
|
92
|
+
` differs from runtime ${provider}/${model}@${dim},` +
|
|
93
|
+
` but state file already matches runtime.` +
|
|
94
|
+
` No column alter; facts would be reembedded to complete the interrupted rebuild.\n`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
process.stderr.write(`[pg-embed-backfill] dry-run: config matches (${provider}/${model}@${dim}), no rebuild needed.\n`);
|
|
98
|
+
}
|
|
99
|
+
let dryRunFactCount = 0;
|
|
100
|
+
if (isResumeState) {
|
|
101
|
+
const countResult = await pool.query("SELECT COUNT(*) AS cnt FROM facts WHERE superseded_by IS NULL AND retired_at IS NULL");
|
|
102
|
+
dryRunFactCount = Number(countResult.rows[0]?.cnt ?? 0);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
total: 0,
|
|
106
|
+
processed: 0,
|
|
107
|
+
succeeded: 0,
|
|
108
|
+
failed: 0,
|
|
109
|
+
skippedAlreadyDone: 0,
|
|
110
|
+
dbMissing: false,
|
|
111
|
+
dryRun: true,
|
|
112
|
+
rebuilt: needsRebuild,
|
|
113
|
+
factsReembedded: dryRunFactCount,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (needsRebuild) {
|
|
117
|
+
const stored = storedConfig;
|
|
118
|
+
process.stderr.write(`[pg-embed-backfill] rebuilding vec tables: stored ${stored.provider}/${stored.model}@${stored.dim}` +
|
|
119
|
+
` vs runtime ${provider}/${model}@${dim}\n`);
|
|
120
|
+
process.stderr.write(`[pg-embed-backfill] DELETE FROM session_embedding_chunks\n`);
|
|
121
|
+
await pool.query("DELETE FROM session_embedding_chunks");
|
|
122
|
+
await pool.query("DROP INDEX IF EXISTS session_chunks_embedding_idx");
|
|
123
|
+
await pool.query(`ALTER TABLE session_embedding_chunks ALTER COLUMN embedding TYPE vector(${dim})`);
|
|
124
|
+
await pool.query(`CREATE INDEX session_chunks_embedding_idx` +
|
|
125
|
+
` ON session_embedding_chunks USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)`);
|
|
126
|
+
process.stderr.write(`[pg-embed-backfill] rebuilding fact_embeddings to vector(${dim})\n`);
|
|
127
|
+
await pool.query("DELETE FROM fact_embeddings");
|
|
128
|
+
await pool.query("DROP INDEX IF EXISTS fact_embeddings_idx");
|
|
129
|
+
await pool.query(`ALTER TABLE fact_embeddings ALTER COLUMN embedding TYPE vector(${dim})`);
|
|
130
|
+
await pool.query(`CREATE INDEX fact_embeddings_idx` +
|
|
131
|
+
` ON fact_embeddings USING ivfflat (embedding vector_l2_ops) WITH (lists = 100)`);
|
|
132
|
+
stateData.done = new Set();
|
|
133
|
+
}
|
|
134
|
+
const limitClause = opts.limit ? ` LIMIT ${Math.trunc(opts.limit)}` : "";
|
|
135
|
+
const sessionResult = await pool.query("SELECT id, label, summary, body FROM sessions" +
|
|
136
|
+
" WHERE body IS NOT NULL OR summary IS NOT NULL OR label IS NOT NULL" +
|
|
137
|
+
" ORDER BY started_at" +
|
|
138
|
+
limitClause);
|
|
139
|
+
const rows = sessionResult.rows;
|
|
140
|
+
const total = rows.length;
|
|
141
|
+
const done = stateData.done;
|
|
142
|
+
let succeeded = 0;
|
|
143
|
+
let failed = 0;
|
|
144
|
+
let skipped = 0;
|
|
145
|
+
for (let i = 0; i < rows.length; i++) {
|
|
146
|
+
const row = rows[i];
|
|
147
|
+
const idx = i + 1;
|
|
148
|
+
if (done.has(row.id)) {
|
|
149
|
+
skipped += 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const chunks = chunkSessionText({
|
|
153
|
+
label: row.label,
|
|
154
|
+
summary: row.summary,
|
|
155
|
+
body: row.body,
|
|
156
|
+
});
|
|
157
|
+
if (chunks.length === 0) {
|
|
158
|
+
failed += 1;
|
|
159
|
+
opts.onProgress?.(idx, total, row.id, "SKIP (no text)");
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
await pool.query("DELETE FROM session_embedding_chunks WHERE session_id = $1", [row.id]);
|
|
163
|
+
const vectors = [];
|
|
164
|
+
let chunkSkipped = 0;
|
|
165
|
+
for (let c = 0; c < chunks.length; c++) {
|
|
166
|
+
const chunk = chunks[c];
|
|
167
|
+
let lastErr;
|
|
168
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
169
|
+
try {
|
|
170
|
+
const out = await opts.embedder.embed(chunk, "document");
|
|
171
|
+
vectors.push({ idx: c, vec: out.vector });
|
|
172
|
+
lastErr = undefined;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
lastErr = e;
|
|
177
|
+
if (!(e instanceof LLMUnreachableError))
|
|
178
|
+
throw e;
|
|
179
|
+
if (attempt === 0)
|
|
180
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (lastErr !== undefined)
|
|
184
|
+
chunkSkipped += 1;
|
|
185
|
+
}
|
|
186
|
+
if (vectors.length === 0) {
|
|
187
|
+
failed += 1;
|
|
188
|
+
opts.onProgress?.(idx, total, row.id, `FAIL (embedder, ${chunkSkipped}/${chunks.length} chunks)`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
for (const { idx: cidx, vec } of vectors) {
|
|
193
|
+
const vecStr = `[${Array.from(vec).join(",")}]`;
|
|
194
|
+
const ins = await pool.query(`INSERT INTO session_embedding_chunks (session_id, chunk_idx, embedding)
|
|
195
|
+
VALUES ($1, $2, $3::vector) RETURNING chunk_id`, [row.id, cidx, vecStr]);
|
|
196
|
+
const chunkId = ins.rows[0].chunk_id;
|
|
197
|
+
await pool.query("INSERT INTO session_chunk_map (chunk_id, session_id, chunk_idx) VALUES ($1, $2, $3)", [chunkId, row.id, cidx]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
failed += 1;
|
|
202
|
+
opts.onProgress?.(idx, total, row.id, `FAIL (db): ${e.message}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
done.add(row.id);
|
|
206
|
+
succeeded += 1;
|
|
207
|
+
const status = chunkSkipped === 0
|
|
208
|
+
? `OK (${vectors.length} chunks)`
|
|
209
|
+
: `PARTIAL (${vectors.length}/${chunks.length} chunks, ${chunkSkipped} skipped)`;
|
|
210
|
+
opts.onProgress?.(idx, total, row.id, status);
|
|
211
|
+
if (succeeded % SAVE_EVERY === 0)
|
|
212
|
+
saveState(statePath, done, { provider, model, dim });
|
|
213
|
+
}
|
|
214
|
+
// Reembed facts whenever the stored config does not match the runtime config
|
|
215
|
+
// (storedConfig exists but configMatch is false). This covers both fresh
|
|
216
|
+
// rebuilds and interrupted rebuilds that resume: the embedding_config row
|
|
217
|
+
// is only updated on successful completion, so storedConfig still holds
|
|
218
|
+
// the old values during a partial run. Gating on needsRebuild alone skips
|
|
219
|
+
// facts in that resume path.
|
|
220
|
+
let factsReembedded = 0;
|
|
221
|
+
if (storedConfig !== null && !configMatch) {
|
|
222
|
+
const factResult = await pool.query("SELECT id, subject, predicate, value FROM facts" +
|
|
223
|
+
" WHERE superseded_by IS NULL AND retired_at IS NULL");
|
|
224
|
+
for (const fact of factResult.rows) {
|
|
225
|
+
const factText = `${fact.subject} ${fact.predicate} ${fact.value}`.trim();
|
|
226
|
+
if (!factText)
|
|
227
|
+
continue;
|
|
228
|
+
try {
|
|
229
|
+
const out = await opts.embedder.embed(factText, "document");
|
|
230
|
+
const vecStr = `[${Array.from(out.vector).join(",")}]`;
|
|
231
|
+
await pool.query(`INSERT INTO fact_embeddings (fact_id, embedding)
|
|
232
|
+
VALUES ($1, $2::vector)
|
|
233
|
+
ON CONFLICT (fact_id) DO UPDATE SET embedding = EXCLUDED.embedding`, [fact.id, vecStr]);
|
|
234
|
+
factsReembedded += 1;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Best-effort: fact row survives; semantic recall misses it until re-run.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
await pool.query(`INSERT INTO embedding_config (lane, provider, model, dim, updated_at)
|
|
242
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
243
|
+
ON CONFLICT (lane) DO UPDATE SET
|
|
244
|
+
provider = EXCLUDED.provider,
|
|
245
|
+
model = EXCLUDED.model,
|
|
246
|
+
dim = EXCLUDED.dim,
|
|
247
|
+
updated_at = EXCLUDED.updated_at`, ["prose", provider, model, dim, new Date().toISOString()]);
|
|
248
|
+
saveState(statePath, done, { provider, model, dim });
|
|
249
|
+
return {
|
|
250
|
+
total,
|
|
251
|
+
processed: succeeded + failed + skipped,
|
|
252
|
+
succeeded,
|
|
253
|
+
failed,
|
|
254
|
+
skippedAlreadyDone: skipped,
|
|
255
|
+
dbMissing: false,
|
|
256
|
+
rebuilt: needsRebuild,
|
|
257
|
+
factsReembedded,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
await pool.end();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=pg-embed-backfill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-embed-backfill.js","sourceRoot":"","sources":["../../../src/core/embedding/pg-embed-backfill.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative lexical dedup suggestion for the entity table.
|
|
3
|
+
*
|
|
4
|
+
* No embedding similarity in v1: the workstream matcher falsification showed
|
|
5
|
+
* lexical-adjacent-but-different entities are exactly where embeddings
|
|
6
|
+
* over-merge. Start conservative; tighten only after adjudication data exists.
|
|
7
|
+
*/
|
|
8
|
+
export interface EntityInput {
|
|
9
|
+
readonly canonical: string;
|
|
10
|
+
readonly sessionCount: number;
|
|
11
|
+
readonly status: string;
|
|
12
|
+
}
|
|
13
|
+
export interface MergeSuggestion {
|
|
14
|
+
readonly source: string;
|
|
15
|
+
readonly target: string;
|
|
16
|
+
readonly cls: "safe" | "likely";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Suggest merge pairs from a flat list of entities.
|
|
20
|
+
*
|
|
21
|
+
* Groups by fold key, then checks likely patterns within each group.
|
|
22
|
+
* Never suggests a pair twice; never chains (one target per group, all
|
|
23
|
+
* remaining members are sources into that target).
|
|
24
|
+
*/
|
|
25
|
+
export declare function suggestMerges(entities: ReadonlyArray<EntityInput>): ReadonlyArray<MergeSuggestion>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative lexical dedup suggestion for the entity table.
|
|
3
|
+
*
|
|
4
|
+
* No embedding similarity in v1: the workstream matcher falsification showed
|
|
5
|
+
* lexical-adjacent-but-different entities are exactly where embeddings
|
|
6
|
+
* over-merge. Start conservative; tighten only after adjudication data exists.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Fold for safe-class comparison: lowercase, strip spaces, hyphens,
|
|
10
|
+
* underscores, and dots.
|
|
11
|
+
*/
|
|
12
|
+
function safeFold(s) {
|
|
13
|
+
return s.toLowerCase().replace(/[\s\-_.]+/g, "");
|
|
14
|
+
}
|
|
15
|
+
// Checked against the already-folded form (hyphens stripped by safeFold).
|
|
16
|
+
const REPO_SUFFIXES_FOLDED = ["ts", "js", "py", "rs", "go"];
|
|
17
|
+
function stripRepoSuffix(folded) {
|
|
18
|
+
for (const suf of REPO_SUFFIXES_FOLDED) {
|
|
19
|
+
if (folded.endsWith(suf) && folded.length > suf.length) {
|
|
20
|
+
return folded.slice(0, -suf.length);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return folded;
|
|
24
|
+
}
|
|
25
|
+
function isSingularPlural(a, b) {
|
|
26
|
+
return b === a + "s" || a === b + "s";
|
|
27
|
+
}
|
|
28
|
+
function isRepoSuffixPair(a, b) {
|
|
29
|
+
const baseA = stripRepoSuffix(a);
|
|
30
|
+
const baseB = stripRepoSuffix(b);
|
|
31
|
+
if (a === baseA && b === baseB)
|
|
32
|
+
return false;
|
|
33
|
+
return baseA === baseB;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pick the target: higher sessionCount wins; on tie, lexicographically smaller
|
|
37
|
+
* canonical is the target (deterministic).
|
|
38
|
+
*/
|
|
39
|
+
function pickTarget(a, b) {
|
|
40
|
+
if (a.sessionCount !== b.sessionCount) {
|
|
41
|
+
return a.sessionCount > b.sessionCount
|
|
42
|
+
? { target: a, source: b }
|
|
43
|
+
: { target: b, source: a };
|
|
44
|
+
}
|
|
45
|
+
return a.canonical <= b.canonical
|
|
46
|
+
? { target: a, source: b }
|
|
47
|
+
: { target: b, source: a };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Suggest merge pairs from a flat list of entities.
|
|
51
|
+
*
|
|
52
|
+
* Groups by fold key, then checks likely patterns within each group.
|
|
53
|
+
* Never suggests a pair twice; never chains (one target per group, all
|
|
54
|
+
* remaining members are sources into that target).
|
|
55
|
+
*/
|
|
56
|
+
export function suggestMerges(entities) {
|
|
57
|
+
const active = entities.filter((e) => e.status !== "retired");
|
|
58
|
+
const safeGroups = new Map();
|
|
59
|
+
for (const e of active) {
|
|
60
|
+
const key = safeFold(e.canonical);
|
|
61
|
+
if (!safeGroups.has(key))
|
|
62
|
+
safeGroups.set(key, []);
|
|
63
|
+
safeGroups.get(key).push(e);
|
|
64
|
+
}
|
|
65
|
+
// Emission-order invariant the CLI applier depends on: every pair that uses
|
|
66
|
+
// an entity as TARGET is emitted before the (single) pair that consumes it as
|
|
67
|
+
// SOURCE, so sequential application never merges into an already-retired row.
|
|
68
|
+
// Reordering emissions (or applying suggestions out of list order) breaks this.
|
|
69
|
+
const suggestions = [];
|
|
70
|
+
const consumedAsSource = new Set();
|
|
71
|
+
for (const group of safeGroups.values()) {
|
|
72
|
+
if (group.length < 2)
|
|
73
|
+
continue;
|
|
74
|
+
const sorted = [...group].sort((a, b) => {
|
|
75
|
+
if (b.sessionCount !== a.sessionCount)
|
|
76
|
+
return b.sessionCount - a.sessionCount;
|
|
77
|
+
return a.canonical <= b.canonical ? -1 : 1;
|
|
78
|
+
});
|
|
79
|
+
const target = sorted[0];
|
|
80
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
81
|
+
const source = sorted[i];
|
|
82
|
+
if (consumedAsSource.has(source.canonical))
|
|
83
|
+
continue;
|
|
84
|
+
suggestions.push({ source: source.canonical, target: target.canonical, cls: "safe" });
|
|
85
|
+
consumedAsSource.add(source.canonical);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const activeNonSafe = active.filter((e) => !consumedAsSource.has(e.canonical));
|
|
89
|
+
for (let i = 0; i < activeNonSafe.length; i++) {
|
|
90
|
+
for (let j = i + 1; j < activeNonSafe.length; j++) {
|
|
91
|
+
const a = activeNonSafe[i];
|
|
92
|
+
const b = activeNonSafe[j];
|
|
93
|
+
if (consumedAsSource.has(a.canonical) || consumedAsSource.has(b.canonical))
|
|
94
|
+
continue;
|
|
95
|
+
const fa = safeFold(a.canonical);
|
|
96
|
+
const fb = safeFold(b.canonical);
|
|
97
|
+
const isLikely = isSingularPlural(fa, fb) || isRepoSuffixPair(fa, fb);
|
|
98
|
+
if (!isLikely)
|
|
99
|
+
continue;
|
|
100
|
+
const { target, source } = pickTarget(a, b);
|
|
101
|
+
suggestions.push({ source: source.canonical, target: target.canonical, cls: "likely" });
|
|
102
|
+
consumedAsSource.add(source.canonical);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return suggestions;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=dedup-suggest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedup-suggest.js","sourceRoot":"","sources":["../../../src/core/entities/dedup-suggest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ClassifyResult } from "../../ports/llm-client.js";
|
|
2
|
+
export interface FixtureEvalResult {
|
|
3
|
+
readonly perTranscript: ReadonlyArray<{
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly schemaValid: boolean;
|
|
6
|
+
readonly timedOutOrUnreachable: boolean;
|
|
7
|
+
readonly labelMatch: boolean;
|
|
8
|
+
readonly entityPrecision: number;
|
|
9
|
+
readonly entityRecall: number;
|
|
10
|
+
readonly decisionPrecision: number;
|
|
11
|
+
readonly decisionRecall: number;
|
|
12
|
+
readonly confidence: number;
|
|
13
|
+
readonly confidenceCalibrated: boolean;
|
|
14
|
+
readonly elapsedMs: number;
|
|
15
|
+
}>;
|
|
16
|
+
readonly aggregate: {
|
|
17
|
+
readonly schemaValidRate: number;
|
|
18
|
+
readonly labelAccuracy: number;
|
|
19
|
+
readonly entityF1: number;
|
|
20
|
+
readonly decisionF1: number;
|
|
21
|
+
readonly confidenceCalibrationRate: number;
|
|
22
|
+
readonly p50LatencyMs: number;
|
|
23
|
+
readonly p95LatencyMs: number;
|
|
24
|
+
readonly timedOutOrUnreachable: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare function runClassifierFixtureEval(classify: (transcript: string) => Promise<ClassifyResult>, fixturesDir: string, opts?: {
|
|
28
|
+
readonly limit?: number;
|
|
29
|
+
}): Promise<FixtureEvalResult>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { ClassifierSchemaError, LLMUnreachableError } from "../../ports/llm-client.js";
|
|
4
|
+
import { rate } from "../eval/extraction-scoring.js";
|
|
5
|
+
function loadReference(fixturesDir) {
|
|
6
|
+
const raw = readFileSync(join(fixturesDir, "reference.json"), "utf-8");
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
}
|
|
9
|
+
function loadTranscript(fixturesDir, id) {
|
|
10
|
+
return readFileSync(join(fixturesDir, "transcripts", `${id}.txt`), "utf-8");
|
|
11
|
+
}
|
|
12
|
+
function listTranscriptIds(fixturesDir) {
|
|
13
|
+
const dir = join(fixturesDir, "transcripts");
|
|
14
|
+
return readdirSync(dir)
|
|
15
|
+
.filter((f) => f.endsWith(".txt"))
|
|
16
|
+
.map((f) => basename(f, ".txt"))
|
|
17
|
+
.sort();
|
|
18
|
+
}
|
|
19
|
+
function normalizeStr(s) {
|
|
20
|
+
return s.toLowerCase().trim();
|
|
21
|
+
}
|
|
22
|
+
function labelMatches(extracted, ref) {
|
|
23
|
+
const e = normalizeStr(extracted);
|
|
24
|
+
const candidates = [ref.label, ...ref.labelAlternates].map(normalizeStr);
|
|
25
|
+
for (const c of candidates) {
|
|
26
|
+
if (e === c)
|
|
27
|
+
return true;
|
|
28
|
+
if (e.includes(c) || c.includes(e))
|
|
29
|
+
return true;
|
|
30
|
+
if (keyTokenOverlap(e, c) >= 0.5)
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function keyTokenOverlap(a, b) {
|
|
36
|
+
const tokA = keyTokens(a);
|
|
37
|
+
const tokB = keyTokens(b);
|
|
38
|
+
if (tokA.size === 0 || tokB.size === 0)
|
|
39
|
+
return 0;
|
|
40
|
+
let hits = 0;
|
|
41
|
+
for (const t of tokA)
|
|
42
|
+
if (tokB.has(t))
|
|
43
|
+
hits++;
|
|
44
|
+
return hits / Math.max(tokA.size, tokB.size);
|
|
45
|
+
}
|
|
46
|
+
function keyTokens(s) {
|
|
47
|
+
const STOPWORDS = new Set([
|
|
48
|
+
"a", "an", "the", "and", "or", "for", "to", "of", "in", "on", "with",
|
|
49
|
+
"at", "by", "from", "up", "as", "is", "it", "its", "into", "that",
|
|
50
|
+
"this", "be", "are", "was", "were", "has", "have", "had",
|
|
51
|
+
]);
|
|
52
|
+
return new Set(s
|
|
53
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
54
|
+
.split(/\s+/)
|
|
55
|
+
.filter((t) => t.length >= 3 && !STOPWORDS.has(t)));
|
|
56
|
+
}
|
|
57
|
+
function entityMatchesAny(refEntity, extracted) {
|
|
58
|
+
const rn = normalizeStr(refEntity);
|
|
59
|
+
for (const e of extracted) {
|
|
60
|
+
const en = normalizeStr(e);
|
|
61
|
+
if (en === rn || en.includes(rn) || rn.includes(en))
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function decisionMatchesAny(refDecision, extracted) {
|
|
67
|
+
const refToks = keyTokens(normalizeStr(refDecision));
|
|
68
|
+
if (refToks.size === 0)
|
|
69
|
+
return extracted.length > 0;
|
|
70
|
+
for (const e of extracted) {
|
|
71
|
+
const eToks = keyTokens(normalizeStr(e));
|
|
72
|
+
let hits = 0;
|
|
73
|
+
for (const t of refToks)
|
|
74
|
+
if (eToks.has(t))
|
|
75
|
+
hits++;
|
|
76
|
+
if (refToks.size > 0 && hits / refToks.size >= 0.5)
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function scoreEntityPrecision(extracted, reference) {
|
|
82
|
+
if (extracted.length === 0)
|
|
83
|
+
return reference.length === 0 ? 1 : 0;
|
|
84
|
+
const hits = extracted.filter((e) => entityMatchesAny(e, reference));
|
|
85
|
+
return hits.length / extracted.length;
|
|
86
|
+
}
|
|
87
|
+
function scoreEntityRecall(extracted, reference) {
|
|
88
|
+
if (reference.length === 0)
|
|
89
|
+
return 1;
|
|
90
|
+
const verdicts = reference.map((r) => entityMatchesAny(r, extracted));
|
|
91
|
+
return (rate(verdicts, Boolean) ?? 0);
|
|
92
|
+
}
|
|
93
|
+
function scoreDecisionPrecision(extracted, reference) {
|
|
94
|
+
if (extracted.length === 0)
|
|
95
|
+
return reference.length === 0 ? 1 : 0;
|
|
96
|
+
const hits = extracted.filter((e) => decisionMatchesAny(e, reference));
|
|
97
|
+
return hits.length / extracted.length;
|
|
98
|
+
}
|
|
99
|
+
function scoreDecisionRecall(extracted, reference) {
|
|
100
|
+
if (reference.length === 0)
|
|
101
|
+
return 1;
|
|
102
|
+
const verdicts = reference.map((r) => decisionMatchesAny(r, extracted));
|
|
103
|
+
return (rate(verdicts, Boolean) ?? 0);
|
|
104
|
+
}
|
|
105
|
+
function percentile(sorted, p) {
|
|
106
|
+
if (sorted.length === 0)
|
|
107
|
+
return 0;
|
|
108
|
+
const idx = Math.floor((p / 100) * (sorted.length - 1));
|
|
109
|
+
return sorted[idx] ?? 0;
|
|
110
|
+
}
|
|
111
|
+
function f1(precision, recall) {
|
|
112
|
+
const denom = precision + recall;
|
|
113
|
+
if (denom === 0)
|
|
114
|
+
return 0;
|
|
115
|
+
return (2 * precision * recall) / denom;
|
|
116
|
+
}
|
|
117
|
+
function macroF1(perRow) {
|
|
118
|
+
if (perRow.length === 0)
|
|
119
|
+
return 0;
|
|
120
|
+
const sum = perRow.reduce((acc, x) => acc + f1(x.p, x.r), 0);
|
|
121
|
+
return sum / perRow.length;
|
|
122
|
+
}
|
|
123
|
+
export async function runClassifierFixtureEval(classify, fixturesDir, opts) {
|
|
124
|
+
const allRefs = loadReference(fixturesDir);
|
|
125
|
+
const allIds = listTranscriptIds(fixturesDir);
|
|
126
|
+
const refById = new Map(allRefs.map((r) => [r.id, r]));
|
|
127
|
+
const ids = opts?.limit != null ? allIds.slice(0, opts.limit) : allIds;
|
|
128
|
+
const perTranscript = [];
|
|
129
|
+
for (const id of ids) {
|
|
130
|
+
const ref = refById.get(id);
|
|
131
|
+
if (ref === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
const transcript = loadTranscript(fixturesDir, id);
|
|
134
|
+
const start = Date.now();
|
|
135
|
+
let result = null;
|
|
136
|
+
let schemaValid = true;
|
|
137
|
+
let timedOutOrUnreachable = false;
|
|
138
|
+
try {
|
|
139
|
+
result = await classify(transcript);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err instanceof ClassifierSchemaError) {
|
|
143
|
+
schemaValid = false;
|
|
144
|
+
}
|
|
145
|
+
else if (err instanceof LLMUnreachableError) {
|
|
146
|
+
schemaValid = false;
|
|
147
|
+
timedOutOrUnreachable = true;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const elapsedMs = Date.now() - start;
|
|
154
|
+
if (!schemaValid || result === null) {
|
|
155
|
+
perTranscript.push({
|
|
156
|
+
id,
|
|
157
|
+
schemaValid: false,
|
|
158
|
+
timedOutOrUnreachable,
|
|
159
|
+
labelMatch: false,
|
|
160
|
+
entityPrecision: 0,
|
|
161
|
+
entityRecall: 0,
|
|
162
|
+
decisionPrecision: 0,
|
|
163
|
+
decisionRecall: 0,
|
|
164
|
+
confidence: 0,
|
|
165
|
+
confidenceCalibrated: false,
|
|
166
|
+
elapsedMs,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const lm = labelMatches(result.label, ref);
|
|
171
|
+
const ep = scoreEntityPrecision(result.entities, ref.entities);
|
|
172
|
+
const er = scoreEntityRecall(result.entities, ref.entities);
|
|
173
|
+
const dp = scoreDecisionPrecision(result.decisions, ref.decisions);
|
|
174
|
+
const dr = scoreDecisionRecall(result.decisions, ref.decisions);
|
|
175
|
+
const confidence = result.confidence;
|
|
176
|
+
const confidenceCalibrated = ref.expectLowConfidence
|
|
177
|
+
? confidence <= 0.4
|
|
178
|
+
: confidence > 0.4;
|
|
179
|
+
perTranscript.push({
|
|
180
|
+
id,
|
|
181
|
+
schemaValid: true,
|
|
182
|
+
timedOutOrUnreachable: false,
|
|
183
|
+
labelMatch: lm,
|
|
184
|
+
entityPrecision: ep,
|
|
185
|
+
entityRecall: er,
|
|
186
|
+
decisionPrecision: dp,
|
|
187
|
+
decisionRecall: dr,
|
|
188
|
+
confidence,
|
|
189
|
+
confidenceCalibrated,
|
|
190
|
+
elapsedMs,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const n = perTranscript.length;
|
|
194
|
+
const schemaValidRate = n === 0 ? 0 : perTranscript.filter((r) => r.schemaValid).length / n;
|
|
195
|
+
const labelAccuracy = n === 0 ? 0 : perTranscript.filter((r) => r.labelMatch).length / n;
|
|
196
|
+
const confidenceCalibrationRate = n === 0 ? 0 : perTranscript.filter((r) => r.confidenceCalibrated).length / n;
|
|
197
|
+
const timedOutOrUnreachable = perTranscript.filter((r) => r.timedOutOrUnreachable).length;
|
|
198
|
+
const entityF1 = macroF1(perTranscript.map((r) => ({ p: r.entityPrecision, r: r.entityRecall })));
|
|
199
|
+
const decisionF1 = macroF1(perTranscript.map((r) => ({ p: r.decisionPrecision, r: r.decisionRecall })));
|
|
200
|
+
const sortedLatency = [...perTranscript.map((r) => r.elapsedMs)].sort((a, b) => a - b);
|
|
201
|
+
const p50LatencyMs = percentile(sortedLatency, 50);
|
|
202
|
+
const p95LatencyMs = percentile(sortedLatency, 95);
|
|
203
|
+
return {
|
|
204
|
+
perTranscript,
|
|
205
|
+
aggregate: {
|
|
206
|
+
schemaValidRate,
|
|
207
|
+
labelAccuracy,
|
|
208
|
+
entityF1,
|
|
209
|
+
decisionF1,
|
|
210
|
+
confidenceCalibrationRate,
|
|
211
|
+
p50LatencyMs,
|
|
212
|
+
p95LatencyMs,
|
|
213
|
+
timedOutOrUnreachable,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=classifier-fixture-eval.js.map
|