akm-cli 0.7.0-rc1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{src/cli.js → cli.js} +100 -16
- package/dist/{src/commands → commands}/config-cli.js +42 -0
- package/dist/{src/commands → commands}/history.js +78 -7
- package/dist/{src/commands → commands}/registry-search.js +69 -6
- package/dist/{src/commands → commands}/search.js +30 -3
- package/dist/{src/commands → commands}/show.js +29 -0
- package/dist/{src/commands → commands}/source-add.js +5 -1
- package/dist/{src/commands → commands}/source-manage.js +7 -1
- package/dist/{src/core → core}/config.js +28 -0
- package/dist/{src/indexer → indexer}/db-search.js +1 -0
- package/dist/{src/indexer → indexer}/indexer.js +16 -2
- package/dist/{src/indexer → indexer}/matchers.js +1 -1
- package/dist/{src/indexer → indexer}/search-source.js +4 -2
- package/dist/{src/integrations → integrations}/agent/profiles.js +1 -1
- package/dist/{src/integrations → integrations}/agent/spawn.js +67 -16
- package/dist/{src/integrations → integrations}/github.js +9 -3
- package/dist/{src/llm → llm}/embedders/remote.js +37 -3
- package/dist/{src/output → output}/cli-hints.js +15 -2
- package/dist/{src/output → output}/renderers.js +3 -1
- package/dist/{src/output → output}/shapes.js +8 -1
- package/dist/{src/output → output}/text.js +156 -3
- package/dist/{src/registry → registry}/build-index.js +5 -4
- package/dist/{src/registry → registry}/providers/static-index.js +3 -1
- package/dist/{src/setup → setup}/setup.js +9 -0
- package/dist/{src/wiki → wiki}/wiki.js +54 -6
- package/dist/{src/workflows → workflows}/runs.js +37 -3
- package/package.json +8 -8
- package/dist/tests/add-website-source.test.js +0 -119
- package/dist/tests/agent/agent-config-loader.test.js +0 -70
- package/dist/tests/agent/agent-config.test.js +0 -221
- package/dist/tests/agent/agent-detect.test.js +0 -100
- package/dist/tests/agent/agent-spawn.test.js +0 -234
- package/dist/tests/agent-output.test.js +0 -186
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +0 -103
- package/dist/tests/architecture/agent-spawn-seam.test.js +0 -193
- package/dist/tests/architecture/llm-stateless-seam.test.js +0 -112
- package/dist/tests/asset-ref.test.js +0 -192
- package/dist/tests/asset-registry.test.js +0 -103
- package/dist/tests/asset-spec.test.js +0 -241
- package/dist/tests/bench/attribution.test.js +0 -995
- package/dist/tests/bench/cleanup-sigint.test.js +0 -83
- package/dist/tests/bench/cleanup.js +0 -203
- package/dist/tests/bench/cleanup.test.js +0 -166
- package/dist/tests/bench/cli.js +0 -683
- package/dist/tests/bench/cli.test.js +0 -177
- package/dist/tests/bench/compare.test.js +0 -556
- package/dist/tests/bench/corpus.js +0 -314
- package/dist/tests/bench/corpus.test.js +0 -258
- package/dist/tests/bench/driver.js +0 -346
- package/dist/tests/bench/driver.test.js +0 -443
- package/dist/tests/bench/evolve-metrics.js +0 -179
- package/dist/tests/bench/evolve-metrics.test.js +0 -187
- package/dist/tests/bench/evolve.js +0 -580
- package/dist/tests/bench/evolve.test.js +0 -616
- package/dist/tests/bench/failure-modes.test.js +0 -300
- package/dist/tests/bench/feedback-integrity.test.js +0 -456
- package/dist/tests/bench/leakage.test.js +0 -125
- package/dist/tests/bench/learning-curve.test.js +0 -133
- package/dist/tests/bench/metrics.js +0 -2319
- package/dist/tests/bench/metrics.test.js +0 -1144
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +0 -43
- package/dist/tests/bench/report.js +0 -1821
- package/dist/tests/bench/report.test.js +0 -989
- package/dist/tests/bench/runner.js +0 -536
- package/dist/tests/bench/runner.test.js +0 -958
- package/dist/tests/bench/search-bridge.test.js +0 -331
- package/dist/tests/bench/tmp.js +0 -41
- package/dist/tests/bench/trajectory.js +0 -116
- package/dist/tests/bench/trajectory.test.js +0 -127
- package/dist/tests/bench/verifier.js +0 -109
- package/dist/tests/bench/verifier.test.js +0 -118
- package/dist/tests/bench/workflow-evaluator.js +0 -557
- package/dist/tests/bench/workflow-evaluator.test.js +0 -421
- package/dist/tests/bench/workflow-spec.js +0 -358
- package/dist/tests/bench/workflow-spec.test.js +0 -363
- package/dist/tests/bench/workflow-trace.js +0 -438
- package/dist/tests/bench/workflow-trace.test.js +0 -254
- package/dist/tests/benchmark-search-quality.js +0 -536
- package/dist/tests/benchmark-suite.js +0 -1441
- package/dist/tests/capture-cli.test.js +0 -112
- package/dist/tests/cli-errors.test.js +0 -203
- package/dist/tests/commands/events.test.js +0 -370
- package/dist/tests/commands/history.test.js +0 -223
- package/dist/tests/commands/import.test.js +0 -103
- package/dist/tests/commands/proposal-cli.test.js +0 -209
- package/dist/tests/commands/reflect-propose-cli.test.js +0 -333
- package/dist/tests/commands/remember.test.js +0 -97
- package/dist/tests/commands/scope-flags.test.js +0 -300
- package/dist/tests/commands/search.test.js +0 -537
- package/dist/tests/commands/show-indexer-parity.test.js +0 -117
- package/dist/tests/commands/show.test.js +0 -294
- package/dist/tests/common.test.js +0 -266
- package/dist/tests/completions.test.js +0 -142
- package/dist/tests/config-cli.test.js +0 -193
- package/dist/tests/config-llm-features.test.js +0 -139
- package/dist/tests/config.test.js +0 -544
- package/dist/tests/contracts/migration-baseline.test.js +0 -43
- package/dist/tests/contracts/reflect-propose-envelope.test.js +0 -139
- package/dist/tests/contracts/spec-helpers.js +0 -46
- package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +0 -228
- package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +0 -56
- package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +0 -34
- package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +0 -94
- package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +0 -39
- package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +0 -44
- package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +0 -47
- package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +0 -40
- package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +0 -58
- package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +0 -34
- package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +0 -75
- package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +0 -36
- package/dist/tests/core/write-source.test.js +0 -366
- package/dist/tests/curate-command.test.js +0 -87
- package/dist/tests/db-scoring.test.js +0 -201
- package/dist/tests/db.test.js +0 -654
- package/dist/tests/distill-cli-flag.test.js +0 -208
- package/dist/tests/distill.test.js +0 -515
- package/dist/tests/docker-install.test.js +0 -120
- package/dist/tests/e2e.test.js +0 -1398
- package/dist/tests/embedder.test.js +0 -340
- package/dist/tests/embedding-model-config.test.js +0 -379
- package/dist/tests/feedback-command.test.js +0 -172
- package/dist/tests/file-context.test.js +0 -552
- package/dist/tests/fixtures/scripts/git/summarize-diff.js +0 -9
- package/dist/tests/fixtures/scripts/lint/eslint-check.js +0 -7
- package/dist/tests/fixtures/stashes/load.js +0 -166
- package/dist/tests/fixtures/stashes/load.test.js +0 -88
- package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +0 -12
- package/dist/tests/frontmatter.test.js +0 -190
- package/dist/tests/fts-field-weighting.test.js +0 -254
- package/dist/tests/fuzzy-search.test.js +0 -230
- package/dist/tests/git-provider-clone.test.js +0 -45
- package/dist/tests/github.test.js +0 -161
- package/dist/tests/graph-boost-ranking.test.js +0 -305
- package/dist/tests/graph-extraction.test.js +0 -282
- package/dist/tests/helpers/usage-events.js +0 -8
- package/dist/tests/index-pass-llm.test.js +0 -161
- package/dist/tests/indexer.test.js +0 -559
- package/dist/tests/info-command.test.js +0 -166
- package/dist/tests/init.test.js +0 -69
- package/dist/tests/install-script.test.js +0 -246
- package/dist/tests/integration/agent-real-profile.test.js +0 -94
- package/dist/tests/issue-36-repro.test.js +0 -304
- package/dist/tests/issues-191-194.test.js +0 -160
- package/dist/tests/lesson-lint.test.js +0 -111
- package/dist/tests/llm-client.test.js +0 -115
- package/dist/tests/llm-feature-gate.test.js +0 -151
- package/dist/tests/llm.test.js +0 -139
- package/dist/tests/lockfile.test.js +0 -216
- package/dist/tests/manifest.test.js +0 -205
- package/dist/tests/markdown.test.js +0 -126
- package/dist/tests/matchers-unit.test.js +0 -189
- package/dist/tests/memory-inference.test.js +0 -299
- package/dist/tests/merge-scoring.test.js +0 -136
- package/dist/tests/metadata.test.js +0 -313
- package/dist/tests/migration-help.test.js +0 -89
- package/dist/tests/origin-resolve.test.js +0 -124
- package/dist/tests/output-baseline.test.js +0 -217
- package/dist/tests/output-shapes-unit.test.js +0 -476
- package/dist/tests/parallel-search.test.js +0 -272
- package/dist/tests/parameter-metadata.test.js +0 -365
- package/dist/tests/paths.test.js +0 -177
- package/dist/tests/progressive-disclosure.test.js +0 -280
- package/dist/tests/proposals.test.js +0 -279
- package/dist/tests/proposed-quality.test.js +0 -271
- package/dist/tests/provider-registry.test.js +0 -32
- package/dist/tests/ranking-regression.test.js +0 -548
- package/dist/tests/reflect-propose.test.js +0 -455
- package/dist/tests/registry-build-index.test.js +0 -378
- package/dist/tests/registry-cli.test.js +0 -290
- package/dist/tests/registry-index-v2.test.js +0 -430
- package/dist/tests/registry-install.test.js +0 -728
- package/dist/tests/registry-providers/parity.test.js +0 -189
- package/dist/tests/registry-providers/skills-sh.test.js +0 -309
- package/dist/tests/registry-providers/static-index.test.js +0 -204
- package/dist/tests/registry-resolve.test.js +0 -126
- package/dist/tests/registry-search.test.js +0 -723
- package/dist/tests/remember-frontmatter.test.js +0 -380
- package/dist/tests/remember-unit.test.js +0 -123
- package/dist/tests/ripgrep-install.test.js +0 -251
- package/dist/tests/ripgrep-resolve.test.js +0 -108
- package/dist/tests/ripgrep.test.js +0 -163
- package/dist/tests/save-command.test.js +0 -94
- package/dist/tests/save-trust-qa-fixes.test.js +0 -270
- package/dist/tests/scoring-pipeline.test.js +0 -648
- package/dist/tests/search-include-proposed-cli.test.js +0 -118
- package/dist/tests/self-update.test.js +0 -442
- package/dist/tests/semantic-search-e2e.test.js +0 -512
- package/dist/tests/semantic-status.test.js +0 -471
- package/dist/tests/setup-run.integration.js +0 -877
- package/dist/tests/setup-wizard.test.js +0 -198
- package/dist/tests/setup.test.js +0 -131
- package/dist/tests/source-add.test.js +0 -11
- package/dist/tests/source-clone.test.js +0 -254
- package/dist/tests/source-manage.test.js +0 -366
- package/dist/tests/source-providers/filesystem.test.js +0 -82
- package/dist/tests/source-providers/git.test.js +0 -252
- package/dist/tests/source-providers/website.test.js +0 -128
- package/dist/tests/source-qa-fixes.test.js +0 -268
- package/dist/tests/source-registry.test.js +0 -350
- package/dist/tests/source-resolve.test.js +0 -100
- package/dist/tests/source-source.test.js +0 -221
- package/dist/tests/source.test.js +0 -533
- package/dist/tests/tar-utils-scan.test.js +0 -73
- package/dist/tests/toggle-components.test.js +0 -73
- package/dist/tests/usage-telemetry.test.js +0 -265
- package/dist/tests/utility-scoring.test.js +0 -558
- package/dist/tests/vault-load-error.test.js +0 -78
- package/dist/tests/vault-qa-fixes.test.js +0 -194
- package/dist/tests/vault.test.js +0 -429
- package/dist/tests/vector-search.test.js +0 -608
- package/dist/tests/walker.test.js +0 -252
- package/dist/tests/wave2-cluster-bc.test.js +0 -228
- package/dist/tests/wave2-cluster-d.test.js +0 -180
- package/dist/tests/wave2-cluster-e.test.js +0 -179
- package/dist/tests/wiki-qa-fixes.test.js +0 -270
- package/dist/tests/wiki.test.js +0 -529
- package/dist/tests/workflow-cli.test.js +0 -271
- package/dist/tests/workflow-markdown.test.js +0 -171
- package/dist/tests/workflow-path-escape.test.js +0 -132
- package/dist/tests/workflow-qa-fixes.test.js +0 -377
- package/dist/tests/workflows/indexer-rejection.test.js +0 -213
- /package/dist/{src/commands → commands}/completions.js +0 -0
- /package/dist/{src/commands → commands}/curate.js +0 -0
- /package/dist/{src/commands → commands}/distill.js +0 -0
- /package/dist/{src/commands → commands}/events.js +0 -0
- /package/dist/{src/commands → commands}/info.js +0 -0
- /package/dist/{src/commands → commands}/init.js +0 -0
- /package/dist/{src/commands → commands}/install-audit.js +0 -0
- /package/dist/{src/commands → commands}/installed-stashes.js +0 -0
- /package/dist/{src/commands → commands}/migration-help.js +0 -0
- /package/dist/{src/commands → commands}/proposal.js +0 -0
- /package/dist/{src/commands → commands}/propose.js +0 -0
- /package/dist/{src/commands → commands}/reflect.js +0 -0
- /package/dist/{src/commands → commands}/remember.js +0 -0
- /package/dist/{src/commands → commands}/self-update.js +0 -0
- /package/dist/{src/commands → commands}/source-clone.js +0 -0
- /package/dist/{src/commands → commands}/vault.js +0 -0
- /package/dist/{src/core → core}/asset-ref.js +0 -0
- /package/dist/{src/core → core}/asset-registry.js +0 -0
- /package/dist/{src/core → core}/asset-spec.js +0 -0
- /package/dist/{src/core → core}/common.js +0 -0
- /package/dist/{src/core → core}/errors.js +0 -0
- /package/dist/{src/core → core}/events.js +0 -0
- /package/dist/{src/core → core}/frontmatter.js +0 -0
- /package/dist/{src/core → core}/lesson-lint.js +0 -0
- /package/dist/{src/core → core}/markdown.js +0 -0
- /package/dist/{src/core → core}/paths.js +0 -0
- /package/dist/{src/core → core}/proposals.js +0 -0
- /package/dist/{src/core → core}/warn.js +0 -0
- /package/dist/{src/core → core}/write-source.js +0 -0
- /package/dist/{src/indexer → indexer}/db.js +0 -0
- /package/dist/{src/indexer → indexer}/file-context.js +0 -0
- /package/dist/{src/indexer → indexer}/graph-boost.js +0 -0
- /package/dist/{src/indexer → indexer}/graph-extraction.js +0 -0
- /package/dist/{src/indexer → indexer}/manifest.js +0 -0
- /package/dist/{src/indexer → indexer}/memory-inference.js +0 -0
- /package/dist/{src/indexer → indexer}/metadata.js +0 -0
- /package/dist/{src/indexer → indexer}/search-fields.js +0 -0
- /package/dist/{src/indexer → indexer}/semantic-status.js +0 -0
- /package/dist/{src/indexer → indexer}/usage-events.js +0 -0
- /package/dist/{src/indexer → indexer}/walker.js +0 -0
- /package/dist/{src/integrations → integrations}/agent/config.js +0 -0
- /package/dist/{src/integrations → integrations}/agent/detect.js +0 -0
- /package/dist/{src/integrations → integrations}/agent/index.js +0 -0
- /package/dist/{src/integrations → integrations}/agent/prompts.js +0 -0
- /package/dist/{src/integrations → integrations}/lockfile.js +0 -0
- /package/dist/{src/llm → llm}/client.js +0 -0
- /package/dist/{src/llm → llm}/embedder.js +0 -0
- /package/dist/{src/llm → llm}/embedders/cache.js +0 -0
- /package/dist/{src/llm → llm}/embedders/local.js +0 -0
- /package/dist/{src/llm → llm}/embedders/types.js +0 -0
- /package/dist/{src/llm → llm}/feature-gate.js +0 -0
- /package/dist/{src/llm → llm}/graph-extract.js +0 -0
- /package/dist/{src/llm → llm}/index-passes.js +0 -0
- /package/dist/{src/llm → llm}/memory-infer.js +0 -0
- /package/dist/{src/llm → llm}/metadata-enhance.js +0 -0
- /package/dist/{src/output → output}/context.js +0 -0
- /package/dist/{src/registry → registry}/create-provider-registry.js +0 -0
- /package/dist/{src/registry → registry}/factory.js +0 -0
- /package/dist/{src/registry → registry}/origin-resolve.js +0 -0
- /package/dist/{src/registry → registry}/providers/index.js +0 -0
- /package/dist/{src/registry → registry}/providers/skills-sh.js +0 -0
- /package/dist/{src/registry → registry}/providers/types.js +0 -0
- /package/dist/{src/registry → registry}/resolve.js +0 -0
- /package/dist/{src/registry → registry}/types.js +0 -0
- /package/dist/{src/setup → setup}/detect.js +0 -0
- /package/dist/{src/setup → setup}/ripgrep-install.js +0 -0
- /package/dist/{src/setup → setup}/ripgrep-resolve.js +0 -0
- /package/dist/{src/setup → setup}/steps.js +0 -0
- /package/dist/{src/sources → sources}/include.js +0 -0
- /package/dist/{src/sources → sources}/provider-factory.js +0 -0
- /package/dist/{src/sources → sources}/provider.js +0 -0
- /package/dist/{src/sources → sources}/providers/filesystem.js +0 -0
- /package/dist/{src/sources → sources}/providers/git.js +0 -0
- /package/dist/{src/sources → sources}/providers/index.js +0 -0
- /package/dist/{src/sources → sources}/providers/install-types.js +0 -0
- /package/dist/{src/sources → sources}/providers/npm.js +0 -0
- /package/dist/{src/sources → sources}/providers/provider-utils.js +0 -0
- /package/dist/{src/sources → sources}/providers/sync-from-ref.js +0 -0
- /package/dist/{src/sources → sources}/providers/tar-utils.js +0 -0
- /package/dist/{src/sources → sources}/providers/website.js +0 -0
- /package/dist/{src/sources → sources}/resolve.js +0 -0
- /package/dist/{src/sources → sources}/types.js +0 -0
- /package/dist/{src/templates → templates}/wiki-templates.js +0 -0
- /package/dist/{src/version.js → version.js} +0 -0
- /package/dist/{src/workflows → workflows}/authoring.js +0 -0
- /package/dist/{src/workflows → workflows}/cli.js +0 -0
- /package/dist/{src/workflows → workflows}/db.js +0 -0
- /package/dist/{src/workflows → workflows}/document-cache.js +0 -0
- /package/dist/{src/workflows → workflows}/parser.js +0 -0
- /package/dist/{src/workflows → workflows}/renderer.js +0 -0
- /package/dist/{src/workflows → workflows}/schema.js +0 -0
- /package/dist/{src/workflows → workflows}/validator.js +0 -0
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ranking regression tests for akm search system.
|
|
3
|
-
*
|
|
4
|
-
* Uses the shared `ranking-baseline` fixture under
|
|
5
|
-
* tests/fixtures/stashes/ranking-baseline/ to validate search ranking
|
|
6
|
-
* invariants: score differentiation, exact name matching, type ranking,
|
|
7
|
-
* fuzzy/prefix matching, score preservation, and provider merge behavior.
|
|
8
|
-
*
|
|
9
|
-
* The fixture stash is materialised once in beforeAll via the shared
|
|
10
|
-
* `loadFixtureStash` helper, then re-indexed in place through the internal
|
|
11
|
-
* indexer DB API so all tests share the same index.
|
|
12
|
-
*/
|
|
13
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
14
|
-
import fs from "node:fs";
|
|
15
|
-
import os from "node:os";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import { akmSearch } from "../src/commands/search";
|
|
18
|
-
import { saveConfig } from "../src/core/config";
|
|
19
|
-
import { getDbPath } from "../src/core/paths";
|
|
20
|
-
import { closeDatabase, openDatabase, rebuildFts, setMeta, upsertEntry } from "../src/indexer/db";
|
|
21
|
-
import { buildSearchText } from "../src/indexer/search-fields";
|
|
22
|
-
import { loadFixtureStash } from "./fixtures/stashes/load";
|
|
23
|
-
// Local test helper — mirrors the pre-v1 mergeStashHits logic that was removed
|
|
24
|
-
// from production code when the OpenViking provider was dropped (Phase 1).
|
|
25
|
-
function mergeStashHits(localHits, additionalHits, limit) {
|
|
26
|
-
if (additionalHits.length === 0)
|
|
27
|
-
return localHits.slice(0, limit);
|
|
28
|
-
const localKeys = new Set();
|
|
29
|
-
for (const h of localHits)
|
|
30
|
-
localKeys.add(h.path ?? h.ref ?? h.name);
|
|
31
|
-
const providerOnly = additionalHits.filter((h) => !localKeys.has(h.path ?? h.ref ?? h.name));
|
|
32
|
-
return [...localHits, ...providerOnly].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, limit);
|
|
33
|
-
}
|
|
34
|
-
// ── Fixture path ────────────────────────────────────────────────────────────
|
|
35
|
-
let FIXTURE_STASH;
|
|
36
|
-
let fixtureCleanup;
|
|
37
|
-
// ── Temp directory tracking ─────────────────────────────────────────────────
|
|
38
|
-
const createdTmpDirs = [];
|
|
39
|
-
function createTmpDir(prefix = "akm-ranking-") {
|
|
40
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
41
|
-
createdTmpDirs.push(dir);
|
|
42
|
-
return dir;
|
|
43
|
-
}
|
|
44
|
-
// ── Environment isolation ───────────────────────────────────────────────────
|
|
45
|
-
let originalXdgCacheHome;
|
|
46
|
-
let originalXdgConfigHome;
|
|
47
|
-
let originalAkmStashDir;
|
|
48
|
-
let testCacheDir;
|
|
49
|
-
let testConfigDir;
|
|
50
|
-
beforeAll(async () => {
|
|
51
|
-
originalXdgCacheHome = process.env.XDG_CACHE_HOME;
|
|
52
|
-
originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
53
|
-
originalAkmStashDir = process.env.AKM_STASH_DIR;
|
|
54
|
-
testCacheDir = createTmpDir("akm-ranking-cache-");
|
|
55
|
-
testConfigDir = createTmpDir("akm-ranking-config-");
|
|
56
|
-
// Materialise the shared ranking-baseline fixture into a tmp dir. This
|
|
57
|
-
// test rebuilds the index from scratch via `buildFixtureIndex()` below
|
|
58
|
-
// against its own XDG_CACHE_HOME, so we skip the helper's `akm index`
|
|
59
|
-
// spawn (~200-300ms saved per run).
|
|
60
|
-
const loaded = loadFixtureStash("ranking-baseline", { skipIndex: true });
|
|
61
|
-
FIXTURE_STASH = loaded.stashDir;
|
|
62
|
-
fixtureCleanup = loaded.cleanup;
|
|
63
|
-
process.env.XDG_CACHE_HOME = testCacheDir;
|
|
64
|
-
process.env.XDG_CONFIG_HOME = testConfigDir;
|
|
65
|
-
process.env.AKM_STASH_DIR = FIXTURE_STASH;
|
|
66
|
-
saveConfig({
|
|
67
|
-
semanticSearchMode: "off",
|
|
68
|
-
sources: [{ type: "filesystem", path: FIXTURE_STASH }],
|
|
69
|
-
registries: [],
|
|
70
|
-
});
|
|
71
|
-
buildFixtureIndex();
|
|
72
|
-
});
|
|
73
|
-
afterAll(() => {
|
|
74
|
-
if (originalXdgCacheHome === undefined)
|
|
75
|
-
delete process.env.XDG_CACHE_HOME;
|
|
76
|
-
else
|
|
77
|
-
process.env.XDG_CACHE_HOME = originalXdgCacheHome;
|
|
78
|
-
if (originalXdgConfigHome === undefined)
|
|
79
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
80
|
-
else
|
|
81
|
-
process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
|
|
82
|
-
if (originalAkmStashDir === undefined)
|
|
83
|
-
delete process.env.AKM_STASH_DIR;
|
|
84
|
-
else
|
|
85
|
-
process.env.AKM_STASH_DIR = originalAkmStashDir;
|
|
86
|
-
fixtureCleanup?.();
|
|
87
|
-
for (const dir of createdTmpDirs) {
|
|
88
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
// ── Index builder ───────────────────────────────────────────────────────────
|
|
92
|
-
/**
|
|
93
|
-
* Walk the fixture stash, read .stash.json files, and index all entries
|
|
94
|
-
* directly into the SQLite database.
|
|
95
|
-
*/
|
|
96
|
-
function buildFixtureIndex() {
|
|
97
|
-
const dbPath = getDbPath();
|
|
98
|
-
const db = openDatabase(dbPath);
|
|
99
|
-
try {
|
|
100
|
-
const stashJsonPaths = findStashJsonFiles(FIXTURE_STASH);
|
|
101
|
-
for (const stashJsonPath of stashJsonPaths) {
|
|
102
|
-
const dirPath = path.dirname(stashJsonPath);
|
|
103
|
-
const raw = JSON.parse(fs.readFileSync(stashJsonPath, "utf8"));
|
|
104
|
-
if (!raw || !Array.isArray(raw.entries))
|
|
105
|
-
continue;
|
|
106
|
-
const stash = { entries: raw.entries };
|
|
107
|
-
for (const entry of stash.entries) {
|
|
108
|
-
const entryPath = entry.filename ? path.join(dirPath, entry.filename) : dirPath;
|
|
109
|
-
const entryKey = `${FIXTURE_STASH}:${entry.type}:${entry.name}`;
|
|
110
|
-
const searchText = buildSearchText(entry);
|
|
111
|
-
let entryWithSize = entry;
|
|
112
|
-
try {
|
|
113
|
-
const size = fs.statSync(entryPath).size;
|
|
114
|
-
entryWithSize = { ...entry, fileSize: size };
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
// File might not exist for some entries
|
|
118
|
-
}
|
|
119
|
-
upsertEntry(db, entryKey, dirPath, entryPath, FIXTURE_STASH, entryWithSize, searchText);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
rebuildFts(db);
|
|
123
|
-
setMeta(db, "stashDir", FIXTURE_STASH);
|
|
124
|
-
setMeta(db, "builtAt", new Date().toISOString());
|
|
125
|
-
setMeta(db, "stashDirs", JSON.stringify([FIXTURE_STASH]));
|
|
126
|
-
setMeta(db, "hasEmbeddings", "0");
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
closeDatabase(db);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
function findStashJsonFiles(dir) {
|
|
133
|
-
const results = [];
|
|
134
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
135
|
-
for (const entry of entries) {
|
|
136
|
-
const fullPath = path.join(dir, entry.name);
|
|
137
|
-
if (entry.isDirectory()) {
|
|
138
|
-
results.push(...findStashJsonFiles(fullPath));
|
|
139
|
-
}
|
|
140
|
-
else if (entry.name === ".stash.json") {
|
|
141
|
-
results.push(fullPath);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return results;
|
|
145
|
-
}
|
|
146
|
-
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
147
|
-
async function search(query, limit = 20) {
|
|
148
|
-
const result = await akmSearch({ query, source: "stash", limit });
|
|
149
|
-
return result.hits.filter((h) => h.type !== "registry");
|
|
150
|
-
}
|
|
151
|
-
function findHit(hits, name) {
|
|
152
|
-
return hits.find((h) => h.name === name);
|
|
153
|
-
}
|
|
154
|
-
/** Assert that a hit exists and return it (avoids non-null assertions). */
|
|
155
|
-
function expectHit(hits, name) {
|
|
156
|
-
const hit = findHit(hits, name);
|
|
157
|
-
expect(hit).toBeDefined();
|
|
158
|
-
// biome-ignore lint/style/noNonNullAssertion: guarded by expect above
|
|
159
|
-
return hit;
|
|
160
|
-
}
|
|
161
|
-
/** Get the score of a hit, asserting it is defined. */
|
|
162
|
-
function scoreOf(hit) {
|
|
163
|
-
expect(hit.score).toBeDefined();
|
|
164
|
-
return hit.score ?? 0;
|
|
165
|
-
}
|
|
166
|
-
function rankOf(hits, name) {
|
|
167
|
-
const idx = hits.findIndex((h) => h.name === name);
|
|
168
|
-
return idx === -1 ? Infinity : idx + 1; // 1-based rank
|
|
169
|
-
}
|
|
170
|
-
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
171
|
-
describe("Score differentiation", () => {
|
|
172
|
-
test('"docker homelab" returns skill:docker-homelab in top 3', async () => {
|
|
173
|
-
const hits = await search("docker homelab");
|
|
174
|
-
expect(hits.length).toBeGreaterThanOrEqual(2);
|
|
175
|
-
// docker-homelab should appear in the top results (within top 3)
|
|
176
|
-
// Sub-references also contain "docker-homelab" in their name, so they
|
|
177
|
-
// may rank highly on FTS name-field matching.
|
|
178
|
-
const skillRank = rankOf(hits, "docker-homelab");
|
|
179
|
-
expect(skillRank).toBeLessThanOrEqual(3);
|
|
180
|
-
// The skill should have a meaningful score (not RRF-compressed)
|
|
181
|
-
const skillHit = expectHit(hits, "docker-homelab");
|
|
182
|
-
expect(scoreOf(skillHit)).toBeGreaterThan(0.5);
|
|
183
|
-
});
|
|
184
|
-
test('"docker" returns docker-homelab and docker-clean', async () => {
|
|
185
|
-
const hits = await search("docker");
|
|
186
|
-
expect(hits.length).toBeGreaterThanOrEqual(2);
|
|
187
|
-
// Both docker-related assets should appear in results
|
|
188
|
-
expectHit(hits, "docker-homelab");
|
|
189
|
-
expectHit(hits, "docker-clean");
|
|
190
|
-
});
|
|
191
|
-
test('"svelte component" -> skill:svelte-components ranks #1, above sub-references', async () => {
|
|
192
|
-
const hits = await search("svelte component");
|
|
193
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
194
|
-
const skillRank = rankOf(hits, "svelte-components");
|
|
195
|
-
expect(skillRank).toBe(1);
|
|
196
|
-
// Sub-reference should rank below the skill
|
|
197
|
-
const refHit = findHit(hits, "svelte-components/references/web-components");
|
|
198
|
-
if (refHit) {
|
|
199
|
-
const refRank = rankOf(hits, "svelte-components/references/web-components");
|
|
200
|
-
expect(refRank).toBeGreaterThan(skillRank);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
test('"code review" -> command or agent ranks above knowledge docs', async () => {
|
|
204
|
-
const hits = await search("code review");
|
|
205
|
-
expect(hits.length).toBeGreaterThanOrEqual(2);
|
|
206
|
-
// Find the top-ranked actionable asset (skill, command, or agent)
|
|
207
|
-
const actionableTypes = new Set(["skill", "command", "agent"]);
|
|
208
|
-
const topActionable = hits.find((h) => actionableTypes.has(h.type));
|
|
209
|
-
expect(topActionable).toBeDefined();
|
|
210
|
-
// Find the top-ranked knowledge doc
|
|
211
|
-
const topKnowledge = hits.find((h) => h.type === "knowledge");
|
|
212
|
-
if (topKnowledge && topActionable) {
|
|
213
|
-
const actionableRank = rankOf(hits, topActionable.name);
|
|
214
|
-
const knowledgeRank = rankOf(hits, topKnowledge.name);
|
|
215
|
-
expect(actionableRank).toBeLessThan(knowledgeRank);
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
test('"mem0 search" -> script:mem0-search ranks #1', async () => {
|
|
219
|
-
const hits = await search("mem0 search");
|
|
220
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
221
|
-
expect(rankOf(hits, "mem0-search")).toBe(1);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
describe("Exact/near-exact name matching", () => {
|
|
225
|
-
test('"docker-homelab" (exact) -> skill:docker-homelab appears in top 3', async () => {
|
|
226
|
-
const hits = await search("docker-homelab");
|
|
227
|
-
expect(hits.length).toBeGreaterThanOrEqual(2);
|
|
228
|
-
// The skill entry and its sub-references all contain "docker-homelab"
|
|
229
|
-
// in their names. The skill gets a name-match boost but sub-references
|
|
230
|
-
// also match on FTS name field. Verify the skill is in the top 3.
|
|
231
|
-
const skillRank = rankOf(hits, "docker-homelab");
|
|
232
|
-
expect(skillRank).toBeLessThanOrEqual(3);
|
|
233
|
-
// The skill should have a strong score
|
|
234
|
-
const skillHit = expectHit(hits, "docker-homelab");
|
|
235
|
-
expect(scoreOf(skillHit)).toBeGreaterThan(0.5);
|
|
236
|
-
});
|
|
237
|
-
test('"mem0-search" (exact) -> script:mem0-search is #1', async () => {
|
|
238
|
-
const hits = await search("mem0-search");
|
|
239
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
240
|
-
expect(hits[0].name).toBe("mem0-search");
|
|
241
|
-
});
|
|
242
|
-
test('"security-review" (exact) -> command:security-review is #1', async () => {
|
|
243
|
-
const hits = await search("security-review");
|
|
244
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
245
|
-
expect(hits[0].name).toBe("security-review");
|
|
246
|
-
expect(hits[0].type).toBe("command");
|
|
247
|
-
});
|
|
248
|
-
test('"k8s-deploy" (exact) -> skill:k8s-deploy is #1', async () => {
|
|
249
|
-
const hits = await search("k8s-deploy");
|
|
250
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
251
|
-
expect(hits[0].name).toBe("k8s-deploy");
|
|
252
|
-
expect(hits[0].type).toBe("skill");
|
|
253
|
-
});
|
|
254
|
-
test('"code-reviewer" (exact) -> agent:code-reviewer is #1', async () => {
|
|
255
|
-
const hits = await search("code-reviewer");
|
|
256
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
257
|
-
expect(hits[0].name).toBe("code-reviewer");
|
|
258
|
-
expect(hits[0].type).toBe("agent");
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
describe("Type ranking", () => {
|
|
262
|
-
test('for "deploy", skills/commands/scripts rank above knowledge docs', async () => {
|
|
263
|
-
const hits = await search("deploy");
|
|
264
|
-
expect(hits.length).toBeGreaterThanOrEqual(2);
|
|
265
|
-
const actionableTypes = new Set(["skill", "command", "agent", "script"]);
|
|
266
|
-
const topActionable = hits.find((h) => actionableTypes.has(h.type));
|
|
267
|
-
const topKnowledge = hits.find((h) => h.type === "knowledge");
|
|
268
|
-
expect(topActionable).toBeDefined();
|
|
269
|
-
if (topKnowledge && topActionable) {
|
|
270
|
-
expect(rankOf(hits, topActionable.name)).toBeLessThan(rankOf(hits, topKnowledge.name));
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
test('for "review", agents/commands/skills rank above knowledge docs', async () => {
|
|
274
|
-
const hits = await search("review");
|
|
275
|
-
expect(hits.length).toBeGreaterThanOrEqual(2);
|
|
276
|
-
const actionableTypes = new Set(["skill", "command", "agent"]);
|
|
277
|
-
const topActionable = hits.find((h) => actionableTypes.has(h.type));
|
|
278
|
-
const topKnowledge = hits.find((h) => h.type === "knowledge");
|
|
279
|
-
expect(topActionable).toBeDefined();
|
|
280
|
-
if (topKnowledge && topActionable) {
|
|
281
|
-
expect(rankOf(hits, topActionable.name)).toBeLessThan(rankOf(hits, topKnowledge.name));
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
describe("Fuzzy/prefix matching", () => {
|
|
286
|
-
test('"kube" finds k8s-deploy via alias', async () => {
|
|
287
|
-
const hits = await search("kube");
|
|
288
|
-
expectHit(hits, "k8s-deploy");
|
|
289
|
-
});
|
|
290
|
-
test('"dock" finds docker-homelab via prefix', async () => {
|
|
291
|
-
const hits = await search("dock");
|
|
292
|
-
expectHit(hits, "docker-homelab");
|
|
293
|
-
});
|
|
294
|
-
test('"incident" finds the runbook', async () => {
|
|
295
|
-
const hits = await search("incident");
|
|
296
|
-
expectHit(hits, "incident-response-runbook");
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
describe("Score preservation (not RRF-flattened)", () => {
|
|
300
|
-
test("top result score > 0.5 (not capped at 0.0164)", async () => {
|
|
301
|
-
const hits = await search("docker homelab");
|
|
302
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
303
|
-
expect(scoreOf(hits[0])).toBeGreaterThan(0.5);
|
|
304
|
-
});
|
|
305
|
-
test("top result for exact name query has strong differentiation", async () => {
|
|
306
|
-
// Use a query that uniquely targets one asset.
|
|
307
|
-
// Per the locked v1 contract (CLAUDE.md / spec §9), SearchHit.score is
|
|
308
|
-
// bounded in [0,1]. An exact-name match accumulates large additive
|
|
309
|
-
// boosts that are clamped at the final emit step, so the top score for
|
|
310
|
-
// a uniquely-matching exact-name query must reach the ceiling (1.0).
|
|
311
|
-
const hits = await search("mem0 search");
|
|
312
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
313
|
-
const topScore = scoreOf(hits[0]);
|
|
314
|
-
expect(topScore).toBe(1);
|
|
315
|
-
// If there are additional results, the top should be at least as high.
|
|
316
|
-
// Below-ceiling differentiation is asserted by the broader
|
|
317
|
-
// "scores are monotonically decreasing" case below; here we just
|
|
318
|
-
// confirm the top hit reaches the maximum.
|
|
319
|
-
if (hits.length >= 2) {
|
|
320
|
-
expect(topScore).toBeGreaterThanOrEqual(scoreOf(hits[1]));
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
test("scores are monotonically decreasing", async () => {
|
|
324
|
-
const hits = await search("docker");
|
|
325
|
-
for (let i = 1; i < hits.length; i++) {
|
|
326
|
-
const prev = hits[i - 1].score ?? 0;
|
|
327
|
-
const curr = hits[i].score ?? 0;
|
|
328
|
-
expect(prev).toBeGreaterThanOrEqual(curr);
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
test("scores are not compressed to a narrow range", async () => {
|
|
332
|
-
const hits = await search("docker");
|
|
333
|
-
expect(hits.length).toBeGreaterThanOrEqual(3);
|
|
334
|
-
const topScore = scoreOf(hits[0]);
|
|
335
|
-
const lastScore = scoreOf(hits[hits.length - 1]);
|
|
336
|
-
const range = topScore - lastScore;
|
|
337
|
-
// Score range should be meaningful, not compressed to ~0.001 like RRF.
|
|
338
|
-
// Per the locked v1 contract (CLAUDE.md / spec §9), scores are bounded
|
|
339
|
-
// in [0,1] — multiple top hits on a popular query may all clamp to 1.0,
|
|
340
|
-
// which collapses the visible top-end differential. The bottom of the
|
|
341
|
-
// range still shows clear separation from the top, well above what RRF
|
|
342
|
-
// would compress to (~0.001).
|
|
343
|
-
expect(range).toBeGreaterThan(0.01);
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
describe("Provider merge (score not destroyed)", () => {
|
|
347
|
-
test("when additional provider hits exist, local scores are preserved", () => {
|
|
348
|
-
const localHits = [
|
|
349
|
-
{
|
|
350
|
-
type: "skill",
|
|
351
|
-
name: "local-skill-1",
|
|
352
|
-
path: "/test/skills/local-1/SKILL.md",
|
|
353
|
-
ref: "skill:local-skill-1",
|
|
354
|
-
origin: null,
|
|
355
|
-
score: 2.5,
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
type: "command",
|
|
359
|
-
name: "local-cmd-1",
|
|
360
|
-
path: "/test/commands/local-1.md",
|
|
361
|
-
ref: "command:local-cmd-1",
|
|
362
|
-
origin: null,
|
|
363
|
-
score: 1.8,
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
type: "knowledge",
|
|
367
|
-
name: "local-doc-1",
|
|
368
|
-
path: "/test/knowledge/local-1.md",
|
|
369
|
-
ref: "knowledge:local-doc-1",
|
|
370
|
-
origin: null,
|
|
371
|
-
score: 0.9,
|
|
372
|
-
},
|
|
373
|
-
];
|
|
374
|
-
const additionalHits = [
|
|
375
|
-
{
|
|
376
|
-
type: "skill",
|
|
377
|
-
name: "remote-skill-1",
|
|
378
|
-
path: "/remote/skills/remote-1/SKILL.md",
|
|
379
|
-
ref: "skill:remote-skill-1",
|
|
380
|
-
origin: "remote",
|
|
381
|
-
score: 0.85,
|
|
382
|
-
},
|
|
383
|
-
];
|
|
384
|
-
const merged = mergeStashHits(localHits, additionalHits, 20);
|
|
385
|
-
// Local hits should retain their original scores
|
|
386
|
-
const mergedLocal1 = expectHit(merged, "local-skill-1");
|
|
387
|
-
expect(mergedLocal1.score).toBe(2.5);
|
|
388
|
-
const mergedLocal2 = expectHit(merged, "local-cmd-1");
|
|
389
|
-
expect(mergedLocal2.score).toBe(1.8);
|
|
390
|
-
});
|
|
391
|
-
test("provider hits sort fairly by score alongside local hits", () => {
|
|
392
|
-
const localHits = [
|
|
393
|
-
{
|
|
394
|
-
type: "skill",
|
|
395
|
-
name: "local-high",
|
|
396
|
-
path: "/test/skills/high/SKILL.md",
|
|
397
|
-
ref: "skill:local-high",
|
|
398
|
-
origin: null,
|
|
399
|
-
score: 2.0,
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
type: "skill",
|
|
403
|
-
name: "local-low",
|
|
404
|
-
path: "/test/skills/low/SKILL.md",
|
|
405
|
-
ref: "skill:local-low",
|
|
406
|
-
origin: null,
|
|
407
|
-
score: 0.5,
|
|
408
|
-
},
|
|
409
|
-
];
|
|
410
|
-
const additionalHits = [
|
|
411
|
-
{
|
|
412
|
-
type: "skill",
|
|
413
|
-
name: "remote-1",
|
|
414
|
-
path: "/remote/skills/1/SKILL.md",
|
|
415
|
-
ref: "skill:remote-1",
|
|
416
|
-
origin: "remote",
|
|
417
|
-
score: 1.0, // Normalized provider score between local-high and local-low
|
|
418
|
-
},
|
|
419
|
-
];
|
|
420
|
-
const merged = mergeStashHits(localHits, additionalHits, 20);
|
|
421
|
-
// Provider hit keeps its original score and sorts by score
|
|
422
|
-
const remoteRank = merged.findIndex((h) => h.name === "remote-1") + 1;
|
|
423
|
-
const localHighRank = merged.findIndex((h) => h.name === "local-high") + 1;
|
|
424
|
-
const localLowRank = merged.findIndex((h) => h.name === "local-low") + 1;
|
|
425
|
-
// remote-1 (1.0) should rank between local-high (2.0) and local-low (0.5)
|
|
426
|
-
expect(remoteRank).toBeGreaterThan(localHighRank);
|
|
427
|
-
expect(remoteRank).toBeLessThan(localLowRank);
|
|
428
|
-
});
|
|
429
|
-
test("duplicate provider hits are deduplicated (local version wins)", () => {
|
|
430
|
-
const localHits = [
|
|
431
|
-
{
|
|
432
|
-
type: "skill",
|
|
433
|
-
name: "shared-skill",
|
|
434
|
-
path: "/test/skills/shared/SKILL.md",
|
|
435
|
-
ref: "skill:shared-skill",
|
|
436
|
-
origin: null,
|
|
437
|
-
score: 2.0,
|
|
438
|
-
},
|
|
439
|
-
];
|
|
440
|
-
const additionalHits = [
|
|
441
|
-
{
|
|
442
|
-
type: "skill",
|
|
443
|
-
name: "shared-skill",
|
|
444
|
-
path: "/test/skills/shared/SKILL.md", // Same path = duplicate
|
|
445
|
-
ref: "skill:shared-skill",
|
|
446
|
-
origin: "remote",
|
|
447
|
-
score: 0.5,
|
|
448
|
-
},
|
|
449
|
-
];
|
|
450
|
-
const merged = mergeStashHits(localHits, additionalHits, 20);
|
|
451
|
-
// Only one instance of the shared skill should appear
|
|
452
|
-
const sharedHits = merged.filter((h) => h.name === "shared-skill");
|
|
453
|
-
expect(sharedHits.length).toBe(1);
|
|
454
|
-
// And it should have the local score
|
|
455
|
-
expect(sharedHits[0].score).toBe(2.0);
|
|
456
|
-
});
|
|
457
|
-
test("merge preserves sort order by score descending", () => {
|
|
458
|
-
const localHits = [
|
|
459
|
-
{ type: "skill", name: "a", path: "/a", ref: "skill:a", origin: null, score: 3.0 },
|
|
460
|
-
{ type: "skill", name: "b", path: "/b", ref: "skill:b", origin: null, score: 1.0 },
|
|
461
|
-
];
|
|
462
|
-
const additionalHits = [
|
|
463
|
-
{ type: "skill", name: "c", path: "/c", ref: "skill:c", origin: "remote", score: 2.0 },
|
|
464
|
-
];
|
|
465
|
-
const merged = mergeStashHits(localHits, additionalHits, 20);
|
|
466
|
-
for (let i = 1; i < merged.length; i++) {
|
|
467
|
-
const prev = merged[i - 1].score ?? 0;
|
|
468
|
-
const curr = merged[i].score ?? 0;
|
|
469
|
-
expect(prev).toBeGreaterThanOrEqual(curr);
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
describe("Cross-type search consistency", () => {
|
|
474
|
-
test("searching for 'docker' returns docker-homelab and docker-clean", async () => {
|
|
475
|
-
const hits = await search("docker");
|
|
476
|
-
expect(hits.length).toBeGreaterThanOrEqual(2);
|
|
477
|
-
const dockerNames = hits.map((h) => h.name);
|
|
478
|
-
expect(dockerNames).toContain("docker-homelab");
|
|
479
|
-
expect(dockerNames).toContain("docker-clean");
|
|
480
|
-
});
|
|
481
|
-
test("multi-word queries narrow results appropriately", async () => {
|
|
482
|
-
const narrowHits = await search("deploy check");
|
|
483
|
-
// The narrow query should return deploy-check at a high rank
|
|
484
|
-
const deployCheckRank = rankOf(narrowHits, "deploy-check");
|
|
485
|
-
expect(deployCheckRank).toBeLessThanOrEqual(3);
|
|
486
|
-
});
|
|
487
|
-
test("searching for svelte returns both skill and agent", async () => {
|
|
488
|
-
const hits = await search("svelte");
|
|
489
|
-
const svelteNames = hits.map((h) => h.name);
|
|
490
|
-
expect(svelteNames).toContain("svelte-components");
|
|
491
|
-
expect(svelteNames).toContain("svelte-expert");
|
|
492
|
-
});
|
|
493
|
-
test("searching for 'release' finds the release-manager command", async () => {
|
|
494
|
-
const hits = await search("release");
|
|
495
|
-
const releaseHit = expectHit(hits, "release-manager");
|
|
496
|
-
expect(releaseHit.type).toBe("command");
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
describe("Metadata signal strength", () => {
|
|
500
|
-
test("skill with rich metadata appears in results for broad queries", async () => {
|
|
501
|
-
// docker-homelab has rich tags, aliases, searchHints, and curated quality
|
|
502
|
-
const hits = await search("container management");
|
|
503
|
-
const skillHit = expectHit(hits, "docker-homelab");
|
|
504
|
-
expect(scoreOf(skillHit)).toBeGreaterThan(0);
|
|
505
|
-
});
|
|
506
|
-
test("curated quality assets include the curated metadata boost reason", async () => {
|
|
507
|
-
const hits = await search("kubernetes deploy");
|
|
508
|
-
expect(hits.length).toBeGreaterThanOrEqual(1);
|
|
509
|
-
const k8sHit = expectHit(hits, "k8s-deploy");
|
|
510
|
-
expect(k8sHit.whyMatched).toContain("curated metadata boost");
|
|
511
|
-
});
|
|
512
|
-
test("searchHints contribute to matching", async () => {
|
|
513
|
-
// "troubleshoot docker" is a search hint on docker-homelab
|
|
514
|
-
const hits = await search("troubleshoot docker");
|
|
515
|
-
const skillHit = expectHit(hits, "docker-homelab");
|
|
516
|
-
expect(skillHit.whyMatched).toContain("matched searchHints");
|
|
517
|
-
});
|
|
518
|
-
test("aliases contribute to matching", async () => {
|
|
519
|
-
// "docker-compose" is an alias for docker-homelab
|
|
520
|
-
const hits = await search("docker compose");
|
|
521
|
-
const skillHit = expectHit(hits, "docker-homelab");
|
|
522
|
-
expect(skillHit.whyMatched).toContain("matched aliases");
|
|
523
|
-
});
|
|
524
|
-
test("tags contribute to matching", async () => {
|
|
525
|
-
const hits = await search("homelab");
|
|
526
|
-
const skillHit = expectHit(hits, "docker-homelab");
|
|
527
|
-
expect(skillHit.whyMatched).toContain("matched tags");
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
describe("Empty and edge case queries", () => {
|
|
531
|
-
test("empty query returns all entries", async () => {
|
|
532
|
-
const result = await akmSearch({ query: "", source: "stash" });
|
|
533
|
-
expect(result.hits.length).toBeGreaterThan(0);
|
|
534
|
-
});
|
|
535
|
-
test("query with no matches returns empty results with tip", async () => {
|
|
536
|
-
const result = await akmSearch({ query: "xyznonexistent123", source: "stash" });
|
|
537
|
-
const hits = result.hits.filter((h) => h.type !== "registry");
|
|
538
|
-
expect(hits.length).toBe(0);
|
|
539
|
-
});
|
|
540
|
-
test("single character query returns results when prefix matches", async () => {
|
|
541
|
-
// Single char queries are too short for prefix expansion (< 3 chars)
|
|
542
|
-
// but may still match on exact tokens
|
|
543
|
-
const result = await akmSearch({ query: "k", source: "stash" });
|
|
544
|
-
// This may or may not return results depending on FTS tokenizer behavior
|
|
545
|
-
// The important thing is it does not crash
|
|
546
|
-
expect(result).toBeDefined();
|
|
547
|
-
});
|
|
548
|
-
});
|