akm-cli 0.9.0-beta.6 → 0.9.0-rc.0
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/CHANGELOG.md +663 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -14
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +153 -147
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1083
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1313 -1278
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +243 -599
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +55 -4
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +160 -7
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +32 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +510 -2537
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +212 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +7 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -882
- package/dist/commands/read/curate.js +419 -103
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +29 -41
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +389 -58
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +21 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +142 -1091
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +356 -780
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +103 -24
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +106 -0
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +291 -310
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +57 -15
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2718 -2354
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -8,16 +8,23 @@
|
|
|
8
8
|
* high-signal set of stash + registry hits and enrich each with the data
|
|
9
9
|
* needed to act (ref, run, parameters, follow-up command).
|
|
10
10
|
*
|
|
11
|
-
* The exported `akmCurate()` API is the single entry point. Internal
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
11
|
+
* The exported `akmCurate()` API is the single entry point. Internal helpers
|
|
12
|
+
* stay private. Tests can drive the public API or call the smaller pure
|
|
13
|
+
* helpers (`curateSearchResults`, `deriveCurateFallbackQueries`,
|
|
14
|
+
* `mergeCurateSearchResponses`) by importing them directly.
|
|
15
15
|
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
18
|
+
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
19
|
+
import { getIndexPassConfig, loadConfig } from "../../core/config/config.js";
|
|
16
20
|
import { rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
17
21
|
import { appendEvent } from "../../core/events.js";
|
|
18
|
-
import {
|
|
22
|
+
import { computeBodyHash } from "../../indexer/db/db.js";
|
|
23
|
+
import { enqueueGraphExtraction, hasGraphData } from "../../indexer/db/graph-db.js";
|
|
24
|
+
import { findSourceForPath, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
19
25
|
import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
|
|
20
26
|
import { truncateDescription } from "../../output/shapes.js";
|
|
27
|
+
import { TELEMETRY_BUSY_TIMEOUT_MS, withIndexDb } from "../../storage/repositories/index-db.js";
|
|
21
28
|
import { akmSearch, parseSearchSource } from "./search.js";
|
|
22
29
|
import { akmShowUnified } from "./show.js";
|
|
23
30
|
const CURATE_FALLBACK_FILTER_WORDS = new Set([
|
|
@@ -34,26 +41,31 @@ const CURATE_FALLBACK_FILTER_WORDS = new Set([
|
|
|
34
41
|
"to",
|
|
35
42
|
"with",
|
|
36
43
|
]);
|
|
37
|
-
const
|
|
38
|
-
const CURATED_TYPE_FALLBACK_INDEX = new Map(CURATED_TYPE_FALLBACK_ORDER.map((type, index) => [type, index]));
|
|
44
|
+
const CURATE_SHORT_FALLBACK_TOKENS = new Set(["ai", "ci", "cd", "go", "js", "ts"]);
|
|
39
45
|
const MIN_CURATE_FALLBACK_TOKEN_LENGTH = 3;
|
|
40
46
|
const MAX_CURATE_FALLBACK_KEYWORDS = 6;
|
|
41
47
|
export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
|
|
42
48
|
export const MIN_CURATE_SEARCH_LIMIT = 12;
|
|
43
49
|
const DEFAULT_CURATE_LIMIT = 4;
|
|
50
|
+
const CURATE_CLOSE_SCORE_BAND = 0.12;
|
|
51
|
+
const CURATE_TAIL_SCORE_FLOOR = 0.35;
|
|
52
|
+
const CURATE_RELATIVE_SCORE_FLOOR = 0.7;
|
|
53
|
+
const CURATE_FALLBACK_TOP_SCORE_THRESHOLD = 0.8;
|
|
54
|
+
const CURATE_FALLBACK_STRONG_SCORE_FLOOR = 0.35;
|
|
55
|
+
const MAX_CURATE_SUPPORT_REFS = 2;
|
|
56
|
+
const CURATE_REFERENCE_QUERY_RE = /\b(?:reference|docs?|guide|how|explain|learn|readme|why)\b/;
|
|
44
57
|
/**
|
|
45
58
|
* Fire-and-forget: log a curate event to the usage_events table and events.jsonl.
|
|
46
59
|
* Never blocks the caller; errors are silently ignored.
|
|
47
60
|
*/
|
|
48
|
-
function logCurateEvent(query, result) {
|
|
61
|
+
function logCurateEvent(query, result, eventSource = "user") {
|
|
49
62
|
const itemRefs = result.items.map((item) => ("ref" in item ? item.ref : `registry:${item.id}`));
|
|
50
63
|
appendEvent({
|
|
51
64
|
eventType: "curate",
|
|
52
65
|
metadata: { query, itemCount: result.items.length, itemRefs },
|
|
53
66
|
});
|
|
54
67
|
try {
|
|
55
|
-
|
|
56
|
-
try {
|
|
68
|
+
withIndexDb((db) => {
|
|
57
69
|
insertUsageEvent(db, {
|
|
58
70
|
event_type: "curate",
|
|
59
71
|
query,
|
|
@@ -61,22 +73,24 @@ function logCurateEvent(query, result) {
|
|
|
61
73
|
itemCount: result.items.length,
|
|
62
74
|
itemRefs,
|
|
63
75
|
}),
|
|
64
|
-
source:
|
|
76
|
+
source: eventSource,
|
|
65
77
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
for (const item of result.items) {
|
|
79
|
+
if (!("ref" in item) || typeof item.ref !== "string")
|
|
80
|
+
continue;
|
|
81
|
+
insertUsageEvent(db, {
|
|
82
|
+
event_type: "curate",
|
|
83
|
+
query,
|
|
84
|
+
entry_ref: item.ref,
|
|
85
|
+
source: eventSource,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}, { busyTimeoutMs: TELEMETRY_BUSY_TIMEOUT_MS });
|
|
70
89
|
}
|
|
71
90
|
catch (err) {
|
|
72
91
|
rethrowIfTestIsolationError(err);
|
|
73
|
-
/* ignore logging failures */
|
|
74
92
|
}
|
|
75
93
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Public curate entry point. Performs the search itself when
|
|
78
|
-
* `options.searchResponse` is not supplied.
|
|
79
|
-
*/
|
|
80
94
|
export async function akmCurate(options) {
|
|
81
95
|
const trimmedQuery = options.query.trim();
|
|
82
96
|
if (!trimmedQuery) {
|
|
@@ -88,36 +102,33 @@ export async function akmCurate(options) {
|
|
|
88
102
|
(await searchForCuration({
|
|
89
103
|
query: options.query,
|
|
90
104
|
type: options.type,
|
|
91
|
-
// Search deeper than the final curated count so we can pick one strong
|
|
92
|
-
// match per type and still have room for fallback retries.
|
|
93
105
|
limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
|
|
94
106
|
source,
|
|
95
107
|
}));
|
|
96
108
|
const result = await curateSearchResults(options.query, searchResponse, limit, options.type);
|
|
97
|
-
logCurateEvent(options.query, result);
|
|
109
|
+
logCurateEvent(options.query, result, options.eventSource);
|
|
98
110
|
return result;
|
|
99
111
|
}
|
|
100
112
|
export async function curateSearchResults(query, result, limit, selectedType) {
|
|
101
113
|
const stashHits = result.hits.filter((hit) => hit.type !== "registry");
|
|
102
114
|
const registryHits = result.registryHits ?? [];
|
|
103
115
|
let selectedStashHits;
|
|
116
|
+
let supportRefsByRef = new Map();
|
|
104
117
|
if (selectedType && selectedType !== "any") {
|
|
105
118
|
selectedStashHits = stashHits.slice(0, limit);
|
|
106
119
|
}
|
|
107
120
|
else {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
|
|
114
|
-
selectedStashHits = orderedTypes
|
|
115
|
-
.map((type) => bestByType.get(type))
|
|
116
|
-
.filter((hit) => Boolean(hit));
|
|
121
|
+
const selected = selectCuratedStashHits(query, stashHits, limit);
|
|
122
|
+
const preferred = preferBroadRootRepresentative(query, selected.selected, stashHits, selected.supportRefsByRef);
|
|
123
|
+
selectedStashHits = preferred.selected;
|
|
124
|
+
supportRefsByRef = preferred.supportRefsByRef;
|
|
117
125
|
}
|
|
118
126
|
const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, Math.min(2, limit - selectedStashHits.length));
|
|
127
|
+
const selectedRefs = new Set(selectedStashHits.map((hit) => hit.ref));
|
|
119
128
|
const items = [
|
|
120
|
-
...(await Promise.all(selectedStashHits
|
|
129
|
+
...(await Promise.all(selectedStashHits
|
|
130
|
+
.slice(0, limit)
|
|
131
|
+
.map((hit) => enrichCuratedStashHit(query, hit, supportRefsByRef.get(hit.ref) ?? [], selectedRefs)))),
|
|
121
132
|
...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
|
|
122
133
|
].slice(0, limit);
|
|
123
134
|
return {
|
|
@@ -128,37 +139,7 @@ export async function curateSearchResults(query, result, limit, selectedType) {
|
|
|
128
139
|
...(result.tip ? { tip: result.tip } : {}),
|
|
129
140
|
};
|
|
130
141
|
}
|
|
131
|
-
|
|
132
|
-
const lower = query.toLowerCase();
|
|
133
|
-
const boosts = new Map();
|
|
134
|
-
const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
|
|
135
|
-
if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
|
|
136
|
-
addBoost("script", 6);
|
|
137
|
-
addBoost("command", 4);
|
|
138
|
-
}
|
|
139
|
-
if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
|
|
140
|
-
addBoost("knowledge", 6);
|
|
141
|
-
addBoost("skill", 4);
|
|
142
|
-
}
|
|
143
|
-
if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
|
|
144
|
-
addBoost("agent", 6);
|
|
145
|
-
addBoost("skill", 3);
|
|
146
|
-
}
|
|
147
|
-
if (/(config|template|release|generate|command)/.test(lower)) {
|
|
148
|
-
addBoost("command", 5);
|
|
149
|
-
}
|
|
150
|
-
if (/(memory|context|recall|remember)/.test(lower)) {
|
|
151
|
-
addBoost("memory", 6);
|
|
152
|
-
}
|
|
153
|
-
return [...types].sort((a, b) => {
|
|
154
|
-
const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
|
|
155
|
-
if (boostDiff !== 0)
|
|
156
|
-
return boostDiff;
|
|
157
|
-
return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
158
|
-
(CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
async function enrichCuratedStashHit(query, hit) {
|
|
142
|
+
async function enrichCuratedStashHit(query, hit, supportRefs, selectedRefs) {
|
|
162
143
|
let shown;
|
|
163
144
|
try {
|
|
164
145
|
shown = await akmShowUnified({ ref: hit.ref });
|
|
@@ -166,8 +147,14 @@ async function enrichCuratedStashHit(query, hit) {
|
|
|
166
147
|
catch {
|
|
167
148
|
shown = undefined;
|
|
168
149
|
}
|
|
150
|
+
// #624-P3: when lazy graph extraction is opted in, enqueue an ungraphed
|
|
151
|
+
// asset for a later pass to extract. Fire-and-forget, non-blocking, NO inline
|
|
152
|
+
// extraction and NO LLM call here. Default-off (flag unset) = byte-identical.
|
|
153
|
+
if (shown?.path)
|
|
154
|
+
maybeEnqueueLazyGraph(shown.path);
|
|
169
155
|
const description = shown?.description ?? hit.description;
|
|
170
156
|
const preview = buildCuratedPreview(shown, hit);
|
|
157
|
+
const mergedSupportRefs = mergeCurateSupportRefs(supportRefs, shown?.related?.hits, selectedRefs, hit.ref);
|
|
171
158
|
return {
|
|
172
159
|
source: "stash",
|
|
173
160
|
type: shown?.type ?? hit.type,
|
|
@@ -178,11 +165,50 @@ async function enrichCuratedStashHit(query, hit) {
|
|
|
178
165
|
...(shown?.keys?.length ? { keys: shown.keys } : {}),
|
|
179
166
|
...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
|
|
180
167
|
...(shown?.run ? { run: shown.run } : {}),
|
|
168
|
+
...(mergedSupportRefs.length > 0 ? { supportRefs: mergedSupportRefs } : {}),
|
|
181
169
|
followUp: `akm show ${hit.ref}`,
|
|
182
170
|
reason: buildCuratedReason(query, shown?.type ?? hit.type),
|
|
183
171
|
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
184
172
|
};
|
|
185
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* #624-P3 — enqueue an ungraphed asset for lazy graph extraction when the
|
|
176
|
+
* `index.graph.lazyGraphExtraction` flag is on. Pure side-effect, fully
|
|
177
|
+
* best-effort: any failure (config, fs, db) is swallowed so curate never fails
|
|
178
|
+
* on it. NO LLM call and NO inline extraction — only a cheap queue insert.
|
|
179
|
+
* Default-off (flag unset) returns immediately = byte-identical behavior.
|
|
180
|
+
*/
|
|
181
|
+
function maybeEnqueueLazyGraph(assetPath) {
|
|
182
|
+
try {
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
if (getIndexPassConfig(config.index, "graph")?.lazyGraphExtraction !== true)
|
|
185
|
+
return;
|
|
186
|
+
const sources = resolveSourceEntries();
|
|
187
|
+
const source = findSourceForPath(assetPath, sources);
|
|
188
|
+
const stashRoot = source?.path;
|
|
189
|
+
if (!stashRoot)
|
|
190
|
+
return;
|
|
191
|
+
let raw;
|
|
192
|
+
try {
|
|
193
|
+
raw = fs.readFileSync(assetPath, "utf8");
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const body = parseFrontmatter(raw).content.trim();
|
|
199
|
+
if (!body)
|
|
200
|
+
return;
|
|
201
|
+
const bodyHash = computeBodyHash(body);
|
|
202
|
+
withIndexDb((db) => {
|
|
203
|
+
if (!hasGraphData(db, stashRoot, assetPath)) {
|
|
204
|
+
enqueueGraphExtraction(db, stashRoot, assetPath, bodyHash, 0);
|
|
205
|
+
}
|
|
206
|
+
}, { busyTimeoutMs: TELEMETRY_BUSY_TIMEOUT_MS });
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
rethrowIfTestIsolationError(err);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
186
212
|
function buildCuratedRegistryItem(query, hit) {
|
|
187
213
|
return {
|
|
188
214
|
source: "registry",
|
|
@@ -209,19 +235,19 @@ function buildCuratedPreview(shown, hit) {
|
|
|
209
235
|
function buildCuratedReason(query, type) {
|
|
210
236
|
switch (type) {
|
|
211
237
|
case "script":
|
|
212
|
-
return `
|
|
238
|
+
return `Strong runnable script match for "${query}".`;
|
|
213
239
|
case "command":
|
|
214
|
-
return `
|
|
240
|
+
return `Strong reusable command/template match for "${query}".`;
|
|
215
241
|
case "knowledge":
|
|
216
|
-
return `
|
|
242
|
+
return `Strong reference document match for "${query}".`;
|
|
217
243
|
case "skill":
|
|
218
|
-
return `
|
|
244
|
+
return `Strong instructions/workflow match for "${query}".`;
|
|
219
245
|
case "agent":
|
|
220
|
-
return `
|
|
246
|
+
return `Strong specialized agent prompt match for "${query}".`;
|
|
221
247
|
case "memory":
|
|
222
|
-
return `
|
|
248
|
+
return `Strong saved context match for "${query}".`;
|
|
223
249
|
default:
|
|
224
|
-
return `
|
|
250
|
+
return `Strong ${type} match for "${query}".`;
|
|
225
251
|
}
|
|
226
252
|
}
|
|
227
253
|
function buildCurateSummary(query, items) {
|
|
@@ -229,56 +255,108 @@ function buildCurateSummary(query, items) {
|
|
|
229
255
|
return `No curated assets were selected for "${query}".`;
|
|
230
256
|
}
|
|
231
257
|
const labels = items.map((item) => `${item.type}:${item.name}`);
|
|
232
|
-
return `Selected ${items.length}
|
|
258
|
+
return `Selected ${items.length} curated result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
|
|
233
259
|
}
|
|
234
|
-
function hasSearchResults(result) {
|
|
235
|
-
return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Extract a small set of fallback keywords when a prompt-style curate query
|
|
239
|
-
* returns no hits as a whole phrase.
|
|
240
|
-
*
|
|
241
|
-
* We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
|
|
242
|
-
* or common filler words so follow-up searches stay inexpensive while focusing
|
|
243
|
-
* on higher-signal terms.
|
|
244
|
-
*/
|
|
245
260
|
export function deriveCurateFallbackQueries(query) {
|
|
246
|
-
|
|
261
|
+
const normalizedWhole = query
|
|
262
|
+
.toLowerCase()
|
|
263
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
264
|
+
.trim();
|
|
265
|
+
const tokens = Array.from(new Set(query
|
|
247
266
|
.toLowerCase()
|
|
248
267
|
.split(/[^a-z0-9]+/)
|
|
249
268
|
.map((token) => token.trim())
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
.filter((token) => token.length > 0 &&
|
|
270
|
+
!CURATE_FALLBACK_FILTER_WORDS.has(token) &&
|
|
271
|
+
(token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH || CURATE_SHORT_FALLBACK_TOKENS.has(token))))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
|
|
272
|
+
if (tokens.length === 1 && tokens[0] === normalizedWhole)
|
|
273
|
+
return [];
|
|
274
|
+
return tokens;
|
|
253
275
|
}
|
|
254
276
|
export function mergeCurateSearchResponses(base, extras) {
|
|
255
|
-
|
|
277
|
+
// The base (full-query) ranking is the relevance signal — keyword fallback
|
|
278
|
+
// searches exist only to ADD recall when that ranking is thin, never to
|
|
279
|
+
// re-rank it. So we PRESERVE base order and APPEND fallback-only hits below
|
|
280
|
+
// it. A single-token fallback match on an exact title/path normalizes to a
|
|
281
|
+
// high FTS score, but those scores are not comparable to the full-query
|
|
282
|
+
// (hybrid) scores; re-sorting the union by raw score (the prior behaviour)
|
|
283
|
+
// let that keyword junk leapfrog the contextually-relevant full-query hits.
|
|
284
|
+
// Dup refs (present in both base and a fallback) keep their base POSITION but
|
|
285
|
+
// take the MAX score, since matching both the full query and a key term is a
|
|
286
|
+
// stronger relevance signal for the downstream score floor.
|
|
287
|
+
const bestExtraStashScore = new Map();
|
|
288
|
+
for (const result of extras) {
|
|
289
|
+
for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
|
|
290
|
+
const prev = bestExtraStashScore.get(hit.ref);
|
|
291
|
+
if (prev === undefined || (hit.score ?? 0) > prev)
|
|
292
|
+
bestExtraStashScore.set(hit.ref, hit.score ?? 0);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const baseRefs = new Set();
|
|
296
|
+
const baseStash = [];
|
|
256
297
|
for (const hit of base.hits.filter((entry) => entry.type !== "registry")) {
|
|
257
|
-
|
|
298
|
+
baseRefs.add(hit.ref);
|
|
299
|
+
const extraScore = bestExtraStashScore.get(hit.ref);
|
|
300
|
+
baseStash.push(extraScore !== undefined && extraScore > (hit.score ?? 0) ? { ...hit, score: extraScore } : hit);
|
|
258
301
|
}
|
|
302
|
+
const extraOnly = new Map();
|
|
259
303
|
for (const result of extras) {
|
|
260
304
|
for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
305
|
+
if (baseRefs.has(hit.ref))
|
|
306
|
+
continue;
|
|
307
|
+
const existing = extraOnly.get(hit.ref);
|
|
308
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0))
|
|
309
|
+
extraOnly.set(hit.ref, hit);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Fallback-only hits must rank BELOW every full-query hit through the rest of
|
|
313
|
+
// the pipeline. The downstream selector (`selectCuratedStashHits`) RE-SORTS by
|
|
314
|
+
// score and derives its relevance floor from the top score, so preserving
|
|
315
|
+
// order here is not enough — a single-token FTS match (normalized ~0.9) would
|
|
316
|
+
// otherwise become the leader and evict the contextual full-query memories.
|
|
317
|
+
// We therefore restamp fallback-only scores into a band strictly below the
|
|
318
|
+
// minimum base score (keeping their own relative order). When there are no
|
|
319
|
+
// base hits, fallback IS the result, so scores are kept as-is.
|
|
320
|
+
const sortedExtra = [...extraOnly.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
321
|
+
const minBaseScore = baseStash.length
|
|
322
|
+
? Math.min(...baseStash.map((hit) => hit.score ?? 0))
|
|
323
|
+
: Number.POSITIVE_INFINITY;
|
|
324
|
+
const cappedExtra = baseStash.length
|
|
325
|
+
? sortedExtra.map((hit, i) => ({ ...hit, score: minBaseScore - 1e-6 * (i + 1) }))
|
|
326
|
+
: sortedExtra;
|
|
327
|
+
const mergedHits = [...baseStash, ...cappedExtra];
|
|
328
|
+
// Registry hits are supplemental fill — same rule: base first (max score on
|
|
329
|
+
// dups), then fallback-only registry hits appended by score.
|
|
330
|
+
const bestExtraRegScore = new Map();
|
|
331
|
+
for (const result of extras) {
|
|
332
|
+
for (const hit of result.registryHits ?? []) {
|
|
333
|
+
const prev = bestExtraRegScore.get(hit.id);
|
|
334
|
+
if (prev === undefined || (hit.score ?? 0) > prev)
|
|
335
|
+
bestExtraRegScore.set(hit.id, hit.score ?? 0);
|
|
265
336
|
}
|
|
266
337
|
}
|
|
267
|
-
const
|
|
338
|
+
const baseRegIds = new Set();
|
|
339
|
+
const baseReg = [];
|
|
268
340
|
for (const hit of base.registryHits ?? []) {
|
|
269
|
-
|
|
341
|
+
baseRegIds.add(hit.id);
|
|
342
|
+
const extraScore = bestExtraRegScore.get(hit.id);
|
|
343
|
+
baseReg.push(extraScore !== undefined && extraScore > (hit.score ?? 0) ? { ...hit, score: extraScore } : hit);
|
|
270
344
|
}
|
|
345
|
+
const extraRegOnly = new Map();
|
|
271
346
|
for (const result of extras) {
|
|
272
347
|
for (const hit of result.registryHits ?? []) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
348
|
+
if (baseRegIds.has(hit.id))
|
|
349
|
+
continue;
|
|
350
|
+
const existing = extraRegOnly.get(hit.id);
|
|
351
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0))
|
|
352
|
+
extraRegOnly.set(hit.id, hit);
|
|
277
353
|
}
|
|
278
354
|
}
|
|
279
355
|
const warnings = Array.from(new Set([...(base.warnings ?? []), ...extras.flatMap((result) => result.warnings ?? [])]));
|
|
280
|
-
const
|
|
281
|
-
|
|
356
|
+
const mergedRegistryHits = [
|
|
357
|
+
...baseReg,
|
|
358
|
+
...[...extraRegOnly.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)),
|
|
359
|
+
];
|
|
282
360
|
return {
|
|
283
361
|
...base,
|
|
284
362
|
hits: mergedHits,
|
|
@@ -289,10 +367,10 @@ export function mergeCurateSearchResponses(base, extras) {
|
|
|
289
367
|
}
|
|
290
368
|
export async function searchForCuration(input) {
|
|
291
369
|
const initial = await akmSearch({ ...input, skipLogging: true });
|
|
292
|
-
if (
|
|
370
|
+
if (!shouldRunCurateFallback(initial, input.limit))
|
|
293
371
|
return initial;
|
|
294
372
|
const fallbackQueries = deriveCurateFallbackQueries(input.query);
|
|
295
|
-
if (fallbackQueries.length
|
|
373
|
+
if (fallbackQueries.length === 0)
|
|
296
374
|
return initial;
|
|
297
375
|
const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
|
|
298
376
|
query: token,
|
|
@@ -303,3 +381,241 @@ export async function searchForCuration(input) {
|
|
|
303
381
|
})));
|
|
304
382
|
return mergeCurateSearchResponses(initial, fallbackResults);
|
|
305
383
|
}
|
|
384
|
+
function parseCurateIntent(query) {
|
|
385
|
+
const lower = query.toLowerCase();
|
|
386
|
+
return {
|
|
387
|
+
executionHeavy: /(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower),
|
|
388
|
+
multiStep: /(plan|workflow|steps?|procedure|rollout|review|migration|release|checklist)/.test(lower),
|
|
389
|
+
delegation: /(agent|assistant|planner|reviewer|architect|prompt)/.test(lower),
|
|
390
|
+
recall: /(memory|context|recall|remember)/.test(lower),
|
|
391
|
+
reference: CURATE_REFERENCE_QUERY_RE.test(lower),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function computeCurateTypeNudge(type, intent) {
|
|
395
|
+
let nudge = 0;
|
|
396
|
+
if (intent.executionHeavy) {
|
|
397
|
+
if (type === "script")
|
|
398
|
+
nudge += 0.06;
|
|
399
|
+
else if (type === "command")
|
|
400
|
+
nudge += 0.04;
|
|
401
|
+
else if (type === "memory")
|
|
402
|
+
nudge -= 0.04;
|
|
403
|
+
}
|
|
404
|
+
if (intent.multiStep) {
|
|
405
|
+
if (type === "workflow")
|
|
406
|
+
nudge += 0.06;
|
|
407
|
+
else if (type === "skill")
|
|
408
|
+
nudge += 0.04;
|
|
409
|
+
else if (type === "knowledge")
|
|
410
|
+
nudge -= 0.02;
|
|
411
|
+
}
|
|
412
|
+
if (intent.delegation && type === "agent")
|
|
413
|
+
nudge += 0.06;
|
|
414
|
+
if (intent.recall && type === "memory")
|
|
415
|
+
nudge += 0.08;
|
|
416
|
+
if (intent.reference) {
|
|
417
|
+
if (type === "knowledge")
|
|
418
|
+
nudge += 0.05;
|
|
419
|
+
else if (type === "skill")
|
|
420
|
+
nudge += 0.02;
|
|
421
|
+
}
|
|
422
|
+
return nudge;
|
|
423
|
+
}
|
|
424
|
+
function getCurateFamily(ref) {
|
|
425
|
+
try {
|
|
426
|
+
const parsed = parseAssetRef(ref);
|
|
427
|
+
if (parsed.type === "skill") {
|
|
428
|
+
return { key: parsed.name, role: "root" };
|
|
429
|
+
}
|
|
430
|
+
if (parsed.type !== "knowledge")
|
|
431
|
+
return undefined;
|
|
432
|
+
const match = /^skills\/(.+?)\/references\/(.+)$/.exec(parsed.name);
|
|
433
|
+
if (!match)
|
|
434
|
+
return undefined;
|
|
435
|
+
return {
|
|
436
|
+
key: match[1],
|
|
437
|
+
role: "reference",
|
|
438
|
+
topicTokens: match[2]
|
|
439
|
+
.split(/[^a-z0-9]+/i)
|
|
440
|
+
.map((token) => token.trim().toLowerCase())
|
|
441
|
+
.filter(Boolean),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function annotateCurateHit(query, hit, index, intent) {
|
|
449
|
+
const rawScore = hit.score ?? 0;
|
|
450
|
+
const family = getCurateFamily(hit.ref);
|
|
451
|
+
let adjustedScore = rawScore + computeCurateTypeNudge(hit.type, intent);
|
|
452
|
+
if (family?.role === "root" && !isNarrowReferenceFamilyQuery(query, family))
|
|
453
|
+
adjustedScore += 0.07;
|
|
454
|
+
if (family?.role === "reference" && isNarrowReferenceFamilyQuery(query, family))
|
|
455
|
+
adjustedScore += 0.07;
|
|
456
|
+
return {
|
|
457
|
+
hit,
|
|
458
|
+
rawScore,
|
|
459
|
+
adjustedScore,
|
|
460
|
+
originalIndex: index,
|
|
461
|
+
family,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function compareCurateHits(a, b) {
|
|
465
|
+
const rawDiff = b.rawScore - a.rawScore;
|
|
466
|
+
if (Math.abs(rawDiff) > CURATE_CLOSE_SCORE_BAND)
|
|
467
|
+
return rawDiff;
|
|
468
|
+
const adjustedDiff = b.adjustedScore - a.adjustedScore;
|
|
469
|
+
if (adjustedDiff !== 0)
|
|
470
|
+
return adjustedDiff;
|
|
471
|
+
if (rawDiff !== 0)
|
|
472
|
+
return rawDiff;
|
|
473
|
+
return a.originalIndex - b.originalIndex;
|
|
474
|
+
}
|
|
475
|
+
function passesCurateScoreFloor(hit, leaderScore) {
|
|
476
|
+
if (leaderScore === undefined)
|
|
477
|
+
return true;
|
|
478
|
+
return hit.rawScore >= Math.max(CURATE_TAIL_SCORE_FLOOR, leaderScore * CURATE_RELATIVE_SCORE_FLOOR);
|
|
479
|
+
}
|
|
480
|
+
function isNarrowReferenceFamilyQuery(query, family) {
|
|
481
|
+
if (!family || family.role !== "reference")
|
|
482
|
+
return false;
|
|
483
|
+
const lower = query.toLowerCase();
|
|
484
|
+
if (CURATE_REFERENCE_QUERY_RE.test(lower))
|
|
485
|
+
return true;
|
|
486
|
+
return family.topicTokens.some((token) => token.length >= 3 && lower.includes(token));
|
|
487
|
+
}
|
|
488
|
+
function appendCurateSupportRef(supportRefsByRef, ownerRef, supportRef) {
|
|
489
|
+
const existing = supportRefsByRef.get(ownerRef) ?? [];
|
|
490
|
+
if (existing.some((entry) => entry.ref === supportRef.ref))
|
|
491
|
+
return;
|
|
492
|
+
supportRefsByRef.set(ownerRef, [...existing, supportRef]);
|
|
493
|
+
}
|
|
494
|
+
function selectCuratedStashHits(query, hits, limit) {
|
|
495
|
+
const intent = parseCurateIntent(query);
|
|
496
|
+
const collapsed = collapseCurateFamilies(query, hits);
|
|
497
|
+
const ranked = collapsed.hits
|
|
498
|
+
.map(({ hit, originalIndex }) => annotateCurateHit(query, hit, originalIndex, intent))
|
|
499
|
+
.sort(compareCurateHits);
|
|
500
|
+
const selected = [];
|
|
501
|
+
const supportRefsByRef = collapsed.supportRefsByRef;
|
|
502
|
+
let leaderScore;
|
|
503
|
+
for (const candidate of ranked) {
|
|
504
|
+
if (!passesCurateScoreFloor(candidate, leaderScore))
|
|
505
|
+
continue;
|
|
506
|
+
selected.push(candidate);
|
|
507
|
+
if (leaderScore === undefined)
|
|
508
|
+
leaderScore = candidate.rawScore;
|
|
509
|
+
if (selected.length >= limit)
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
return { selected: selected.map((entry) => entry.hit), supportRefsByRef };
|
|
513
|
+
}
|
|
514
|
+
function collapseCurateFamilies(query, hits) {
|
|
515
|
+
const passthrough = [];
|
|
516
|
+
const supportRefsByRef = new Map();
|
|
517
|
+
const groups = new Map();
|
|
518
|
+
for (const [index, hit] of hits.entries()) {
|
|
519
|
+
const family = getCurateFamily(hit.ref);
|
|
520
|
+
if (!family) {
|
|
521
|
+
passthrough.push({ hit, originalIndex: index });
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const group = groups.get(family.key) ?? { references: [] };
|
|
525
|
+
if (family.role === "root") {
|
|
526
|
+
if (!group.root)
|
|
527
|
+
group.root = { hit, originalIndex: index };
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
group.references.push({ hit, originalIndex: index });
|
|
531
|
+
}
|
|
532
|
+
groups.set(family.key, group);
|
|
533
|
+
}
|
|
534
|
+
const collapsedFamilies = [];
|
|
535
|
+
for (const group of groups.values()) {
|
|
536
|
+
const bestReference = group.references[0];
|
|
537
|
+
const representative = group.root && !isNarrowReferenceFamilyQuery(query, getCurateFamily(bestReference?.hit.ref ?? group.root.hit.ref))
|
|
538
|
+
? group.root
|
|
539
|
+
: (bestReference ?? group.root);
|
|
540
|
+
if (!representative)
|
|
541
|
+
continue;
|
|
542
|
+
collapsedFamilies.push(representative);
|
|
543
|
+
const supportCandidates = [group.root, ...group.references].filter((entry) => {
|
|
544
|
+
return entry !== undefined && entry.hit.ref !== representative.hit.ref;
|
|
545
|
+
});
|
|
546
|
+
for (const support of supportCandidates) {
|
|
547
|
+
appendCurateSupportRef(supportRefsByRef, representative.hit.ref, {
|
|
548
|
+
ref: support.hit.ref,
|
|
549
|
+
type: support.hit.type,
|
|
550
|
+
reason: "Related family asset to inspect next.",
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
hits: [...passthrough, ...collapsedFamilies].sort((a, b) => a.originalIndex - b.originalIndex),
|
|
556
|
+
supportRefsByRef,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function preferBroadRootRepresentative(query, selected, allHits, supportRefsByRef) {
|
|
560
|
+
const first = selected[0];
|
|
561
|
+
if (!first)
|
|
562
|
+
return { selected, supportRefsByRef };
|
|
563
|
+
const match = /^knowledge:skills\/(.+?)\/references\/(.+)$/.exec(first.ref);
|
|
564
|
+
if (!match)
|
|
565
|
+
return { selected, supportRefsByRef };
|
|
566
|
+
const lower = query.toLowerCase();
|
|
567
|
+
const topicTokens = match[2].split(/[^a-z0-9]+/i).filter(Boolean);
|
|
568
|
+
const wantsReference = CURATE_REFERENCE_QUERY_RE.test(lower) ||
|
|
569
|
+
topicTokens.some((token) => token.length >= 3 && lower.includes(token.toLowerCase()));
|
|
570
|
+
if (wantsReference)
|
|
571
|
+
return { selected, supportRefsByRef };
|
|
572
|
+
const rootRef = `skill:${match[1]}`;
|
|
573
|
+
const rootHit = allHits.find((hit) => hit.ref === rootRef);
|
|
574
|
+
if (!rootHit)
|
|
575
|
+
return { selected, supportRefsByRef };
|
|
576
|
+
const next = [rootHit, ...selected.filter((hit) => hit.ref !== first.ref && hit.ref !== rootRef)];
|
|
577
|
+
const merged = new Map(supportRefsByRef);
|
|
578
|
+
const priorSupport = merged.get(first.ref) ?? [];
|
|
579
|
+
for (const entry of priorSupport)
|
|
580
|
+
appendCurateSupportRef(merged, rootRef, entry);
|
|
581
|
+
appendCurateSupportRef(merged, rootRef, {
|
|
582
|
+
ref: first.ref,
|
|
583
|
+
type: first.type,
|
|
584
|
+
reason: "Related family asset to inspect next.",
|
|
585
|
+
});
|
|
586
|
+
merged.delete(first.ref);
|
|
587
|
+
return { selected: next, supportRefsByRef: merged };
|
|
588
|
+
}
|
|
589
|
+
function mergeCurateSupportRefs(seeded, relatedHits, selectedRefs, ownerRef) {
|
|
590
|
+
const merged = [];
|
|
591
|
+
for (const entry of seeded) {
|
|
592
|
+
if (entry.ref === ownerRef || selectedRefs.has(entry.ref))
|
|
593
|
+
continue;
|
|
594
|
+
if (merged.some((existing) => existing.ref === entry.ref))
|
|
595
|
+
continue;
|
|
596
|
+
merged.push(entry);
|
|
597
|
+
if (merged.length >= MAX_CURATE_SUPPORT_REFS)
|
|
598
|
+
return merged;
|
|
599
|
+
}
|
|
600
|
+
if (!Array.isArray(relatedHits))
|
|
601
|
+
return merged;
|
|
602
|
+
for (const hit of relatedHits) {
|
|
603
|
+
if (!hit.ref || hit.ref === ownerRef || selectedRefs.has(hit.ref))
|
|
604
|
+
continue;
|
|
605
|
+
if (merged.some((existing) => existing.ref === hit.ref))
|
|
606
|
+
continue;
|
|
607
|
+
merged.push({ ref: hit.ref, type: hit.type, reason: "Related asset via shared entities." });
|
|
608
|
+
if (merged.length >= MAX_CURATE_SUPPORT_REFS)
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
return merged;
|
|
612
|
+
}
|
|
613
|
+
function shouldRunCurateFallback(initial, desiredCount) {
|
|
614
|
+
const stashHits = initial.hits.filter((hit) => hit.type !== "registry");
|
|
615
|
+
if (stashHits.length === 0)
|
|
616
|
+
return true;
|
|
617
|
+
const topScore = stashHits[0]?.score ?? 0;
|
|
618
|
+
const strongFloor = Math.max(CURATE_FALLBACK_STRONG_SCORE_FLOOR, topScore * CURATE_RELATIVE_SCORE_FLOOR);
|
|
619
|
+
const strongCount = stashHits.filter((hit) => (hit.score ?? 0) >= strongFloor).length;
|
|
620
|
+
return !(topScore >= CURATE_FALLBACK_TOP_SCORE_THRESHOLD && strongCount >= Math.min(2, desiredCount));
|
|
621
|
+
}
|