akm-cli 0.10.261016-beta → 0.10.261017-beta
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 +100 -0
- package/README.md +8 -10
- package/SECURITY.md +7 -99
- package/STABILITY.md +57 -98
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -2
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +11 -17
- package/dist/cli.js +32 -41
- package/dist/commands/feedback-cli.js +5 -4
- package/dist/commands/health.js +15 -16
- package/dist/commands/proposal/proposal-cli.js +1 -1
- package/dist/commands/read/registry-search.js +4 -1
- package/dist/commands/read/search.js +6 -6
- package/dist/commands/sources/self-update.js +9 -5
- package/dist/commands/tasks/tasks.js +12 -13
- package/dist/core/adapter/adapters/agent-skills-adapter.js +7 -7
- package/dist/core/adapter/adapters/akm-task-adapter.js +1 -1
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/adapters/claude-adapter.js +1 -1
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +1 -1
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +1 -1
- package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -1
- package/dist/core/bundle-rename.js +17 -7
- package/dist/indexer/bundle-identity-guard.js +6 -6
- package/dist/indexer/index-written-assets.js +3 -3
- package/dist/indexer/indexer.js +12 -8
- package/dist/indexer/materialize-embeddings.js +12 -2
- package/dist/indexer/passes/metadata.js +103 -46
- package/dist/indexer/search/ranking.js +7 -3
- package/dist/indexer/search/search-fields.js +1 -1
- package/dist/indexer/usage/usage-events.js +30 -8
- package/dist/indexer/walk/walker.js +1 -1
- package/dist/output/shapes/helpers.js +3 -3
- package/dist/output/text/command-format.js +5 -6
- package/dist/scripts/akm-migrate-node.js +31 -3
- package/dist/scripts/akm-migrate.js +31 -3
- package/dist/sources/providers/git-provider.js +2 -4
- package/dist/sources/providers/provider-utils.js +8 -13
- package/dist/sources/snapshot-fetchers/website-ingest.js +6 -7
- package/dist/storage/repositories/events-repository.js +15 -0
- package/dist/storage/repositories/improve-ledger-repository.js +13 -0
- package/dist/storage/repositories/index-entries-repository.js +43 -5
- package/dist/storage/repositories/outcome-repository.js +12 -0
- package/dist/storage/repositories/proposals-repository.js +5 -5
- package/dist/storage/repositories/salience-repository.js +13 -0
- package/dist/storage/repositories/task-history-repository.js +8 -7
- package/dist/storage/repositories/workflow-runs-repository.js +10 -8
- package/docs/README.md +24 -60
- package/docs/agents/README.md +3 -8
- package/docs/architecture/README.md +18 -20
- package/docs/architecture/adapters.md +40 -43
- package/docs/architecture/architecture.md +171 -151
- package/docs/architecture/improvement.md +95 -91
- package/docs/architecture/internals/classification.md +101 -119
- package/docs/architecture/internals/fresh-host-rebuild-runbook.md +33 -35
- package/docs/architecture/internals/health-advisories.md +59 -46
- package/docs/architecture/internals/improve-workflow.md +166 -693
- package/docs/architecture/internals/indexing.md +65 -465
- package/docs/architecture/internals/storage-locations.md +301 -848
- package/docs/architecture/persisted-data-compat.md +6 -6
- package/docs/architecture/specs/okf-support.md +67 -95
- package/docs/architecture/specs/ref.md +67 -125
- package/docs/architecture/testing/testing-workflow.md +107 -167
- package/docs/architecture/workflow-engine.md +34 -33
- package/docs/guides/README.md +5 -5
- package/docs/guides/author-bundles.md +72 -98
- package/docs/guides/author-workflows.md +11 -18
- package/docs/guides/bundles.md +91 -123
- package/docs/guides/capture-knowledge.md +10 -9
- package/docs/guides/concepts.md +45 -30
- package/docs/guides/discover-and-load.md +39 -32
- package/docs/guides/getting-started.md +24 -24
- package/docs/guides/improve-the-library.md +98 -80
- package/docs/guides/recipes/headless-install.md +4 -4
- package/docs/guides/recipes/website-source.md +9 -5
- package/docs/guides/run-workflows.md +10 -11
- package/docs/guides/scheduling.md +11 -17
- package/docs/guides/use-with-any-agent.md +15 -14
- package/docs/guides/wikis.md +6 -9
- package/docs/integration/bundling-akm.md +21 -26
- package/docs/maintainers/README.md +2 -2
- package/docs/maintainers/local-development.md +3 -16
- package/docs/migration/README.md +2 -1
- package/docs/migration/release-notes/0.10.0.md +167 -22
- package/docs/migration/release-notes/README.md +2 -2
- package/docs/migration/v0.9-to-v0.10.md +47 -7
- package/docs/product-surface.md +10 -8
- package/docs/reference/README.md +26 -15
- package/docs/reference/asset-types.md +11 -12
- package/docs/reference/bundle-types.md +6 -5
- package/docs/reference/cli.md +52 -28
- package/docs/reference/configuration.md +46 -60
- package/docs/reference/data-and-telemetry.md +204 -421
- package/docs/reference/env-and-secrets.md +80 -83
- package/docs/reference/memory.md +1 -1
- package/docs/reference/refs.md +23 -36
- package/docs/reference/registry.md +138 -348
- package/docs/reference/supported-formats.md +8 -8
- package/docs/reference/tasks.md +13 -19
- package/docs/reference/website-sources.md +23 -15
- package/docs/reference/workflow-schema.md +28 -47
- package/docs/reference/workflows.md +33 -37
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* Conformance oracle (authored, DO NOT modify): fixture
|
|
27
27
|
* `tests/fixtures/bundles/dotenv/` + goldens
|
|
28
|
-
* `tests/fixtures/format-family-goldens/dotenv/{recognition,
|
|
28
|
+
* `tests/fixtures/format-family-goldens/dotenv/{recognition,lint,renderer}.json`.
|
|
29
29
|
*/
|
|
30
30
|
import fs from "node:fs";
|
|
31
31
|
import path from "node:path";
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Conformance oracle (authored, DO NOT modify): fixture
|
|
21
21
|
* `tests/fixtures/bundles/generic-files/` + goldens
|
|
22
|
-
* `tests/fixtures/format-family-goldens/generic-files/{recognition,
|
|
22
|
+
* `tests/fixtures/format-family-goldens/generic-files/{recognition,lint,renderer}.json`.
|
|
23
23
|
*/
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { parseFrontmatter } from "../../asset/frontmatter.js";
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Conformance oracle (authored, DO NOT modify): fixture
|
|
13
13
|
* `tests/fixtures/bundles/llm-wiki/` + goldens
|
|
14
|
-
* `tests/fixtures/format-family-goldens/llm-wiki/{recognition,
|
|
14
|
+
* `tests/fixtures/format-family-goldens/llm-wiki/{recognition,renderer,lint}.json`.
|
|
15
15
|
* The adapter is built to MATCH those goldens (driven by
|
|
16
16
|
* `tests/core/adapter/llm-wiki-adapter.test.ts`).
|
|
17
17
|
*
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Conformance oracle (authored, DO NOT modify): fixture
|
|
18
18
|
* `tests/fixtures/bundles/opencode/` + goldens
|
|
19
|
-
* `tests/fixtures/format-family-goldens/opencode/{recognition,
|
|
19
|
+
* `tests/fixtures/format-family-goldens/opencode/{recognition,lint,renderer}.json`.
|
|
20
20
|
*/
|
|
21
21
|
import fs from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Conformance oracle (authored, DO NOT modify): fixture
|
|
24
24
|
* `tests/fixtures/bundles/website-snapshot/` + goldens
|
|
25
|
-
* `tests/fixtures/format-family-goldens/website-snapshot/{recognition,
|
|
25
|
+
* `tests/fixtures/format-family-goldens/website-snapshot/{recognition,lint,renderer}.json`.
|
|
26
26
|
*/
|
|
27
27
|
import fs from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* bundle's key. A bundle id is a mass identity prefix: every durable ref this
|
|
7
7
|
* tool minted (`entries.item_ref`/`bundle_id`, `proposals.ref`, a pending
|
|
8
8
|
* proposal's `proposedTarget.source`, a workflow task_history row's
|
|
9
|
-
* `target_ref`, a `workflow_runs.workflow_ref`, `
|
|
9
|
+
* `target_ref`, a `workflow_runs.workflow_ref`, a `usage_events.entry_ref`, an
|
|
10
|
+
* `events.ref`, an `asset_salience`/`asset_outcome` `asset_ref`, an
|
|
11
|
+
* `improve_ledger.ref`, `scheduler.enabled[].ref`) carries it, and a hand-edited
|
|
10
12
|
* `bundles` key strands all of it (see `src/indexer/bundle-identity-guard.ts`
|
|
11
13
|
* §11.5). This module is the rekey transaction that guard's docstring points
|
|
12
14
|
* to.
|
|
@@ -20,17 +22,17 @@
|
|
|
20
22
|
* `akm mv`/editing territory, not a config-identity rename. `--dry-run`
|
|
21
23
|
* (and every real run) lists the indexed files that still spell the old
|
|
22
24
|
* `<old>//` prefix so the operator can follow up.
|
|
23
|
-
*
|
|
24
|
-
* Left behind on purpose: `usage_events`/`asset_salience`/`asset_outcome`
|
|
25
|
-
* keyed by the old bundle prefix (learned utility/outcome history) — outside
|
|
26
|
-
* this item's scope; `warnOnBundleRenameDrift` already documents this class
|
|
27
|
-
* of gap and `akm index --full` re-mints under the new id.
|
|
28
25
|
*/
|
|
29
26
|
import fs from "node:fs";
|
|
27
|
+
import { renameUsageEventRefs } from "../indexer/usage/usage-events.js";
|
|
30
28
|
import { readLockfile, renameLockEntry } from "../integrations/lockfile.js";
|
|
29
|
+
import { renameEventRefs } from "../storage/repositories/events-repository.js";
|
|
30
|
+
import { renameImproveLedgerRefs } from "../storage/repositories/improve-ledger-repository.js";
|
|
31
31
|
import { closeDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../storage/repositories/index-connection.js";
|
|
32
32
|
import { getFilePathsByBundle, renameEntriesBundleId } from "../storage/repositories/index-entries-repository.js";
|
|
33
|
+
import { renameAssetOutcomeRefs } from "../storage/repositories/outcome-repository.js";
|
|
33
34
|
import { countProposalsForBundleRename, renameProposalsBundleRef } from "../storage/repositories/proposals-repository.js";
|
|
35
|
+
import { renameAssetSalienceRefs } from "../storage/repositories/salience-repository.js";
|
|
34
36
|
import { countTaskHistoryTargetRefs, renameTaskHistoryTargetRefs, } from "../storage/repositories/task-history-repository.js";
|
|
35
37
|
import { countWorkflowRunRefsForRename, renameWorkflowRunRefs } from "../storage/repositories/workflow-runs-repository.js";
|
|
36
38
|
import { selectBackend } from "../tasks/backends/index.js";
|
|
@@ -262,12 +264,20 @@ export async function renameBundle(oldId, newId, options = {}, deps) {
|
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
// State: proposals.ref, a pending proposal's proposedTarget.source,
|
|
265
|
-
// workflow task_history.target_ref,
|
|
267
|
+
// workflow task_history.target_ref, workflow_runs.workflow_ref, and the
|
|
268
|
+
// learned history keyed by item ref: usage_events.entry_ref (search ranking
|
|
269
|
+
// counts feedback by it), events.ref and improve_ledger.ref (improve reads
|
|
270
|
+
// feedback and its own attempts by them), and asset_salience/asset_outcome.asset_ref.
|
|
266
271
|
if (fs.existsSync(getStateDbPath())) {
|
|
267
272
|
withStateDb((db) => {
|
|
268
273
|
renameProposalsBundleRef(db, oldId, newId);
|
|
269
274
|
renameTaskHistoryTargetRefs(db, oldId, newId);
|
|
270
275
|
renameWorkflowRunRefs(db, oldId, newId);
|
|
276
|
+
renameUsageEventRefs(db, oldId, newId);
|
|
277
|
+
renameEventRefs(db, oldId, newId);
|
|
278
|
+
renameImproveLedgerRefs(db, oldId, newId);
|
|
279
|
+
renameAssetSalienceRefs(db, oldId, newId);
|
|
280
|
+
renameAssetOutcomeRefs(db, oldId, newId);
|
|
271
281
|
});
|
|
272
282
|
}
|
|
273
283
|
// Scheduler: remove native rows still naming `<old>//` (see
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Bundle-rename startup guard.
|
|
6
6
|
*
|
|
7
7
|
* Renaming a workspace bundle id is a mass identity migration (every ref in the
|
|
8
8
|
* bundle changes without any file moving) and MUST go through the
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* every ref-keyed state table atomically. If a user instead hand-edits the
|
|
11
11
|
* `bundles` KEY in config.json, the durable index/state rows still carry the OLD
|
|
12
12
|
* bundle prefix while the config names the NEW id. Silently re-minting fresh
|
|
13
|
-
* state under the new id would strand the old rows
|
|
14
|
-
* and
|
|
13
|
+
* state under the new id would strand the old rows, so this guard detects it
|
|
14
|
+
* and warns.
|
|
15
15
|
*
|
|
16
16
|
* This guard implements the cheap heuristic form (WARN, never silently re-mint):
|
|
17
17
|
* it compares the configured bundle ids against the DISTINCT `bundle_id`
|
|
18
|
-
* (item_ref bundle prefix) values already persisted in the index —
|
|
19
|
-
*
|
|
18
|
+
* (item_ref bundle prefix) values already persisted in the index — the same
|
|
19
|
+
* prefixes key the state.db usage/salience rows.
|
|
20
20
|
* The hand-rename signature is: a configured bundle id with NO indexed rows
|
|
21
21
|
* co-existing with indexed rows under a bundle id that is no longer configured.
|
|
22
22
|
*
|
|
@@ -70,7 +70,7 @@ function indexedBundlePaths(dbPath) {
|
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* Warn (once per process) when the config's bundle ids and the index's bundle
|
|
73
|
-
* prefixes show the hand-rename signature
|
|
73
|
+
* prefixes show the hand-rename signature. No-op for an old-shape config
|
|
74
74
|
* (no `bundles`), an absent/empty index, or when the guard already settled.
|
|
75
75
|
*/
|
|
76
76
|
export function warnOnBundleRenameDrift(config = loadConfig()) {
|
|
@@ -31,7 +31,7 @@ import { warn, warnVerbose } from "../core/warn.js";
|
|
|
31
31
|
import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
32
32
|
import { deleteEntriesByIds, getEntryCount, upsertEntry } from "../storage/repositories/index-entries-repository.js";
|
|
33
33
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
34
|
-
import { generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
|
|
34
|
+
import { embeddingInputCap, generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
|
|
35
35
|
import { withFileSize } from "./passes/metadata.js";
|
|
36
36
|
import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
37
37
|
import { buildFileContext } from "./walk/file-context.js";
|
|
@@ -153,6 +153,7 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
|
|
|
153
153
|
db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
|
|
154
154
|
if (getEntryCount(db) === 0)
|
|
155
155
|
return true;
|
|
156
|
+
const config = loadConfig();
|
|
156
157
|
const targetEntryIds = new Set();
|
|
157
158
|
let mutated = false;
|
|
158
159
|
db.transaction(() => {
|
|
@@ -194,12 +195,11 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
|
|
|
194
195
|
.prepare("SELECT id FROM entries WHERE file_path = ? AND item_ref <> ?")
|
|
195
196
|
.all(file, provenance.itemRef);
|
|
196
197
|
deleteEntriesByIds(db, supersededIds.map((row) => row.id));
|
|
197
|
-
targetEntryIds.add(upsertEntry(db, file, entryWithSize, provenance, contentHash));
|
|
198
|
+
targetEntryIds.add(upsertEntry(db, file, entryWithSize, provenance, contentHash, embeddingInputCap(config)));
|
|
198
199
|
mutated = true;
|
|
199
200
|
}
|
|
200
201
|
})();
|
|
201
202
|
if (mutated) {
|
|
202
|
-
const config = loadConfig();
|
|
203
203
|
await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
|
|
204
204
|
publishTargetedEmbeddingMeta(db, config);
|
|
205
205
|
}
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -28,9 +28,9 @@ import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from
|
|
|
28
28
|
import { reclassifyIndexDbContention } from "./index-db-contention.js";
|
|
29
29
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
30
30
|
import { indexedPathMatchesOwner, resolveConceptOwner } from "./lookup/adapter-concept-owner.js";
|
|
31
|
-
import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
31
|
+
import { embeddingInputCap, generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
32
32
|
import { canUseIncrementalSkip, computeDirFingerprint, getCachedDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
|
|
33
|
-
import { isWorkflowSkipWarning, withFileSize } from "./passes/metadata.js";
|
|
33
|
+
import { CONTENT_PROJECTION_VERSION, isWorkflowSkipWarning, withFileSize, } from "./passes/metadata.js";
|
|
34
34
|
import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
35
35
|
import { purgeOldUsageEvents, USAGE_EVENT_RETENTION_DAYS } from "./usage/usage-events.js";
|
|
36
36
|
import { SEMANTIC_SEARCH_META_KEY, } from "./walk/index-context.js";
|
|
@@ -446,7 +446,7 @@ async function akmIndexReal(options) {
|
|
|
446
446
|
// …then write it in one transaction: `item_ref = <bundle>//<conceptId>`
|
|
447
447
|
// plus canonical component/adapter provenance per source root.
|
|
448
448
|
const bundleByRoot = buildIndexedSourceOwners(sources);
|
|
449
|
-
persistDirRecords(db, dirRecords, warnings, bundleByRoot);
|
|
449
|
+
persistDirRecords(db, dirRecords, warnings, bundleByRoot, embeddingInputCap(config));
|
|
450
450
|
onProgress({
|
|
451
451
|
phase: "scan",
|
|
452
452
|
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
@@ -819,7 +819,9 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
|
|
|
819
819
|
warn(`Skipping component "${component.id}": unknown adapter id "${component.adapter}".`);
|
|
820
820
|
continue;
|
|
821
821
|
}
|
|
822
|
-
|
|
822
|
+
// The content projection's version is part of the variant: a change to what Markdown bodies index re-projects
|
|
823
|
+
// every directory once instead of keeping the old text for unchanged files.
|
|
824
|
+
const indexVariant = `${adapter.id}@${adapter.version}+content${CONTENT_PROJECTION_VERSION}`;
|
|
823
825
|
for (const removal of removals) {
|
|
824
826
|
dirRecords.push(removal);
|
|
825
827
|
scannedDirs++;
|
|
@@ -938,7 +940,7 @@ function requiresWorkflowSourcePreflight(ctxs) {
|
|
|
938
940
|
});
|
|
939
941
|
}
|
|
940
942
|
/** Phase 2 (sync): write all pre-generated scan records inside a single transaction. */
|
|
941
|
-
function persistDirRecords(db, dirRecords, warnings, bundleByRoot) {
|
|
943
|
+
function persistDirRecords(db, dirRecords, warnings, bundleByRoot, maxInputTokens) {
|
|
942
944
|
const deletedUsageEntryIds = new Set();
|
|
943
945
|
const findPersisted = db.prepare("SELECT id, content_hash, file_path, adapter_id FROM entries WHERE item_ref = ?");
|
|
944
946
|
const insertTransaction = db.transaction(() => {
|
|
@@ -1038,7 +1040,7 @@ function persistDirRecords(db, dirRecords, warnings, bundleByRoot) {
|
|
|
1038
1040
|
if (unchanged) {
|
|
1039
1041
|
continue;
|
|
1040
1042
|
}
|
|
1041
|
-
upsertEntry(db, entryPath, withFileSize(entry, entryPath), provenance, contentHash);
|
|
1043
|
+
upsertEntry(db, entryPath, withFileSize(entry, entryPath), provenance, contentHash, maxInputTokens);
|
|
1042
1044
|
}
|
|
1043
1045
|
}
|
|
1044
1046
|
// Prune the departed rows: everything under this dir NOT re-upserted above
|
|
@@ -1332,8 +1334,10 @@ export function recomputeUtilityScores(db, stateDb, onlyEntryId) {
|
|
|
1332
1334
|
// (Chunk-8 WI-8.3); its table is created by state migration 020.
|
|
1333
1335
|
if (!scoped)
|
|
1334
1336
|
purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS);
|
|
1335
|
-
//
|
|
1336
|
-
//
|
|
1337
|
+
// Decay is raised to the days since the last run, floored at one day. Every
|
|
1338
|
+
// run therefore applies at least one full round of EMA decay (so runs less
|
|
1339
|
+
// than a day apart each decay a full round), and a longer gap applies
|
|
1340
|
+
// proportionally more rounds. The first run counts as one day.
|
|
1337
1341
|
const lastComputedAt = getMeta(db, "last_utility_computed_at");
|
|
1338
1342
|
let elapsedDays = 1; // default for first run
|
|
1339
1343
|
if (lastComputedAt) {
|
|
@@ -32,6 +32,13 @@ export function deriveSemanticProviderFingerprint(embedding) {
|
|
|
32
32
|
}
|
|
33
33
|
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}${template}`;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The most estimated tokens of an entry's search text that are embedded
|
|
37
|
+
* (`embedding.maxInputTokens`); the rest of the text never reaches the embedder.
|
|
38
|
+
*/
|
|
39
|
+
export function embeddingInputCap(config) {
|
|
40
|
+
return effectiveEmbedding(config)?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
41
|
+
}
|
|
35
42
|
/**
|
|
36
43
|
* The heartbeat text emitted every 15s while a provider request is in
|
|
37
44
|
* flight, and by default (not `--verbose`-only) since silence indistinguishable
|
|
@@ -103,7 +110,7 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
103
110
|
let targetEntryIds = entryIds;
|
|
104
111
|
/** Set only when every entry is (re)embedded, so the up-front "Re-embedding N entries" line names why. */
|
|
105
112
|
let rebuildReason;
|
|
106
|
-
const maxInputTokens =
|
|
113
|
+
const maxInputTokens = embeddingInputCap(config);
|
|
107
114
|
if (opts?.forceReembed) {
|
|
108
115
|
// The explicit operator override: purge and re-embed everything. The model
|
|
109
116
|
// is recorded in the SAME transaction as the purge, before any embedding
|
|
@@ -171,9 +178,12 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
171
178
|
const message = `[embed] Re-embedding ${pendingEntries.length} entr${pendingEntries.length === 1 ? "y" : "ies"} because ${rebuildReason}`;
|
|
172
179
|
onProgress({ phase: "embeddings", message });
|
|
173
180
|
}
|
|
181
|
+
// Names the size of the work before the first request, so a first index after an upgrade that
|
|
182
|
+
// re-embeds part of the corpus is not mistaken for a hang.
|
|
183
|
+
const unchangedCount = Math.max(0, getEmbeddableEntryCount(db) - pendingEntries.length);
|
|
174
184
|
onProgress({
|
|
175
185
|
phase: "embeddings",
|
|
176
|
-
message: `
|
|
186
|
+
message: `Embedding ${pendingEntries.length.toLocaleString()} changed entr${pendingEntries.length === 1 ? "y" : "ies"} (${unchangedCount.toLocaleString()} unchanged)`,
|
|
177
187
|
});
|
|
178
188
|
// Per-document sizes; the provider's actual request grouping is reported
|
|
179
189
|
// per batch below (and each skipped document is listed at the end).
|
|
@@ -136,7 +136,7 @@ export function validateStashEntry(entry) {
|
|
|
136
136
|
const sources = normalizeNonEmptyStringList(e.sources);
|
|
137
137
|
if (sources)
|
|
138
138
|
result.sources = sources;
|
|
139
|
-
//
|
|
139
|
+
// `category` must survive the projection. Non-string values are
|
|
140
140
|
// dropped, not coerced.
|
|
141
141
|
if (typeof e.category === "string" && e.category.trim().length > 0) {
|
|
142
142
|
result.category = e.category.trim();
|
|
@@ -346,7 +346,7 @@ export function applyCuratedFrontmatter(entry, fmData) {
|
|
|
346
346
|
const quality = asNonEmptyString(fmData.quality);
|
|
347
347
|
if (quality)
|
|
348
348
|
entry.quality = normalizeQuality(quality);
|
|
349
|
-
//
|
|
349
|
+
// Capture the `category:` frontmatter key (e.g. `convention`,
|
|
350
350
|
// `meta` on facts) must land on the indexed entry so category-keyed
|
|
351
351
|
// policies can see it. Trimmed; blank/non-string values are ignored.
|
|
352
352
|
const category = asNonEmptyString(fmData.category);
|
|
@@ -384,13 +384,13 @@ export function applyCuratedFrontmatter(entry, fmData) {
|
|
|
384
384
|
entry.evidenceSources = evidenceSources;
|
|
385
385
|
if (typeof fmData.evidence === "string" && fmData.evidence.trim().length > 0)
|
|
386
386
|
entry.evidence = fmData.evidence;
|
|
387
|
-
//
|
|
387
|
+
// Capture the parent ref for derived memories.
|
|
388
388
|
// Memory-inference writes `source: "memories/<parent>"` and `inferred: true`
|
|
389
389
|
// (and a derived child name suffix `.derived`). We mirror that source ref into
|
|
390
390
|
// `entry.derivedFrom` so the indexer can populate the dedicated `derived_from`
|
|
391
|
-
// column.
|
|
392
|
-
//
|
|
393
|
-
//
|
|
391
|
+
// column. The column is stored as the `memories/<name>` conceptId, matching
|
|
392
|
+
// the `getDerivedForParent` lookup key (search-hit-enrichers) so producer and
|
|
393
|
+
// consumer speak one grammar.
|
|
394
394
|
// Non-derived entries leave this field unset.
|
|
395
395
|
if (entry.type === "memory") {
|
|
396
396
|
const isDerivedByName = entry.name.toLowerCase().endsWith(".derived");
|
|
@@ -473,7 +473,7 @@ function isWikiRole(value) {
|
|
|
473
473
|
/**
|
|
474
474
|
* Parse one `verified:` family value into the `IndexDocument.provenance` shape.
|
|
475
475
|
* Accepts OKF v0.2's list form and its documented single-mapping shorthand
|
|
476
|
-
* ("consumers MUST treat a bare mapping as a one-element list",
|
|
476
|
+
* ("consumers MUST treat a bare mapping as a one-element list", OKF v0.2 §5.2).
|
|
477
477
|
* Tolerant: an entry without a usable `by` is dropped individually.
|
|
478
478
|
*/
|
|
479
479
|
function parseVerifiedFamily(value) {
|
|
@@ -491,9 +491,9 @@ function parseVerifiedFamily(value) {
|
|
|
491
491
|
}
|
|
492
492
|
/**
|
|
493
493
|
* Extract the OKF v0.2 provenance families that `promoteProposal` stamps onto
|
|
494
|
-
* accepted AKM-native proposals (
|
|
494
|
+
* accepted AKM-native proposals (#730), and apply them to the entry.
|
|
495
495
|
*
|
|
496
|
-
* The on-disk shape is deliberately **hybrid** (
|
|
496
|
+
* The on-disk shape is deliberately **hybrid** (#730 review):
|
|
497
497
|
* `generated:` and `verified:` are stamped BARE at the top level, exactly as
|
|
498
498
|
* OKF v0.2 spells them, because neither has any pre-existing AKM consumer — so
|
|
499
499
|
* a third-party OKF v0.2 reader treating an AKM stash as an OKF bundle sees
|
|
@@ -505,8 +505,8 @@ function parseVerifiedFamily(value) {
|
|
|
505
505
|
* non-strings).
|
|
506
506
|
*
|
|
507
507
|
* The nested `provenance.generatedBy` / `.generatedAt` / `.verified` spellings
|
|
508
|
-
* are still read as a fallback so assets stamped by an earlier build
|
|
509
|
-
*
|
|
508
|
+
* are still read as a fallback so assets stamped by an earlier build keep
|
|
509
|
+
* resolving. Tolerant throughout: any malformed sub-field is
|
|
510
510
|
* dropped individually rather than rejecting the whole block.
|
|
511
511
|
*/
|
|
512
512
|
export function applyProvenanceFrontmatter(entry, fmData) {
|
|
@@ -545,8 +545,8 @@ export function applyProvenanceFrontmatter(entry, fmData) {
|
|
|
545
545
|
entry.provenance = result;
|
|
546
546
|
}
|
|
547
547
|
// AKM-stash indexing policy (env/vaults/secrets sensitive-marker + wiki-infra
|
|
548
|
-
// exclusions)
|
|
549
|
-
// abstention
|
|
548
|
+
// exclusions) lives in the `akm` adapter's `recognize` as path/stat-based
|
|
549
|
+
// abstention. See
|
|
550
550
|
// `akmStashAbstains` in `src/core/adapter/adapters/akm-adapter.ts`.
|
|
551
551
|
/**
|
|
552
552
|
* Extract `@param` JSDoc tags from a script file's leading comment block.
|
|
@@ -764,11 +764,9 @@ function mergeAliases(existing, generated) {
|
|
|
764
764
|
/**
|
|
765
765
|
* Maximum native Markdown prose carried by the low-weight `content` field.
|
|
766
766
|
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
* input with no visible signal (the cut was reported via `warnVerbose`,
|
|
771
|
-
* silent unless `AKM_VERBOSE` was set). The remaining bound exists only to
|
|
767
|
+
* Set far past any real authored document, so ordinary long-form
|
|
768
|
+
* skills/knowledge docs never lose their tail from FTS or the embedding
|
|
769
|
+
* input. The bound exists only to
|
|
772
770
|
* stop a truly pathological single file (a committed data dump, a decompressed
|
|
773
771
|
* log) from ballooning index size — not to shave real content — so a caller
|
|
774
772
|
* that hits it is always told, unconditionally.
|
|
@@ -873,6 +871,58 @@ function isMarkdownFenceClosing(line, fence) {
|
|
|
873
871
|
return false;
|
|
874
872
|
return /^[\t ]*$/.test(line.slice(cursor));
|
|
875
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* Version of what {@link projectMarkdownContent} indexes. It is part of every
|
|
876
|
+
* directory's index fingerprint, so bumping it makes the next `akm index`
|
|
877
|
+
* re-project every directory once instead of keeping the old text for
|
|
878
|
+
* unchanged files. 2: identifiers from fenced code (akm#1260).
|
|
879
|
+
*/
|
|
880
|
+
export const CONTENT_PROJECTION_VERSION = 2;
|
|
881
|
+
/** At or above this length a token with a digit reads as a key or hash, not a name. */
|
|
882
|
+
const TOKEN_LIKE_MIN_LENGTH = 20;
|
|
883
|
+
/**
|
|
884
|
+
* The searchable identifiers of one line of fenced code, without its literal
|
|
885
|
+
* values (akm#1260). Fenced code holds the names agents look for (functions,
|
|
886
|
+
* classes, CLI flags, config keys, environment variable names, paths) and
|
|
887
|
+
* also example secrets, so it keeps names and drops values:
|
|
888
|
+
*
|
|
889
|
+
* - a quoted string survives only as a key (`"name":`) or an absolute path;
|
|
890
|
+
* every other quoted literal is dropped;
|
|
891
|
+
* - the value of an assignment (`KEY=value`, `key: value`, `"key": value`,
|
|
892
|
+
* `Authorization: Bearer token`) is dropped unless it is a dotted code
|
|
893
|
+
* reference (`process.env.CI`); the name stays;
|
|
894
|
+
* - long token-like strings with digits (keys, hashes) are dropped, base64
|
|
895
|
+
* included: its `/`, `+` and `=` do not cut it into short kept pieces;
|
|
896
|
+
* - camelCase, snake_case, dotted and dashed names also add their parts.
|
|
897
|
+
*
|
|
898
|
+
* Numbers, punctuation and operators never reach the result.
|
|
899
|
+
*/
|
|
900
|
+
export function fencedCodeIdentifiers(line) {
|
|
901
|
+
const cleaned = line
|
|
902
|
+
.replace(/(["'`])((?:\\.|(?!\1).)*)\1(\s*:)?/g, (match, _quote, body, colon, offset) => {
|
|
903
|
+
// Keep a key's colon when an unquoted value follows, so the assignment rule below drops that value.
|
|
904
|
+
if (colon)
|
|
905
|
+
return /^\s*["'`]/.test(line.slice(offset + match.length)) ? ` ${body} ` : ` ${body}: `;
|
|
906
|
+
return /^\/[\w./-]+$/.test(body) ? ` ${body} ` : " ";
|
|
907
|
+
})
|
|
908
|
+
.replace(/([A-Za-z_][A-Za-z0-9_.-]*\s*[:=]\s*)((?:[Bb]earer|[Bb]asic|[Tt]oken|[Dd]igest)\s+\S+|\S+)/g, (_match, name, value) => /^[!(]*[A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)+[()\]}]*[,;]?$/.test(value) ? `${name} ${value}` : name)
|
|
909
|
+
// Base64 is one token: its `/`, `+` and `=` would otherwise cut a key body into short pieces that are kept.
|
|
910
|
+
.replace(/(?<![\w.-])[A-Za-z0-9+/=]{20,}(?![\w.-])/g, (run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /\d/.test(run) ? " " : run);
|
|
911
|
+
const words = [];
|
|
912
|
+
for (let token of cleaned.match(/--?[A-Za-z][A-Za-z0-9-]*|[A-Za-z_][A-Za-z0-9_.-]*/g) ?? []) {
|
|
913
|
+
token = token.replace(/[.-]+$/, "");
|
|
914
|
+
const bare = token.replace(/^-+/, "");
|
|
915
|
+
if (bare.length < 2)
|
|
916
|
+
continue;
|
|
917
|
+
if (bare.length >= TOKEN_LIKE_MIN_LENGTH && /\d/.test(bare))
|
|
918
|
+
continue;
|
|
919
|
+
words.push(token);
|
|
920
|
+
const parts = bare.split(/[_.-]|(?<=[a-z0-9])(?=[A-Z])/).filter((part) => part.length > 1);
|
|
921
|
+
if (parts.length > 1)
|
|
922
|
+
words.push(...parts);
|
|
923
|
+
}
|
|
924
|
+
return words.join(" ");
|
|
925
|
+
}
|
|
876
926
|
function stripMarkdownHtmlComments(line, state) {
|
|
877
927
|
let cursor = 0;
|
|
878
928
|
let visible = "";
|
|
@@ -1048,17 +1098,23 @@ function stripMarkdownLinkDestinations(text, nesting = 0) {
|
|
|
1048
1098
|
}
|
|
1049
1099
|
/**
|
|
1050
1100
|
* Derive the one low-weight search projection for an AKM-native Markdown body.
|
|
1051
|
-
* Frontmatter is removed by the caller. This projection drops comments,
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1101
|
+
* Frontmatter is removed by the caller. This projection drops comments, raw
|
|
1102
|
+
* link targets, and structural punctuation while retaining prose, headings,
|
|
1103
|
+
* link labels, and inline identifiers. From fenced code it keeps only the
|
|
1104
|
+
* identifiers ({@link fencedCodeIdentifiers}): names, flags, keys and paths,
|
|
1105
|
+
* never literal values (akm#1260). It never receives secret/env/session bytes;
|
|
1106
|
+
* that policy is enforced at the adapter metadata boundary below.
|
|
1107
|
+
*
|
|
1108
|
+
* Changing what this returns changes the indexed text of every Markdown entry,
|
|
1109
|
+
* so bump {@link CONTENT_PROJECTION_VERSION} with it: the indexer re-projects
|
|
1110
|
+
* every directory once on the next `akm index`.
|
|
1056
1111
|
*/
|
|
1057
1112
|
export function projectMarkdownContent(body, truncationInfo) {
|
|
1058
1113
|
const lines = body.split(/\r?\n/);
|
|
1059
1114
|
const innerBlock = findInnerFrontmatterBlock(lines);
|
|
1060
1115
|
const start = innerBlock && isFrontmatterShaped(lines, innerBlock) ? innerBlock.close + 1 : 0;
|
|
1061
1116
|
const projected = [];
|
|
1117
|
+
const fenced = [];
|
|
1062
1118
|
forEachVisibleMarkdownLine(lines, start, (_index, visible) => {
|
|
1063
1119
|
let trimmed = visible.trim();
|
|
1064
1120
|
if (!trimmed || /^(-{3,}|\*{3,}|_{3,}|=+)$/.test(trimmed))
|
|
@@ -1077,8 +1133,13 @@ export function projectMarkdownContent(body, truncationInfo) {
|
|
|
1077
1133
|
trimmed = trimmed.replace(/\s+/g, " ").trim();
|
|
1078
1134
|
if (trimmed)
|
|
1079
1135
|
projected.push(trimmed);
|
|
1136
|
+
}, (_index, fencedLine) => {
|
|
1137
|
+
const identifiers = fencedCodeIdentifiers(fencedLine);
|
|
1138
|
+
if (identifiers)
|
|
1139
|
+
fenced.push(identifiers);
|
|
1080
1140
|
});
|
|
1081
|
-
|
|
1141
|
+
// Fenced identifiers follow the prose, so a head-truncated embedding input keeps the prose first.
|
|
1142
|
+
const text = [...projected, ...fenced].join(" ").replace(/\s+/g, " ").trim();
|
|
1082
1143
|
if (!text)
|
|
1083
1144
|
return undefined;
|
|
1084
1145
|
if (truncationInfo)
|
|
@@ -1088,8 +1149,10 @@ export function projectMarkdownContent(body, truncationInfo) {
|
|
|
1088
1149
|
/**
|
|
1089
1150
|
* Visit each line from `start` that lies outside fenced code, with HTML
|
|
1090
1151
|
* comments removed (`visible`); fence lines themselves are never visited.
|
|
1152
|
+
* `visitFenced`, when given, receives each raw line inside a fence (not the
|
|
1153
|
+
* opening or closing fence line), from the same fence state.
|
|
1091
1154
|
*/
|
|
1092
|
-
function forEachVisibleMarkdownLine(lines, start, visit) {
|
|
1155
|
+
function forEachVisibleMarkdownLine(lines, start, visit, visitFenced) {
|
|
1093
1156
|
let fence;
|
|
1094
1157
|
const htmlComment = { inComment: false };
|
|
1095
1158
|
for (let index = start; index < lines.length; index += 1) {
|
|
@@ -1097,6 +1160,8 @@ function forEachVisibleMarkdownLine(lines, start, visit) {
|
|
|
1097
1160
|
if (fence) {
|
|
1098
1161
|
if (isMarkdownFenceClosing(rawLine, fence))
|
|
1099
1162
|
fence = undefined;
|
|
1163
|
+
else
|
|
1164
|
+
visitFenced?.(index, rawLine);
|
|
1100
1165
|
continue;
|
|
1101
1166
|
}
|
|
1102
1167
|
// A leading fence owns the whole line, including any info string that
|
|
@@ -1174,13 +1239,12 @@ export function projectMarkdownFragmentContent(raw) {
|
|
|
1174
1239
|
/**
|
|
1175
1240
|
* Priorities 1-2 of the metadata pipeline — package.json (P1), `.md`
|
|
1176
1241
|
* frontmatter (P2), and script `@param`/comment metadata (P2b) — everything
|
|
1177
|
-
* that runs BEFORE the renderer-contributor step (P3).
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1242
|
+
* that runs BEFORE the renderer-contributor step (P3). Shared by the `akm`
|
|
1243
|
+
* adapter's synchronous `recognize` and the live index-drain path; the two
|
|
1244
|
+
* differ ONLY in how they obtain the
|
|
1180
1245
|
* P3 renderer metadata (the adapter uses the synchronous
|
|
1181
1246
|
* `foldRecognizedMetadata`), guaranteeing P1/P2/P4 parity by construction.
|
|
1182
|
-
*
|
|
1183
|
-
* block. Mutates `entry` in place.
|
|
1247
|
+
* Mutates `entry` in place.
|
|
1184
1248
|
*/
|
|
1185
1249
|
export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
1186
1250
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -1207,7 +1271,7 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1207
1271
|
entry.parameters = fmParams;
|
|
1208
1272
|
// Pass wiki-pattern frontmatter through onto the entry
|
|
1209
1273
|
applyWikiFrontmatter(entry, parsed.data);
|
|
1210
|
-
//
|
|
1274
|
+
// #730: reread the namespaced `provenance:` block promoteProposal stamps.
|
|
1211
1275
|
applyProvenanceFrontmatter(entry, parsed.data);
|
|
1212
1276
|
// Native Markdown has one bounded low-weight body projection. Sensitive
|
|
1213
1277
|
// types and raw session/checkpoint material never cross this boundary.
|
|
@@ -1220,7 +1284,7 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1220
1284
|
entry.content = contentProjection;
|
|
1221
1285
|
if (truncationInfo.truncated) {
|
|
1222
1286
|
entry.contentTruncated = true;
|
|
1223
|
-
// Unconditional, not warnVerbose: this bound
|
|
1287
|
+
// Unconditional, not warnVerbose: this bound sits far past any
|
|
1224
1288
|
// real document, so tripping it means
|
|
1225
1289
|
// something unusual is in the bundle and the operator should see
|
|
1226
1290
|
// it without having to pass --verbose.
|
|
@@ -1252,8 +1316,8 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1252
1316
|
* Priority 4 of the metadata pipeline — filename-heuristic fallbacks (P4) that
|
|
1253
1317
|
* run AFTER the renderer-contributor step (P3): a filename description when none
|
|
1254
1318
|
* was set, path/dir-derived tags, tag normalization, and alias generation.
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
1319
|
+
* Shared with {@link applyPreContributorFields} by both pipeline paths.
|
|
1320
|
+
* Mutates `entry`.
|
|
1257
1321
|
*/
|
|
1258
1322
|
export function applyPostContributorFields(entry, file, canonicalName, dirPath) {
|
|
1259
1323
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -1267,7 +1331,7 @@ export function applyPostContributorFields(entry, file, canonicalName, dirPath)
|
|
|
1267
1331
|
if (!entry.tags || entry.tags.length === 0) {
|
|
1268
1332
|
entry.tags = extractTagsFromPath(file, dirPath);
|
|
1269
1333
|
}
|
|
1270
|
-
// Stash-organization conventions
|
|
1334
|
+
// Stash-organization conventions: directory (scope/domain) tokens
|
|
1271
1335
|
// always reach the tags column, even when the author set explicit tags, so
|
|
1272
1336
|
// nested assets keep the exact-tag ranking boost for their scope token.
|
|
1273
1337
|
// Derived from canonicalName (the ref subpath) rather than the filesystem
|
|
@@ -1284,13 +1348,6 @@ export function applyPostContributorFields(entry, file, canonicalName, dirPath)
|
|
|
1284
1348
|
// Heuristic search hints are too noisy to be useful for search quality
|
|
1285
1349
|
entry.filename = path.basename(file);
|
|
1286
1350
|
}
|
|
1287
|
-
// The pre-0.9.0 flat-walk matcher-pass metadata source was DELETED in Chunk 5
|
|
1288
|
-
// F4a M-core-3. Its role (recognize a stash root's files into durable entries)
|
|
1289
|
-
// is now the `akm` adapter's `recognize`, drained by `indexer/scan/drain-dir.ts`
|
|
1290
|
-
// (`drainDirDocuments` for the live indexer, `recognizeStashEntries` for the
|
|
1291
|
-
// `manifest` fallback / `registry` index builder / metadata unit tests). The
|
|
1292
|
-
// flip is the F4 engine swap; the shadow-parity gate proved recognize produced
|
|
1293
|
-
// the identical entries before the old pass was removed.
|
|
1294
1351
|
export function buildMetadataSkipWarning(filePath, assetType, error) {
|
|
1295
1352
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1296
1353
|
// Workflow errors are already multi-line `path:line — message` blocks; print
|
|
@@ -1327,9 +1384,9 @@ function normalizeTerms(values) {
|
|
|
1327
1384
|
if (!cleaned)
|
|
1328
1385
|
continue;
|
|
1329
1386
|
normalized.add(cleaned);
|
|
1330
|
-
//
|
|
1331
|
-
//
|
|
1332
|
-
// edge cases like "kubernetes" and "status" that
|
|
1387
|
+
// No de-pluralization here: the FTS5 porter stemmer (configured with
|
|
1388
|
+
// `tokenize='porter unicode61'`) handles stemming correctly, including
|
|
1389
|
+
// edge cases like "kubernetes" and "status" that a naive s-strip mangles.
|
|
1333
1390
|
}
|
|
1334
1391
|
return Array.from(normalized);
|
|
1335
1392
|
}
|
|
@@ -1437,7 +1494,7 @@ export function extractTagsFromPath(filePath, rootDir) {
|
|
|
1437
1494
|
* Unlike {@link extractTagsFromPath} this never tokenizes the filename
|
|
1438
1495
|
* segment: it exists so a nested asset's directory (scope/domain) tokens can
|
|
1439
1496
|
* be merged into explicit author tags without dragging every filename word
|
|
1440
|
-
* into exact-tag matching
|
|
1497
|
+
* into exact-tag matching.
|
|
1441
1498
|
* Tokenization mirrors `extractTagsFromPath`: each segment splits on `-`/`_`/
|
|
1442
1499
|
* `.`, lowercased, single-character tokens dropped. A name with no directory
|
|
1443
1500
|
* segments yields no tags.
|
|
@@ -41,9 +41,13 @@ export function reciprocalRankFusion(channels) {
|
|
|
41
41
|
* 1 + w·(ln(1+pos) − ln(1+neg)) moves a score by about 2% / 3% / 5% / 9% for
|
|
42
42
|
* 1 / 2 / 5 / 20 positives at 0.03. Measured on the retrieval suite's fused
|
|
43
43
|
* lists, 5 positives lift a hit from rank 10 by a median of 3 places (from rank 5,
|
|
44
|
-
* by 2) and 20 by 4 to 5
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* by 2) and 20 by 4 to 5. What a vote can pass depends on the channels. In
|
|
45
|
+
* keyword-only search every fused score is exactly 1 / (RRF_K + rank), so
|
|
46
|
+
* neighbours in the first 20 sit 1.3 to 1.6% apart and one vote (about 2%) moves
|
|
47
|
+
* a hit one place (through rank 38) whatever the keyword margin between them. When both channels contribute, their
|
|
48
|
+
* ranks spread the scores and a vote passes only near-ties. At 0.05 the same
|
|
49
|
+
* votes lift it 4 to 5 and 6 to 7 places, and two wrong votes cost about 1.6
|
|
50
|
+
* times the nDCG@10 they cost at 0.03.
|
|
47
51
|
*/
|
|
48
52
|
export const FEEDBACK_WEIGHT = 0.03;
|
|
49
53
|
/**
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - tags: tags + aliases joined
|
|
14
14
|
* - hints: searchHints + examples + usage + intent fields
|
|
15
15
|
* - content: bounded native/adapter body projection + TOC headings + parameters
|
|
16
|
-
* (
|
|
16
|
+
* (the catch-all; BM25 weighs all five fields equally)
|
|
17
17
|
*/
|
|
18
18
|
export function buildSearchFields(entry) {
|
|
19
19
|
const name = entry.name.replace(/[-_]/g, " ");
|