@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
|
@@ -1,398 +1,398 @@
|
|
|
1
|
-
// queries.js 的回归锁(M1 读侧)。全部用例走真实 T0(fixture 仓库 apply)建数据,
|
|
2
|
-
// 不手插 DB 行——读侧的契约是「DB 索引 × 磁盘真源」的对账,手插行测不出盘上缺页
|
|
3
|
-
// 这类真实故障。锁的点:slug 白名单解析(查不到返回 null,绝不拿用户输入拼路径)、
|
|
4
|
-
// 盘上缺页如实报 page-missing、检索分节与行号、repo 解析的歧义处理。
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, unlinkSync, readFileSync } from "node:fs";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { execFileSync } from "node:child_process";
|
|
11
|
-
import { runChoroT0 } from "../src/wiki/t0.js";
|
|
12
|
-
import { createStore } from "../src/store.js";
|
|
13
|
-
import { createChoroQueries } from "../src/wiki/queries.js";
|
|
14
|
-
|
|
15
|
-
const FIXED_NOW = () => "2026-11-05T00:00:00.000Z";
|
|
16
|
-
|
|
17
|
-
function makeFixture() {
|
|
18
|
-
const root = mkdtempSync(join(tmpdir(), "choro-queries-"));
|
|
19
|
-
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "qfix", scripts: { test: "node --test" } }, null, 2) + "\n", "utf8");
|
|
20
|
-
mkdirSync(join(root, "src"), { recursive: true });
|
|
21
|
-
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nimport h from "left-pad";\nexport const a = () => b();\n', "utf8");
|
|
22
|
-
writeFileSync(join(root, "src", "b.js"), 'import { b2 } from "./b2.js";\nexport const b = () => b2();\n', "utf8");
|
|
23
|
-
writeFileSync(join(root, "src", "b2.js"), "export const b2 = () => 1;\n", "utf8");
|
|
24
|
-
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "pipe" });
|
|
25
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
26
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "init", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
27
|
-
return root;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function rmTempBestEffort(dir) {
|
|
31
|
-
for (let i = 0; i < 5; i++) {
|
|
32
|
-
try {
|
|
33
|
-
rmSync(dir, { recursive: true, force: true });
|
|
34
|
-
return;
|
|
35
|
-
} catch {
|
|
36
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
console.warn(`[wiki-queries.test] 临时目录清理未完成:${dir}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function setup() {
|
|
43
|
-
const root = makeFixture();
|
|
44
|
-
const store = createStore(":memory:");
|
|
45
|
-
runChoroT0({ repoRoot: root, store, apply: true, now: FIXED_NOW });
|
|
46
|
-
return { root, store, q: createChoroQueries({ store }) };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// stub embedder:文本含 "src" → [1,0],否则 [0,1](固定二维,可断言方向与失配)
|
|
50
|
-
function stubEmbedder(model = "stub-1") {
|
|
51
|
-
return {
|
|
52
|
-
model,
|
|
53
|
-
embedSingle: async (text) => (/src/.test(text) ? [1, 0] : [0, 1]),
|
|
54
|
-
embed: async (text) => (/src/.test(text) ? [1, 0] : [0, 1]),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// --- 收尾档 P37–P39:向量惰性物化与检索 -------------------------------------
|
|
59
|
-
|
|
60
|
-
test("embedPageVectors:首次全嵌,generated_at 没变就跳过,变了重嵌(staleness 两维之一)", async () => {
|
|
61
|
-
const { root, store, q } = setup();
|
|
62
|
-
try {
|
|
63
|
-
const repo = q.resolveRepo(undefined);
|
|
64
|
-
const emb = stubEmbedder();
|
|
65
|
-
const r1 = await q.embedPageVectors(repo.id, emb);
|
|
66
|
-
assert.equal(r1.embedded, 2, "两页全嵌");
|
|
67
|
-
const r2 = await q.embedPageVectors(repo.id, emb);
|
|
68
|
-
assert.equal(r2.embedded, 0);
|
|
69
|
-
assert.equal(r2.skipped, 2, "generated_at 没变 → 全跳过");
|
|
70
|
-
// 页面内容变了(generated_at 推进)→ 只重嵌变的那页
|
|
71
|
-
const pagePath = join(root, ".chorography", "modules", "src.md");
|
|
72
|
-
const before = readFileSync(pagePath, "utf8");
|
|
73
|
-
writeFileSync(pagePath, `${before}<!-- CHORO:HUMAN:START -->\n人工补充\n<!-- CHORO:HUMAN:END -->\n`, "utf8");
|
|
74
|
-
store.db.prepare(
|
|
75
|
-
"UPDATE choro_pages SET generated_at = ? WHERE generated_at IS NOT NULL AND id IN (SELECT p.id FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id WHERE m.slug = 'src')"
|
|
76
|
-
).run("2026-11-06T00:00:00.000Z");
|
|
77
|
-
const r3 = await q.embedPageVectors(repo.id, emb);
|
|
78
|
-
assert.equal(r3.embedded, 1, "只有 src 页重嵌");
|
|
79
|
-
assert.equal(r3.stale, 1, "旧向量按 stale 记账");
|
|
80
|
-
} finally {
|
|
81
|
-
store.close();
|
|
82
|
-
rmTempBestEffort(root);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("dim 失配(换 embedder 模型)→ 惰性全量重嵌(P38:不锁死,代价只是几十页)", async () => {
|
|
87
|
-
const { root, store, q } = setup();
|
|
88
|
-
try {
|
|
89
|
-
const repo = q.resolveRepo(undefined);
|
|
90
|
-
await q.embedPageVectors(repo.id, stubEmbedder("stub-1"));
|
|
91
|
-
// 换模型:维度从 2 → 3。expectDim 来自查询向量(路由先嵌查询再物化)
|
|
92
|
-
const emb3 = {
|
|
93
|
-
model: "stub-2",
|
|
94
|
-
embedSingle: async () => [1, 0, 0],
|
|
95
|
-
embed: async () => [1, 0, 0],
|
|
96
|
-
};
|
|
97
|
-
const r = await q.embedPageVectors(repo.id, emb3, { expectDim: 3 });
|
|
98
|
-
assert.equal(r.embedded, 2, "维度变了 → 全部重嵌(fresh 检查被 dimDrift 压过)");
|
|
99
|
-
assert.equal(r.dimMismatch, true, "失配被记账");
|
|
100
|
-
const rows = store.getChoroPageVectors(repo.id);
|
|
101
|
-
assert.ok(rows.every((row) => row.dim === 3 && row.model === "stub-2"), "旧维度行不复存");
|
|
102
|
-
// 不给 expectDim(无查询场景)→ fresh 行照旧跳过
|
|
103
|
-
const r2 = await q.embedPageVectors(repo.id, emb3);
|
|
104
|
-
assert.equal(r2.embedded, 0);
|
|
105
|
-
assert.equal(r2.skipped, 2);
|
|
106
|
-
} finally {
|
|
107
|
-
store.close();
|
|
108
|
-
rmTempBestEffort(root);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("searchSemantic:cosine 排序 + dim 失配行被过滤 + embedder 缺席返回 null", async () => {
|
|
113
|
-
const { root, store, q } = setup();
|
|
114
|
-
try {
|
|
115
|
-
const repo = q.resolveRepo(undefined);
|
|
116
|
-
await q.embedPageVectors(repo.id, stubEmbedder("stub-1"));
|
|
117
|
-
const hits = await q.searchSemantic(repo.id, stubEmbedder("stub-1"), "src 相关的页");
|
|
118
|
-
assert.ok(Array.isArray(hits) && hits.length >= 1);
|
|
119
|
-
assert.equal(hits[0].slug, "src", "查询向量 [1,0] 与 src 页同向 → 排前");
|
|
120
|
-
// 查询向量三维:与所有行 dim 失配 → 无可比行,返回空数组而不是抛
|
|
121
|
-
const dim3 = { embedSingle: async () => [1, 0, 0], embed: async () => [1, 0, 0] };
|
|
122
|
-
const empty = await q.searchSemantic(repo.id, dim3, "x");
|
|
123
|
-
assert.deepEqual(empty, []);
|
|
124
|
-
assert.equal(await q.searchSemantic(repo.id, null, "x"), null);
|
|
125
|
-
assert.deepEqual(await q.searchSemantic(repo.id, stubEmbedder(), " "), [], "空查询不嵌入、返回空数组");
|
|
126
|
-
} finally {
|
|
127
|
-
store.close();
|
|
128
|
-
rmTempBestEffort(root);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("listRepos / resolveRepo:单仓自动收敛,多仓歧义要显式给键", () => {
|
|
133
|
-
const { root, store, q } = setup();
|
|
134
|
-
try {
|
|
135
|
-
const repos = q.listRepos();
|
|
136
|
-
assert.equal(repos.length, 1);
|
|
137
|
-
assert.equal(repos[0].module_count, 2, "src 与 root 两个模块(1 条跨模块边不触发合并)");
|
|
138
|
-
assert.equal(repos[0].page_count, 2);
|
|
139
|
-
const auto = q.resolveRepo(undefined);
|
|
140
|
-
assert.equal(auto.id, repos[0].id, "只有一个仓库时不带键也能解析");
|
|
141
|
-
assert.ok(q.resolveRepo("qfix").id, "按显示名解析");
|
|
142
|
-
assert.equal(q.resolveRepo("no-such-repo"), null);
|
|
143
|
-
// 造第二个仓库让解析变歧义
|
|
144
|
-
const root2 = makeFixture();
|
|
145
|
-
try {
|
|
146
|
-
runChoroT0({ repoRoot: root2, store, apply: true, now: FIXED_NOW });
|
|
147
|
-
const amb = q.resolveRepo(undefined);
|
|
148
|
-
assert.ok(amb.ambiguous, "多仓无键 → 歧义清单而不是猜一个");
|
|
149
|
-
assert.equal(amb.ambiguous.length, 2);
|
|
150
|
-
} finally {
|
|
151
|
-
rmTempBestEffort(root2);
|
|
152
|
-
}
|
|
153
|
-
} finally {
|
|
154
|
-
store.close();
|
|
155
|
-
rmTempBestEffort(root);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("moduleMap:模块、页面状态与跨模块边同源返回", () => {
|
|
160
|
-
const { root, store, q } = setup();
|
|
161
|
-
try {
|
|
162
|
-
const repo = q.resolveRepo(undefined);
|
|
163
|
-
const { modules, edges } = q.moduleMap(repo.id);
|
|
164
|
-
const slugs = modules.map((m) => m.slug).sort();
|
|
165
|
-
assert.deepEqual(slugs, ["root", "src"]);
|
|
166
|
-
const src = modules.find((m) => m.slug === "src");
|
|
167
|
-
assert.equal(src.page_status, "skeleton");
|
|
168
|
-
// 面板从地志页导入知识点时读的是 mod.relPath(契约 §3.6);SQL 行里只有
|
|
169
|
-
// page_rel_path,缺这层映射会让「选完模块点导入」永远提示请先选模块(#607)。
|
|
170
|
-
// 同时锁住原 snake_case 字段还在——choro_module_map 的 render 与 CLI 都在用。
|
|
171
|
-
assert.equal(typeof src.relPath, "string", "relPath 必须与 page_rel_path 同值");
|
|
172
|
-
assert.equal(src.relPath, src.page_rel_path);
|
|
173
|
-
assert.ok(src.relPath.endsWith(".md"), "页相对路径是磁盘上真实存在的 .md");
|
|
174
|
-
assert.equal(typeof src.file_count, "number", "file_count 原字段必须保留(choro_module_map 的 render 在吃)");
|
|
175
|
-
// fixture 的引用全在 src 模块内部(a→b→b2),left-pad 是外部依赖,root 只有
|
|
176
|
-
// package.json 没有代码——跨模块边应为空集。跨模块聚erge本身由 wiki-store 的
|
|
177
|
-
// 整仓替换用例与 t0 的 moduleEdges 排序覆盖。
|
|
178
|
-
assert.deepEqual(edges, []);
|
|
179
|
-
} finally {
|
|
180
|
-
store.close();
|
|
181
|
-
rmTempBestEffort(root);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("readPage:正文来自磁盘真源;盘上缺页如实报 page-missing;未知 slug 返回 null", () => {
|
|
186
|
-
const { root, store, q } = setup();
|
|
187
|
-
try {
|
|
188
|
-
const repo = q.resolveRepo(undefined);
|
|
189
|
-
const page = q.readPage(repo.id, "src");
|
|
190
|
-
assert.equal(page.slug, "src");
|
|
191
|
-
assert.ok(page.content.includes("## 文件清单"), "正文是骨架页");
|
|
192
|
-
assert.equal(page.content.split("\n").length > 10, true);
|
|
193
|
-
assert.equal(q.readPage(repo.id, "../../etc/passwd"), null, "未知 slug 一律 null(路径零用户输入)");
|
|
194
|
-
// 盘上删页:索引还在、读侧如实报缺
|
|
195
|
-
unlinkSync(join(root, ".chorography", "modules", "src.md"));
|
|
196
|
-
const missing = q.readPage(repo.id, "src");
|
|
197
|
-
assert.equal(missing.error, "page-missing");
|
|
198
|
-
} finally {
|
|
199
|
-
store.close();
|
|
200
|
-
rmTempBestEffort(root);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("search:按节返回命中与行号,空 q 返回空数组,module 过滤生效", () => {
|
|
205
|
-
const { root, store, q } = setup();
|
|
206
|
-
try {
|
|
207
|
-
const repo = q.resolveRepo(undefined);
|
|
208
|
-
// 骨架页里必有「文件清单」节标题与 b2.js 文件名
|
|
209
|
-
const hits = q.search(repo.id, "文件清单");
|
|
210
|
-
assert.ok(hits.length >= 2, "两页都有文件清单节");
|
|
211
|
-
assert.ok(hits.every((h) => h.line >= 1 && typeof h.section === "string"));
|
|
212
|
-
const byFile = q.search(repo.id, "b2.js");
|
|
213
|
-
// 命中两处是契约而非巧合:frontmatter 的 sourceFiles(节=页首)+ 正文「文件
|
|
214
|
-
// 清单」表。分节让调用方能区分这两层。
|
|
215
|
-
assert.equal(byFile.length, 2);
|
|
216
|
-
assert.ok(byFile.every((h) => h.slug === "src"), "b2.js 只属于 src 页");
|
|
217
|
-
assert.equal(byFile.filter((h) => h.section.startsWith("文件清单")).length, 1, "节名带数量后缀(文件清单(N 个)),按前缀匹配");
|
|
218
|
-
assert.deepEqual(q.search(repo.id, " "), [], "空 q 不报错返回空");
|
|
219
|
-
const inRoot = q.search(repo.id, "package.json", { moduleSlug: "root" });
|
|
220
|
-
assert.equal(inRoot.length, 2, "与 b2.js 同款两层命中:frontmatter(页首)+ 文件清单表");
|
|
221
|
-
assert.equal(inRoot[0].slug, "root");
|
|
222
|
-
assert.deepEqual(q.search(repo.id, "package.json", { moduleSlug: "src" }), [], "模块过滤真生效");
|
|
223
|
-
} finally {
|
|
224
|
-
store.close();
|
|
225
|
-
rmTempBestEffort(root);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
test("repoByCwd:祖先前缀命中、最长 root 胜出、cwd 不存在返回 null", () => {
|
|
229
|
-
const { root, store, q } = setup();
|
|
230
|
-
let nested = null;
|
|
231
|
-
try {
|
|
232
|
-
// 精确根目录
|
|
233
|
-
const exact = q.repoByCwd(root);
|
|
234
|
-
assert.ok(exact, "仓库根命中");
|
|
235
|
-
// 子目录也要命中——真实会话的 cwd 常常是 D:/repo/src 这种
|
|
236
|
-
const sub = q.repoByCwd(join(root, "src"));
|
|
237
|
-
assert.equal(sub.id, exact.id, "子目录按祖先前缀命中同一个仓库");
|
|
238
|
-
assert.equal(q.repoByCwd(join(root, "no-such-dir")), null, "cwd 不存在 → null(不是抛错,注入在热路径上)");
|
|
239
|
-
assert.equal(q.repoByCwd(""), null);
|
|
240
|
-
assert.equal(q.repoByCwd(null), null);
|
|
241
|
-
// 嵌套仓库:外层先登记,内层后登记 → 内层会话必须被内层抢走(最长前缀)
|
|
242
|
-
nested = makeFixture();
|
|
243
|
-
runChoroT0({ repoRoot: nested, store, apply: true, now: FIXED_NOW });
|
|
244
|
-
const inner = q.repoByCwd(join(nested, "src"));
|
|
245
|
-
assert.equal(inner.root_path, nested, "嵌套时取最深那层,否则内层会话被外层仓库抢走");
|
|
246
|
-
assert.equal(q.repoByCwd(root).root_path, root, "外层 cwd 仍落外层");
|
|
247
|
-
} finally {
|
|
248
|
-
store.close();
|
|
249
|
-
if (nested) rmTempBestEffort(nested);
|
|
250
|
-
rmTempBestEffort(root);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("outline:模块名 + 文件数 + 页状态 + 页相对路径,不读正文", () => {
|
|
255
|
-
const { root, store, q } = setup();
|
|
256
|
-
try {
|
|
257
|
-
const repo = q.resolveRepo(undefined);
|
|
258
|
-
const out = q.outline(repo.id);
|
|
259
|
-
assert.equal(out.repo.id, repo.id);
|
|
260
|
-
assert.equal(out.repo.name, "qfix");
|
|
261
|
-
assert.equal(out.modules.length, 2);
|
|
262
|
-
const srcMod = out.modules.find((m) => m.slug === "src");
|
|
263
|
-
assert.equal(srcMod.status, "skeleton");
|
|
264
|
-
assert.equal(srcMod.relPath, ".chorography/modules/src.md");
|
|
265
|
-
assert.ok(srcMod.fileCount >= 3);
|
|
266
|
-
assert.equal(srcMod.parentSlug, null);
|
|
267
|
-
assert.ok(out.generatedAt, "生成时间由页行的 generated_at 汇总");
|
|
268
|
-
// 「只注标题级」的机制锁:提纲里不许出现页正文片段(骨架页的节标题/说明行)
|
|
269
|
-
const serialized = JSON.stringify(out);
|
|
270
|
-
assert.equal(serialized.includes("## 文件清单"), false, "提纲不含正文节标题");
|
|
271
|
-
assert.equal(serialized.includes("确定性骨架"), false, "提纲不含页内说明行");
|
|
272
|
-
assert.equal(q.outline(9999), null, "未知 repo → null");
|
|
273
|
-
} finally {
|
|
274
|
-
store.close();
|
|
275
|
-
rmTempBestEffort(root);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
// --- claim 粒度失效(Grounded Claims):DB 索引 × git diff 的对账 --------------
|
|
279
|
-
|
|
280
|
-
test("claimDrift:基线 == HEAD 说 clean:true,改了锚点所在行 → 只有那页那条进 atRisk", () => {
|
|
281
|
-
const { root, store, q } = setup();
|
|
282
|
-
try {
|
|
283
|
-
const repo = q.resolveRepo(undefined);
|
|
284
|
-
const page = store.choroPageBySlug(repo.id, "src");
|
|
285
|
-
store.saveChoroPageClaims(page.id, [
|
|
286
|
-
{ statement: "a 依赖 b", kind: "architecture", sourcePath: "src/a.js", startLine: 3, endLine: 3, createdAt: FIXED_NOW() },
|
|
287
|
-
{ statement: "b2 只是常量", kind: "architecture", sourcePath: "src/b2.js", startLine: 1, endLine: 1, createdAt: FIXED_NOW() },
|
|
288
|
-
]);
|
|
289
|
-
|
|
290
|
-
const same = q.claimDrift(repo.id);
|
|
291
|
-
assert.equal(same.clean, true, "基线就是 HEAD:确实没变,可以说没事");
|
|
292
|
-
assert.equal(same.anchorStale, false);
|
|
293
|
-
assert.equal(same.totals.claims, 2);
|
|
294
|
-
assert.equal(same.totals.atRiskClaims, 0);
|
|
295
|
-
|
|
296
|
-
// 改掉 a.js 第 3 行(claim 的锚点)并提交 → 基线落后 HEAD。
|
|
297
|
-
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nimport h from "left-pad";\nexport const a = () => b() + 1;\n', "utf8");
|
|
298
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-am", "change a", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
299
|
-
|
|
300
|
-
const drift = q.claimDrift(repo.id);
|
|
301
|
-
assert.equal(drift.clean, false);
|
|
302
|
-
assert.notEqual(drift.baseline, drift.head, "基线落后了");
|
|
303
|
-
assert.equal(drift.totals.claims, 2);
|
|
304
|
-
assert.equal(drift.totals.atRiskClaims, 1, "只有锚在 a.js 第 3 行那条受影响");
|
|
305
|
-
const pageRisk = drift.pages.find((p) => p.slug === "src");
|
|
306
|
-
assert.equal(pageRisk.atRisk.length, 1);
|
|
307
|
-
assert.equal(pageRisk.atRisk[0].reason, "touched");
|
|
308
|
-
assert.equal(pageRisk.atRisk[0].sourcePath, "src/a.js");
|
|
309
|
-
// 排序契约:有风险的页排最前(面板直接顺着读,不再排一遍)。
|
|
310
|
-
assert.equal(drift.pages[0].slug, "src");
|
|
311
|
-
} finally {
|
|
312
|
-
store.close();
|
|
313
|
-
rmTempBestEffort(root);
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("claimDrift:基线认不出时说 anchorStale,不说「都还准」也不说「全失效」", () => {
|
|
318
|
-
const { root, store, q } = setup();
|
|
319
|
-
try {
|
|
320
|
-
const repo = q.resolveRepo(undefined);
|
|
321
|
-
// force-push / 浅克隆的形态:基线 sha 合法但对象不在库里的任何一棵树上。
|
|
322
|
-
store.db.prepare("UPDATE choro_repos SET git_baseline = ? WHERE id = ?").run("0123456789abcdef0123456789abcdef01234567", repo.id);
|
|
323
|
-
const drift = q.claimDrift(repo.id);
|
|
324
|
-
assert.equal(drift.clean, false);
|
|
325
|
-
assert.equal(drift.anchorStale, true);
|
|
326
|
-
assert.equal(drift.reason, "diff-failed");
|
|
327
|
-
assert.equal(drift.totals.atRiskClaims, 0, "算不出来不等于有风险——一条都不许判");
|
|
328
|
-
} finally {
|
|
329
|
-
store.close();
|
|
330
|
-
rmTempBestEffort(root);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test("claimDrift:验证过却没记下基线(verified_at 有、verified_baseline 空)→ 判不了,不回退全仓基线", () => {
|
|
335
|
-
const { root, store, q } = setup();
|
|
336
|
-
try {
|
|
337
|
-
const repo = q.resolveRepo(undefined);
|
|
338
|
-
const page = store.choroPageBySlug(repo.id, "src");
|
|
339
|
-
store.saveChoroPageClaims(page.id, [
|
|
340
|
-
{ statement: "a 依赖 b", kind: "architecture", sourcePath: "src/a.js", startLine: 3, endLine: 3, createdAt: FIXED_NOW() },
|
|
341
|
-
]);
|
|
342
|
-
// 后半段的验证没留下凭据:T1 写了 verified_at,却没写 verified_baseline。
|
|
343
|
-
store.db.prepare("UPDATE choro_pages SET verified_at = ?, verified_baseline = NULL WHERE id = ?").run(FIXED_NOW(), page.id);
|
|
344
|
-
|
|
345
|
-
const drift = q.claimDrift(repo.id);
|
|
346
|
-
const p = drift.pages.find((x) => x.slug === "src");
|
|
347
|
-
// 退到全仓基线会把「富化之后才发生」的改动当成「富化之前就有」而放过——漏报。
|
|
348
|
-
// 不知道起点就不装作知道:diffBaseline 必须为 null,且这一页判 baselineUnknown。
|
|
349
|
-
assert.equal(p.diffBaseline, null, "没有验证点就不许回退全仓基线");
|
|
350
|
-
assert.equal(p.baselineApprox, false, "这不是「老数据退而求其次」,别标成约等于");
|
|
351
|
-
assert.equal(p.baselineUnknown, true);
|
|
352
|
-
assert.equal(drift.totals.baselineUnknownPages, 1);
|
|
353
|
-
// 基线 == HEAD 时 clean 仍可为真:一条提交都没发生过,锚点不可能被碰到,
|
|
354
|
-
// 「验证点没记下」在这里不影响结论(影响的是「变了之后能不能判」)。
|
|
355
|
-
assert.equal(drift.clean, true, "没变就是没变,不必因为起点没记下而说不知道");
|
|
356
|
-
|
|
357
|
-
// 有提交之后就不一样了:起点认不出的页不许被那些提交的改动「顺带放过」。
|
|
358
|
-
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nexport const a = () => b() + 1;\n', "utf8");
|
|
359
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-am", "change a", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
360
|
-
const after = q.claimDrift(repo.id);
|
|
361
|
-
assert.equal(after.clean, false, "有判不了的页,就不能说干净");
|
|
362
|
-
assert.equal(after.totals.baselineUnknownPages, 1);
|
|
363
|
-
assert.equal(after.totals.atRiskClaims, 0, "判不了不等于有风险,一条都不许判");
|
|
364
|
-
} finally {
|
|
365
|
-
store.close();
|
|
366
|
-
rmTempBestEffort(root);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test("claimDrift:repoAtHead 也不能掩盖 inconsistentPages(有语义段却没 claims 的页)", () => {
|
|
371
|
-
const { root, store, q } = setup();
|
|
372
|
-
try {
|
|
373
|
-
const repo = q.resolveRepo(undefined);
|
|
374
|
-
// 造一次「T1 落盘不完整」:页里有语义段,库里一条 claims 都没有。
|
|
375
|
-
// 仓库基线仍等于 HEAD,旧实现在这里会短路成 clean:true——把残局说成没事。
|
|
376
|
-
const pagePath = join(root, ".chorography", "modules", "src.md");
|
|
377
|
-
writeFileSync(pagePath, readFileSync(pagePath, "utf8") + "\n<!-- CHORO:T1:START -->\n## 架构\n\na.js 是入口。\n<!-- CHORO:T1:END -->\n", "utf8");
|
|
378
|
-
|
|
379
|
-
const drift = q.claimDrift(repo.id);
|
|
380
|
-
assert.equal(drift.baseline, drift.head, "前提:仓库基线就是 HEAD(走短路分支)");
|
|
381
|
-
assert.equal(drift.totals.inconsistentPages, 1, "有语义段、零 claims 的页要被数出来");
|
|
382
|
-
assert.equal(drift.totals.atRiskClaims, 0, "它一条结论都没有,谈不上有风险");
|
|
383
|
-
assert.equal(drift.clean, false, "残局不许被说成「全部干净」");
|
|
384
|
-
} finally {
|
|
385
|
-
store.close();
|
|
386
|
-
rmTempBestEffort(root);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test("claimDrift:未知仓库返回 null(调用方据此 404,不是空清单)", () => {
|
|
391
|
-
const { root, store, q } = setup();
|
|
392
|
-
try {
|
|
393
|
-
assert.equal(q.claimDrift(999999), null);
|
|
394
|
-
} finally {
|
|
395
|
-
store.close();
|
|
396
|
-
rmTempBestEffort(root);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
1
|
+
// queries.js 的回归锁(M1 读侧)。全部用例走真实 T0(fixture 仓库 apply)建数据,
|
|
2
|
+
// 不手插 DB 行——读侧的契约是「DB 索引 × 磁盘真源」的对账,手插行测不出盘上缺页
|
|
3
|
+
// 这类真实故障。锁的点:slug 白名单解析(查不到返回 null,绝不拿用户输入拼路径)、
|
|
4
|
+
// 盘上缺页如实报 page-missing、检索分节与行号、repo 解析的歧义处理。
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, unlinkSync, readFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { runChoroT0 } from "../src/wiki/t0.js";
|
|
12
|
+
import { createStore } from "../src/store.js";
|
|
13
|
+
import { createChoroQueries } from "../src/wiki/queries.js";
|
|
14
|
+
|
|
15
|
+
const FIXED_NOW = () => "2026-11-05T00:00:00.000Z";
|
|
16
|
+
|
|
17
|
+
function makeFixture() {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), "choro-queries-"));
|
|
19
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "qfix", scripts: { test: "node --test" } }, null, 2) + "\n", "utf8");
|
|
20
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
21
|
+
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nimport h from "left-pad";\nexport const a = () => b();\n', "utf8");
|
|
22
|
+
writeFileSync(join(root, "src", "b.js"), 'import { b2 } from "./b2.js";\nexport const b = () => b2();\n', "utf8");
|
|
23
|
+
writeFileSync(join(root, "src", "b2.js"), "export const b2 = () => 1;\n", "utf8");
|
|
24
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "pipe" });
|
|
25
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
26
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "init", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rmTempBestEffort(dir) {
|
|
31
|
+
for (let i = 0; i < 5; i++) {
|
|
32
|
+
try {
|
|
33
|
+
rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
return;
|
|
35
|
+
} catch {
|
|
36
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.warn(`[wiki-queries.test] 临时目录清理未完成:${dir}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setup() {
|
|
43
|
+
const root = makeFixture();
|
|
44
|
+
const store = createStore(":memory:");
|
|
45
|
+
runChoroT0({ repoRoot: root, store, apply: true, now: FIXED_NOW });
|
|
46
|
+
return { root, store, q: createChoroQueries({ store }) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// stub embedder:文本含 "src" → [1,0],否则 [0,1](固定二维,可断言方向与失配)
|
|
50
|
+
function stubEmbedder(model = "stub-1") {
|
|
51
|
+
return {
|
|
52
|
+
model,
|
|
53
|
+
embedSingle: async (text) => (/src/.test(text) ? [1, 0] : [0, 1]),
|
|
54
|
+
embed: async (text) => (/src/.test(text) ? [1, 0] : [0, 1]),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- 收尾档 P37–P39:向量惰性物化与检索 -------------------------------------
|
|
59
|
+
|
|
60
|
+
test("embedPageVectors:首次全嵌,generated_at 没变就跳过,变了重嵌(staleness 两维之一)", async () => {
|
|
61
|
+
const { root, store, q } = setup();
|
|
62
|
+
try {
|
|
63
|
+
const repo = q.resolveRepo(undefined);
|
|
64
|
+
const emb = stubEmbedder();
|
|
65
|
+
const r1 = await q.embedPageVectors(repo.id, emb);
|
|
66
|
+
assert.equal(r1.embedded, 2, "两页全嵌");
|
|
67
|
+
const r2 = await q.embedPageVectors(repo.id, emb);
|
|
68
|
+
assert.equal(r2.embedded, 0);
|
|
69
|
+
assert.equal(r2.skipped, 2, "generated_at 没变 → 全跳过");
|
|
70
|
+
// 页面内容变了(generated_at 推进)→ 只重嵌变的那页
|
|
71
|
+
const pagePath = join(root, ".chorography", "modules", "src.md");
|
|
72
|
+
const before = readFileSync(pagePath, "utf8");
|
|
73
|
+
writeFileSync(pagePath, `${before}<!-- CHORO:HUMAN:START -->\n人工补充\n<!-- CHORO:HUMAN:END -->\n`, "utf8");
|
|
74
|
+
store.db.prepare(
|
|
75
|
+
"UPDATE choro_pages SET generated_at = ? WHERE generated_at IS NOT NULL AND id IN (SELECT p.id FROM choro_pages p JOIN choro_modules m ON p.module_id = m.id WHERE m.slug = 'src')"
|
|
76
|
+
).run("2026-11-06T00:00:00.000Z");
|
|
77
|
+
const r3 = await q.embedPageVectors(repo.id, emb);
|
|
78
|
+
assert.equal(r3.embedded, 1, "只有 src 页重嵌");
|
|
79
|
+
assert.equal(r3.stale, 1, "旧向量按 stale 记账");
|
|
80
|
+
} finally {
|
|
81
|
+
store.close();
|
|
82
|
+
rmTempBestEffort(root);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("dim 失配(换 embedder 模型)→ 惰性全量重嵌(P38:不锁死,代价只是几十页)", async () => {
|
|
87
|
+
const { root, store, q } = setup();
|
|
88
|
+
try {
|
|
89
|
+
const repo = q.resolveRepo(undefined);
|
|
90
|
+
await q.embedPageVectors(repo.id, stubEmbedder("stub-1"));
|
|
91
|
+
// 换模型:维度从 2 → 3。expectDim 来自查询向量(路由先嵌查询再物化)
|
|
92
|
+
const emb3 = {
|
|
93
|
+
model: "stub-2",
|
|
94
|
+
embedSingle: async () => [1, 0, 0],
|
|
95
|
+
embed: async () => [1, 0, 0],
|
|
96
|
+
};
|
|
97
|
+
const r = await q.embedPageVectors(repo.id, emb3, { expectDim: 3 });
|
|
98
|
+
assert.equal(r.embedded, 2, "维度变了 → 全部重嵌(fresh 检查被 dimDrift 压过)");
|
|
99
|
+
assert.equal(r.dimMismatch, true, "失配被记账");
|
|
100
|
+
const rows = store.getChoroPageVectors(repo.id);
|
|
101
|
+
assert.ok(rows.every((row) => row.dim === 3 && row.model === "stub-2"), "旧维度行不复存");
|
|
102
|
+
// 不给 expectDim(无查询场景)→ fresh 行照旧跳过
|
|
103
|
+
const r2 = await q.embedPageVectors(repo.id, emb3);
|
|
104
|
+
assert.equal(r2.embedded, 0);
|
|
105
|
+
assert.equal(r2.skipped, 2);
|
|
106
|
+
} finally {
|
|
107
|
+
store.close();
|
|
108
|
+
rmTempBestEffort(root);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("searchSemantic:cosine 排序 + dim 失配行被过滤 + embedder 缺席返回 null", async () => {
|
|
113
|
+
const { root, store, q } = setup();
|
|
114
|
+
try {
|
|
115
|
+
const repo = q.resolveRepo(undefined);
|
|
116
|
+
await q.embedPageVectors(repo.id, stubEmbedder("stub-1"));
|
|
117
|
+
const hits = await q.searchSemantic(repo.id, stubEmbedder("stub-1"), "src 相关的页");
|
|
118
|
+
assert.ok(Array.isArray(hits) && hits.length >= 1);
|
|
119
|
+
assert.equal(hits[0].slug, "src", "查询向量 [1,0] 与 src 页同向 → 排前");
|
|
120
|
+
// 查询向量三维:与所有行 dim 失配 → 无可比行,返回空数组而不是抛
|
|
121
|
+
const dim3 = { embedSingle: async () => [1, 0, 0], embed: async () => [1, 0, 0] };
|
|
122
|
+
const empty = await q.searchSemantic(repo.id, dim3, "x");
|
|
123
|
+
assert.deepEqual(empty, []);
|
|
124
|
+
assert.equal(await q.searchSemantic(repo.id, null, "x"), null);
|
|
125
|
+
assert.deepEqual(await q.searchSemantic(repo.id, stubEmbedder(), " "), [], "空查询不嵌入、返回空数组");
|
|
126
|
+
} finally {
|
|
127
|
+
store.close();
|
|
128
|
+
rmTempBestEffort(root);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("listRepos / resolveRepo:单仓自动收敛,多仓歧义要显式给键", () => {
|
|
133
|
+
const { root, store, q } = setup();
|
|
134
|
+
try {
|
|
135
|
+
const repos = q.listRepos();
|
|
136
|
+
assert.equal(repos.length, 1);
|
|
137
|
+
assert.equal(repos[0].module_count, 2, "src 与 root 两个模块(1 条跨模块边不触发合并)");
|
|
138
|
+
assert.equal(repos[0].page_count, 2);
|
|
139
|
+
const auto = q.resolveRepo(undefined);
|
|
140
|
+
assert.equal(auto.id, repos[0].id, "只有一个仓库时不带键也能解析");
|
|
141
|
+
assert.ok(q.resolveRepo("qfix").id, "按显示名解析");
|
|
142
|
+
assert.equal(q.resolveRepo("no-such-repo"), null);
|
|
143
|
+
// 造第二个仓库让解析变歧义
|
|
144
|
+
const root2 = makeFixture();
|
|
145
|
+
try {
|
|
146
|
+
runChoroT0({ repoRoot: root2, store, apply: true, now: FIXED_NOW });
|
|
147
|
+
const amb = q.resolveRepo(undefined);
|
|
148
|
+
assert.ok(amb.ambiguous, "多仓无键 → 歧义清单而不是猜一个");
|
|
149
|
+
assert.equal(amb.ambiguous.length, 2);
|
|
150
|
+
} finally {
|
|
151
|
+
rmTempBestEffort(root2);
|
|
152
|
+
}
|
|
153
|
+
} finally {
|
|
154
|
+
store.close();
|
|
155
|
+
rmTempBestEffort(root);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("moduleMap:模块、页面状态与跨模块边同源返回", () => {
|
|
160
|
+
const { root, store, q } = setup();
|
|
161
|
+
try {
|
|
162
|
+
const repo = q.resolveRepo(undefined);
|
|
163
|
+
const { modules, edges } = q.moduleMap(repo.id);
|
|
164
|
+
const slugs = modules.map((m) => m.slug).sort();
|
|
165
|
+
assert.deepEqual(slugs, ["root", "src"]);
|
|
166
|
+
const src = modules.find((m) => m.slug === "src");
|
|
167
|
+
assert.equal(src.page_status, "skeleton");
|
|
168
|
+
// 面板从地志页导入知识点时读的是 mod.relPath(契约 §3.6);SQL 行里只有
|
|
169
|
+
// page_rel_path,缺这层映射会让「选完模块点导入」永远提示请先选模块(#607)。
|
|
170
|
+
// 同时锁住原 snake_case 字段还在——choro_module_map 的 render 与 CLI 都在用。
|
|
171
|
+
assert.equal(typeof src.relPath, "string", "relPath 必须与 page_rel_path 同值");
|
|
172
|
+
assert.equal(src.relPath, src.page_rel_path);
|
|
173
|
+
assert.ok(src.relPath.endsWith(".md"), "页相对路径是磁盘上真实存在的 .md");
|
|
174
|
+
assert.equal(typeof src.file_count, "number", "file_count 原字段必须保留(choro_module_map 的 render 在吃)");
|
|
175
|
+
// fixture 的引用全在 src 模块内部(a→b→b2),left-pad 是外部依赖,root 只有
|
|
176
|
+
// package.json 没有代码——跨模块边应为空集。跨模块聚erge本身由 wiki-store 的
|
|
177
|
+
// 整仓替换用例与 t0 的 moduleEdges 排序覆盖。
|
|
178
|
+
assert.deepEqual(edges, []);
|
|
179
|
+
} finally {
|
|
180
|
+
store.close();
|
|
181
|
+
rmTempBestEffort(root);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("readPage:正文来自磁盘真源;盘上缺页如实报 page-missing;未知 slug 返回 null", () => {
|
|
186
|
+
const { root, store, q } = setup();
|
|
187
|
+
try {
|
|
188
|
+
const repo = q.resolveRepo(undefined);
|
|
189
|
+
const page = q.readPage(repo.id, "src");
|
|
190
|
+
assert.equal(page.slug, "src");
|
|
191
|
+
assert.ok(page.content.includes("## 文件清单"), "正文是骨架页");
|
|
192
|
+
assert.equal(page.content.split("\n").length > 10, true);
|
|
193
|
+
assert.equal(q.readPage(repo.id, "../../etc/passwd"), null, "未知 slug 一律 null(路径零用户输入)");
|
|
194
|
+
// 盘上删页:索引还在、读侧如实报缺
|
|
195
|
+
unlinkSync(join(root, ".chorography", "modules", "src.md"));
|
|
196
|
+
const missing = q.readPage(repo.id, "src");
|
|
197
|
+
assert.equal(missing.error, "page-missing");
|
|
198
|
+
} finally {
|
|
199
|
+
store.close();
|
|
200
|
+
rmTempBestEffort(root);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("search:按节返回命中与行号,空 q 返回空数组,module 过滤生效", () => {
|
|
205
|
+
const { root, store, q } = setup();
|
|
206
|
+
try {
|
|
207
|
+
const repo = q.resolveRepo(undefined);
|
|
208
|
+
// 骨架页里必有「文件清单」节标题与 b2.js 文件名
|
|
209
|
+
const hits = q.search(repo.id, "文件清单");
|
|
210
|
+
assert.ok(hits.length >= 2, "两页都有文件清单节");
|
|
211
|
+
assert.ok(hits.every((h) => h.line >= 1 && typeof h.section === "string"));
|
|
212
|
+
const byFile = q.search(repo.id, "b2.js");
|
|
213
|
+
// 命中两处是契约而非巧合:frontmatter 的 sourceFiles(节=页首)+ 正文「文件
|
|
214
|
+
// 清单」表。分节让调用方能区分这两层。
|
|
215
|
+
assert.equal(byFile.length, 2);
|
|
216
|
+
assert.ok(byFile.every((h) => h.slug === "src"), "b2.js 只属于 src 页");
|
|
217
|
+
assert.equal(byFile.filter((h) => h.section.startsWith("文件清单")).length, 1, "节名带数量后缀(文件清单(N 个)),按前缀匹配");
|
|
218
|
+
assert.deepEqual(q.search(repo.id, " "), [], "空 q 不报错返回空");
|
|
219
|
+
const inRoot = q.search(repo.id, "package.json", { moduleSlug: "root" });
|
|
220
|
+
assert.equal(inRoot.length, 2, "与 b2.js 同款两层命中:frontmatter(页首)+ 文件清单表");
|
|
221
|
+
assert.equal(inRoot[0].slug, "root");
|
|
222
|
+
assert.deepEqual(q.search(repo.id, "package.json", { moduleSlug: "src" }), [], "模块过滤真生效");
|
|
223
|
+
} finally {
|
|
224
|
+
store.close();
|
|
225
|
+
rmTempBestEffort(root);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
test("repoByCwd:祖先前缀命中、最长 root 胜出、cwd 不存在返回 null", () => {
|
|
229
|
+
const { root, store, q } = setup();
|
|
230
|
+
let nested = null;
|
|
231
|
+
try {
|
|
232
|
+
// 精确根目录
|
|
233
|
+
const exact = q.repoByCwd(root);
|
|
234
|
+
assert.ok(exact, "仓库根命中");
|
|
235
|
+
// 子目录也要命中——真实会话的 cwd 常常是 D:/repo/src 这种
|
|
236
|
+
const sub = q.repoByCwd(join(root, "src"));
|
|
237
|
+
assert.equal(sub.id, exact.id, "子目录按祖先前缀命中同一个仓库");
|
|
238
|
+
assert.equal(q.repoByCwd(join(root, "no-such-dir")), null, "cwd 不存在 → null(不是抛错,注入在热路径上)");
|
|
239
|
+
assert.equal(q.repoByCwd(""), null);
|
|
240
|
+
assert.equal(q.repoByCwd(null), null);
|
|
241
|
+
// 嵌套仓库:外层先登记,内层后登记 → 内层会话必须被内层抢走(最长前缀)
|
|
242
|
+
nested = makeFixture();
|
|
243
|
+
runChoroT0({ repoRoot: nested, store, apply: true, now: FIXED_NOW });
|
|
244
|
+
const inner = q.repoByCwd(join(nested, "src"));
|
|
245
|
+
assert.equal(inner.root_path, nested, "嵌套时取最深那层,否则内层会话被外层仓库抢走");
|
|
246
|
+
assert.equal(q.repoByCwd(root).root_path, root, "外层 cwd 仍落外层");
|
|
247
|
+
} finally {
|
|
248
|
+
store.close();
|
|
249
|
+
if (nested) rmTempBestEffort(nested);
|
|
250
|
+
rmTempBestEffort(root);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("outline:模块名 + 文件数 + 页状态 + 页相对路径,不读正文", () => {
|
|
255
|
+
const { root, store, q } = setup();
|
|
256
|
+
try {
|
|
257
|
+
const repo = q.resolveRepo(undefined);
|
|
258
|
+
const out = q.outline(repo.id);
|
|
259
|
+
assert.equal(out.repo.id, repo.id);
|
|
260
|
+
assert.equal(out.repo.name, "qfix");
|
|
261
|
+
assert.equal(out.modules.length, 2);
|
|
262
|
+
const srcMod = out.modules.find((m) => m.slug === "src");
|
|
263
|
+
assert.equal(srcMod.status, "skeleton");
|
|
264
|
+
assert.equal(srcMod.relPath, ".chorography/modules/src.md");
|
|
265
|
+
assert.ok(srcMod.fileCount >= 3);
|
|
266
|
+
assert.equal(srcMod.parentSlug, null);
|
|
267
|
+
assert.ok(out.generatedAt, "生成时间由页行的 generated_at 汇总");
|
|
268
|
+
// 「只注标题级」的机制锁:提纲里不许出现页正文片段(骨架页的节标题/说明行)
|
|
269
|
+
const serialized = JSON.stringify(out);
|
|
270
|
+
assert.equal(serialized.includes("## 文件清单"), false, "提纲不含正文节标题");
|
|
271
|
+
assert.equal(serialized.includes("确定性骨架"), false, "提纲不含页内说明行");
|
|
272
|
+
assert.equal(q.outline(9999), null, "未知 repo → null");
|
|
273
|
+
} finally {
|
|
274
|
+
store.close();
|
|
275
|
+
rmTempBestEffort(root);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
// --- claim 粒度失效(Grounded Claims):DB 索引 × git diff 的对账 --------------
|
|
279
|
+
|
|
280
|
+
test("claimDrift:基线 == HEAD 说 clean:true,改了锚点所在行 → 只有那页那条进 atRisk", () => {
|
|
281
|
+
const { root, store, q } = setup();
|
|
282
|
+
try {
|
|
283
|
+
const repo = q.resolveRepo(undefined);
|
|
284
|
+
const page = store.choroPageBySlug(repo.id, "src");
|
|
285
|
+
store.saveChoroPageClaims(page.id, [
|
|
286
|
+
{ statement: "a 依赖 b", kind: "architecture", sourcePath: "src/a.js", startLine: 3, endLine: 3, createdAt: FIXED_NOW() },
|
|
287
|
+
{ statement: "b2 只是常量", kind: "architecture", sourcePath: "src/b2.js", startLine: 1, endLine: 1, createdAt: FIXED_NOW() },
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
const same = q.claimDrift(repo.id);
|
|
291
|
+
assert.equal(same.clean, true, "基线就是 HEAD:确实没变,可以说没事");
|
|
292
|
+
assert.equal(same.anchorStale, false);
|
|
293
|
+
assert.equal(same.totals.claims, 2);
|
|
294
|
+
assert.equal(same.totals.atRiskClaims, 0);
|
|
295
|
+
|
|
296
|
+
// 改掉 a.js 第 3 行(claim 的锚点)并提交 → 基线落后 HEAD。
|
|
297
|
+
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nimport h from "left-pad";\nexport const a = () => b() + 1;\n', "utf8");
|
|
298
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-am", "change a", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
299
|
+
|
|
300
|
+
const drift = q.claimDrift(repo.id);
|
|
301
|
+
assert.equal(drift.clean, false);
|
|
302
|
+
assert.notEqual(drift.baseline, drift.head, "基线落后了");
|
|
303
|
+
assert.equal(drift.totals.claims, 2);
|
|
304
|
+
assert.equal(drift.totals.atRiskClaims, 1, "只有锚在 a.js 第 3 行那条受影响");
|
|
305
|
+
const pageRisk = drift.pages.find((p) => p.slug === "src");
|
|
306
|
+
assert.equal(pageRisk.atRisk.length, 1);
|
|
307
|
+
assert.equal(pageRisk.atRisk[0].reason, "touched");
|
|
308
|
+
assert.equal(pageRisk.atRisk[0].sourcePath, "src/a.js");
|
|
309
|
+
// 排序契约:有风险的页排最前(面板直接顺着读,不再排一遍)。
|
|
310
|
+
assert.equal(drift.pages[0].slug, "src");
|
|
311
|
+
} finally {
|
|
312
|
+
store.close();
|
|
313
|
+
rmTempBestEffort(root);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("claimDrift:基线认不出时说 anchorStale,不说「都还准」也不说「全失效」", () => {
|
|
318
|
+
const { root, store, q } = setup();
|
|
319
|
+
try {
|
|
320
|
+
const repo = q.resolveRepo(undefined);
|
|
321
|
+
// force-push / 浅克隆的形态:基线 sha 合法但对象不在库里的任何一棵树上。
|
|
322
|
+
store.db.prepare("UPDATE choro_repos SET git_baseline = ? WHERE id = ?").run("0123456789abcdef0123456789abcdef01234567", repo.id);
|
|
323
|
+
const drift = q.claimDrift(repo.id);
|
|
324
|
+
assert.equal(drift.clean, false);
|
|
325
|
+
assert.equal(drift.anchorStale, true);
|
|
326
|
+
assert.equal(drift.reason, "diff-failed");
|
|
327
|
+
assert.equal(drift.totals.atRiskClaims, 0, "算不出来不等于有风险——一条都不许判");
|
|
328
|
+
} finally {
|
|
329
|
+
store.close();
|
|
330
|
+
rmTempBestEffort(root);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("claimDrift:验证过却没记下基线(verified_at 有、verified_baseline 空)→ 判不了,不回退全仓基线", () => {
|
|
335
|
+
const { root, store, q } = setup();
|
|
336
|
+
try {
|
|
337
|
+
const repo = q.resolveRepo(undefined);
|
|
338
|
+
const page = store.choroPageBySlug(repo.id, "src");
|
|
339
|
+
store.saveChoroPageClaims(page.id, [
|
|
340
|
+
{ statement: "a 依赖 b", kind: "architecture", sourcePath: "src/a.js", startLine: 3, endLine: 3, createdAt: FIXED_NOW() },
|
|
341
|
+
]);
|
|
342
|
+
// 后半段的验证没留下凭据:T1 写了 verified_at,却没写 verified_baseline。
|
|
343
|
+
store.db.prepare("UPDATE choro_pages SET verified_at = ?, verified_baseline = NULL WHERE id = ?").run(FIXED_NOW(), page.id);
|
|
344
|
+
|
|
345
|
+
const drift = q.claimDrift(repo.id);
|
|
346
|
+
const p = drift.pages.find((x) => x.slug === "src");
|
|
347
|
+
// 退到全仓基线会把「富化之后才发生」的改动当成「富化之前就有」而放过——漏报。
|
|
348
|
+
// 不知道起点就不装作知道:diffBaseline 必须为 null,且这一页判 baselineUnknown。
|
|
349
|
+
assert.equal(p.diffBaseline, null, "没有验证点就不许回退全仓基线");
|
|
350
|
+
assert.equal(p.baselineApprox, false, "这不是「老数据退而求其次」,别标成约等于");
|
|
351
|
+
assert.equal(p.baselineUnknown, true);
|
|
352
|
+
assert.equal(drift.totals.baselineUnknownPages, 1);
|
|
353
|
+
// 基线 == HEAD 时 clean 仍可为真:一条提交都没发生过,锚点不可能被碰到,
|
|
354
|
+
// 「验证点没记下」在这里不影响结论(影响的是「变了之后能不能判」)。
|
|
355
|
+
assert.equal(drift.clean, true, "没变就是没变,不必因为起点没记下而说不知道");
|
|
356
|
+
|
|
357
|
+
// 有提交之后就不一样了:起点认不出的页不许被那些提交的改动「顺带放过」。
|
|
358
|
+
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nexport const a = () => b() + 1;\n', "utf8");
|
|
359
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-am", "change a", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
360
|
+
const after = q.claimDrift(repo.id);
|
|
361
|
+
assert.equal(after.clean, false, "有判不了的页,就不能说干净");
|
|
362
|
+
assert.equal(after.totals.baselineUnknownPages, 1);
|
|
363
|
+
assert.equal(after.totals.atRiskClaims, 0, "判不了不等于有风险,一条都不许判");
|
|
364
|
+
} finally {
|
|
365
|
+
store.close();
|
|
366
|
+
rmTempBestEffort(root);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("claimDrift:repoAtHead 也不能掩盖 inconsistentPages(有语义段却没 claims 的页)", () => {
|
|
371
|
+
const { root, store, q } = setup();
|
|
372
|
+
try {
|
|
373
|
+
const repo = q.resolveRepo(undefined);
|
|
374
|
+
// 造一次「T1 落盘不完整」:页里有语义段,库里一条 claims 都没有。
|
|
375
|
+
// 仓库基线仍等于 HEAD,旧实现在这里会短路成 clean:true——把残局说成没事。
|
|
376
|
+
const pagePath = join(root, ".chorography", "modules", "src.md");
|
|
377
|
+
writeFileSync(pagePath, readFileSync(pagePath, "utf8") + "\n<!-- CHORO:T1:START -->\n## 架构\n\na.js 是入口。\n<!-- CHORO:T1:END -->\n", "utf8");
|
|
378
|
+
|
|
379
|
+
const drift = q.claimDrift(repo.id);
|
|
380
|
+
assert.equal(drift.baseline, drift.head, "前提:仓库基线就是 HEAD(走短路分支)");
|
|
381
|
+
assert.equal(drift.totals.inconsistentPages, 1, "有语义段、零 claims 的页要被数出来");
|
|
382
|
+
assert.equal(drift.totals.atRiskClaims, 0, "它一条结论都没有,谈不上有风险");
|
|
383
|
+
assert.equal(drift.clean, false, "残局不许被说成「全部干净」");
|
|
384
|
+
} finally {
|
|
385
|
+
store.close();
|
|
386
|
+
rmTempBestEffort(root);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("claimDrift:未知仓库返回 null(调用方据此 404,不是空清单)", () => {
|
|
391
|
+
const { root, store, q } = setup();
|
|
392
|
+
try {
|
|
393
|
+
assert.equal(q.claimDrift(999999), null);
|
|
394
|
+
} finally {
|
|
395
|
+
store.close();
|
|
396
|
+
rmTempBestEffort(root);
|
|
397
|
+
}
|
|
398
|
+
});
|