@modusensus/dsh-mneme 0.14.0-beta.1 → 0.14.0-beta.2
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/LICENSE +21 -21
- package/README.md +830 -830
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +964 -964
- package/bin/dsh-mneme-choro.mjs +262 -262
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +407 -404
- package/docs/CONTINUITY.md +139 -139
- package/docs/DAEMON.md +57 -57
- package/docs/DIRECTORIES.md +114 -114
- package/docs/DISTILL.md +397 -371
- package/docs/DSH-GO.md +97 -97
- package/docs/ENTITIES.md +247 -247
- package/docs/FORGET.md +77 -77
- package/docs/KB.md +110 -110
- package/docs/LOCAL_MODEL.md +222 -222
- package/docs/SEMANTIC.md +256 -256
- package/docs/SHARE.md +552 -552
- package/docs/SLEEP.md +202 -202
- package/docs/STORAGE.md +122 -122
- package/docs/WORKFLOWS.md +366 -366
- package/lib/api-standalone.js +1007 -1007
- package/lib/api.js +3600 -3600
- package/lib/attachment-text.js +122 -122
- package/lib/bootstrap-http.js +105 -105
- package/lib/bootstrap.js +215 -215
- package/lib/candidates.js +454 -454
- package/lib/capabilities.js +819 -819
- package/lib/choro-command.js +111 -111
- package/lib/client.js +14420 -14322
- package/lib/commands.js +95 -95
- package/lib/config-keys.js +144 -144
- package/lib/config.js +1361 -1344
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +258 -258
- package/lib/date-hint.js +32 -0
- package/lib/diary/auto.js +190 -190
- package/lib/diary/compose.js +440 -440
- package/lib/diary/dir.js +59 -59
- package/lib/diary/export.js +97 -97
- package/lib/diary/index.js +26 -26
- package/lib/diary/library.js +189 -189
- package/lib/diary/qq-inbox.js +142 -142
- package/lib/diary/rollover.js +61 -61
- package/lib/diary/session-provider.js +122 -122
- package/lib/document-index.js +94 -94
- package/lib/document.js +322 -322
- package/lib/dream/clustering.js +123 -123
- package/lib/dream/decisions.js +945 -945
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +2027 -2027
- package/lib/dream/triage.js +139 -139
- package/lib/dream.js +1879 -1879
- package/lib/dsh-go/host.js +162 -162
- package/lib/dsh-go/panel.js +2091 -2091
- package/lib/dsh-go/text.js +342 -342
- package/lib/dsh-go/views/common.js +68 -68
- package/lib/dsh-go/views/entity.js +48 -48
- package/lib/dsh-go/views/memory.js +198 -198
- package/lib/dsh-go/views/root.js +30 -30
- package/lib/dsh-go/views/settings.js +398 -398
- package/lib/dsh-go/views/status.js +51 -51
- package/lib/embedding.js +342 -342
- package/lib/entities/extractor.js +285 -285
- package/lib/fence.js +24 -24
- package/lib/graph/anchoring.js +184 -184
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +119 -119
- package/lib/image-describe.js +158 -0
- package/lib/index.js +852 -852
- package/lib/inject.js +656 -651
- package/lib/kb/index.js +309 -309
- package/lib/kb/routes.js +219 -219
- package/lib/kb/tools.js +148 -148
- package/lib/lang.js +607 -595
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +162 -162
- package/lib/memory-source.js +9 -9
- package/lib/mirror.js +330 -330
- package/lib/organize.js +456 -456
- package/lib/peak-hours.js +172 -172
- package/lib/pipeline-activities.js +178 -178
- package/lib/pipeline-attribution.js +165 -165
- package/lib/pipeline-command.js +279 -279
- package/lib/pipeline-confirm.js +142 -142
- package/lib/pipeline-exec.js +174 -174
- package/lib/pipeline-nudge-auto.js +129 -129
- package/lib/pipeline-nudge.js +1623 -1623
- package/lib/pipeline-observations.js +181 -181
- package/lib/pipeline-observe.js +133 -133
- package/lib/pipeline-runner.js +361 -361
- package/lib/pipeline-runs.js +442 -442
- package/lib/pipeline.js +716 -716
- package/lib/placement.js +13 -13
- package/lib/pre-inject-gate.js +354 -354
- package/lib/profile-extract.js +164 -164
- package/lib/profile-facts.js +356 -356
- package/lib/provenance-http.js +251 -251
- package/lib/quality-filter.js +162 -162
- package/lib/recall-stats.js +190 -190
- package/lib/reclaim-http.js +150 -150
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -223
- package/lib/safe-slice.js +18 -18
- package/lib/scope-state.js +277 -277
- package/lib/scope.js +187 -187
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +572 -572
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +228 -228
- package/lib/service.js +3268 -3268
- package/lib/session-gate.js +91 -91
- package/lib/session-usage.js +350 -350
- package/lib/settings.js +827 -817
- package/lib/share-http.js +281 -281
- package/lib/share-import.js +234 -234
- package/lib/share.js +335 -335
- package/lib/store.js +5895 -5895
- package/lib/summarize.js +1388 -1343
- package/lib/tools.js +1370 -1356
- package/lib/user-settings.js +230 -230
- package/lib/vector-index.js +180 -180
- package/lib/version-check.js +122 -122
- package/lib/wiki/claims.js +267 -267
- package/lib/wiki/cli-hint.js +90 -90
- package/lib/wiki/inject.js +164 -164
- package/lib/wiki/partition.js +200 -200
- package/lib/wiki/plan.js +214 -214
- package/lib/wiki/queries.js +640 -640
- package/lib/wiki/render.js +261 -261
- package/lib/wiki/scan.js +371 -371
- package/lib/wiki/t0.js +500 -500
- package/lib/wiki/t1-jobs.js +232 -232
- package/lib/wiki/t1.js +609 -609
- package/lib/write-admission.js +404 -404
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +1 -1
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +91 -91
- package/scripts/check-sync.js +44 -44
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/publish-gate.mjs +197 -197
- package/scripts/release-docs.mjs +210 -210
- package/scripts/release-notes.mjs +164 -164
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +48 -48
- package/src/api-standalone.js +1007 -1007
- package/src/api.js +3600 -3600
- package/src/attachment-text.js +122 -122
- package/src/bootstrap-http.js +105 -105
- package/src/bootstrap.js +215 -215
- package/src/candidates.js +454 -454
- package/src/capabilities.js +819 -819
- package/src/choro-command.js +111 -111
- package/src/commands.js +95 -95
- package/src/config-keys.js +144 -144
- package/src/config.js +1361 -1344
- package/src/content-hash.js +54 -54
- package/src/continuity.js +258 -258
- package/src/date-hint.js +32 -0
- package/src/diary/auto.js +190 -190
- package/src/diary/compose.js +440 -440
- package/src/diary/dir.js +59 -59
- package/src/diary/export.js +97 -97
- package/src/diary/index.js +26 -26
- package/src/diary/library.js +189 -189
- package/src/diary/qq-inbox.js +142 -142
- package/src/diary/rollover.js +61 -61
- package/src/diary/session-provider.js +122 -122
- package/src/document-index.js +94 -94
- package/src/document.js +322 -322
- package/src/dream/clustering.js +123 -123
- package/src/dream/decisions.js +945 -945
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +2027 -2027
- package/src/dream/triage.js +139 -139
- package/src/dream.js +1879 -1879
- package/src/dsh-go/host.js +162 -162
- package/src/dsh-go/panel.js +2091 -2091
- package/src/dsh-go/text.js +342 -342
- package/src/dsh-go/views/common.js +68 -68
- package/src/dsh-go/views/entity.js +48 -48
- package/src/dsh-go/views/memory.js +198 -198
- package/src/dsh-go/views/root.js +30 -30
- package/src/dsh-go/views/settings.js +398 -398
- package/src/dsh-go/views/status.js +51 -51
- package/src/embedding.js +342 -342
- package/src/entities/extractor.js +285 -285
- package/src/fence.js +24 -24
- package/src/graph/anchoring.js +184 -184
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +119 -119
- package/src/image-describe.js +158 -0
- package/src/index.js +852 -852
- package/src/inject.js +656 -651
- package/src/kb/index.js +309 -309
- package/src/kb/routes.js +219 -219
- package/src/kb/tools.js +148 -148
- package/src/lang.js +607 -595
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +162 -162
- package/src/memory-source.js +9 -9
- package/src/mirror.js +330 -330
- package/src/organize.js +456 -456
- package/src/peak-hours.js +172 -172
- package/src/pipeline-activities.js +178 -178
- package/src/pipeline-attribution.js +165 -165
- package/src/pipeline-command.js +279 -279
- package/src/pipeline-confirm.js +142 -142
- package/src/pipeline-exec.js +174 -174
- package/src/pipeline-nudge-auto.js +129 -129
- package/src/pipeline-nudge.js +1623 -1623
- package/src/pipeline-observations.js +181 -181
- package/src/pipeline-observe.js +133 -133
- package/src/pipeline-runner.js +361 -361
- package/src/pipeline-runs.js +442 -442
- package/src/pipeline.js +716 -716
- package/src/placement.js +13 -13
- package/src/pre-inject-gate.js +354 -354
- package/src/profile-extract.js +164 -164
- package/src/profile-facts.js +356 -356
- package/src/provenance-http.js +251 -251
- package/src/quality-filter.js +162 -162
- package/src/recall-stats.js +190 -190
- package/src/reclaim-http.js +150 -150
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -223
- package/src/safe-slice.js +18 -18
- package/src/scope-state.js +277 -277
- package/src/scope.js +187 -187
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +572 -572
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +228 -228
- package/src/service.js +3268 -3268
- package/src/session-gate.js +91 -91
- package/src/session-usage.js +350 -350
- package/src/settings.js +827 -817
- package/src/share-http.js +281 -281
- package/src/share-import.js +234 -234
- package/src/share.js +335 -335
- package/src/store.js +5895 -5895
- package/src/summarize.js +1388 -1343
- package/src/tools.js +1370 -1356
- package/src/user-settings.js +230 -230
- package/src/vector-index.js +180 -180
- package/src/version-check.js +122 -122
- package/src/wiki/claims.js +267 -267
- package/src/wiki/cli-hint.js +90 -90
- package/src/wiki/inject.js +164 -164
- package/src/wiki/partition.js +200 -200
- package/src/wiki/plan.js +214 -214
- package/src/wiki/queries.js +640 -640
- package/src/wiki/render.js +261 -261
- package/src/wiki/scan.js +371 -371
- package/src/wiki/t0.js +500 -500
- package/src/wiki/t1-jobs.js +232 -232
- package/src/wiki/t1.js +609 -609
- package/src/write-admission.js +404 -404
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-hardening.test.js +173 -173
- package/test/api-nudge-route.test.js +259 -259
- package/test/api-routes-gaps.test.js +1092 -1092
- package/test/api.test.js +3007 -2998
- package/test/attachment-text.test.js +139 -139
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bm25-corpus-cache.test.js +145 -145
- package/test/bootstrap.test.js +228 -228
- package/test/candidates.test.js +345 -345
- package/test/capabilities.test.js +577 -577
- package/test/capacity-warning.test.js +262 -262
- package/test/changelog-prep.test.js +125 -125
- package/test/choro-command.test.js +218 -218
- package/test/cli-enums.test.js +37 -37
- package/test/cli-flag-value.test.js +86 -86
- package/test/cli-receive.test.js +390 -390
- package/test/cli-share.test.js +437 -437
- package/test/client-share.test.js +225 -225
- package/test/client.test.js +4595 -4425
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config-keys-routes.test.js +233 -233
- package/test/config.test.js +153 -153
- package/test/conflict-freeze.test.js +344 -344
- package/test/conflict-ledger-enforce.test.js +452 -452
- package/test/conflict-ledger.test.js +242 -242
- package/test/conflict-reviewed.test.js +79 -79
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +452 -452
- package/test/contrast.test.js +432 -432
- package/test/date-hint.test.js +156 -0
- package/test/dedupe-candidate-window.test.js +180 -180
- package/test/dependency-overrides.test.js +78 -78
- package/test/diary-library.test.js +187 -187
- package/test/diary.test.js +576 -576
- package/test/differentiate-feedback.test.js +248 -248
- package/test/distill-tx-embed-gap.test.js +110 -110
- package/test/docs-config-keys.test.js +65 -65
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +703 -703
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-baseline-clamp.test.js +174 -174
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-decision-evidence.test.js +314 -314
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-fence.test.js +207 -207
- package/test/dream-maxtokens-fallback.test.js +132 -132
- package/test/dream-merge-guardrail.test.js +219 -219
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1528 -1528
- package/test/dsh-go-host.test.js +134 -134
- package/test/dsh-go-panel-actions.test.js +854 -854
- package/test/dsh-go-panel.test.js +251 -251
- package/test/embed-ready-poll.test.js +94 -94
- package/test/embed-text-reasons.test.js +112 -112
- package/test/embedding-cache-f32.test.js +276 -276
- package/test/embedding-ready.test.js +226 -226
- package/test/entities.test.js +711 -711
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/failure-query.test.js +147 -147
- package/test/failure-retention.test.js +105 -105
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/forget-corrections.test.js +70 -70
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +333 -333
- package/test/graph-boosted-relations.test.js +189 -189
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-multipath.test.js +269 -269
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +86 -86
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +312 -312
- package/test/hot-memory.test.js +259 -259
- package/test/image-description.test.js +271 -0
- package/test/image-memory.test.js +170 -170
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +134 -109
- package/test/inject-pin-explicit.test.js +300 -300
- package/test/inject-pin-vector-crowd.test.js +225 -225
- package/test/inject-pools.test.js +195 -195
- package/test/inject-recall.test.js +172 -172
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +553 -553
- package/test/issue-617-step1-query-and-fusion.test.js +361 -361
- package/test/kb-api-wiring.test.js +128 -128
- package/test/kb-collections.test.js +272 -272
- package/test/kb-routes.test.js +379 -379
- package/test/lang.test.js +98 -98
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +116 -116
- package/test/llm-audit.test.js +815 -815
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +501 -501
- package/test/mcp-stdio.test.js +385 -385
- package/test/mint-source.test.js +311 -311
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize-tool.test.js +232 -232
- package/test/organize.test.js +418 -418
- package/test/panel-chart-scale.test.js +90 -90
- package/test/panel-tokens.test.js +245 -245
- package/test/peak-hours-observability.test.js +70 -70
- package/test/peer-blockers.test.js +193 -193
- package/test/persona-addressing.test.js +83 -83
- package/test/persona-api.test.js +172 -172
- package/test/persona-by-session.test.js +568 -568
- package/test/persona-flag.test.js +36 -36
- package/test/persona-inject.test.js +145 -145
- package/test/persona-settings.test.js +198 -198
- package/test/pipeline-activities.test.js +155 -155
- package/test/pipeline-attribution.test.js +175 -175
- package/test/pipeline-command.test.js +243 -243
- package/test/pipeline-confirm.test.js +148 -148
- package/test/pipeline-exec.test.js +172 -172
- package/test/pipeline-nudge-auto.test.js +248 -248
- package/test/pipeline-nudge.test.js +1278 -1278
- package/test/pipeline-observations.test.js +157 -157
- package/test/pipeline-observe.test.js +127 -127
- package/test/pipeline-r1r2.test.js +278 -278
- package/test/pipeline-runner.test.js +468 -468
- package/test/pipeline-runs.test.js +767 -767
- package/test/pipeline.test.js +636 -636
- package/test/placement.test.js +36 -36
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +444 -444
- package/test/profile-extract.test.js +269 -269
- package/test/profile-facts.test.js +492 -492
- package/test/prompt-stance.test.js +128 -128
- package/test/provenance-http.test.js +367 -367
- package/test/publish-gate.test.js +272 -272
- package/test/quality-backfill.test.js +290 -290
- package/test/quality-filter.test.js +295 -295
- package/test/readme-claims.test.js +113 -113
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-retention.test.js +128 -128
- package/test/recall-stats.test.js +274 -274
- package/test/receipt-chain.test.js +451 -451
- package/test/reclaim-http.test.js +209 -209
- package/test/reflection-update-gate.test.js +129 -129
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +475 -475
- package/test/reindex-scheduler.test.js +369 -369
- package/test/release-docs.test.js +156 -156
- package/test/release-notes.test.js +265 -265
- package/test/release-workflow.test.js +93 -93
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/route-policy.test.js +113 -113
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -319
- package/test/scope-candidates.test.js +491 -491
- package/test/scope-distill-stamp.test.js +134 -134
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-optional-service.test.js +110 -110
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-state.test.js +610 -610
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/security-doc.test.js +188 -188
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +251 -251
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +258 -258
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +410 -410
- package/test/session-gate.test.js +264 -264
- package/test/session-usage.test.js +317 -317
- package/test/settings.test.js +273 -273
- package/test/share-foreign-rows.test.js +144 -144
- package/test/share-http.test.js +144 -144
- package/test/share-import.test.js +577 -577
- package/test/share-imported-guard.test.js +311 -311
- package/test/share-imported-identity.test.js +111 -111
- package/test/share-pack.test.js +511 -511
- package/test/share-receive-http.test.js +756 -756
- package/test/share-routes.test.js +337 -337
- package/test/share-store.test.js +133 -133
- package/test/share-validate.test.js +101 -101
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +441 -441
- package/test/sleep-catchup.test.js +315 -315
- package/test/sleep-failure-backoff.test.js +156 -156
- package/test/sleep-heat.test.js +174 -174
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +1455 -1455
- package/test/standalone-api.test.js +1344 -1344
- package/test/store-vector-cache.test.js +98 -98
- package/test/store-write-path-columns.test.js +191 -191
- package/test/store.test.js +744 -744
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -127
- package/test/summarize-parse-diagnostics.test.js +75 -75
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +2251 -2251
- package/test/supersede-state-tracking.test.js +394 -394
- package/test/test-count-sync.test.js +71 -71
- package/test/tool-state-ignore.test.js +65 -65
- package/test/tools-dto-schema-matrix.test.js +398 -398
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +585 -585
- package/test/updated-at-semantics.test.js +113 -113
- package/test/user-settings-preview.test.js +127 -127
- package/test/user-settings-segments.test.js +527 -527
- package/test/vector-index.test.js +334 -334
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/wiki-api.test.js +734 -734
- package/test/wiki-claims.test.js +194 -194
- package/test/wiki-cli-hint.test.js +118 -118
- package/test/wiki-inject.test.js +265 -265
- package/test/wiki-partition.test.js +112 -112
- package/test/wiki-plan.test.js +80 -80
- package/test/wiki-provenance.test.js +202 -202
- package/test/wiki-queries.test.js +398 -398
- package/test/wiki-scan.test.js +208 -208
- package/test/wiki-store.test.js +190 -190
- package/test/wiki-t0.test.js +456 -456
- package/test/wiki-t1-jobs.test.js +125 -125
- package/test/wiki-t1.test.js +460 -460
- package/test/wiki-tools.test.js +176 -176
- package/test/workflow-guards.test.js +155 -155
- package/test/write-admission.test.js +668 -668
package/src/kb/index.js
CHANGED
|
@@ -1,309 +1,309 @@
|
|
|
1
|
-
// dsh-mneme/src/kb/index.js
|
|
2
|
-
// 知识库第二层(§3.3/§3.4):集合 + 文档入库(本地文件 / 网页 / 仓库地志页)+
|
|
3
|
-
// 分块 + BM25 检索。真源在 kb_* 三张表里(不像 chorography 那样以磁盘 MD 为真源
|
|
4
|
-
// ——知识库收的就是「外来原文的副本」,用户随时可能把原文件挪走/删掉,检索面不能
|
|
5
|
-
// 跟着消失),所以读侧零 IO。
|
|
6
|
-
//
|
|
7
|
-
// 物理隔离:本模块只碰 kb_* 三张表,绝不写 memories,也不进巩固/质量过滤任何池子
|
|
8
|
-
// (与 wiki 行同一条纪律,也是 §3.3 的红线)。
|
|
9
|
-
import { readFileSync, statSync } from "node:fs";
|
|
10
|
-
import { basename } from "node:path";
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
|
-
import { createBM25Index } from "../search/bm25.js";
|
|
13
|
-
|
|
14
|
-
// 单文档上限:知识库是检索面不是文件仓,几十兆的语料进来只会把 BM25 全量重算
|
|
15
|
-
// 变成卡顿源。超限给明确 error 回执(面板要能显示「这份为什么不进来」),不静默截断。
|
|
16
|
-
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
17
|
-
// 单块目标长度:BM25 的 length normalization 对超长块惩罚很重,而太碎的块丢上下文。
|
|
18
|
-
// 1200 字符约等于中文 600 字,是「一段完整论述」的常见尺度。
|
|
19
|
-
const CHUNK_TARGET_CHARS = 1200;
|
|
20
|
-
|
|
21
|
-
const KINDS = new Set(["file", "url", "choro_page"]);
|
|
22
|
-
|
|
23
|
-
function sha256(text) {
|
|
24
|
-
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 按行切块,**严格连续、不跳行**——这是 readDoc 能逐字节还原原文、以及检索命中
|
|
29
|
-
* 能给出准确行号(起始行 = 1 + 前面所有块的行数)的前提。破坏它这两件事同时坏。
|
|
30
|
-
* `## ` 标题开启新节并重置 target(一节尽量整块进来,除非它自己就超长);
|
|
31
|
-
* 超长单行单独成块不拆(行号与原文还原都靠这个假设)。
|
|
32
|
-
*/
|
|
33
|
-
function chunkContent(content) {
|
|
34
|
-
const lines = String(content ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
35
|
-
const chunks = [];
|
|
36
|
-
let heading = "";
|
|
37
|
-
let buf = [];
|
|
38
|
-
let bufChars = 0;
|
|
39
|
-
|
|
40
|
-
const flush = () => {
|
|
41
|
-
if (!buf.length) return;
|
|
42
|
-
// 一个空行都不剥:块间用 \n 直接拼接就是原文(readDoc 靠这条),而且空白行
|
|
43
|
-
// 不产生 token,对 BM25 的 len 分母零成本——剥它反而是纯损。
|
|
44
|
-
chunks.push({ heading, text: buf.join("\n") });
|
|
45
|
-
buf = [];
|
|
46
|
-
bufChars = 0;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
const isH2 = /^##\s+/.test(line);
|
|
51
|
-
const startsSection = isH2 && buf.length > 0;
|
|
52
|
-
if (startsSection) flush();
|
|
53
|
-
if (isH2) heading = line.replace(/^##\s+/, "").trim();
|
|
54
|
-
buf.push(line);
|
|
55
|
-
bufChars += line.length + 1;
|
|
56
|
-
if (bufChars >= CHUNK_TARGET_CHARS) flush();
|
|
57
|
-
}
|
|
58
|
-
flush();
|
|
59
|
-
return chunks;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function makeExcerpt(text, max = 200) {
|
|
63
|
-
return String(text ?? "").replace(/\s+/g, " ").trim().slice(0, max);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @param {object} deps
|
|
68
|
-
* @param {object} deps.store createStore() 的返回值(用到 kb* 那组方法)
|
|
69
|
-
* @param {object} [deps.embedder] v1 不接向量(BM25 打底);保留位,配了也不影响
|
|
70
|
-
* 检索结果,接向量是下一档的事
|
|
71
|
-
* @param {object} [deps.cfg]
|
|
72
|
-
*/
|
|
73
|
-
export function createKb({ store, embedder = null, cfg = {} } = {}) {
|
|
74
|
-
if (!store || !store.db) throw new TypeError("createKb: store is required");
|
|
75
|
-
const maxBytes = Number(cfg?.kbMaxBytes) > 0 ? Number(cfg.kbMaxBytes) : DEFAULT_MAX_BYTES;
|
|
76
|
-
// 检索索引按集合懒物化:kb 是 opt-in 子系统,一次 ingest 之后反复检索的收益远大于
|
|
77
|
-
// 「每次重建一遍 corpus」。写路径(ingest / delete / upsert)令其失效。
|
|
78
|
-
const indexCache = new Map();
|
|
79
|
-
|
|
80
|
-
function resolveCollection(key) {
|
|
81
|
-
return store.kbCollectionByKey(String(key ?? "").trim());
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function invalidate(collectionKey) {
|
|
85
|
-
if (collectionKey == null) indexCache.clear();
|
|
86
|
-
else indexCache.delete(String(collectionKey));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function listCollections() {
|
|
90
|
-
return store.listKbCollections().map((r) => ({
|
|
91
|
-
key: r.key,
|
|
92
|
-
title: r.title,
|
|
93
|
-
description: r.description ?? "",
|
|
94
|
-
docs: Number(r.doc_count ?? 0),
|
|
95
|
-
chunks: Number(r.chunk_count ?? 0),
|
|
96
|
-
updatedAt: r.updated_at,
|
|
97
|
-
}));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function upsertCollection({ key, title, description = "" } = {}) {
|
|
101
|
-
const k = String(key ?? "").trim();
|
|
102
|
-
if (k === "") throw new TypeError("upsertCollection: key is required");
|
|
103
|
-
const row = store.upsertKbCollection({
|
|
104
|
-
key: k,
|
|
105
|
-
title: String(title ?? "").trim() || k,
|
|
106
|
-
description: String(description ?? ""),
|
|
107
|
-
at: new Date().toISOString(),
|
|
108
|
-
});
|
|
109
|
-
invalidate(k);
|
|
110
|
-
return { key: row.key, title: row.title, description: row.description ?? "" };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function deleteCollection(key) {
|
|
114
|
-
const k = String(key ?? "").trim();
|
|
115
|
-
if (k === "") return false;
|
|
116
|
-
const gone = store.deleteKbCollection(k);
|
|
117
|
-
invalidate(k);
|
|
118
|
-
return gone;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function listDocs({ collection } = {}) {
|
|
122
|
-
const col = resolveCollection(collection);
|
|
123
|
-
if (!col) return [];
|
|
124
|
-
return store.listKbDocs(col.id).map((d) => ({
|
|
125
|
-
relKey: d.rel_key,
|
|
126
|
-
kind: d.kind,
|
|
127
|
-
title: d.title,
|
|
128
|
-
bytes: Number(d.bytes ?? 0),
|
|
129
|
-
indexedAt: d.indexed_at,
|
|
130
|
-
status: d.status,
|
|
131
|
-
error: d.error ?? null,
|
|
132
|
-
}));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** 读来源正文。三种 kind 各自的最短路径,出错一律抛,由 ingest 收成 error 回执。 */
|
|
136
|
-
async function readSource({ kind, source }) {
|
|
137
|
-
const src = String(source ?? "").trim();
|
|
138
|
-
if (src === "") throw new Error("empty-source");
|
|
139
|
-
if (kind === "url") {
|
|
140
|
-
if (typeof fetch !== "function") throw new Error("fetch-unavailable");
|
|
141
|
-
const res = await fetch(src, { redirect: "follow" });
|
|
142
|
-
if (!res.ok) throw new Error(`http-${res.status}`);
|
|
143
|
-
const raw = await res.text();
|
|
144
|
-
return { text: raw, relKey: src, title: src };
|
|
145
|
-
}
|
|
146
|
-
// file / choro_page:choro_page 的 source 就是该地志页 MD 的绝对路径(面板从
|
|
147
|
-
// 地志页「导入」时拿到的正是它)。kind 只作分类与展示,读取路径与普通文件一致
|
|
148
|
-
// ——不为它去跨域读 choro_* 表,那会把两条线的写作用域搅在一起。
|
|
149
|
-
let size = 0;
|
|
150
|
-
try {
|
|
151
|
-
size = statSync(src).size;
|
|
152
|
-
} catch (cause) {
|
|
153
|
-
throw new Error(cause.code === "ENOENT" ? "file-not-found" : "file-unreadable");
|
|
154
|
-
}
|
|
155
|
-
if (size > maxBytes) throw new Error("too-large");
|
|
156
|
-
const text = readFileSync(src, "utf8");
|
|
157
|
-
return { text, relKey: src, title: basename(src) };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* 入库。同 rel_key + 同 sha256 是 no-op(回执 skipped=true,chunks 是库里现有的
|
|
162
|
-
* 块数不是重新切出来的块数)——这条是整个幂等契约的落点,编辑器保存一次不能
|
|
163
|
-
* 触发全量重切块。
|
|
164
|
-
*/
|
|
165
|
-
async function ingest({ collection, kind, source, title } = {}) {
|
|
166
|
-
const colKey = String(collection ?? "").trim();
|
|
167
|
-
const k = String(kind ?? "").trim();
|
|
168
|
-
const now = new Date().toISOString();
|
|
169
|
-
if (!KINDS.has(k)) {
|
|
170
|
-
return { docId: null, chunks: 0, skipped: false, error: "bad-kind" };
|
|
171
|
-
}
|
|
172
|
-
const col = resolveCollection(colKey);
|
|
173
|
-
if (!col) {
|
|
174
|
-
// 不静默建集合:拼错 key 的入库会把资料塞进一个凭空出现的集合里,用户永远
|
|
175
|
-
// 找不到。面板的流程是先建集合再入库,工具侧也是先 list 再 ingest。
|
|
176
|
-
return { docId: null, chunks: 0, skipped: false, error: "collection-not-found" };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
let body;
|
|
180
|
-
try {
|
|
181
|
-
body = await readSource({ kind: k, source });
|
|
182
|
-
} catch (cause) {
|
|
183
|
-
const message = String(cause?.message ?? cause);
|
|
184
|
-
const docId = store.markKbDocError({
|
|
185
|
-
collectionId: col.id,
|
|
186
|
-
relKey: String(source ?? "").trim() || "(empty)",
|
|
187
|
-
kind: k,
|
|
188
|
-
sourcePath: String(source ?? "").trim() || null,
|
|
189
|
-
title: String(title ?? "").trim() || String(source ?? ""),
|
|
190
|
-
error: message,
|
|
191
|
-
at: now,
|
|
192
|
-
});
|
|
193
|
-
invalidate(colKey);
|
|
194
|
-
return { docId, chunks: 0, skipped: false, error: message };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const chunks = chunkContent(body.text);
|
|
198
|
-
const result = store.replaceKbDoc({
|
|
199
|
-
collectionId: col.id,
|
|
200
|
-
relKey: body.relKey,
|
|
201
|
-
kind: k,
|
|
202
|
-
sourcePath: body.relKey,
|
|
203
|
-
title: String(title ?? "").trim() || body.title,
|
|
204
|
-
bytes: Buffer.byteLength(body.text, "utf8"),
|
|
205
|
-
sha256: sha256(body.text),
|
|
206
|
-
at: now,
|
|
207
|
-
chunks: chunks.map((c, i) => ({ ordinal: i, heading: c.heading, text: c.text })),
|
|
208
|
-
});
|
|
209
|
-
invalidate(colKey);
|
|
210
|
-
return { docId: result.docId, chunks: result.chunks, skipped: result.skipped };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** 集合级语料的 BM25 索引(懒物化,写路径失效)。 */
|
|
214
|
-
function indexFor(col) {
|
|
215
|
-
const cacheKey = col ? String(col.key) : "*";
|
|
216
|
-
const cached = indexCache.get(cacheKey);
|
|
217
|
-
if (cached) return cached;
|
|
218
|
-
const rows = store.kbChunksForSearch(col ? col.id : null);
|
|
219
|
-
const docs = rows.map((r) => ({
|
|
220
|
-
id: r.id,
|
|
221
|
-
// 标题与本节标题都进索引文本:用户搜「安装」时,命中「## 安装」比命中正文里
|
|
222
|
-
// 偶然出现的「安装」更该排前面,而 BM25 只看词频,标题必须显式入语料。
|
|
223
|
-
title: `${r.title} ${r.heading ?? ""}`.trim(),
|
|
224
|
-
content: r.text ?? "",
|
|
225
|
-
row: r,
|
|
226
|
-
}));
|
|
227
|
-
const built = { index: createBM25Index(docs), byId: new Map(docs.map((d) => [d.id, d])) };
|
|
228
|
-
indexCache.set(cacheKey, built);
|
|
229
|
-
return built;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function search({ query, collection = null, limit = 20 } = {}) {
|
|
233
|
-
const q = String(query ?? "").trim();
|
|
234
|
-
if (q === "") return { hits: [], unreadable: [] };
|
|
235
|
-
let col = null;
|
|
236
|
-
if (collection != null && String(collection).trim() !== "") {
|
|
237
|
-
col = resolveCollection(collection);
|
|
238
|
-
if (!col) return { hits: [], unreadable: [] };
|
|
239
|
-
}
|
|
240
|
-
const cap = Math.max(1, Math.min(100, Number(limit) || 20));
|
|
241
|
-
const { index, byId } = indexFor(col);
|
|
242
|
-
const ranked = index.search(q, { limit: cap });
|
|
243
|
-
// 行号:块是**严格连续按行切**的,所以某块起始行 = 1 + 它前面所有块的行数。
|
|
244
|
-
// 这里按需回算(只算命中的那一篇),避免给 kb_chunks 加一列只为省这点算术。
|
|
245
|
-
const lineCache = new Map();
|
|
246
|
-
const lineOf = (row) => {
|
|
247
|
-
if (!lineCache.has(row.doc_id)) {
|
|
248
|
-
const all = store.kbChunksByDoc(row.doc_id);
|
|
249
|
-
let cursor = 1;
|
|
250
|
-
const map = new Map();
|
|
251
|
-
for (const c of all) {
|
|
252
|
-
map.set(c.id, cursor);
|
|
253
|
-
// 块间用 \n 拼接,所以上一块的行数就是游标增量(不 +1)。
|
|
254
|
-
cursor += String(c.text ?? "").split("\n").length;
|
|
255
|
-
}
|
|
256
|
-
lineCache.set(row.doc_id, map);
|
|
257
|
-
}
|
|
258
|
-
return lineCache.get(row.doc_id).get(row.id) ?? 1;
|
|
259
|
-
};
|
|
260
|
-
const hits = ranked.map((hit) => {
|
|
261
|
-
const row = byId.get(hit.id).row;
|
|
262
|
-
return {
|
|
263
|
-
collection: row.collection_key,
|
|
264
|
-
relKey: row.rel_key,
|
|
265
|
-
title: row.title,
|
|
266
|
-
heading: row.heading ?? "",
|
|
267
|
-
line: lineOf(row),
|
|
268
|
-
excerpt: makeExcerpt(row.text),
|
|
269
|
-
score: hit.score,
|
|
270
|
-
};
|
|
271
|
-
});
|
|
272
|
-
return { hits, unreadable: [] };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** 读整篇:块按行连续切,用 \n 拼回即是原文(见 chunkContent 的注释)。 */
|
|
276
|
-
function readDoc({ collection, relKey } = {}) {
|
|
277
|
-
const col = resolveCollection(collection);
|
|
278
|
-
if (!col) return null;
|
|
279
|
-
const doc = store.kbDocByRelKey(col.id, String(relKey ?? ""));
|
|
280
|
-
if (!doc) return null;
|
|
281
|
-
const chunks = store.kbChunksByDoc(doc.id);
|
|
282
|
-
return {
|
|
283
|
-
title: doc.title,
|
|
284
|
-
content: chunks.map((c) => c.text).join("\n"),
|
|
285
|
-
kind: doc.kind,
|
|
286
|
-
relKey: doc.rel_key,
|
|
287
|
-
status: doc.status,
|
|
288
|
-
error: doc.error ?? null,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function dispose() {
|
|
293
|
-
indexCache.clear();
|
|
294
|
-
try {
|
|
295
|
-
embedder?.dispose?.();
|
|
296
|
-
} catch { /* 释放期的失败不该盖住调用方的正常退出 */ }
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
listCollections,
|
|
301
|
-
upsertCollection,
|
|
302
|
-
deleteCollection,
|
|
303
|
-
listDocs,
|
|
304
|
-
ingest,
|
|
305
|
-
search,
|
|
306
|
-
readDoc,
|
|
307
|
-
dispose,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
1
|
+
// dsh-mneme/src/kb/index.js
|
|
2
|
+
// 知识库第二层(§3.3/§3.4):集合 + 文档入库(本地文件 / 网页 / 仓库地志页)+
|
|
3
|
+
// 分块 + BM25 检索。真源在 kb_* 三张表里(不像 chorography 那样以磁盘 MD 为真源
|
|
4
|
+
// ——知识库收的就是「外来原文的副本」,用户随时可能把原文件挪走/删掉,检索面不能
|
|
5
|
+
// 跟着消失),所以读侧零 IO。
|
|
6
|
+
//
|
|
7
|
+
// 物理隔离:本模块只碰 kb_* 三张表,绝不写 memories,也不进巩固/质量过滤任何池子
|
|
8
|
+
// (与 wiki 行同一条纪律,也是 §3.3 的红线)。
|
|
9
|
+
import { readFileSync, statSync } from "node:fs";
|
|
10
|
+
import { basename } from "node:path";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { createBM25Index } from "../search/bm25.js";
|
|
13
|
+
|
|
14
|
+
// 单文档上限:知识库是检索面不是文件仓,几十兆的语料进来只会把 BM25 全量重算
|
|
15
|
+
// 变成卡顿源。超限给明确 error 回执(面板要能显示「这份为什么不进来」),不静默截断。
|
|
16
|
+
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
17
|
+
// 单块目标长度:BM25 的 length normalization 对超长块惩罚很重,而太碎的块丢上下文。
|
|
18
|
+
// 1200 字符约等于中文 600 字,是「一段完整论述」的常见尺度。
|
|
19
|
+
const CHUNK_TARGET_CHARS = 1200;
|
|
20
|
+
|
|
21
|
+
const KINDS = new Set(["file", "url", "choro_page"]);
|
|
22
|
+
|
|
23
|
+
function sha256(text) {
|
|
24
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 按行切块,**严格连续、不跳行**——这是 readDoc 能逐字节还原原文、以及检索命中
|
|
29
|
+
* 能给出准确行号(起始行 = 1 + 前面所有块的行数)的前提。破坏它这两件事同时坏。
|
|
30
|
+
* `## ` 标题开启新节并重置 target(一节尽量整块进来,除非它自己就超长);
|
|
31
|
+
* 超长单行单独成块不拆(行号与原文还原都靠这个假设)。
|
|
32
|
+
*/
|
|
33
|
+
function chunkContent(content) {
|
|
34
|
+
const lines = String(content ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
35
|
+
const chunks = [];
|
|
36
|
+
let heading = "";
|
|
37
|
+
let buf = [];
|
|
38
|
+
let bufChars = 0;
|
|
39
|
+
|
|
40
|
+
const flush = () => {
|
|
41
|
+
if (!buf.length) return;
|
|
42
|
+
// 一个空行都不剥:块间用 \n 直接拼接就是原文(readDoc 靠这条),而且空白行
|
|
43
|
+
// 不产生 token,对 BM25 的 len 分母零成本——剥它反而是纯损。
|
|
44
|
+
chunks.push({ heading, text: buf.join("\n") });
|
|
45
|
+
buf = [];
|
|
46
|
+
bufChars = 0;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const isH2 = /^##\s+/.test(line);
|
|
51
|
+
const startsSection = isH2 && buf.length > 0;
|
|
52
|
+
if (startsSection) flush();
|
|
53
|
+
if (isH2) heading = line.replace(/^##\s+/, "").trim();
|
|
54
|
+
buf.push(line);
|
|
55
|
+
bufChars += line.length + 1;
|
|
56
|
+
if (bufChars >= CHUNK_TARGET_CHARS) flush();
|
|
57
|
+
}
|
|
58
|
+
flush();
|
|
59
|
+
return chunks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeExcerpt(text, max = 200) {
|
|
63
|
+
return String(text ?? "").replace(/\s+/g, " ").trim().slice(0, max);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {object} deps
|
|
68
|
+
* @param {object} deps.store createStore() 的返回值(用到 kb* 那组方法)
|
|
69
|
+
* @param {object} [deps.embedder] v1 不接向量(BM25 打底);保留位,配了也不影响
|
|
70
|
+
* 检索结果,接向量是下一档的事
|
|
71
|
+
* @param {object} [deps.cfg]
|
|
72
|
+
*/
|
|
73
|
+
export function createKb({ store, embedder = null, cfg = {} } = {}) {
|
|
74
|
+
if (!store || !store.db) throw new TypeError("createKb: store is required");
|
|
75
|
+
const maxBytes = Number(cfg?.kbMaxBytes) > 0 ? Number(cfg.kbMaxBytes) : DEFAULT_MAX_BYTES;
|
|
76
|
+
// 检索索引按集合懒物化:kb 是 opt-in 子系统,一次 ingest 之后反复检索的收益远大于
|
|
77
|
+
// 「每次重建一遍 corpus」。写路径(ingest / delete / upsert)令其失效。
|
|
78
|
+
const indexCache = new Map();
|
|
79
|
+
|
|
80
|
+
function resolveCollection(key) {
|
|
81
|
+
return store.kbCollectionByKey(String(key ?? "").trim());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function invalidate(collectionKey) {
|
|
85
|
+
if (collectionKey == null) indexCache.clear();
|
|
86
|
+
else indexCache.delete(String(collectionKey));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function listCollections() {
|
|
90
|
+
return store.listKbCollections().map((r) => ({
|
|
91
|
+
key: r.key,
|
|
92
|
+
title: r.title,
|
|
93
|
+
description: r.description ?? "",
|
|
94
|
+
docs: Number(r.doc_count ?? 0),
|
|
95
|
+
chunks: Number(r.chunk_count ?? 0),
|
|
96
|
+
updatedAt: r.updated_at,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function upsertCollection({ key, title, description = "" } = {}) {
|
|
101
|
+
const k = String(key ?? "").trim();
|
|
102
|
+
if (k === "") throw new TypeError("upsertCollection: key is required");
|
|
103
|
+
const row = store.upsertKbCollection({
|
|
104
|
+
key: k,
|
|
105
|
+
title: String(title ?? "").trim() || k,
|
|
106
|
+
description: String(description ?? ""),
|
|
107
|
+
at: new Date().toISOString(),
|
|
108
|
+
});
|
|
109
|
+
invalidate(k);
|
|
110
|
+
return { key: row.key, title: row.title, description: row.description ?? "" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function deleteCollection(key) {
|
|
114
|
+
const k = String(key ?? "").trim();
|
|
115
|
+
if (k === "") return false;
|
|
116
|
+
const gone = store.deleteKbCollection(k);
|
|
117
|
+
invalidate(k);
|
|
118
|
+
return gone;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function listDocs({ collection } = {}) {
|
|
122
|
+
const col = resolveCollection(collection);
|
|
123
|
+
if (!col) return [];
|
|
124
|
+
return store.listKbDocs(col.id).map((d) => ({
|
|
125
|
+
relKey: d.rel_key,
|
|
126
|
+
kind: d.kind,
|
|
127
|
+
title: d.title,
|
|
128
|
+
bytes: Number(d.bytes ?? 0),
|
|
129
|
+
indexedAt: d.indexed_at,
|
|
130
|
+
status: d.status,
|
|
131
|
+
error: d.error ?? null,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 读来源正文。三种 kind 各自的最短路径,出错一律抛,由 ingest 收成 error 回执。 */
|
|
136
|
+
async function readSource({ kind, source }) {
|
|
137
|
+
const src = String(source ?? "").trim();
|
|
138
|
+
if (src === "") throw new Error("empty-source");
|
|
139
|
+
if (kind === "url") {
|
|
140
|
+
if (typeof fetch !== "function") throw new Error("fetch-unavailable");
|
|
141
|
+
const res = await fetch(src, { redirect: "follow" });
|
|
142
|
+
if (!res.ok) throw new Error(`http-${res.status}`);
|
|
143
|
+
const raw = await res.text();
|
|
144
|
+
return { text: raw, relKey: src, title: src };
|
|
145
|
+
}
|
|
146
|
+
// file / choro_page:choro_page 的 source 就是该地志页 MD 的绝对路径(面板从
|
|
147
|
+
// 地志页「导入」时拿到的正是它)。kind 只作分类与展示,读取路径与普通文件一致
|
|
148
|
+
// ——不为它去跨域读 choro_* 表,那会把两条线的写作用域搅在一起。
|
|
149
|
+
let size = 0;
|
|
150
|
+
try {
|
|
151
|
+
size = statSync(src).size;
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
throw new Error(cause.code === "ENOENT" ? "file-not-found" : "file-unreadable");
|
|
154
|
+
}
|
|
155
|
+
if (size > maxBytes) throw new Error("too-large");
|
|
156
|
+
const text = readFileSync(src, "utf8");
|
|
157
|
+
return { text, relKey: src, title: basename(src) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 入库。同 rel_key + 同 sha256 是 no-op(回执 skipped=true,chunks 是库里现有的
|
|
162
|
+
* 块数不是重新切出来的块数)——这条是整个幂等契约的落点,编辑器保存一次不能
|
|
163
|
+
* 触发全量重切块。
|
|
164
|
+
*/
|
|
165
|
+
async function ingest({ collection, kind, source, title } = {}) {
|
|
166
|
+
const colKey = String(collection ?? "").trim();
|
|
167
|
+
const k = String(kind ?? "").trim();
|
|
168
|
+
const now = new Date().toISOString();
|
|
169
|
+
if (!KINDS.has(k)) {
|
|
170
|
+
return { docId: null, chunks: 0, skipped: false, error: "bad-kind" };
|
|
171
|
+
}
|
|
172
|
+
const col = resolveCollection(colKey);
|
|
173
|
+
if (!col) {
|
|
174
|
+
// 不静默建集合:拼错 key 的入库会把资料塞进一个凭空出现的集合里,用户永远
|
|
175
|
+
// 找不到。面板的流程是先建集合再入库,工具侧也是先 list 再 ingest。
|
|
176
|
+
return { docId: null, chunks: 0, skipped: false, error: "collection-not-found" };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let body;
|
|
180
|
+
try {
|
|
181
|
+
body = await readSource({ kind: k, source });
|
|
182
|
+
} catch (cause) {
|
|
183
|
+
const message = String(cause?.message ?? cause);
|
|
184
|
+
const docId = store.markKbDocError({
|
|
185
|
+
collectionId: col.id,
|
|
186
|
+
relKey: String(source ?? "").trim() || "(empty)",
|
|
187
|
+
kind: k,
|
|
188
|
+
sourcePath: String(source ?? "").trim() || null,
|
|
189
|
+
title: String(title ?? "").trim() || String(source ?? ""),
|
|
190
|
+
error: message,
|
|
191
|
+
at: now,
|
|
192
|
+
});
|
|
193
|
+
invalidate(colKey);
|
|
194
|
+
return { docId, chunks: 0, skipped: false, error: message };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const chunks = chunkContent(body.text);
|
|
198
|
+
const result = store.replaceKbDoc({
|
|
199
|
+
collectionId: col.id,
|
|
200
|
+
relKey: body.relKey,
|
|
201
|
+
kind: k,
|
|
202
|
+
sourcePath: body.relKey,
|
|
203
|
+
title: String(title ?? "").trim() || body.title,
|
|
204
|
+
bytes: Buffer.byteLength(body.text, "utf8"),
|
|
205
|
+
sha256: sha256(body.text),
|
|
206
|
+
at: now,
|
|
207
|
+
chunks: chunks.map((c, i) => ({ ordinal: i, heading: c.heading, text: c.text })),
|
|
208
|
+
});
|
|
209
|
+
invalidate(colKey);
|
|
210
|
+
return { docId: result.docId, chunks: result.chunks, skipped: result.skipped };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 集合级语料的 BM25 索引(懒物化,写路径失效)。 */
|
|
214
|
+
function indexFor(col) {
|
|
215
|
+
const cacheKey = col ? String(col.key) : "*";
|
|
216
|
+
const cached = indexCache.get(cacheKey);
|
|
217
|
+
if (cached) return cached;
|
|
218
|
+
const rows = store.kbChunksForSearch(col ? col.id : null);
|
|
219
|
+
const docs = rows.map((r) => ({
|
|
220
|
+
id: r.id,
|
|
221
|
+
// 标题与本节标题都进索引文本:用户搜「安装」时,命中「## 安装」比命中正文里
|
|
222
|
+
// 偶然出现的「安装」更该排前面,而 BM25 只看词频,标题必须显式入语料。
|
|
223
|
+
title: `${r.title} ${r.heading ?? ""}`.trim(),
|
|
224
|
+
content: r.text ?? "",
|
|
225
|
+
row: r,
|
|
226
|
+
}));
|
|
227
|
+
const built = { index: createBM25Index(docs), byId: new Map(docs.map((d) => [d.id, d])) };
|
|
228
|
+
indexCache.set(cacheKey, built);
|
|
229
|
+
return built;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function search({ query, collection = null, limit = 20 } = {}) {
|
|
233
|
+
const q = String(query ?? "").trim();
|
|
234
|
+
if (q === "") return { hits: [], unreadable: [] };
|
|
235
|
+
let col = null;
|
|
236
|
+
if (collection != null && String(collection).trim() !== "") {
|
|
237
|
+
col = resolveCollection(collection);
|
|
238
|
+
if (!col) return { hits: [], unreadable: [] };
|
|
239
|
+
}
|
|
240
|
+
const cap = Math.max(1, Math.min(100, Number(limit) || 20));
|
|
241
|
+
const { index, byId } = indexFor(col);
|
|
242
|
+
const ranked = index.search(q, { limit: cap });
|
|
243
|
+
// 行号:块是**严格连续按行切**的,所以某块起始行 = 1 + 它前面所有块的行数。
|
|
244
|
+
// 这里按需回算(只算命中的那一篇),避免给 kb_chunks 加一列只为省这点算术。
|
|
245
|
+
const lineCache = new Map();
|
|
246
|
+
const lineOf = (row) => {
|
|
247
|
+
if (!lineCache.has(row.doc_id)) {
|
|
248
|
+
const all = store.kbChunksByDoc(row.doc_id);
|
|
249
|
+
let cursor = 1;
|
|
250
|
+
const map = new Map();
|
|
251
|
+
for (const c of all) {
|
|
252
|
+
map.set(c.id, cursor);
|
|
253
|
+
// 块间用 \n 拼接,所以上一块的行数就是游标增量(不 +1)。
|
|
254
|
+
cursor += String(c.text ?? "").split("\n").length;
|
|
255
|
+
}
|
|
256
|
+
lineCache.set(row.doc_id, map);
|
|
257
|
+
}
|
|
258
|
+
return lineCache.get(row.doc_id).get(row.id) ?? 1;
|
|
259
|
+
};
|
|
260
|
+
const hits = ranked.map((hit) => {
|
|
261
|
+
const row = byId.get(hit.id).row;
|
|
262
|
+
return {
|
|
263
|
+
collection: row.collection_key,
|
|
264
|
+
relKey: row.rel_key,
|
|
265
|
+
title: row.title,
|
|
266
|
+
heading: row.heading ?? "",
|
|
267
|
+
line: lineOf(row),
|
|
268
|
+
excerpt: makeExcerpt(row.text),
|
|
269
|
+
score: hit.score,
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
return { hits, unreadable: [] };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** 读整篇:块按行连续切,用 \n 拼回即是原文(见 chunkContent 的注释)。 */
|
|
276
|
+
function readDoc({ collection, relKey } = {}) {
|
|
277
|
+
const col = resolveCollection(collection);
|
|
278
|
+
if (!col) return null;
|
|
279
|
+
const doc = store.kbDocByRelKey(col.id, String(relKey ?? ""));
|
|
280
|
+
if (!doc) return null;
|
|
281
|
+
const chunks = store.kbChunksByDoc(doc.id);
|
|
282
|
+
return {
|
|
283
|
+
title: doc.title,
|
|
284
|
+
content: chunks.map((c) => c.text).join("\n"),
|
|
285
|
+
kind: doc.kind,
|
|
286
|
+
relKey: doc.rel_key,
|
|
287
|
+
status: doc.status,
|
|
288
|
+
error: doc.error ?? null,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function dispose() {
|
|
293
|
+
indexCache.clear();
|
|
294
|
+
try {
|
|
295
|
+
embedder?.dispose?.();
|
|
296
|
+
} catch { /* 释放期的失败不该盖住调用方的正常退出 */ }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
listCollections,
|
|
301
|
+
upsertCollection,
|
|
302
|
+
deleteCollection,
|
|
303
|
+
listDocs,
|
|
304
|
+
ingest,
|
|
305
|
+
search,
|
|
306
|
+
readDoc,
|
|
307
|
+
dispose,
|
|
308
|
+
};
|
|
309
|
+
}
|