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. **File the source under `raw/`.**
17
+ 2. **Discover the pending raw queue.**
14
18
  ```sh
15
- akm wiki stash {{WIKI_NAME}} <path-or-url-to-source>
16
- # or: cat <source> | akm wiki stash {{WIKI_NAME}} -
19
+ akm wiki lint {{WIKI_NAME}}
17
20
  ```
18
- Returns `{ slug, path, ref }`. The raw copy is immutable never edit it.
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. **Find related existing pages.**
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 ingest: date, source slug, one-line
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, unique slugs, ref validation, index
54
- regeneration, structural lint).
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
  }
@@ -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.46",
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": [