akm-cli 0.9.0-beta.46 → 0.9.0-beta.47
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.
|
@@ -6,20 +6,27 @@ Schema: {{SCHEMA_PATH}}
|
|
|
6
6
|
Follow these steps. akm commands handle the invariants; use your native
|
|
7
7
|
Read/Write/Edit tools for page edits.
|
|
8
8
|
|
|
9
|
+
This workflow is for ingesting sources that are ALREADY present under
|
|
10
|
+
`{{WIKI_DIR}}/raw/`. Do not ask the user for a source unless the raw queue is
|
|
11
|
+
empty and the caller explicitly asked for interactive ingest.
|
|
12
|
+
|
|
9
13
|
1. **Read the schema.** Open `{{SCHEMA_PATH}}`. It defines the voice, page
|
|
10
14
|
kinds, contradiction policy, and any wiki-specific conventions. Do not
|
|
11
15
|
skip this step even on familiar wikis — the schema may have changed.
|
|
12
16
|
|
|
13
|
-
2. **
|
|
17
|
+
2. **Discover the pending raw queue.**
|
|
14
18
|
```sh
|
|
15
|
-
akm wiki
|
|
16
|
-
# or: cat <source> | akm wiki stash {{WIKI_NAME}} -
|
|
19
|
+
akm wiki lint {{WIKI_NAME}}
|
|
17
20
|
```
|
|
18
|
-
|
|
21
|
+
Focus on `uncited-raw` findings: those raw files exist under `raw/` but are
|
|
22
|
+
not yet cited by any authored page. Treat each `uncited-raw` finding as a
|
|
23
|
+
pending ingest item. If there are no `uncited-raw` findings, exit cleanly
|
|
24
|
+
after a final `akm index` + `akm wiki lint {{WIKI_NAME}}` verification.
|
|
19
25
|
|
|
20
|
-
3. **
|
|
26
|
+
3. **For each pending raw file, read the source and find related pages.**
|
|
27
|
+
Open the raw file directly from `{{WIKI_DIR}}/raw/`, then search:
|
|
21
28
|
```sh
|
|
22
|
-
akm wiki search {{WIKI_NAME}} "<key terms from the source>"
|
|
29
|
+
akm wiki search {{WIKI_NAME}} "<key terms from the raw source>"
|
|
23
30
|
```
|
|
24
31
|
Read the top hits with `akm show wiki:{{WIKI_NAME}}/<page>`. Use
|
|
25
32
|
`akm show wiki:{{WIKI_NAME}}/<page> toc` for large pages.
|
|
@@ -39,8 +46,8 @@ Read/Write/Edit tools for page edits.
|
|
|
39
46
|
6. **Update xrefs both ways.** If page A now xrefs page B, page B must xref
|
|
40
47
|
page A. `akm wiki lint {{WIKI_NAME}}` will flag violations.
|
|
41
48
|
|
|
42
|
-
7. **Append to `log.md`.** One entry per
|
|
43
|
-
summary, refs to created/edited pages. Newest at the top.
|
|
49
|
+
7. **Append to `log.md`.** One entry per ingested raw source: date, raw slug,
|
|
50
|
+
one-line summary, refs to created/edited pages. Newest at the top.
|
|
44
51
|
|
|
45
52
|
8. **Regenerate the index + verify.**
|
|
46
53
|
```sh
|
|
@@ -50,5 +57,5 @@ Read/Write/Edit tools for page edits.
|
|
|
50
57
|
Resolve any lint findings before calling the ingest done.
|
|
51
58
|
|
|
52
59
|
That's it. `akm` never calls an LLM — reasoning is your job; it just owns
|
|
53
|
-
the invariants (raw immutability,
|
|
54
|
-
|
|
60
|
+
the invariants (raw immutability, ref validation, index regeneration,
|
|
61
|
+
structural lint).
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import { writeFileAtomic } from "../../core/common.js";
|
|
6
6
|
import { getCacheDir, getSemanticStatusPath } from "../../core/paths.js";
|
|
7
|
+
import { DETERMINISTIC_EMBED_MODEL_ID, isDeterministicEmbedEnabled } from "../../llm/embedders/deterministic.js";
|
|
7
8
|
import { DEFAULT_LOCAL_MODEL } from "../../llm/embedders/local.js";
|
|
8
9
|
export function deriveSemanticProviderFingerprint(embedding) {
|
|
10
|
+
if (isDeterministicEmbedEnabled()) {
|
|
11
|
+
return `deterministic:${DETERMINISTIC_EMBED_MODEL_ID}`;
|
|
12
|
+
}
|
|
9
13
|
if (embedding?.endpoint) {
|
|
10
14
|
return `remote:${embedding.endpoint}|${embedding.model}|${embedding.dimension ?? "default"}`;
|
|
11
15
|
}
|
package/dist/llm/embedder.js
CHANGED
|
@@ -2,6 +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
|
import { embedCacheKey, getCachedEmbedding, setCachedEmbedding } from "./embedders/cache.js";
|
|
5
|
+
import { DETERMINISTIC_EMBED_MODEL_ID, deterministicEmbed, isDeterministicEmbedEnabled, } from "./embedders/deterministic.js";
|
|
5
6
|
import { DEFAULT_LOCAL_MODEL, isTransformersAvailable, LocalEmbedder } from "./embedders/local.js";
|
|
6
7
|
import { hasRemoteEndpoint, RemoteEmbedder } from "./embedders/remote.js";
|
|
7
8
|
// ── Re-exports (public API) ─────────────────────────────────────────────────
|
|
@@ -39,6 +40,10 @@ export function resetLocalEmbedder() {
|
|
|
39
40
|
* and embedding config. Repeated identical queries return the cached vector.
|
|
40
41
|
*/
|
|
41
42
|
export async function embed(text, embeddingConfig, signal) {
|
|
43
|
+
// Deterministic mode (env-gated, test/bench only): model-free, stable.
|
|
44
|
+
if (isDeterministicEmbedEnabled()) {
|
|
45
|
+
return deterministicEmbed(text);
|
|
46
|
+
}
|
|
42
47
|
const key = embedCacheKey(text, embeddingConfig);
|
|
43
48
|
const cached = getCachedEmbedding(key);
|
|
44
49
|
if (cached)
|
|
@@ -58,6 +63,10 @@ export async function embed(text, embeddingConfig, signal) {
|
|
|
58
63
|
export async function embedBatch(texts, embeddingConfig, signal) {
|
|
59
64
|
if (texts.length === 0)
|
|
60
65
|
return [];
|
|
66
|
+
// Deterministic mode (env-gated, test/bench only): model-free, stable.
|
|
67
|
+
if (isDeterministicEmbedEnabled()) {
|
|
68
|
+
return texts.map((t) => deterministicEmbed(t));
|
|
69
|
+
}
|
|
61
70
|
if (embeddingConfig && hasRemoteEndpoint(embeddingConfig)) {
|
|
62
71
|
return new RemoteEmbedder(embeddingConfig).embedBatch(texts, signal);
|
|
63
72
|
}
|
|
@@ -95,6 +104,8 @@ export { cosineSimilarity } from "./embedders/types.js";
|
|
|
95
104
|
* - No config: use `DEFAULT_LOCAL_MODEL` (the shared singleton model).
|
|
96
105
|
*/
|
|
97
106
|
export function resolveEmbeddingModelId(embeddingConfig) {
|
|
107
|
+
if (isDeterministicEmbedEnabled())
|
|
108
|
+
return DETERMINISTIC_EMBED_MODEL_ID;
|
|
98
109
|
if (!embeddingConfig)
|
|
99
110
|
return DEFAULT_LOCAL_MODEL;
|
|
100
111
|
if (hasRemoteEndpoint(embeddingConfig))
|
|
@@ -106,6 +117,10 @@ export function resolveEmbeddingModelId(embeddingConfig) {
|
|
|
106
117
|
* Check whether embedding is available with a detailed reason on failure.
|
|
107
118
|
*/
|
|
108
119
|
export async function checkEmbeddingAvailability(embeddingConfig) {
|
|
120
|
+
// Deterministic mode (env-gated): always available — no model, no network.
|
|
121
|
+
if (isDeterministicEmbedEnabled()) {
|
|
122
|
+
return { available: true };
|
|
123
|
+
}
|
|
109
124
|
if (embeddingConfig && hasRemoteEndpoint(embeddingConfig)) {
|
|
110
125
|
try {
|
|
111
126
|
await new RemoteEmbedder(embeddingConfig).embed("test");
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/** Env var that switches the whole embedding facade into deterministic mode. */
|
|
5
|
+
export const DETERMINISTIC_EMBED_ENV = "AKM_EMBED_DETERMINISTIC";
|
|
6
|
+
/**
|
|
7
|
+
* Vector width. Matches the default local model (`bge-small`, 384 dims) so the
|
|
8
|
+
* index DB's embedding column and sqlite-vec table dimensions line up without
|
|
9
|
+
* any extra config.
|
|
10
|
+
*/
|
|
11
|
+
export const DETERMINISTIC_EMBED_DIM = 384;
|
|
12
|
+
/**
|
|
13
|
+
* Stable model id reported for deterministic mode. Used as the embedding
|
|
14
|
+
* `model_id` and folded into the provider fingerprint so a deterministic index
|
|
15
|
+
* is never confused with a real-model index (and vice versa).
|
|
16
|
+
*/
|
|
17
|
+
export const DETERMINISTIC_EMBED_MODEL_ID = "akm-deterministic-hash-v1";
|
|
18
|
+
/** True when deterministic embedding is enabled via env. */
|
|
19
|
+
export function isDeterministicEmbedEnabled() {
|
|
20
|
+
return process.env[DETERMINISTIC_EMBED_ENV] === "1";
|
|
21
|
+
}
|
|
22
|
+
/** FNV-1a 32-bit hash. Platform- and version-stable. */
|
|
23
|
+
function fnv1a(str) {
|
|
24
|
+
let h = 0x811c9dc5;
|
|
25
|
+
for (let i = 0; i < str.length; i++) {
|
|
26
|
+
h ^= str.charCodeAt(i);
|
|
27
|
+
// 32-bit FNV prime multiply via shifts to stay in uint32.
|
|
28
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
29
|
+
}
|
|
30
|
+
return h >>> 0;
|
|
31
|
+
}
|
|
32
|
+
/** Lowercase, split on non-alphanumeric, drop empties. */
|
|
33
|
+
function tokenize(text) {
|
|
34
|
+
return text
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.split(/[^a-z0-9]+/)
|
|
37
|
+
.filter((t) => t.length > 0);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Deterministically embed `text` into a unit-length vector of width `dim`
|
|
41
|
+
* using feature hashing. Empty / token-less input returns a fixed unit
|
|
42
|
+
* vector so cosine similarity never sees a zero vector (NaN guard).
|
|
43
|
+
*/
|
|
44
|
+
export function deterministicEmbed(text, dim = DETERMINISTIC_EMBED_DIM) {
|
|
45
|
+
const vec = new Array(dim).fill(0);
|
|
46
|
+
const tokens = tokenize(text);
|
|
47
|
+
for (const tok of tokens) {
|
|
48
|
+
const h = fnv1a(tok);
|
|
49
|
+
const idx = h % dim;
|
|
50
|
+
// Use a higher bit for the sign so it is independent of the bucket index.
|
|
51
|
+
const sign = (h >>> 16) & 1 ? 1 : -1;
|
|
52
|
+
vec[idx] += sign;
|
|
53
|
+
}
|
|
54
|
+
let norm = 0;
|
|
55
|
+
for (const v of vec)
|
|
56
|
+
norm += v * v;
|
|
57
|
+
norm = Math.sqrt(norm);
|
|
58
|
+
if (norm === 0) {
|
|
59
|
+
// No usable tokens — return a fixed, stable unit vector.
|
|
60
|
+
vec[0] = 1;
|
|
61
|
+
return vec;
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0; i < dim; i++)
|
|
64
|
+
vec[i] /= norm;
|
|
65
|
+
return vec;
|
|
66
|
+
}
|
|
@@ -19,6 +19,10 @@ function extractVideoId(url) {
|
|
|
19
19
|
const id = url.pathname.slice("/shorts/".length).split("/")[0]?.trim();
|
|
20
20
|
return id || null;
|
|
21
21
|
}
|
|
22
|
+
if (url.pathname.startsWith("/embed/")) {
|
|
23
|
+
const id = url.pathname.slice("/embed/".length).split("/")[0]?.trim();
|
|
24
|
+
return id || null;
|
|
25
|
+
}
|
|
22
26
|
return null;
|
|
23
27
|
}
|
|
24
28
|
function canonicalWatchUrl(videoId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.47",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|