@theglitchking/semantic-memory 1.5.1 → 1.5.3
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +43 -0
- package/README.md +4 -0
- package/dist/{chunk-25EIQWXL.js → chunk-44BQDRXD.js} +5 -5
- package/dist/chunk-44BQDRXD.js.map +1 -0
- package/dist/{chunk-2EELGTLX.js → chunk-D5QLNNA4.js} +2 -2
- package/dist/{chunk-A3KYHKB5.js → chunk-FTRXH2WC.js} +8 -4
- package/dist/chunk-FTRXH2WC.js.map +1 -0
- package/dist/{chunk-OWZWMB4P.js → chunk-FURCIJBO.js} +1 -1
- package/dist/chunk-FURCIJBO.js.map +1 -0
- package/dist/{chunk-V45NJIHI.js → chunk-IGHCVAAL.js} +19 -4
- package/dist/chunk-IGHCVAAL.js.map +1 -0
- package/dist/{chunk-S4KW7VH4.js → chunk-LGQPP5FQ.js} +8 -3
- package/dist/chunk-LGQPP5FQ.js.map +1 -0
- package/dist/{chunk-NTYYS53O.js → chunk-NBY4TRP7.js} +7 -3
- package/dist/chunk-NBY4TRP7.js.map +1 -0
- package/dist/chunk-OEF5U5GV.js +175 -0
- package/dist/chunk-OEF5U5GV.js.map +1 -0
- package/dist/{chunk-FM26NFTR.js → chunk-OFXQXVG5.js} +26 -12
- package/dist/chunk-OFXQXVG5.js.map +1 -0
- package/dist/{chunk-CSXZ7QUU.js → chunk-ROKSUJFM.js} +27 -17
- package/dist/chunk-ROKSUJFM.js.map +1 -0
- package/dist/{chunk-JPQHNG3R.js → chunk-SX427M2R.js} +10 -4
- package/dist/chunk-SX427M2R.js.map +1 -0
- package/dist/{chunk-7M3JZOYH.js → chunk-WZEKIUYG.js} +8 -4
- package/dist/chunk-WZEKIUYG.js.map +1 -0
- package/dist/{chunk-WSU4QIPB.js → chunk-XAXK7NM4.js} +14 -8
- package/dist/chunk-XAXK7NM4.js.map +1 -0
- package/dist/cli/index.js +15 -15
- package/dist/core/index.d.ts +24 -4
- package/dist/core/index.js +20 -12
- package/dist/core/redact.d.ts +36 -0
- package/dist/core/redact.js +18 -0
- package/dist/{dossier-2UG724LB.js → dossier-RWTIMGZN.js} +4 -3
- package/dist/healthcheck-A32IWPNV.js +20 -0
- package/dist/indexer-RAKJ7JR3.js +8 -0
- package/dist/{lexicon-5PXQEMUF.js → lexicon-IVNJTMWE.js} +5 -4
- package/dist/lint-HURYGO3V.js +17 -0
- package/dist/log-YIXF6CYM.js +11 -0
- package/dist/mcp/server.js +32 -27
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-LXUEF7TX.js → profile-UE4YVTDC.js} +5 -4
- package/dist/{schema-ATFCLC6Y.js → schema-LY6533NJ.js} +2 -2
- package/dist/{search-AHKEFI2B.js → search-ZS6OW74K.js} +3 -3
- package/dist/{telemetry-GB2TEHVE.js → telemetry-B4KP3UVE.js} +4 -3
- package/dist/telemetry-B4KP3UVE.js.map +1 -0
- package/hooks/vault-context.js +112 -58
- package/package.json +10 -10
- package/dist/chunk-25EIQWXL.js.map +0 -1
- package/dist/chunk-7M3JZOYH.js.map +0 -1
- package/dist/chunk-A3KYHKB5.js.map +0 -1
- package/dist/chunk-CSXZ7QUU.js.map +0 -1
- package/dist/chunk-FM26NFTR.js.map +0 -1
- package/dist/chunk-JPQHNG3R.js.map +0 -1
- package/dist/chunk-NTYYS53O.js.map +0 -1
- package/dist/chunk-OWZWMB4P.js.map +0 -1
- package/dist/chunk-S4KW7VH4.js.map +0 -1
- package/dist/chunk-V45NJIHI.js.map +0 -1
- package/dist/chunk-WSU4QIPB.js.map +0 -1
- package/dist/healthcheck-IMB47LZE.js +0 -19
- package/dist/indexer-YW4O3VVM.js +0 -8
- package/dist/lint-XUQ7JDYX.js +0 -16
- package/dist/log-LQXIJXIA.js +0 -10
- /package/dist/{chunk-2EELGTLX.js.map → chunk-D5QLNNA4.js.map} +0 -0
- /package/dist/{dossier-2UG724LB.js.map → core/redact.js.map} +0 -0
- /package/dist/{healthcheck-IMB47LZE.js.map → dossier-RWTIMGZN.js.map} +0 -0
- /package/dist/{indexer-YW4O3VVM.js.map → healthcheck-A32IWPNV.js.map} +0 -0
- /package/dist/{lexicon-5PXQEMUF.js.map → indexer-RAKJ7JR3.js.map} +0 -0
- /package/dist/{lint-XUQ7JDYX.js.map → lexicon-IVNJTMWE.js.map} +0 -0
- /package/dist/{log-LQXIJXIA.js.map → lint-HURYGO3V.js.map} +0 -0
- /package/dist/{profile-LXUEF7TX.js.map → log-YIXF6CYM.js.map} +0 -0
- /package/dist/{schema-ATFCLC6Y.js.map → profile-UE4YVTDC.js.map} +0 -0
- /package/dist/{telemetry-GB2TEHVE.js.map → schema-LY6533NJ.js.map} +0 -0
- /package/dist/{search-AHKEFI2B.js.map → search-ZS6OW74K.js.map} +0 -0
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Single-plugin marketplace for semantic-memory — the unified memory-layer plugin. v1.1.0 absorbs brain's strengths: AGENTS.md contract artifact, hard-gated verification sessions, multi-agent skill bundler (codex/copilot/pi), and SessionStart drift detection. v1.3 adds confidence decay — age-aware retrieval ranking with verify_note to reset the clock. All backwards-compatible with v1.0.x via deprecation shims.",
|
|
9
|
-
"version": "1.5.
|
|
9
|
+
"version": "1.5.3"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "semantic-memory",
|
|
14
14
|
"description": "Unified memory-layer plugin for Claude Code. One MCP server, 44 tools across multi-corpus search/read/write/graph/lint/sessions/contract. v1.1.0 adds: AGENTS.md contract artifact, hard-gated verification sessions (session_start/run/finish/status), proposal-mode synthesize_note + synthesize_promote, multi-agent skill bundler (codex/copilot/pi via the new `skills` CLI), and SessionStart drift detection. v1.2 completes the hygiene line: `/healthcheck --fix` auto-remediation, `lint_vault({checks:['code_symbols']})` code-path drift, and a fresh-install CI smoke gate. v1.3 adds confidence decay: age-aware search ranking (0.5^(age/half-life), type-aware, floored, composes with load_priority), a `decay` block surfaced on decayed results, `verify_note` to reset the clock, `evergreen` opt-out, and `decay-config`/`decay-trace` CLI introspection. v1.3.1 adds local-only selection-logging telemetry (which note each search result actually got used), the `decay_candidates` lint, and a `selection-stats` CLI. v1.4.0 is the resident-expert bridge: a learned human→artifact lexicon (`manage_lexicon`) with propose-and-confirm alias capture, verbatim symptom capture + symptom-keyed indexing (terse symptom queries match the note), Tier-1 lexicon query expansion in the hook, archive path-class down-weight, injection hygiene (score-gate + dedup + top-3 + compact instructions), section-targeted `read_note`, and opt-in conditional tool registration. v1.5.0 is the expert-character layer: entity dossiers (`manage_dossier`) — per-component living notes (purpose / failure modes / knobs / incident log / current state) that accrete in place, with two-hop retrieval (utterance→entity→dossier injected first); usage-feedback ranking that boosts repeatedly-cited notes (capped, composes with decay/path-class) plus a `decoys` lint for retrieved-but-never-cited notes (surfaced, never auto-down-ranked); session paging (Stop drafts a durable session digest as a proposal, SessionStart pages in a curated digest instead of a broad sweep); and a speaker profile (`manage_profile`) that models how the human communicates — severity calibration, chronic omissions, verbosity, shorthand — injected at SessionStart. Backwards-compatible with v1.0.x — every previous tool name remains callable through deprecation shims (removed in v2.0.0).",
|
|
15
|
-
"version": "1.5.
|
|
15
|
+
"version": "1.5.3",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TheGlitchKing"
|
|
18
18
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semantic-memory",
|
|
3
3
|
"description": "Unified memory-layer plugin for Claude Code. One MCP server, multiple typed corpora (vault / code / plans / docs / research / project-map), unified cross-corpus knowledge graph, conditional tool registration, in-process drift detection, and human↔code translation. Rebrand of `semantic-sidekick` at v1.0.0 — see CHANGELOG for migration.",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TheGlitchKing",
|
|
7
7
|
"email": "theglitchking@users.noreply.github.com"
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to semantic-memory (formerly semantic-sidekick) will be documented here.
|
|
4
4
|
|
|
5
|
+
## [1.5.3] - 2026-10-24 — Secret redaction
|
|
6
|
+
|
|
7
|
+
Patch release. Until now nothing stopped an API key, token or password that passed through the plugin from being written to disk in the clear — and vault notes are usually committed to git. Every write point now masks secrets first, and `lint_vault` can find the ones already there.
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- **Redaction at every write point** (`src/core/redact.ts`), two layers: the exact values of secret-named env vars on the machine (process env + `~/.claude/settings.json` `env`), then patterns — PEM private keys, `user:pass@` URL credentials, `Bearer`/`Basic` headers, secret-named `name=value` / `name: value` / JSON pairs, `--token VALUE` flags, vendor key prefixes (`sk-`/`pk-`/`rk-`, `ghp_`, `github_pat_`, `glpat-`, `xox*-`, `GOCSPX-`, `hvs.`, `AKIA`/`ASIA`, `AIza`, `whsec_`, `re_`, `hf_`, `npm_`), JWTs.
|
|
12
|
+
- **Notes** (create/update, `apply_patch`, synthesize, ingest, promote, `update_frontmatter`, dossiers, speaker profile, lexicon aliases, `log.md`) use a *precise* set tuned so ordinary content survives: commit SHAs, UUIDs, hashes, paths, `pk_customer_orders`, `max_tokens`, `monkey: …`, code such as `api_key = os.getenv` (a name=value only counts when the value looks random: a digit, or 16+ letters) and placeholders (`<your-key>`, `${API_TOKEN}`, `process.env.X`, `changeme`, `xxxx`) are untouched. `update_note` redacts only the incoming text.
|
|
13
|
+
- **Logs and state** (search queries in `selection.jsonl`, `session.json` task / `session_run` command and output tail, the hook's capture-pending excerpt) also mask long base64 and letter-and-digit tokens.
|
|
14
|
+
- The hook imports the redactor from `dist/core/redact.js`; if it cannot load, the capture excerpt is withheld rather than stored raw.
|
|
15
|
+
- Tool results say how many secrets were redacted (`… (2 secrets redacted)`, or `redacted: N` in JSON results), never which.
|
|
16
|
+
- **`lint_vault({ checks: ["secrets"] })`** — opt-in check that reports notes already containing a secret: path, line and kind, never the value; nothing is rewritten. A secret that was ever committed must be **rotated**: redaction cannot remove it from git history.
|
|
17
|
+
- Known limit: a secret written as plain prose ("the password is hunter2") is not caught.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `NoteCrud.create()` returns `{ path, redacted }` instead of the path string; `NoteCrud.update()` returns the same shape (was `void`). Only affects code using `@theglitchking/semantic-memory` as a library.
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
|
|
25
|
+
- Full suite **516 green** (440 in 1.5.2). One integration test per write point through the real MCP tools (all fail without the change), a sweep of the whole vault for the fake values, false-positive cases for ordinary note content, linear-time checks on 1 MB pathological inputs.
|
|
26
|
+
|
|
27
|
+
## [1.5.2] - 2026-10-24 — Docs index freshness (issue #29)
|
|
28
|
+
|
|
29
|
+
Patch release. In the standard two-server setup (`semantic-vault` → `.claude/.vault`, `semantic-sidekick` → `.documentation --read-only`) the docs index went stale after it was first built and the hooks never searched the docs. Fixes for every cause found, plus the dependency audit.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Stale cached index is rebuilt at startup.** A cache whose `meta.totalChunks` no longer matches the notes on disk loaded as `"stale"` and was served forever; startup only reindexed when the cache failed to load. It now rebuilds in the background and serves the stale index meanwhile.
|
|
34
|
+
- **File watcher works under chokidar 5.** `watch("**/*.md")` is a literal path since chokidar 4 dropped globs, so no event ever fired and edits during a session were never reindexed. The watcher now watches the notes folder and filters (`node_modules`, `.semantic-sidekick-index`, `.git`, non-markdown).
|
|
35
|
+
- **One bad doc no longer fails the whole index.** `indexAll()` rejected if any file failed, so a single doc with invalid YAML frontmatter (e.g. an unquoted colon in `purpose:`) left the entire corpus unindexed. Invalid frontmatter now indexes the body with empty frontmatter; any other per-file error skips that file. Both are reported on stderr.
|
|
36
|
+
- **Hooks search every `semantic-*` notes path.** The prompt hook and the SessionStart search used only the first `.mcp.json` entry, normally an (often empty) vault. They now search every entry's `--notes` path in parallel and merge hits by score; with more than one corpus each hit is labelled with its server. Writes (log events, dossiers, session digests, speaker profile) stay on one path: the first entry without `--read-only`. `SIDEKICK_VAULT_PATH` still means a single path. Hook latency on a two-server project: ~670 ms → ~800 ms.
|
|
37
|
+
- **Capture prompts stay in their own session.** `capture-pending.json` is per project, so a cue typed in one session showed up in another concurrent session's Stop prompt, and any SessionStart or Stop wiped every session's items. Items now carry `session_id`; each session asks about and clears only its own. Items from before 1.5.2 go to whichever session reads them first; other sessions' items older than a week are pruned.
|
|
38
|
+
- **Watcher tests can fail again.** They treated their own timeout as "filesystem events not available" and passed while the watcher was dead.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **Dependency audit:** 30 findings (1 critical, 19 high; 22 in production) → 3 (1 in production). `overrides` re-pinned to current patches (several 1.5.1 pins had become the vulnerable versions); in-range updates including `@modelcontextprotocol/sdk` 1.32.2, `@huggingface/transformers` 4.3.1, `onnxruntime-node` 1.30.0, `vitest` 4.1.12. Still open, breaking-only fixes: `zod` (every 3.x is affected; the fix is zod 4, which changes the MCP tool schemas) and dev-only `esbuild` (low).
|
|
43
|
+
|
|
44
|
+
### Tests
|
|
45
|
+
|
|
46
|
+
- Full suite **440 green** (was 418); build and lint clean; golden retrieval eval unchanged (recall@1 86%, MRR 0.929). Each fix has a test that fails on 1.5.1.
|
|
47
|
+
|
|
5
48
|
## [1.5.1] - 2026-07-12 — Dependency audit hardening (supply chain)
|
|
6
49
|
|
|
7
50
|
Patch release: clears the transitive `npm audit` findings surfaced after upgrading from the 1.2.x line, with **no runtime or API change**. Every advisory was unreachable in practice — this is a local **stdio** MCP server plus hook scripts processing the user's **own** files: the `hono`/`express-rate-limit`/`ip-address`/`qs`/`fast-uri` cluster is the MCP SDK's HTTP-server transport, which this plugin never instantiates (stdio only); `protobufjs`/`@xmldom/xmldom` are the local embedding runtime; `js-yaml`/`brace-expansion` parse your own notes/globs.
|
package/README.md
CHANGED
|
@@ -78,6 +78,10 @@ System (2): `get_stats`, `reindex`
|
|
|
78
78
|
|
|
79
79
|
See the originating design discussion in `.planning/semantic-sidekick/` for rationale and phase-by-phase build plan.
|
|
80
80
|
|
|
81
|
+
## Privacy and secrets
|
|
82
|
+
|
|
83
|
+
Everything stays on your machine (local embedding model, no network telemetry). Since v1.5.3 every write masks API keys, tokens, passwords and private keys before they reach disk, and `lint_vault({ checks: ["secrets"] })` finds ones already in notes. Details and limits: [SECURITY.md](SECURITY.md#secret-redaction-v153).
|
|
84
|
+
|
|
81
85
|
## Development
|
|
82
86
|
|
|
83
87
|
```bash
|
|
@@ -124,8 +124,8 @@ var Embedder = class {
|
|
|
124
124
|
max_length: 512,
|
|
125
125
|
return_tensor: false
|
|
126
126
|
});
|
|
127
|
-
const inputIdsRaw = Array.from(encoded.input_ids
|
|
128
|
-
const attentionMaskRaw = Array.from(encoded.attention_mask
|
|
127
|
+
const inputIdsRaw = Array.from(encoded.input_ids);
|
|
128
|
+
const attentionMaskRaw = Array.from(encoded.attention_mask);
|
|
129
129
|
const seqLen = inputIdsRaw.length;
|
|
130
130
|
const inputIds = new this.ort.Tensor(
|
|
131
131
|
"int64",
|
|
@@ -222,8 +222,8 @@ var Embedder = class {
|
|
|
222
222
|
)
|
|
223
223
|
);
|
|
224
224
|
const tokenized = encodings.map((enc) => ({
|
|
225
|
-
ids: Array.from(enc.input_ids
|
|
226
|
-
mask: Array.from(enc.attention_mask
|
|
225
|
+
ids: Array.from(enc.input_ids),
|
|
226
|
+
mask: Array.from(enc.attention_mask)
|
|
227
227
|
}));
|
|
228
228
|
const seqLen = Math.max(...tokenized.map((t) => t.ids.length));
|
|
229
229
|
const flatIds = new BigInt64Array(n * seqLen);
|
|
@@ -648,4 +648,4 @@ export {
|
|
|
648
648
|
VectorIndex,
|
|
649
649
|
TextSearch
|
|
650
650
|
};
|
|
651
|
-
//# sourceMappingURL=chunk-
|
|
651
|
+
//# sourceMappingURL=chunk-44BQDRXD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/embedder.ts","../src/core/graph.ts","../src/core/vector.ts","../src/core/search-text.ts"],"sourcesContent":["import { AutoTokenizer } from \"@huggingface/transformers\";\nimport { readFile, writeFile, mkdir, rename, unlink } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { existsSync, createWriteStream } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { Readable } from \"node:stream\";\nimport { pipeline as streamPipeline } from \"node:stream/promises\";\nimport { Worker } from \"node:worker_threads\";\nimport { fileURLToPath } from \"node:url\";\n\n// MiniLM-L6-v2 is the default: ~3 min to index 2,853 chunks on CPU vs ~16 min for nomic.\n// nomic-embed-text-v1.5 gives higher quality embeddings but is much slower on CPU.\nconst DEFAULT_MODEL = \"sentence-transformers/all-MiniLM-L6-v2\";\nconst CACHE_DIR = join(homedir(), \".semantic-sidekick\", \"models\");\n// Default to 1 worker (serial). Worker threads only help on memory-rich machines\n// (each worker loads its own ONNX session). On typical dev machines\n// with <4 GB free RAM, parallel workers cause swap thrashing and are slower.\n// Enable with --workers N when you have sufficient RAM.\nconst DEFAULT_WORKERS = 1;\n// batch=16 is optimal for MiniLM on CPU (short sequences, low padding waste).\n// For larger models (nomic 768d), batch=1 is faster due to padding overhead.\nconst DEFAULT_BATCH_SIZE = 16;\n// Quantized ONNX: faster on CPU but not all models have a quantized variant.\n// MiniLM fp32 is already fast enough; nomic benefits from quantized.\n// Falls back to fp32 automatically if quantized file is not available.\nconst DEFAULT_QUANTIZED = false;\n\n// Full-precision ONNX model subpaths\nconst ONNX_MODEL_PATHS: Record<string, string> = {\n \"nomic-ai/nomic-embed-text-v1.5\": \"onnx/model.onnx\",\n \"sentence-transformers/all-MiniLM-L6-v2\": \"onnx/model.onnx\",\n};\n\n// Quantized (int8) ONNX model subpaths — faster on CPU, ~same quality\nconst ONNX_QUANTIZED_MODEL_PATHS: Record<string, string> = {\n \"nomic-ai/nomic-embed-text-v1.5\": \"onnx/model_quantized.onnx\",\n \"sentence-transformers/all-MiniLM-L6-v2\": \"onnx/model_quantized.onnx\",\n};\n\ninterface OrtSession {\n run(feeds: Record<string, unknown>): Promise<Record<string, { data: Float32Array; dims: number[] }>>;\n inputNames: string[];\n outputNames: string[];\n}\n\ninterface OrtModule {\n InferenceSession: {\n create(path: string, options?: Record<string, unknown>): Promise<OrtSession>;\n };\n Tensor: new (type: string, data: ArrayLike<number | bigint>, dims: number[]) => unknown;\n}\n\ntype RuntimeLabel = \"native\" | \"wasm\";\n\nasync function resolveOnnxRuntime(): Promise<{ ort: OrtModule; label: RuntimeLabel }> {\n try {\n const ort = await import(\"onnxruntime-node\");\n return { ort: ort as unknown as OrtModule, label: \"native\" };\n } catch {\n const ort = await import(\"onnxruntime-web\");\n return { ort: ort as unknown as OrtModule, label: \"wasm\" };\n }\n}\n\n// Download to a process-unique temp file, then atomically rename onto the\n// final path. Two concurrent downloaders won't corrupt each other's partial\n// writes — the worst case is one wasted download (rename is last-writer-wins\n// but each rename installs a complete, valid file). Without this, multiple\n// processes calling Embedder.init() against the same cache (e.g. parallel\n// vitest workers, or multiple semantic-sidekick servers on a fresh install) race\n// on a single shared writeStream and produce a corrupt ONNX file that fails\n// Protobuf parsing on load.\nasync function downloadFile(url: string, destPath: string): Promise<void> {\n const response = await fetch(url);\n if (!response.ok) throw new Error(`Download failed (${response.status}): ${url}`);\n if (!response.body) throw new Error(`No response body: ${url}`);\n const tempPath = `${destPath}.${process.pid}.${Date.now()}.tmp`;\n try {\n const fileStream = createWriteStream(tempPath);\n await streamPipeline(Readable.fromWeb(response.body as never), fileStream);\n await rename(tempPath, destPath);\n } catch (err) {\n try { await unlink(tempPath); } catch { /* temp may not exist */ }\n throw err;\n }\n}\n\nexport class Embedder {\n private model: string;\n private session: OrtSession | null = null;\n private tokenizer: Awaited<ReturnType<typeof AutoTokenizer.from_pretrained>> | null = null;\n private ort: OrtModule | null = null;\n private dimensions = 0;\n private runtimeLabel: RuntimeLabel = \"wasm\";\n private initialized = false;\n private numWorkers: number;\n private batchSize: number;\n private quantized: boolean;\n private modelPath = \"\";\n\n constructor(\n model: string = DEFAULT_MODEL,\n numWorkers: number = DEFAULT_WORKERS,\n batchSize: number = DEFAULT_BATCH_SIZE,\n quantized: boolean = DEFAULT_QUANTIZED\n ) {\n this.model = model;\n this.numWorkers = numWorkers;\n this.batchSize = batchSize;\n this.quantized = quantized;\n }\n\n async init(): Promise<void> {\n if (this.initialized) return;\n\n const modelDir = join(CACHE_DIR, this.model.replace(/\\//g, \"--\"));\n await mkdir(modelDir, { recursive: true });\n\n // Resolve ONNX runtime (native C++ or WASM fallback)\n const { ort, label } = await resolveOnnxRuntime();\n this.ort = ort;\n this.runtimeLabel = label;\n\n // Download ONNX model if not cached. Tries quantized first if requested,\n // falls back to fp32 if the quantized file is not available for this model.\n let useQuantized = this.quantized;\n const modelFileName = useQuantized ? \"model_quantized.onnx\" : \"model.onnx\";\n this.modelPath = join(modelDir, modelFileName);\n const modelPath = this.modelPath;\n if (!existsSync(modelPath)) {\n const pathMap = useQuantized ? ONNX_QUANTIZED_MODEL_PATHS : ONNX_MODEL_PATHS;\n const onnxSubpath = pathMap[this.model] ?? (useQuantized ? \"onnx/model_quantized.onnx\" : \"onnx/model.onnx\");\n const url = `https://huggingface.co/${this.model}/resolve/main/${onnxSubpath}`;\n process.stderr.write(`Downloading ONNX model: ${this.model} (${useQuantized ? \"quantized\" : \"fp32\"})...\\n`);\n try {\n await downloadFile(url, modelPath);\n } catch (err: any) {\n if (useQuantized && err?.message?.includes(\"404\")) {\n // Quantized not available for this model — fall back to fp32\n process.stderr.write(`Quantized model not available, falling back to fp32\\n`);\n useQuantized = false;\n this.modelPath = join(modelDir, \"model.onnx\");\n const fp32Subpath = ONNX_MODEL_PATHS[this.model] ?? \"onnx/model.onnx\";\n const fp32Url = `https://huggingface.co/${this.model}/resolve/main/${fp32Subpath}`;\n process.stderr.write(`Downloading ONNX model: ${this.model} (fp32)...\\n`);\n await downloadFile(fp32Url, this.modelPath);\n } else {\n throw err;\n }\n }\n process.stderr.write(`Model downloaded to ${modelDir}\\n`);\n }\n // Load tokenizer (uses HF transformers tokenizer infrastructure)\n this.tokenizer = await AutoTokenizer.from_pretrained(this.model, {\n cache_dir: CACHE_DIR,\n });\n\n // Create ONNX inference session (use this.modelPath — may have been updated by fallback)\n this.session = await ort.InferenceSession.create(this.modelPath, {\n executionProviders: [label === \"native\" ? \"cpu\" : \"wasm\"],\n });\n\n // Determine dimensions from a test embedding\n const test = await this.embed(\"test\");\n this.dimensions = test.length;\n this.initialized = true;\n\n const modelShort = this.model.split(\"/\").pop() ?? this.model;\n process.stderr.write(`Embedder ready (${modelShort}, ${label}, ${this.dimensions}d, batch=${this.batchSize})\\n`);\n }\n\n async embed(text: string): Promise<Float32Array> {\n if (!this.session || !this.tokenizer || !this.ort)\n throw new Error(\"Embedder not initialized. Call init() first.\");\n\n // Tokenize\n const encoded = await this.tokenizer(text, {\n padding: true,\n truncation: true,\n max_length: 512,\n return_tensor: false,\n });\n\n const inputIdsRaw: number[] = Array.from(encoded.input_ids);\n const attentionMaskRaw: number[] = Array.from(encoded.attention_mask);\n const seqLen = inputIdsRaw.length;\n\n // Build ONNX input tensors (most models expect int64)\n const inputIds = new this.ort.Tensor(\n \"int64\",\n BigInt64Array.from(inputIdsRaw.map(BigInt)),\n [1, seqLen]\n );\n const attentionMask = new this.ort.Tensor(\n \"int64\",\n BigInt64Array.from(attentionMaskRaw.map(BigInt)),\n [1, seqLen]\n );\n\n const feeds: Record<string, unknown> = { input_ids: inputIds, attention_mask: attentionMask };\n\n // Some models need token_type_ids\n if (this.session.inputNames.includes(\"token_type_ids\")) {\n feeds.token_type_ids = new this.ort.Tensor(\n \"int64\",\n new BigInt64Array(seqLen),\n [1, seqLen]\n );\n }\n\n // Run inference\n const output = await this.session.run(feeds);\n const outputTensor = output[this.session.outputNames[0]];\n const hiddenSize = outputTensor.dims[outputTensor.dims.length - 1];\n\n // Mean pooling with attention mask + L2 normalization\n return this.meanPoolAndNormalize(outputTensor.data, attentionMaskRaw, seqLen, hiddenSize);\n }\n\n private meanPoolAndNormalize(\n embeddings: Float32Array,\n attentionMask: number[],\n seqLen: number,\n hiddenSize: number\n ): Float32Array {\n const result = new Float32Array(hiddenSize);\n let maskSum = 0;\n\n for (let t = 0; t < seqLen; t++) {\n const mask = attentionMask[t];\n maskSum += mask;\n const offset = t * hiddenSize;\n for (let d = 0; d < hiddenSize; d++) {\n result[d] += embeddings[offset + d] * mask;\n }\n }\n\n if (maskSum > 0) {\n for (let d = 0; d < hiddenSize; d++) result[d] /= maskSum;\n }\n\n // L2 normalize\n let norm = 0;\n for (let d = 0; d < hiddenSize; d++) norm += result[d] * result[d];\n norm = Math.sqrt(norm);\n if (norm > 0) {\n for (let d = 0; d < hiddenSize; d++) result[d] /= norm;\n }\n\n return result;\n }\n\n // Mean pool + L2 normalize the output of a batched ONNX forward pass.\n // outputData: flat Float32Array of shape [batchSize, seqLen, hiddenSize]\n // maskData: flat number[] of shape [batchSize, seqLen]\n private meanPoolAndNormalizeMany(\n outputData: Float32Array,\n maskData: number[],\n batchSize: number,\n seqLen: number,\n hiddenSize: number\n ): Float32Array[] {\n const results: Float32Array[] = [];\n for (let b = 0; b < batchSize; b++) {\n const result = new Float32Array(hiddenSize);\n let maskSum = 0;\n for (let t = 0; t < seqLen; t++) {\n const mask = maskData[b * seqLen + t];\n maskSum += mask;\n const offset = (b * seqLen + t) * hiddenSize;\n for (let d = 0; d < hiddenSize; d++) {\n result[d] += outputData[offset + d] * mask;\n }\n }\n if (maskSum > 0) {\n for (let d = 0; d < hiddenSize; d++) result[d] /= maskSum;\n }\n // L2 normalize\n let norm = 0;\n for (let d = 0; d < hiddenSize; d++) norm += result[d] * result[d];\n norm = Math.sqrt(norm);\n if (norm > 0) {\n for (let d = 0; d < hiddenSize; d++) result[d] /= norm;\n }\n results.push(result);\n }\n return results;\n }\n\n // Embed a sub-batch of texts (length <= batchSize) in a single ONNX forward pass.\n //\n // Tokenizes each text individually (well-defined output format for all HF versions),\n // then manually pads to the longest sequence in the batch and builds a [n, seqLen]\n // tensor for one ONNX call. The speedup is from batching the ONNX inference —\n // individual tokenization is negligible (<1ms each).\n private async embedSubBatch(texts: string[]): Promise<Float32Array[]> {\n if (!this.session || !this.tokenizer || !this.ort)\n throw new Error(\"Embedder not initialized. Call init() first.\");\n\n const n = texts.length;\n\n // Tokenize each text individually — avoids ambiguous batch tokenizer output format\n const encodings = await Promise.all(\n texts.map((text) =>\n this.tokenizer!(text, {\n padding: false,\n truncation: true,\n max_length: 512,\n return_tensor: false,\n })\n )\n );\n\n // Extract flat token arrays and find max sequence length for this batch\n const tokenized = encodings.map((enc) => ({\n ids: Array.from(enc.input_ids) as number[],\n mask: Array.from(enc.attention_mask) as number[],\n }));\n const seqLen = Math.max(...tokenized.map((t) => t.ids.length));\n\n // Build flat padded tensors [n * seqLen] — pad with 0 (PAD token, zero attention)\n const flatIds = new BigInt64Array(n * seqLen);\n const flatMask = new BigInt64Array(n * seqLen);\n const flatMaskNums = new Array<number>(n * seqLen).fill(0);\n\n for (let i = 0; i < n; i++) {\n const { ids, mask } = tokenized[i];\n for (let j = 0; j < ids.length; j++) {\n flatIds[i * seqLen + j] = BigInt(ids[j]);\n flatMask[i * seqLen + j] = BigInt(mask[j]);\n flatMaskNums[i * seqLen + j] = mask[j];\n }\n // Positions beyond ids.length remain 0 (padding)\n }\n\n // Build batched ONNX tensors [n, seqLen]\n const inputIds = new this.ort.Tensor(\"int64\", flatIds, [n, seqLen]);\n const attentionMask = new this.ort.Tensor(\"int64\", flatMask, [n, seqLen]);\n\n const feeds: Record<string, unknown> = { input_ids: inputIds, attention_mask: attentionMask };\n\n if (this.session.inputNames.includes(\"token_type_ids\")) {\n feeds.token_type_ids = new this.ort.Tensor(\n \"int64\",\n new BigInt64Array(n * seqLen),\n [n, seqLen]\n );\n }\n\n // Single forward pass → output shape [n, seqLen, hiddenSize]\n const output = await this.session.run(feeds);\n const outputTensor = output[this.session.outputNames[0]];\n const hiddenSize = outputTensor.dims[outputTensor.dims.length - 1];\n\n return this.meanPoolAndNormalizeMany(\n outputTensor.data,\n flatMaskNums,\n n,\n seqLen,\n hiddenSize\n );\n }\n\n async embedBatch(\n texts: string[],\n onProgress?: (embedded: number, total: number, subBatch?: Float32Array[]) => Promise<void> | void\n ): Promise<Float32Array[]> {\n // Workers path (disabled by default; kept for --workers N users)\n if (this.numWorkers > 1 && texts.length >= this.numWorkers * 2) {\n return this.embedBatchParallel(texts, onProgress);\n }\n\n // True batched inference: slice into sub-batches and run one ONNX call each\n const results: Float32Array[] = [];\n for (let i = 0; i < texts.length; i += this.batchSize) {\n const subBatch = texts.slice(i, i + this.batchSize);\n const embeddings = await this.embedSubBatch(subBatch);\n results.push(...embeddings);\n await onProgress?.(Math.min(i + subBatch.length, texts.length), texts.length, embeddings);\n }\n return results;\n }\n\n private async embedBatchParallel(\n texts: string[],\n onProgress?: (embedded: number, total: number, subBatch?: Float32Array[]) => Promise<void> | void\n ): Promise<Float32Array[]> {\n // Resolve worker script path. With tsup splitting, embedder code may land\n // in a top-level chunk (dist/chunk-*.js) rather than dist/core/index.js,\n // so we try two locations: adjacent to this chunk, and dist/core/ relative\n // to the package root (two levels up from a dist/ chunk file).\n const thisDir = dirname(fileURLToPath(import.meta.url));\n let workerPath = join(thisDir, \"embed-worker.js\");\n if (!existsSync(workerPath)) {\n // Chunk file at dist/ level — worker is in dist/core/\n workerPath = join(thisDir, \"core\", \"embed-worker.js\");\n }\n\n if (!existsSync(workerPath)) {\n // Fallback to serial batched if worker script not found\n process.stderr.write(\"Worker script not found, falling back to batched embedding\\n\");\n return this.embedBatch(texts, onProgress);\n }\n\n // Split texts into chunks for each worker\n const chunkSize = Math.ceil(texts.length / this.numWorkers);\n const chunks: { texts: string[]; startIndex: number }[] = [];\n for (let i = 0; i < texts.length; i += chunkSize) {\n chunks.push({ texts: texts.slice(i, i + chunkSize), startIndex: i });\n }\n\n const allResults = new Array<Float32Array>(texts.length);\n let totalDone = 0;\n\n const workerPromises = chunks.map((chunk) => {\n return new Promise<void>((resolve, reject) => {\n const worker = new Worker(workerPath, {\n workerData: {\n modelPath: this.modelPath,\n modelName: this.model,\n cacheDir: CACHE_DIR,\n runtimeLabel: this.runtimeLabel,\n batchSize: this.batchSize,\n },\n });\n\n worker.on(\"message\", (msg: any) => {\n if (msg.type === \"ready\") {\n worker.postMessage({ type: \"embed\", texts: chunk.texts, startIndex: chunk.startIndex });\n } else if (msg.type === \"progress\") {\n totalDone++;\n onProgress?.(totalDone, texts.length);\n } else if (msg.type === \"result\") {\n for (let i = 0; i < msg.embeddings.length; i++) {\n allResults[chunk.startIndex + i] = new Float32Array(msg.embeddings[i]);\n }\n worker.terminate();\n resolve();\n } else if (msg.type === \"error\") {\n worker.terminate();\n reject(new Error(msg.error));\n }\n });\n\n worker.on(\"error\", (err) => {\n worker.terminate();\n reject(err);\n });\n });\n });\n\n await Promise.all(workerPromises);\n return allResults;\n }\n\n getDimensions(): number {\n return this.dimensions;\n }\n\n getModel(): string {\n return this.model;\n }\n\n getRuntime(): RuntimeLabel {\n return this.runtimeLabel;\n }\n\n async saveEmbeddings(\n embeddings: Map<string, Float32Array>,\n indexPath: string\n ): Promise<void> {\n const entries: Array<{ key: string; data: number[] }> = [];\n for (const [key, vec] of embeddings) {\n entries.push({ key, data: Array.from(vec) });\n }\n await writeFile(join(indexPath, \"embeddings.json\"), JSON.stringify(entries));\n }\n\n async loadEmbeddings(\n indexPath: string\n ): Promise<Map<string, Float32Array>> {\n const filePath = join(indexPath, \"embeddings.json\");\n if (!existsSync(filePath)) return new Map();\n\n const raw = await readFile(filePath, \"utf-8\");\n const entries: Array<{ key: string; data: number[] }> = JSON.parse(raw);\n const map = new Map<string, Float32Array>();\n for (const entry of entries) {\n map.set(entry.key, new Float32Array(entry.data));\n }\n return map;\n }\n}\n","import Graph from \"graphology\";\nimport { bfsFromNode } from \"graphology-traversal\";\nimport { bidirectional } from \"graphology-shortest-path\";\nimport type { IndexedDocument, GraphNode, GraphEdge, GraphStats } from \"./types.js\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\n/**\n * Tags shared by more than this many docs are too generic to be meaningful\n * graph edges (e.g. \"api\", \"testing\"). Only specific/rare tags create edges.\n * Docs without frontmatter are unaffected — their inline #tags still participate\n * but only if those tags are specific enough to pass this threshold.\n */\nconst TAG_EDGE_MAX_DOCS = 15;\n\nexport class GraphBuilder {\n private graph: Graph;\n\n constructor() {\n this.graph = new Graph({ type: \"directed\", multi: false });\n }\n\n buildFromDocuments(documents: IndexedDocument[]): void {\n this.graph.clear();\n\n // Add nodes — store optional metadata fields when present\n for (const doc of documents) {\n this.graph.addNode(doc.path, {\n title: doc.title,\n tags: doc.tags,\n ...(doc.status !== undefined && { status: doc.status }),\n ...(doc.loadPriority !== undefined && { loadPriority: doc.loadPriority }),\n });\n }\n\n const pathLookup = new Map<string, string>();\n for (const doc of documents) {\n const nameNoExt = doc.path.replace(/\\.md$/, \"\");\n const basename = nameNoExt.split(\"/\").pop()!;\n pathLookup.set(basename.toLowerCase(), doc.path);\n pathLookup.set(nameNoExt.toLowerCase(), doc.path);\n }\n\n // Add wikilink edges (weight 1.0)\n for (const doc of documents) {\n for (const link of doc.wikilinks) {\n const target = pathLookup.get(link.toLowerCase());\n if (target && target !== doc.path && !this.graph.hasEdge(doc.path, target)) {\n this.graph.addEdge(doc.path, target, { type: \"wikilink\", weight: 1.0 });\n }\n }\n }\n\n // Add related_docs edges (weight 1.0) — only present when frontmatter has related_docs\n // Falls back gracefully: docs without this field are simply skipped\n for (const doc of documents) {\n if (!doc.relatedDocs?.length) continue;\n for (const ref of doc.relatedDocs) {\n // Try full normalized path first, then basename — same strategy as wikilinks\n const normalized = ref.replace(/\\.md$/, \"\").toLowerCase();\n const base = normalized.split(\"/\").pop()!;\n const target = pathLookup.get(normalized) ?? pathLookup.get(base);\n if (target && target !== doc.path && !this.graph.hasEdge(doc.path, target)) {\n this.graph.addEdge(doc.path, target, { type: \"related\", weight: 1.0 });\n }\n }\n }\n\n // Add tag-based edges (weight 0.5) — capped at TAG_EDGE_MAX_DOCS per tag.\n // Generic tags shared by many docs (e.g. \"api\", \"testing\") create noise;\n // only specific tags (e.g. \"qdrant\", \"celery\") create meaningful connections.\n const tagToNotes = new Map<string, string[]>();\n for (const doc of documents) {\n for (const tag of doc.tags) {\n const notes = tagToNotes.get(tag) ?? [];\n notes.push(doc.path);\n tagToNotes.set(tag, notes);\n }\n }\n\n for (const [, notes] of tagToNotes) {\n if (notes.length >= TAG_EDGE_MAX_DOCS) continue; // too generic — skip\n for (let i = 0; i < notes.length; i++) {\n for (let j = i + 1; j < notes.length; j++) {\n if (!this.graph.hasEdge(notes[i], notes[j])) {\n this.graph.addEdge(notes[i], notes[j], { type: \"tag\", weight: 0.5 });\n }\n if (!this.graph.hasEdge(notes[j], notes[i])) {\n this.graph.addEdge(notes[j], notes[i], { type: \"tag\", weight: 0.5 });\n }\n }\n }\n }\n }\n\n backlinks(notePath: string): GraphNode[] {\n if (!this.graph.hasNode(notePath)) return [];\n return this.graph.inNeighbors(notePath).map((n) => this.nodeToGraphNode(n));\n }\n\n forwardlinks(notePath: string): GraphNode[] {\n if (!this.graph.hasNode(notePath)) return [];\n return this.graph.outNeighbors(notePath).map((n) => this.nodeToGraphNode(n));\n }\n\n findPath(from: string, to: string): string[] | null {\n if (!this.graph.hasNode(from) || !this.graph.hasNode(to)) return null;\n const path = bidirectional(this.graph, from, to);\n return path;\n }\n\n searchGraph(concept: string, maxDepth = 2, limit = 20): GraphNode[] {\n const lc = concept.toLowerCase();\n\n const directMatches = new Set<string>(\n this.graph.nodes().filter((n) => {\n const attrs = this.graph.getNodeAttributes(n);\n const title = typeof attrs.title === \"string\" ? attrs.title : String(attrs.title ?? \"\");\n return (\n n.toLowerCase().includes(lc) ||\n title.toLowerCase().includes(lc) ||\n (attrs.tags as string[] | undefined)?.some((t) => t.toLowerCase().includes(lc))\n );\n })\n );\n\n const visited = new Set<string>();\n for (const start of directMatches) {\n bfsFromNode(this.graph, start, (_node, _attr, depth) => {\n if (visited.size >= limit * 3) return true; // gather 3x limit before ranking\n visited.add(_node);\n return depth >= maxDepth;\n });\n }\n\n // Sort: direct matches first, then by loadPriority desc, then by degree desc\n // deprecated/archived nodes sort to the end\n const ranked = [...visited].sort((a, b) => {\n const aAttrs = this.graph.getNodeAttributes(a);\n const bAttrs = this.graph.getNodeAttributes(b);\n const aDeprecated = aAttrs.status === \"deprecated\" || aAttrs.status === \"archived\" ? 1 : 0;\n const bDeprecated = bAttrs.status === \"deprecated\" || bAttrs.status === \"archived\" ? 1 : 0;\n if (aDeprecated !== bDeprecated) return aDeprecated - bDeprecated;\n const aDirect = directMatches.has(a) ? 0 : 1;\n const bDirect = directMatches.has(b) ? 0 : 1;\n if (aDirect !== bDirect) return aDirect - bDirect;\n const aPriority = (aAttrs.loadPriority as number | undefined) ?? 5;\n const bPriority = (bAttrs.loadPriority as number | undefined) ?? 5;\n if (aPriority !== bPriority) return bPriority - aPriority;\n return this.graph.degree(b) - this.graph.degree(a);\n });\n\n return ranked.slice(0, limit).map((n) => this.nodeToGraphNode(n));\n }\n\n statistics(): GraphStats {\n const nodes = this.graph.order;\n const edges = this.graph.size;\n const orphans = this.graph.nodes().filter(\n (n) => this.graph.degree(n) === 0\n );\n\n const connections = this.graph.nodes().map((n) => ({\n path: n,\n connections: this.graph.degree(n),\n }));\n connections.sort((a, b) => b.connections - a.connections);\n\n const maxPossibleEdges = nodes * (nodes - 1);\n const density = maxPossibleEdges > 0 ? edges / maxPossibleEdges : 0;\n\n return {\n totalNodes: nodes,\n totalEdges: edges,\n orphanCount: orphans.length,\n mostConnected: connections.slice(0, 10),\n density,\n };\n }\n\n private nodeToGraphNode(nodePath: string): GraphNode {\n const attrs = this.graph.getNodeAttributes(nodePath);\n return {\n path: nodePath,\n title: typeof attrs.title === \"string\" ? attrs.title : nodePath,\n tags: (attrs.tags as string[]) || [],\n linkCount: this.graph.outDegree(nodePath),\n backlinkCount: this.graph.inDegree(nodePath),\n ...(attrs.loadPriority !== undefined && { loadPriority: attrs.loadPriority as number }),\n ...(attrs.status !== undefined && { status: attrs.status as string }),\n };\n }\n\n async save(indexPath: string): Promise<void> {\n const data = this.graph.export();\n await writeFile(join(indexPath, \"graph.json\"), JSON.stringify(data));\n }\n\n async load(indexPath: string): Promise<boolean> {\n const filePath = join(indexPath, \"graph.json\");\n if (!existsSync(filePath)) return false;\n\n const raw = await readFile(filePath, \"utf-8\");\n const data = JSON.parse(raw);\n this.graph.import(data);\n return true;\n }\n\n getGraph(): Graph {\n return this.graph;\n }\n}\n","import hnswlib from \"hnswlib-node\";\nconst { HierarchicalNSW } = hnswlib;\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport type { SearchResult } from \"./types.js\";\n\ninterface ChunkMeta {\n docPath: string;\n chunkIndex: number;\n text: string;\n}\n\nexport class VectorIndex {\n private index: InstanceType<typeof HierarchicalNSW> | null = null;\n private dimensions: number;\n private chunkMeta: ChunkMeta[] = [];\n\n constructor(dimensions: number) {\n this.dimensions = dimensions;\n }\n\n build(\n embeddings: Float32Array[],\n meta: ChunkMeta[]\n ): void {\n if (embeddings.length === 0) {\n this.index = null;\n this.chunkMeta = [];\n return;\n }\n\n this.index = new HierarchicalNSW(\"cosine\", this.dimensions);\n this.index.initIndex(embeddings.length);\n\n for (let i = 0; i < embeddings.length; i++) {\n this.index.addPoint(Array.from(embeddings[i]), i);\n }\n\n this.chunkMeta = meta;\n }\n\n search(queryEmbedding: Float32Array, k: number = 10): SearchResult[] {\n if (!this.index || this.chunkMeta.length === 0) return [];\n\n const numResults = Math.min(k, this.chunkMeta.length);\n const result = this.index.searchKnn(Array.from(queryEmbedding), numResults);\n\n const seen = new Set<string>();\n const results: SearchResult[] = [];\n\n for (let i = 0; i < result.neighbors.length; i++) {\n const idx = result.neighbors[i];\n const meta = this.chunkMeta[idx];\n if (!meta || seen.has(meta.docPath)) continue;\n seen.add(meta.docPath);\n\n results.push({\n path: meta.docPath,\n title: meta.docPath,\n score: 1 - result.distances[i],\n snippet: meta.text.slice(0, 200),\n matchedChunk: meta.text,\n });\n }\n\n return results;\n }\n\n async save(indexPath: string): Promise<void> {\n if (!this.index) return;\n\n this.index.writeIndexSync(join(indexPath, \"hnsw.bin\"));\n await writeFile(\n join(indexPath, \"hnsw-meta.json\"),\n JSON.stringify(this.chunkMeta)\n );\n }\n\n async load(indexPath: string): Promise<boolean> {\n const hnswPath = join(indexPath, \"hnsw.bin\");\n const metaPath = join(indexPath, \"hnsw-meta.json\");\n\n if (!existsSync(hnswPath) || !existsSync(metaPath)) return false;\n\n const raw = await readFile(metaPath, \"utf-8\");\n this.chunkMeta = JSON.parse(raw);\n\n this.index = new HierarchicalNSW(\"cosine\", this.dimensions);\n this.index.initIndex(this.chunkMeta.length);\n this.index.readIndexSync(hnswPath);\n\n return true;\n }\n\n getChunkMeta(): ChunkMeta[] {\n return this.chunkMeta;\n }\n}\n","import type { IndexedDocument, SearchResult, SearchTextOptions } from \"./types.js\";\nimport { minimatch } from \"minimatch\";\n\nexport class TextSearch {\n private documents: IndexedDocument[] = [];\n\n setDocuments(documents: IndexedDocument[]): void {\n this.documents = documents;\n }\n\n search(options: SearchTextOptions): SearchResult[] {\n const { pattern, regex, caseSensitive, pathGlob, tagFilter, limit = 20 } = options;\n\n let matcher: (text: string) => { matched: boolean; index: number };\n\n if (regex) {\n const flags = caseSensitive ? \"g\" : \"gi\";\n const re = new RegExp(pattern, flags);\n matcher = (text) => {\n re.lastIndex = 0;\n const m = re.exec(text);\n return { matched: !!m, index: m?.index ?? -1 };\n };\n } else {\n const needle = caseSensitive ? pattern : pattern.toLowerCase();\n matcher = (text) => {\n const haystack = caseSensitive ? text : text.toLowerCase();\n const idx = haystack.indexOf(needle);\n return { matched: idx >= 0, index: idx };\n };\n }\n\n const results: SearchResult[] = [];\n\n for (const doc of this.documents) {\n // Path filter\n if (pathGlob && !minimatch(doc.path, pathGlob)) continue;\n\n // Tag filter\n if (tagFilter?.length) {\n const hasTag = tagFilter.some((t) => doc.tags.includes(t));\n if (!hasTag) continue;\n }\n\n const { matched, index } = matcher(doc.content);\n if (!matched) continue;\n\n const snippetStart = Math.max(0, index - 80);\n const snippetEnd = Math.min(doc.content.length, index + 120);\n const snippet = doc.content.slice(snippetStart, snippetEnd).trim();\n\n results.push({\n path: doc.path,\n title: doc.title,\n score: 1.0,\n snippet: (snippetStart > 0 ? \"...\" : \"\") + snippet + (snippetEnd < doc.content.length ? \"...\" : \"\"),\n });\n\n if (results.length >= limit) break;\n }\n\n return results;\n }\n}\n"],"mappings":"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","names":["readFile","writeFile","join","existsSync","readFile","writeFile","join","existsSync"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
lintVault
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-OFXQXVG5.js";
|
|
4
4
|
|
|
5
5
|
// src/core/healthcheck.ts
|
|
6
6
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
@@ -338,4 +338,4 @@ export {
|
|
|
338
338
|
filterToDrift,
|
|
339
339
|
formatDriftBanner
|
|
340
340
|
};
|
|
341
|
-
//# sourceMappingURL=chunk-
|
|
341
|
+
//# sourceMappingURL=chunk-D5QLNNA4.js.map
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listDossiers
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-XAXK7NM4.js";
|
|
4
4
|
import {
|
|
5
5
|
deriveSessionDir
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LGQPP5FQ.js";
|
|
7
|
+
import {
|
|
8
|
+
redactDeep
|
|
9
|
+
} from "./chunk-OEF5U5GV.js";
|
|
7
10
|
|
|
8
11
|
// src/core/lexicon.ts
|
|
9
12
|
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
@@ -88,7 +91,8 @@ function expandQuery(aliases, query) {
|
|
|
88
91
|
const additions = [...new Set(matched.map((a) => a.canonical))].join(" ");
|
|
89
92
|
return { expanded: `${query} ${additions}`, matched };
|
|
90
93
|
}
|
|
91
|
-
async function writeAliasNote(vaultPath, rel,
|
|
94
|
+
async function writeAliasNote(vaultPath, rel, raw) {
|
|
95
|
+
const entry = redactDeep(raw, "precise").value;
|
|
92
96
|
const fm = {
|
|
93
97
|
type: "alias",
|
|
94
98
|
canonical: entry.canonical,
|
|
@@ -173,4 +177,4 @@ export {
|
|
|
173
177
|
findAliasConflicts,
|
|
174
178
|
removeAlias
|
|
175
179
|
};
|
|
176
|
-
//# sourceMappingURL=chunk-
|
|
180
|
+
//# sourceMappingURL=chunk-FTRXH2WC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/lexicon.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, unlink } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"gray-matter\";\nimport { redactDeep } from \"./redact.js\";\nimport { deriveSessionDir } from \"./session.js\";\nimport { listDossiers } from \"./dossier.js\";\n\n/**\n * Lexicon corpus (v1.4 Phase 3) — the learned human→artifact vocabulary.\n *\n * The heart of the human→LLM bridge: maps the human's own phrasing (\"the flaky\n * thing\", \"the deploy dance\") to concrete artifacts (paths / code symbols).\n * Canonical storage is markdown notes of type `alias` under <vault>/lexicon/;\n * a derived JSON cache under .claude/.semantic-memory/ is what the hot-path hook\n * reads for deterministic query expansion.\n */\n\nexport type Confidence = \"low\" | \"medium\" | \"high\";\n\nexport interface AliasEntry {\n /** The concrete target: a note path or a code symbol/path. */\n canonical: string;\n /** The human's actual words that refer to it (verbatim). */\n phrases: string[];\n confidence: Confidence;\n evidence_count: number;\n last_used?: string;\n source: \"learned\" | \"authored\";\n /** The lexicon note path this entry came from. */\n path: string;\n}\n\nconst CACHE_FILE = \"lexicon-cache.json\";\nconst LEXICON_DIR = \"lexicon\";\n\nexport function normalizePhrase(s: string): string {\n return s.toLowerCase().trim().replace(/\\s+/g, \" \");\n}\n\nexport function slugify(s: string): string {\n return s.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 60) || \"alias\";\n}\n\nfunction confidenceFor(evidenceCount: number): Confidence {\n return evidenceCount >= 3 ? \"high\" : evidenceCount >= 2 ? \"medium\" : \"low\";\n}\n\n/** Read every lexicon note into entries and write the derived cache. */\nexport async function compileLexicon(vaultPath: string): Promise<AliasEntry[]> {\n const files = await glob(`${LEXICON_DIR}/**/*.md`, { cwd: vaultPath });\n const entries: AliasEntry[] = [];\n for (const rel of files) {\n try {\n const { data } = matter(await readFile(join(vaultPath, rel), \"utf-8\"));\n if (data.type !== \"alias\" || typeof data.canonical !== \"string\") continue;\n const phrases = Array.isArray(data.phrases) ? data.phrases.map(String).map((p) => p.trim()).filter(Boolean) : [];\n entries.push({\n canonical: data.canonical,\n phrases,\n confidence: ([\"low\", \"medium\", \"high\"] as const).includes(data.confidence) ? data.confidence : confidenceFor(1),\n evidence_count: typeof data.evidence_count === \"number\" ? data.evidence_count : phrases.length ? 1 : 0,\n last_used: typeof data.last_used === \"string\" ? data.last_used : undefined,\n source: data.source === \"authored\" ? \"authored\" : \"learned\",\n path: rel,\n });\n } catch {\n /* skip unreadable note */\n }\n }\n // Fold in dossier aliases (v1.5 Phase 8): a dossier's `aliases:` are the human's\n // phrases for that entity, so they become query-expansion bindings pointing at the\n // dossier note. Authored, not learned — the human named the entity deliberately.\n // A lexicon note for the same canonical wins (it carries real evidence_count).\n try {\n const dossiers = await listDossiers(vaultPath);\n for (const d of dossiers) {\n if (d.aliases.length === 0) continue;\n if (entries.some((e) => e.canonical === d.path)) continue;\n entries.push({\n canonical: d.path,\n phrases: d.aliases,\n confidence: \"high\",\n evidence_count: d.aliases.length,\n source: \"authored\",\n path: d.path,\n });\n }\n } catch {\n /* dossiers optional — lexicon compiles fine without them */\n }\n try {\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n await writeFile(join(dir, CACHE_FILE), JSON.stringify({ compiledAt: new Date().toISOString(), aliases: entries }, null, 2), \"utf-8\");\n } catch {\n /* cache write is best-effort */\n }\n return entries;\n}\n\n/** Fast read for the hook: use the cache if present, else compile. */\nexport async function loadLexiconCache(vaultPath: string): Promise<AliasEntry[]> {\n const cachePath = join(deriveSessionDir(vaultPath), CACHE_FILE);\n if (existsSync(cachePath)) {\n try {\n const data = JSON.parse(await readFile(cachePath, \"utf-8\"));\n if (Array.isArray(data?.aliases)) return data.aliases as AliasEntry[];\n } catch {\n /* fall through to recompile */\n }\n }\n return compileLexicon(vaultPath);\n}\n\nexport function lookupPhrase(aliases: AliasEntry[], phrase: string): AliasEntry[] {\n const n = normalizePhrase(phrase);\n return aliases.filter((a) => a.phrases.some((p) => normalizePhrase(p) === n));\n}\n\n/**\n * Expand a query by appending the canonical targets of any alias phrases that\n * appear in it. Deterministic, no LLM — this is the Tier-1 hook-time expansion.\n */\nexport function expandQuery(aliases: AliasEntry[], query: string): { expanded: string; matched: AliasEntry[] } {\n const nq = normalizePhrase(query);\n const matched = aliases.filter((a) => a.phrases.some((p) => p && nq.includes(normalizePhrase(p))));\n if (matched.length === 0) return { expanded: query, matched: [] };\n const additions = [...new Set(matched.map((a) => a.canonical))].join(\" \");\n return { expanded: `${query} ${additions}`, matched };\n}\n\nasync function writeAliasNote(vaultPath: string, rel: string, raw: Omit<AliasEntry, \"path\">): Promise<void> {\n // phrases are captured from user utterances\n const entry = redactDeep(raw, \"precise\").value;\n const fm = {\n type: \"alias\",\n canonical: entry.canonical,\n phrases: entry.phrases,\n confidence: entry.confidence,\n evidence_count: entry.evidence_count,\n last_used: entry.last_used,\n source: entry.source,\n };\n const body = `# Alias: ${entry.canonical}\\n\\nHuman phrases that refer to \\`${entry.canonical}\\`.\\n`;\n const abs = join(vaultPath, rel);\n await mkdir(dirname(abs), { recursive: true });\n await writeFile(abs, matter.stringify(body, fm), \"utf-8\");\n}\n\n/**\n * Upsert an alias. New canonical → new note (evidence 1). Existing canonical →\n * merge phrases, bump evidence_count, recompute confidence. Recompiles the cache.\n */\nexport async function addAlias(\n vaultPath: string,\n input: { canonical: string; phrases: string[]; source?: \"learned\" | \"authored\"; today?: string }\n): Promise<{ path: string; created: boolean; evidence_count: number; confidence: Confidence }> {\n const existing = await compileLexicon(vaultPath);\n const match = existing.find((a) => a.canonical === input.canonical);\n const today = input.today ?? new Date().toISOString().slice(0, 10);\n const cleanPhrases = input.phrases.map((p) => p.trim()).filter(Boolean);\n\n if (match) {\n const phrases = [...new Set([...match.phrases, ...cleanPhrases])];\n const evidence_count = match.evidence_count + 1;\n const confidence = confidenceFor(evidence_count);\n await writeAliasNote(vaultPath, match.path, { ...match, phrases, evidence_count, confidence, last_used: today });\n await compileLexicon(vaultPath);\n return { path: match.path, created: false, evidence_count, confidence };\n }\n\n const rel = join(LEXICON_DIR, `${slugify(input.canonical)}.md`);\n const evidence_count = 1;\n const confidence = confidenceFor(evidence_count);\n await writeAliasNote(vaultPath, rel, {\n canonical: input.canonical,\n phrases: cleanPhrases,\n confidence,\n evidence_count,\n last_used: today,\n source: input.source ?? \"learned\",\n });\n await compileLexicon(vaultPath);\n return { path: rel, created: true, evidence_count, confidence };\n}\n\nexport interface AliasConflict {\n phrase: string;\n canonicals: string[];\n}\n\n/**\n * Find conflicting bindings: the same (normalized) phrase mapping to more than one\n * canonical target. A wrong alias silently misroutes every future query with that\n * phrase, so conflicts are surfaced (never auto-resolved) for a human to fix.\n */\nexport function findAliasConflicts(aliases: AliasEntry[]): AliasConflict[] {\n const byPhrase = new Map<string, Set<string>>();\n for (const a of aliases) {\n for (const p of a.phrases) {\n const n = normalizePhrase(p);\n if (!n) continue;\n if (!byPhrase.has(n)) byPhrase.set(n, new Set());\n byPhrase.get(n)!.add(a.canonical);\n }\n }\n const conflicts: AliasConflict[] = [];\n for (const [phrase, canonicals] of byPhrase) {\n if (canonicals.size > 1) conflicts.push({ phrase, canonicals: [...canonicals].sort() });\n }\n return conflicts.sort((a, b) => a.phrase.localeCompare(b.phrase));\n}\n\n/** Remove an alias note by canonical. Returns whether one was removed. */\nexport async function removeAlias(vaultPath: string, canonical: string): Promise<boolean> {\n const existing = await compileLexicon(vaultPath);\n const match = existing.find((a) => a.canonical === canonical);\n if (!match) return false;\n try {\n await unlink(join(vaultPath, match.path));\n } catch {\n return false;\n }\n await compileLexicon(vaultPath);\n return true;\n}\n"],"mappings":"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","names":["evidence_count","confidence"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/schema.ts","../src/core/schema-default.ts"],"sourcesContent":["import { readFile, writeFile, access } from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport matter from \"gray-matter\";\nimport { DEFAULT_SCHEMA_YAML } from \"./schema-default.js\";\n\nexport interface VaultSchema {\n version: number;\n provenance_fields: string[];\n status_enum: string[];\n confidence_enum: string[];\n severity_enum: string[];\n types: Record<string, { description?: string; required: string[] }>;\n lint: {\n missing_provenance: { applies_to: string[]; severity: \"warn\" | \"error\" };\n stale: { max_age_days: number; severity: \"warn\" | \"error\" };\n schema_violations: { severity: \"warn\" | \"error\" };\n };\n}\n\nexport type Severity = \"warn\" | \"error\";\n\nexport interface LintFinding {\n path: string;\n rule: \"missing_provenance\" | \"stale\" | \"schema_violations\" | \"broken_links\" | \"code_symbols\" | \"decay_candidates\" | \"alias_conflicts\" | \"decoys\" | \"secrets\";\n severity: Severity;\n message: string;\n}\n\nexport async function loadSchema(vaultPath: string): Promise<VaultSchema> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n let raw: string;\n try {\n await access(schemaPath, constants.R_OK);\n raw = await readFile(schemaPath, \"utf-8\");\n } catch {\n raw = DEFAULT_SCHEMA_YAML;\n }\n const parsed = parseYaml(raw) as VaultSchema;\n return parsed;\n}\n\n/**\n * Bootstrap the schema file in a vault by writing the default if none exists.\n * Used by the `--install-schema` CLI flag. Returns the path that was (or would be) written.\n */\nexport async function installDefaultSchema(vaultPath: string, force = false): Promise<{ written: boolean; path: string }> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!force) {\n try {\n await access(schemaPath, constants.F_OK);\n return { written: false, path: schemaPath };\n } catch {\n // not present → write it\n }\n }\n await writeFile(schemaPath, DEFAULT_SCHEMA_YAML, \"utf-8\");\n return { written: true, path: schemaPath };\n}\n\n/**\n * Validate a single note's frontmatter + content against the schema.\n * Returns findings (possibly empty). Does NOT throw — lint never throws.\n */\nexport function validateNote(\n relativePath: string,\n rawFileContent: string,\n schema: VaultSchema,\n opts: { todayIso?: string } = {}\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const { data: fm } = matter(rawFileContent);\n\n const typeName = typeof fm.type === \"string\" ? fm.type : \"note\";\n const typeDef = schema.types[typeName];\n\n if (!typeDef) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `unknown type \"${typeName}\" — allowed: ${Object.keys(schema.types).join(\", \")}`,\n });\n return findings; // can't validate required fields if type is unknown\n }\n\n // Required field check\n for (const field of typeDef.required) {\n if (fm[field] === undefined || fm[field] === null || fm[field] === \"\") {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `missing required field \"${field}\" for type \"${typeName}\"`,\n });\n }\n }\n\n // Enum checks\n if (fm.status !== undefined && !schema.status_enum.includes(String(fm.status))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `status \"${fm.status}\" not in enum [${schema.status_enum.join(\", \")}]`,\n });\n }\n if (fm.confidence !== undefined && !schema.confidence_enum.includes(String(fm.confidence))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `confidence \"${fm.confidence}\" not in enum [${schema.confidence_enum.join(\", \")}]`,\n });\n }\n if (typeName === \"gotcha\" && fm.severity !== undefined && !schema.severity_enum.includes(String(fm.severity))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `severity \"${fm.severity}\" not in enum [${schema.severity_enum.join(\", \")}]`,\n });\n }\n\n // Missing provenance\n if (schema.lint.missing_provenance.applies_to.includes(typeName)) {\n const hasSources = Array.isArray(fm.sources) ? fm.sources.length > 0 : typeof fm.sources === \"string\" && fm.sources.length > 0;\n const hasDerived = Array.isArray(fm.derived_from) ? fm.derived_from.length > 0 : typeof fm.derived_from === \"string\" && fm.derived_from.length > 0;\n if (!hasSources && !hasDerived) {\n findings.push({\n path: relativePath,\n rule: \"missing_provenance\",\n severity: schema.lint.missing_provenance.severity,\n message: `no sources or derived_from on ${typeName}`,\n });\n }\n }\n\n // Stale\n if (schema.lint.stale.max_age_days > 0 && fm.last_verified) {\n const today = opts.todayIso ?? new Date().toISOString().slice(0, 10);\n const verified = String(fm.last_verified);\n const ageDays = dayDiff(today, verified);\n if (ageDays !== null && ageDays > schema.lint.stale.max_age_days) {\n findings.push({\n path: relativePath,\n rule: \"stale\",\n severity: schema.lint.stale.severity,\n message: `last_verified ${verified} is ${ageDays} days old (limit ${schema.lint.stale.max_age_days})`,\n });\n }\n }\n\n return findings;\n}\n\nfunction dayDiff(todayIso: string, thenIso: string): number | null {\n const a = Date.parse(todayIso);\n const b = Date.parse(thenIso);\n if (Number.isNaN(a) || Number.isNaN(b)) return null;\n return Math.round((a - b) / 86400000);\n}\n","// Default vault schema shipped with semantic-sidekick.\n// Intentionally minimal — the plan is to grow from pain, not preempt it.\n// Users can override with a vault.schema.yml at the vault root.\n\nexport const DEFAULT_SCHEMA_YAML = `# semantic-sidekick vault schema (minimal starter).\n# Override by placing a vault.schema.yml at the vault root.\n\nversion: 1\n\n# Provenance fields — apply to all types, enforced by lint.find_missing_provenance.\n# sources: URLs or filesystem paths the note was derived from\n# derived_from: wikilinks to other notes this one synthesizes\n# last_verified: ISO date this note was last confirmed true (YYYY-MM-DD).\n# Set at creation and by verify_note; NOT bumped on ordinary edits.\n# Drives confidence decay (see the decay: block below).\n# evergreen: true → the note is pinned against decay while last_verified is\n# within 365 days; after that it decays normally (re-affirm to reset).\n# status: one of: draft | active | stale | deprecated | archived\n# confidence: one of: high | medium | low\nprovenance_fields:\n - sources\n - derived_from\n - last_verified\n - status\n - confidence\n\n# Allowed status values (enforced by schema_violations lint).\nstatus_enum: [draft, active, stale, deprecated, archived]\nconfidence_enum: [high, medium, low]\n\n# Note types. Each type declares its required frontmatter.\n# Any field not listed as required is allowed but not enforced.\ntypes:\n note:\n description: Generic note — default type when \\`type\\` frontmatter is absent\n required: [title, status]\n\n decision:\n description: Architectural or process decision with dated rationale\n required: [title, status, decided_on, decision_maker]\n\n gotcha:\n description: Compressed lesson from a past incident/bug/surprise\n required: [title, status, last_verified, severity]\n\n source:\n description: External reference (paper, blog post, doc) used as provenance\n required: [title, status, source]\n\n alias:\n description: Learned human→artifact vocabulary entry (v1.4 lexicon). Maps the human's phrasing to a concrete path/symbol.\n required: [canonical]\n\n dossier:\n description: Entity-centric living note (v1.5). One per critical component — purpose, failure modes, knobs, incident log, current state. Accretes in place.\n required: [title, status, entity]\n\n profile:\n description: Speaker profile (v1.5). Models how THIS human communicates — severity calibration, chronic omissions, verbosity, shorthand. One human, one profile.\n required: [title, status]\n\n# Severity values for gotcha type.\nseverity_enum: [low, medium, high, critical]\n\n# Lint policy. Each rule has a default severity that pre-commit/CI consumes.\nlint:\n missing_provenance:\n # Triggers when a note type in this list has no sources AND no derived_from.\n applies_to: [note, decision, gotcha]\n severity: warn\n stale:\n # Triggers when last_verified is older than this many days (0 = disabled).\n max_age_days: 180\n severity: warn\n schema_violations:\n # Triggers on missing required fields, unknown type, or enum mismatch.\n severity: error\n\n# Confidence decay (v1.3). Down-weights search results by time since last_verified.\n# Composes multiplicatively with load_priority — it never removes notes, only ranks\n# them lower (floored). Set enabled: false for byte-identical pre-v1.3 ranking.\ndecay:\n enabled: true\n default_half_life_days: 365 # multiplier = 0.5 ^ (age_days / half_life)\n per_type:\n decision: 365\n note: 365\n gotcha: 180\n source: null # null = never decays\n proposal: 14\n floor: 0.1 # notes never drop below this multiplier\n hotness_boost:\n enabled: false # extend half-life for heavily-backlinked notes (opt-in)\n cap: 2.0\n\n# Path-class ranking (v1.4). Down-weights whole regions of the vault by path glob\n# (composes multiplicatively with decay + load_priority). First matching rule wins.\n# The default down-weights archived notes so retired docs stop dominating results.\npath_class:\n enabled: true\n rules:\n - glob: \"archive/**\"\n multiplier: 0.3\n\n# Usage-feedback ranking (v1.5). Boosts notes that get CITED after retrieval (per\n# the local selection log) so load-bearing knowledge floats up. Bounded by cap to\n# stop a feedback runaway; composes multiplicatively with decay + path_class +\n# load_priority. A no-op until the selection log has citations. The complementary\n# signal (retrieved-but-never-cited \"decoys\") is surfaced by lint_vault, NEVER\n# auto-down-ranked. Set enabled: false for byte-identical pre-v1.5 ranking.\nusage_boost:\n enabled: true\n cap: 1.5 # max multiplier a note can earn from citations\n per_citation: 0.1 # multiplier += per_citation × citation_count, then capped\n\n# Token-frugal tool surface (v1.4). When enabled, the server registers a reduced\n# tool set in modes that don't need the full surface (outage-silence → search/read\n# only). DEFAULT OFF — mid-session mode switches don't re-register, so opt in and\n# dogfood before relying on it.\ntools:\n conditional: false\n\n# Selection-logging telemetry (v1.3.1). Records what search returned and which\n# note the answer used, to a LOCAL append-only JSONL at\n# .claude/.semantic-memory/selection.jsonl (gitignored). Never leaves the machine.\n# Feeds the decay_candidates lint and (later) usage-feedback ranking. Set\n# enabled: false to disable all logging.\ntelemetry:\n enabled: true\n`;\n"],"mappings":"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","names":[]}
|
|
@@ -9,6 +9,15 @@ import { existsSync } from "fs";
|
|
|
9
9
|
import { basename, join } from "path";
|
|
10
10
|
var DOCS_CACHE_FILE = "docs.cache.json";
|
|
11
11
|
var CHUNK_TARGET_CHARS = 2e3;
|
|
12
|
+
function parseFrontmatter(raw, file) {
|
|
13
|
+
try {
|
|
14
|
+
return matter(raw);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
process.stderr.write(`Invalid frontmatter in ${file}, indexing body only: ${err?.message?.split("\n")[0] ?? err}
|
|
17
|
+
`);
|
|
18
|
+
return { data: {}, content: raw.replace(/^---\r?\n[\s\S]*?\r?\n---[^\S\r\n]*(\r?\n|$)/, "") };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
12
21
|
var Indexer = class {
|
|
13
22
|
notesPath;
|
|
14
23
|
processor;
|
|
@@ -19,9 +28,15 @@ var Indexer = class {
|
|
|
19
28
|
async indexAll() {
|
|
20
29
|
const files = await glob("**/*.md", { cwd: this.notesPath, follow: true });
|
|
21
30
|
const docs = await Promise.all(
|
|
22
|
-
files.map(
|
|
31
|
+
files.map(
|
|
32
|
+
(file) => this.indexFile(join(this.notesPath, file), file).catch((err) => {
|
|
33
|
+
process.stderr.write(`Skipping ${file}: ${err?.message ?? err}
|
|
34
|
+
`);
|
|
35
|
+
return null;
|
|
36
|
+
})
|
|
37
|
+
)
|
|
23
38
|
);
|
|
24
|
-
return docs;
|
|
39
|
+
return docs.filter((d) => d !== null);
|
|
25
40
|
}
|
|
26
41
|
/**
|
|
27
42
|
* Load parsed-doc cache from disk. Returns null if missing or unreadable.
|
|
@@ -51,7 +66,7 @@ var Indexer = class {
|
|
|
51
66
|
readFile(absolutePath, "utf-8"),
|
|
52
67
|
stat(absolutePath)
|
|
53
68
|
]);
|
|
54
|
-
const { data: frontmatter, content } =
|
|
69
|
+
const { data: frontmatter, content } = parseFrontmatter(raw, relativePath);
|
|
55
70
|
const tree = this.processor.parse(content);
|
|
56
71
|
const wikilinks = this.extractWikilinks(tree);
|
|
57
72
|
const tags = this.extractTags(content, frontmatter);
|
|
@@ -168,4 +183,4 @@ var Indexer = class {
|
|
|
168
183
|
export {
|
|
169
184
|
Indexer
|
|
170
185
|
};
|
|
171
|
-
//# sourceMappingURL=chunk-
|
|
186
|
+
//# sourceMappingURL=chunk-IGHCVAAL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/indexer.ts"],"sourcesContent":["import { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkWikiLink from \"remark-wiki-link\";\nimport matter from \"gray-matter\";\nimport { glob } from \"glob\";\nimport { readFile, writeFile, stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative } from \"node:path\";\nimport type { IndexedDocument } from \"./types.js\";\n\nconst DOCS_CACHE_FILE = \"docs.cache.json\";\n\nconst CHUNK_TARGET_CHARS = 2000; // ~512 tokens\n\n// Invalid YAML (e.g. an unquoted colon in a value) keeps the body searchable with empty frontmatter\nfunction parseFrontmatter(raw: string, file: string): { data: Record<string, unknown>; content: string } {\n try {\n return matter(raw);\n } catch (err: any) {\n process.stderr.write(`Invalid frontmatter in ${file}, indexing body only: ${err?.message?.split(\"\\n\")[0] ?? err}\\n`);\n return { data: {}, content: raw.replace(/^---\\r?\\n[\\s\\S]*?\\r?\\n---[^\\S\\r\\n]*(\\r?\\n|$)/, \"\") };\n }\n}\n\nexport class Indexer {\n private notesPath: string;\n private processor: any;\n\n constructor(notesPath: string) {\n this.notesPath = notesPath;\n this.processor = unified().use(remarkParse).use(remarkWikiLink);\n }\n\n async indexAll(): Promise<IndexedDocument[]> {\n // follow: true descends into symlinked directories. Without this, users\n // who symlink an external folder into their vault (a common pattern for\n // sharing notes across projects) get silently empty indexing for the\n // symlinked content. glob does cycle detection via visited inodes so\n // circular symlinks are handled safely.\n const files = await glob(\"**/*.md\", { cwd: this.notesPath, follow: true });\n // One unreadable file must not take the whole corpus down with it\n const docs = await Promise.all(\n files.map((file) =>\n this.indexFile(join(this.notesPath, file), file).catch((err) => {\n process.stderr.write(`Skipping ${file}: ${err?.message ?? err}\\n`);\n return null;\n })\n )\n );\n return docs.filter((d): d is IndexedDocument => d !== null);\n }\n\n /**\n * Load parsed-doc cache from disk. Returns null if missing or unreadable.\n * The cache is a verbatim JSON dump of a prior indexAll() result, written by\n * saveDocsCache. Consumers (search CLI / hook path) prefer this over re-parsing\n * 500+ markdown files; the tradeoff is that cache may be stale if the vault\n * changed since the last reindex — same staleness window as the vector cache.\n */\n async loadDocsCache(indexPath: string): Promise<IndexedDocument[] | null> {\n const p = join(indexPath, DOCS_CACHE_FILE);\n if (!existsSync(p)) return null;\n try {\n const raw = await readFile(p, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return null;\n return parsed as IndexedDocument[];\n } catch {\n return null;\n }\n }\n\n async saveDocsCache(indexPath: string, docs: IndexedDocument[]): Promise<void> {\n const p = join(indexPath, DOCS_CACHE_FILE);\n await writeFile(p, JSON.stringify(docs), \"utf-8\");\n }\n\n async indexFile(\n absolutePath: string,\n relativePath: string\n ): Promise<IndexedDocument> {\n const [raw, fileStat] = await Promise.all([\n readFile(absolutePath, \"utf-8\"),\n stat(absolutePath),\n ]);\n const { data: frontmatter, content } = parseFrontmatter(raw, relativePath);\n const tree = this.processor.parse(content);\n\n const wikilinks = this.extractWikilinks(tree);\n const tags = this.extractTags(content, frontmatter);\n const headers = this.extractHeaders(tree);\n const plainText = this.stripMarkdown(content);\n const chunks = this.chunkText(plainText);\n\n // Symptom-keyed indexing (v1.4 Phase 5). Each verbatim `symptoms:` phrase is\n // added as its own chunk, so a terse symptom query (\"it hangs after reindex\")\n // matches the note directly — the fix for asymmetric retrieval (query-shaped\n // keys stored alongside document-shaped content). Synthetic child chunks point\n // at the same note path; no index-format change.\n const symptoms = Array.isArray(frontmatter.symptoms)\n ? (frontmatter.symptoms as unknown[]).filter((s): s is string => typeof s === \"string\" && s.trim().length > 0).map((s) => s.trim())\n : [];\n const allChunks = symptoms.length > 0 ? [...chunks, ...symptoms] : chunks;\n\n const title =\n (frontmatter.title as string) ||\n headers[0] ||\n basename(relativePath, \".md\");\n\n // Resolve modification time: prefer frontmatter date fields over fs.stat\n // Supports hit-em-with-the-docs (last_updated) and common alternatives\n const mtime = this.resolveMtime(frontmatter, fileStat.mtime);\n\n // Optional hit-em-with-the-docs fields (only populated when present)\n const loadPriority =\n typeof frontmatter.load_priority === \"number\"\n ? Math.min(10, Math.max(1, frontmatter.load_priority))\n : undefined;\n const status =\n typeof frontmatter.status === \"string\" ? frontmatter.status : undefined;\n const tier =\n typeof frontmatter.tier === \"string\" ? frontmatter.tier : undefined;\n const domains = Array.isArray(frontmatter.domains)\n ? (frontmatter.domains as string[])\n : undefined;\n const purpose =\n typeof frontmatter.purpose === \"string\" ? frontmatter.purpose : undefined;\n const relatedDocs = Array.isArray(frontmatter.related_docs)\n ? (frontmatter.related_docs as string[]).filter((v) => typeof v === \"string\")\n : undefined;\n\n return {\n path: relativePath,\n title,\n content: plainText,\n frontmatter,\n wikilinks,\n tags,\n headers,\n chunks: allChunks,\n mtime,\n ...(loadPriority !== undefined && { loadPriority }),\n ...(status !== undefined && { status }),\n ...(tier !== undefined && { tier }),\n ...(domains !== undefined && { domains }),\n ...(purpose !== undefined && { purpose }),\n ...(relatedDocs !== undefined && { relatedDocs }),\n };\n }\n\n /**\n * Resolve the best available modification date for a document.\n * Priority: last_updated → updated → date → lastmod → fs.stat mtime\n * Accepts YYYY-MM-DD strings or full ISO timestamps.\n */\n private resolveMtime(\n frontmatter: Record<string, unknown>,\n statMtime: Date\n ): string {\n const candidates = [\n frontmatter.last_updated,\n frontmatter.updated,\n frontmatter.date,\n frontmatter.lastmod,\n ];\n for (const val of candidates) {\n if (!val) continue;\n const str = val instanceof Date ? val.toISOString() : String(val);\n const parsed = new Date(str);\n if (!isNaN(parsed.getTime())) return parsed.toISOString();\n }\n return statMtime.toISOString();\n }\n\n private extractWikilinks(tree: any): string[] {\n const links: string[] = [];\n const walk = (node: any) => {\n if (node.type === \"wikiLink\") {\n links.push(node.value || node.data?.alias || \"\");\n }\n if (node.children) {\n for (const child of node.children) walk(child);\n }\n };\n walk(tree);\n return [...new Set(links.filter(Boolean))];\n }\n\n private extractTags(content: string, frontmatter: Record<string, unknown>): string[] {\n const inlineTags = [...content.matchAll(/(?:^|\\s)#([a-zA-Z][\\w-/]*)/g)].map(\n (m) => m[1]\n );\n\n const fmTags = Array.isArray(frontmatter.tags)\n ? (frontmatter.tags as string[])\n : [];\n\n return [...new Set([...fmTags, ...inlineTags])];\n }\n\n private extractHeaders(tree: any): string[] {\n const headers: string[] = [];\n const walk = (node: any) => {\n if (node.type === \"heading\") {\n const text = this.nodeToText(node);\n if (text) headers.push(text);\n }\n if (node.children) {\n for (const child of node.children) walk(child);\n }\n };\n walk(tree);\n return headers;\n }\n\n private nodeToText(node: any): string {\n if (node.type === \"text\") return node.value;\n if (node.children) return node.children.map((c: any) => this.nodeToText(c)).join(\"\");\n return \"\";\n }\n\n private stripMarkdown(content: string): string {\n return content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]+`/g, \"\")\n .replace(/!\\[.*?\\]\\(.*?\\)/g, \"\")\n .replace(/\\[([^\\]]+)\\]\\(.*?\\)/g, \"$1\")\n .replace(/#{1,6}\\s+/g, \"\")\n .replace(/[*_~]{1,3}/g, \"\")\n .replace(/>\\s+/g, \"\")\n .replace(/\\|.*\\|/g, \"\")\n .replace(/-{3,}/g, \"\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n }\n\n chunkText(text: string): string[] {\n if (text.length <= CHUNK_TARGET_CHARS) return [text];\n\n const sentences = text.match(/[^.!?\\n]+[.!?\\n]+|[^.!?\\n]+$/g) || [text];\n const chunks: string[] = [];\n let current = \"\";\n\n for (const sentence of sentences) {\n if (current.length + sentence.length > CHUNK_TARGET_CHARS && current) {\n chunks.push(current.trim());\n current = \"\";\n }\n current += sentence;\n }\n if (current.trim()) chunks.push(current.trim());\n\n return chunks;\n }\n}\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,OAAO,iBAAiB;AACxB,OAAO,oBAAoB;AAC3B,OAAO,YAAY;AACnB,SAAS,YAAY;AACrB,SAAS,UAAU,WAAW,YAAY;AAC1C,SAAS,kBAAkB;AAC3B,SAAS,UAAU,YAAsB;AAGzC,IAAM,kBAAkB;AAExB,IAAM,qBAAqB;AAG3B,SAAS,iBAAiB,KAAa,MAAkE;AACvG,MAAI;AACF,WAAO,OAAO,GAAG;AAAA,EACnB,SAAS,KAAU;AACjB,YAAQ,OAAO,MAAM,0BAA0B,IAAI,yBAAyB,KAAK,SAAS,MAAM,IAAI,EAAE,CAAC,KAAK,GAAG;AAAA,CAAI;AACnH,WAAO,EAAE,MAAM,CAAC,GAAG,SAAS,IAAI,QAAQ,gDAAgD,EAAE,EAAE;AAAA,EAC9F;AACF;AAEO,IAAM,UAAN,MAAc;AAAA,EACX;AAAA,EACA;AAAA,EAER,YAAY,WAAmB;AAC7B,SAAK,YAAY;AACjB,SAAK,YAAY,QAAQ,EAAE,IAAI,WAAW,EAAE,IAAI,cAAc;AAAA,EAChE;AAAA,EAEA,MAAM,WAAuC;AAM3C,UAAM,QAAQ,MAAM,KAAK,WAAW,EAAE,KAAK,KAAK,WAAW,QAAQ,KAAK,CAAC;AAEzE,UAAM,OAAO,MAAM,QAAQ;AAAA,MACzB,MAAM;AAAA,QAAI,CAAC,SACT,KAAK,UAAU,KAAK,KAAK,WAAW,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,QAAQ;AAC9D,kBAAQ,OAAO,MAAM,YAAY,IAAI,KAAK,KAAK,WAAW,GAAG;AAAA,CAAI;AACjE,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO,KAAK,OAAO,CAAC,MAA4B,MAAM,IAAI;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,cAAc,WAAsD;AACxE,UAAM,IAAI,KAAK,WAAW,eAAe;AACzC,QAAI,CAAC,WAAW,CAAC,EAAG,QAAO;AAC3B,QAAI;AACF,YAAM,MAAM,MAAM,SAAS,GAAG,OAAO;AACrC,YAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,UAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO;AACnC,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,WAAmB,MAAwC;AAC7E,UAAM,IAAI,KAAK,WAAW,eAAe;AACzC,UAAM,UAAU,GAAG,KAAK,UAAU,IAAI,GAAG,OAAO;AAAA,EAClD;AAAA,EAEA,MAAM,UACJ,cACA,cAC0B;AAC1B,UAAM,CAAC,KAAK,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MACxC,SAAS,cAAc,OAAO;AAAA,MAC9B,KAAK,YAAY;AAAA,IACnB,CAAC;AACD,UAAM,EAAE,MAAM,aAAa,QAAQ,IAAI,iBAAiB,KAAK,YAAY;AACzE,UAAM,OAAO,KAAK,UAAU,MAAM,OAAO;AAEzC,UAAM,YAAY,KAAK,iBAAiB,IAAI;AAC5C,UAAM,OAAO,KAAK,YAAY,SAAS,WAAW;AAClD,UAAM,UAAU,KAAK,eAAe,IAAI;AACxC,UAAM,YAAY,KAAK,cAAc,OAAO;AAC5C,UAAM,SAAS,KAAK,UAAU,SAAS;AAOvC,UAAM,WAAW,MAAM,QAAQ,YAAY,QAAQ,IAC9C,YAAY,SAAuB,OAAO,CAAC,MAAmB,OAAO,MAAM,YAAY,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAChI,CAAC;AACL,UAAM,YAAY,SAAS,SAAS,IAAI,CAAC,GAAG,QAAQ,GAAG,QAAQ,IAAI;AAEnE,UAAM,QACH,YAAY,SACb,QAAQ,CAAC,KACT,SAAS,cAAc,KAAK;AAI9B,UAAM,QAAQ,KAAK,aAAa,aAAa,SAAS,KAAK;AAG3D,UAAM,eACJ,OAAO,YAAY,kBAAkB,WACjC,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,YAAY,aAAa,CAAC,IACnD;AACN,UAAM,SACJ,OAAO,YAAY,WAAW,WAAW,YAAY,SAAS;AAChE,UAAM,OACJ,OAAO,YAAY,SAAS,WAAW,YAAY,OAAO;AAC5D,UAAM,UAAU,MAAM,QAAQ,YAAY,OAAO,IAC5C,YAAY,UACb;AACJ,UAAM,UACJ,OAAO,YAAY,YAAY,WAAW,YAAY,UAAU;AAClE,UAAM,cAAc,MAAM,QAAQ,YAAY,YAAY,IACrD,YAAY,aAA0B,OAAO,CAAC,MAAM,OAAO,MAAM,QAAQ,IAC1E;AAEJ,WAAO;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,MACA,GAAI,iBAAiB,UAAa,EAAE,aAAa;AAAA,MACjD,GAAI,WAAW,UAAa,EAAE,OAAO;AAAA,MACrC,GAAI,SAAS,UAAa,EAAE,KAAK;AAAA,MACjC,GAAI,YAAY,UAAa,EAAE,QAAQ;AAAA,MACvC,GAAI,YAAY,UAAa,EAAE,QAAQ;AAAA,MACvC,GAAI,gBAAgB,UAAa,EAAE,YAAY;AAAA,IACjD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,aACN,aACA,WACQ;AACR,UAAM,aAAa;AAAA,MACjB,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,YAAY;AAAA,IACd;AACA,eAAW,OAAO,YAAY;AAC5B,UAAI,CAAC,IAAK;AACV,YAAM,MAAM,eAAe,OAAO,IAAI,YAAY,IAAI,OAAO,GAAG;AAChE,YAAM,SAAS,IAAI,KAAK,GAAG;AAC3B,UAAI,CAAC,MAAM,OAAO,QAAQ,CAAC,EAAG,QAAO,OAAO,YAAY;AAAA,IAC1D;AACA,WAAO,UAAU,YAAY;AAAA,EAC/B;AAAA,EAEQ,iBAAiB,MAAqB;AAC5C,UAAM,QAAkB,CAAC;AACzB,UAAM,OAAO,CAAC,SAAc;AAC1B,UAAI,KAAK,SAAS,YAAY;AAC5B,cAAM,KAAK,KAAK,SAAS,KAAK,MAAM,SAAS,EAAE;AAAA,MACjD;AACA,UAAI,KAAK,UAAU;AACjB,mBAAW,SAAS,KAAK,SAAU,MAAK,KAAK;AAAA,MAC/C;AAAA,IACF;AACA,SAAK,IAAI;AACT,WAAO,CAAC,GAAG,IAAI,IAAI,MAAM,OAAO,OAAO,CAAC,CAAC;AAAA,EAC3C;AAAA,EAEQ,YAAY,SAAiB,aAAgD;AACnF,UAAM,aAAa,CAAC,GAAG,QAAQ,SAAS,6BAA6B,CAAC,EAAE;AAAA,MACtE,CAAC,MAAM,EAAE,CAAC;AAAA,IACZ;AAEA,UAAM,SAAS,MAAM,QAAQ,YAAY,IAAI,IACxC,YAAY,OACb,CAAC;AAEL,WAAO,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,QAAQ,GAAG,UAAU,CAAC,CAAC;AAAA,EAChD;AAAA,EAEQ,eAAe,MAAqB;AAC1C,UAAM,UAAoB,CAAC;AAC3B,UAAM,OAAO,CAAC,SAAc;AAC1B,UAAI,KAAK,SAAS,WAAW;AAC3B,cAAM,OAAO,KAAK,WAAW,IAAI;AACjC,YAAI,KAAM,SAAQ,KAAK,IAAI;AAAA,MAC7B;AACA,UAAI,KAAK,UAAU;AACjB,mBAAW,SAAS,KAAK,SAAU,MAAK,KAAK;AAAA,MAC/C;AAAA,IACF;AACA,SAAK,IAAI;AACT,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,MAAmB;AACpC,QAAI,KAAK,SAAS,OAAQ,QAAO,KAAK;AACtC,QAAI,KAAK,SAAU,QAAO,KAAK,SAAS,IAAI,CAAC,MAAW,KAAK,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE;AACnF,WAAO;AAAA,EACT;AAAA,EAEQ,cAAc,SAAyB;AAC7C,WAAO,QACJ,QAAQ,mBAAmB,EAAE,EAC7B,QAAQ,YAAY,EAAE,EACtB,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,wBAAwB,IAAI,EACpC,QAAQ,cAAc,EAAE,EACxB,QAAQ,eAAe,EAAE,EACzB,QAAQ,SAAS,EAAE,EACnB,QAAQ,WAAW,EAAE,EACrB,QAAQ,UAAU,EAAE,EACpB,QAAQ,WAAW,MAAM,EACzB,KAAK;AAAA,EACV;AAAA,EAEA,UAAU,MAAwB;AAChC,QAAI,KAAK,UAAU,mBAAoB,QAAO,CAAC,IAAI;AAEnD,UAAM,YAAY,KAAK,MAAM,+BAA+B,KAAK,CAAC,IAAI;AACtE,UAAM,SAAmB,CAAC;AAC1B,QAAI,UAAU;AAEd,eAAW,YAAY,WAAW;AAChC,UAAI,QAAQ,SAAS,SAAS,SAAS,sBAAsB,SAAS;AACpE,eAAO,KAAK,QAAQ,KAAK,CAAC;AAC1B,kBAAU;AAAA,MACZ;AACA,iBAAW;AAAA,IACb;AACA,QAAI,QAAQ,KAAK,EAAG,QAAO,KAAK,QAAQ,KAAK,CAAC;AAE9C,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
redact
|
|
3
|
+
} from "./chunk-OEF5U5GV.js";
|
|
4
|
+
|
|
1
5
|
// src/core/session.ts
|
|
2
6
|
import { mkdir, readFile, writeFile, rm } from "fs/promises";
|
|
3
7
|
import { existsSync } from "fs";
|
|
@@ -34,7 +38,7 @@ var SessionManager = class {
|
|
|
34
38
|
const id = `ses_${now.replace(/[:.]/g, "-")}`;
|
|
35
39
|
const state = {
|
|
36
40
|
id,
|
|
37
|
-
task: task.trim(),
|
|
41
|
+
task: redact(task.trim(), "broad").text,
|
|
38
42
|
started_at: now,
|
|
39
43
|
last_activity_at: now,
|
|
40
44
|
verifications: [],
|
|
@@ -59,7 +63,8 @@ var SessionManager = class {
|
|
|
59
63
|
return { ok: false, error: "session_run requires a non-empty command." };
|
|
60
64
|
}
|
|
61
65
|
const start = Date.now();
|
|
62
|
-
const
|
|
66
|
+
const raw = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
67
|
+
const record = { ...raw, cmd: redact(raw.cmd, "broad").text, tail: redact(raw.tail, "broad").text };
|
|
63
68
|
record.duration_ms = Date.now() - start;
|
|
64
69
|
state.verifications.push(record);
|
|
65
70
|
state.last_activity_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -200,4 +205,4 @@ export {
|
|
|
200
205
|
deriveProjectRoot,
|
|
201
206
|
deriveSessionDir
|
|
202
207
|
};
|
|
203
|
-
//# sourceMappingURL=chunk-
|
|
208
|
+
//# sourceMappingURL=chunk-LGQPP5FQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/session.ts"],"sourcesContent":["import { mkdir, readFile, writeFile, rm } from \"node:fs/promises\";\nimport { redact } from \"./redact.js\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { spawn } from \"node:child_process\";\n\nexport interface VerificationRecord {\n cmd: string;\n exit: number | null;\n signal: string | null;\n duration_ms: number;\n tail: string;\n at: string;\n}\n\nexport interface SessionState {\n id: string;\n task: string;\n started_at: string;\n last_activity_at: string;\n verifications: VerificationRecord[];\n notes_touched: string[];\n closed_at?: string;\n closed_summary?: string;\n closed_verified_waived?: boolean;\n closed_waiver_reason?: string;\n}\n\nexport type SessionStatus =\n | { state: \"no_session\" }\n | (SessionState & { state: \"active\" | \"stale\" });\n\nexport interface SessionStartOptions {\n /** When true and a session for the same task is already open, return the existing id rather than failing. Defaults to true. */\n reuseSameTask?: boolean;\n}\n\nexport interface SessionFinishInput {\n summary: string;\n /**\n * When false, the caller is explicitly waiving verification (e.g. for doc-only edits).\n * Requires a non-empty `reason`. When true (default) and no verifications have been\n * recorded, the call is refused.\n */\n verified?: boolean;\n reason?: string;\n}\n\nconst STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\nconst TAIL_BYTES = 4096;\n\n/**\n * SessionManager owns a single session.json file at `<sessionDir>/session.json`.\n *\n * State is durable across MCP server restarts (the file persists). At most one session\n * is active per project at a time — concurrent sessions are explicitly out of scope\n * for v1.1; brain has the same constraint.\n *\n * Hard gates:\n * - session_finish refuses without verifications unless verified=false + reason\n * - session_run refuses without an open session\n * - session_start refuses if another session is open with a different task\n *\n * Stale detection: a session whose last_activity_at is >24h old is reported as\n * state:\"stale\" by status(). Stale sessions can still be finished or extended; the\n * label is informational so callers can decide whether to abandon (rm + start new) or\n * resume.\n */\nexport class SessionManager {\n constructor(private sessionDir: string) {}\n\n get sessionPath(): string {\n return join(this.sessionDir, \"session.json\");\n }\n\n async start(task: string, opts: SessionStartOptions = {}): Promise<{ ok: true; id: string; reused?: boolean } | { ok: false; error: string }> {\n if (!task || task.trim().length === 0) {\n return { ok: false, error: \"session_start requires a non-empty task.\" };\n }\n await mkdir(this.sessionDir, { recursive: true });\n const existing = await this.read();\n if (existing && !existing.closed_at) {\n const reuseSameTask = opts.reuseSameTask !== false;\n if (reuseSameTask && existing.task === task) {\n return { ok: true, id: existing.id, reused: true };\n }\n return {\n ok: false,\n error: `Session already open: id=${existing.id}, task=${JSON.stringify(existing.task)}. Close it with session_finish first.`,\n };\n }\n const now = new Date().toISOString();\n const id = `ses_${now.replace(/[:.]/g, \"-\")}`;\n const state: SessionState = {\n id,\n task: redact(task.trim(), \"broad\").text,\n started_at: now,\n last_activity_at: now,\n verifications: [],\n notes_touched: [],\n };\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n return { ok: true, id };\n }\n\n async status(): Promise<SessionStatus> {\n const state = await this.read();\n if (!state || state.closed_at) return { state: \"no_session\" };\n const age = Date.now() - new Date(state.last_activity_at).getTime();\n const label: \"active\" | \"stale\" = age > STALE_THRESHOLD_MS ? \"stale\" : \"active\";\n return { ...state, state: label };\n }\n\n async run(cmd: string, timeoutMs?: number): Promise<{ ok: true; record: VerificationRecord } | { ok: false; error: string }> {\n const state = await this.read();\n if (!state || state.closed_at) {\n return { ok: false, error: \"No active session. Run session_start first.\" };\n }\n if (!cmd || cmd.trim().length === 0) {\n return { ok: false, error: \"session_run requires a non-empty command.\" };\n }\n const start = Date.now();\n const raw = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);\n // session.json keeps the command and its output tail: either can carry a token\n const record = { ...raw, cmd: redact(raw.cmd, \"broad\").text, tail: redact(raw.tail, \"broad\").text };\n record.duration_ms = Date.now() - start;\n state.verifications.push(record);\n state.last_activity_at = new Date().toISOString();\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n return { ok: true, record };\n }\n\n async finish(input: SessionFinishInput): Promise<{ ok: true; closed: SessionState } | { ok: false; error: string }> {\n const state = await this.read();\n if (!state || state.closed_at) {\n return { ok: false, error: \"No active session to finish.\" };\n }\n const explicitWaiver = input.verified === false;\n if (!explicitWaiver && state.verifications.length === 0) {\n return {\n ok: false,\n error:\n \"session_finish refused: no verification commands recorded. Run session_run with a real verification command (tests, lint, etc.) first, OR pass verified=false with a reason to waive verification (e.g. for doc-only edits).\",\n };\n }\n if (explicitWaiver && (!input.reason || input.reason.trim().length === 0)) {\n return {\n ok: false,\n error: \"session_finish refused: verified=false requires a non-empty `reason` explaining why verification is not applicable to this task.\",\n };\n }\n if (!input.summary || input.summary.trim().length === 0) {\n return { ok: false, error: \"session_finish requires a non-empty `summary`.\" };\n }\n const closed: SessionState = {\n ...state,\n closed_at: new Date().toISOString(),\n closed_summary: input.summary.trim(),\n ...(explicitWaiver && {\n closed_verified_waived: true,\n closed_waiver_reason: input.reason!.trim(),\n }),\n };\n // Sessions are ephemeral runtime state — finished sessions are removed from disk.\n // Durable artifacts (synthesize_note proposals, log entries) capture what mattered.\n await rm(this.sessionPath, { force: true });\n return { ok: true, closed };\n }\n\n /**\n * Append paths to notes_touched on the active session, deduplicating. No-op when no\n * session is active. Used by patch tools to record what the session edited.\n */\n async recordNotesTouched(paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const state = await this.read();\n if (!state || state.closed_at) return;\n const set = new Set(state.notes_touched);\n for (const p of paths) set.add(p);\n state.notes_touched = Array.from(set);\n state.last_activity_at = new Date().toISOString();\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n }\n\n /**\n * Read the active session WITHOUT computing the stale label. Returns null when no\n * session is active or when the file is unparseable.\n */\n async readActive(): Promise<SessionState | null> {\n const state = await this.read();\n if (!state || state.closed_at) return null;\n return state;\n }\n\n private async read(): Promise<SessionState | null> {\n if (!existsSync(this.sessionPath)) return null;\n try {\n const raw = await readFile(this.sessionPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (typeof parsed?.id !== \"string\") return null;\n return parsed as SessionState;\n } catch {\n return null;\n }\n }\n\n private spawnAndCapture(cmd: string, timeoutMs: number): Promise<VerificationRecord> {\n return new Promise((resolve) => {\n const proc = spawn(cmd, { shell: true });\n let buf = \"\";\n let killedByTimeout = false;\n const timer = setTimeout(() => {\n killedByTimeout = true;\n proc.kill(\"SIGTERM\");\n setTimeout(() => {\n try { proc.kill(\"SIGKILL\"); } catch { /* already gone */ }\n }, 1000).unref();\n }, timeoutMs);\n\n const onData = (chunk: Buffer) => {\n buf += chunk.toString(\"utf-8\");\n if (buf.length > TAIL_BYTES * 4) {\n buf = buf.slice(buf.length - TAIL_BYTES * 2);\n }\n };\n proc.stdout?.on(\"data\", onData);\n proc.stderr?.on(\"data\", onData);\n\n proc.on(\"close\", (code, signal) => {\n clearTimeout(timer);\n const tail = buf.slice(-TAIL_BYTES);\n resolve({\n cmd,\n exit: killedByTimeout ? null : code,\n signal: killedByTimeout ? \"SIGTERM (timeout)\" : (signal ?? null),\n duration_ms: 0,\n tail,\n at: new Date().toISOString(),\n });\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n const tail = buf + `\\n[spawn error: ${err.message}]`;\n resolve({\n cmd,\n exit: null,\n signal: null,\n duration_ms: 0,\n tail: tail.slice(-TAIL_BYTES),\n at: new Date().toISOString(),\n });\n });\n });\n }\n}\n\n/**\n * Heuristic: derive the project root from the vault path. If the vault path ends in\n * `.claude/.vault`, the project root is two levels up. Otherwise the vault path is\n * treated as the project root.\n */\nexport function deriveProjectRoot(notesPath: string): string {\n const norm = notesPath.replace(/[\\\\/]+$/, \"\");\n if (norm.endsWith(\"/.claude/.vault\") || norm.endsWith(\"\\\\.claude\\\\.vault\")) {\n return norm.slice(0, -\".claude/.vault\".length).replace(/[\\\\/]+$/, \"\");\n }\n return norm;\n}\n\nexport function deriveSessionDir(notesPath: string): string {\n return join(deriveProjectRoot(notesPath), \".claude\", \".semantic-memory\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deriveSessionDir
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LGQPP5FQ.js";
|
|
4
|
+
import {
|
|
5
|
+
redact
|
|
6
|
+
} from "./chunk-OEF5U5GV.js";
|
|
4
7
|
|
|
5
8
|
// src/core/telemetry.ts
|
|
6
9
|
import { appendFile, readFile, mkdir } from "fs/promises";
|
|
@@ -28,7 +31,8 @@ async function appendEvent(vaultPath, event, nowIso) {
|
|
|
28
31
|
if (!telemetryEnabled(vaultPath)) return;
|
|
29
32
|
const dir = deriveSessionDir(vaultPath);
|
|
30
33
|
await mkdir(dir, { recursive: true });
|
|
31
|
-
const
|
|
34
|
+
const safe = event.kind === "search" ? { ...event, query: redact(event.query, "broad").text } : event;
|
|
35
|
+
const line = JSON.stringify({ ts: nowIso ?? (/* @__PURE__ */ new Date()).toISOString(), ...safe });
|
|
32
36
|
await appendFile(selectionLogPath(vaultPath), line + "\n", "utf-8");
|
|
33
37
|
} catch {
|
|
34
38
|
}
|
|
@@ -88,4 +92,4 @@ export {
|
|
|
88
92
|
readSelectionLog,
|
|
89
93
|
computeSelectionStats
|
|
90
94
|
};
|
|
91
|
-
//# sourceMappingURL=chunk-
|
|
95
|
+
//# sourceMappingURL=chunk-NBY4TRP7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/telemetry.ts"],"sourcesContent":["import { appendFile, readFile, mkdir } from \"node:fs/promises\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { redact } from \"./redact.js\";\nimport { parse as parseYaml } from \"yaml\";\nimport { deriveSessionDir } from \"./session.js\";\n\n/**\n * Selection-logging telemetry (v1.3.1).\n *\n * Records what retrieval actually returned and which note the answer used, so a\n * later release (v1.4 usage-feedback ranking) can learn which knowledge is\n * load-bearing. Strictly local, append-only JSONL, fire-and-forget, opt-out.\n *\n * NO NETWORK: this module performs local filesystem I/O only, and never opens a\n * socket or issues a remote request — a unit test greps this file to keep it that way.\n */\n\nconst LOG_FILE = \"selection.jsonl\";\nconst CORRELATION_WINDOW_MS = 60_000;\n\nexport interface SearchEvent {\n kind: \"search\";\n tool: string;\n query: string;\n results: { path: string; score: number; decay?: number }[];\n}\n\nexport interface SelectionEvent {\n kind: \"selection\";\n note_path: string;\n via: string;\n /** True when this selection followed a recent search that returned this path. */\n correlated?: boolean;\n}\n\nexport type TelemetryEvent = (SearchEvent | SelectionEvent) & { ts?: string };\n\n/** Absolute path to the selection log for a given vault. */\nexport function selectionLogPath(vaultPath: string): string {\n return join(deriveSessionDir(vaultPath), LOG_FILE);\n}\n\n/**\n * Whether telemetry is enabled. Reads `telemetry.enabled` from vault.schema.yml;\n * defaults true (opt-out, not opt-in). Fails open to enabled only if the schema\n * is unreadable — a missing `telemetry` block means \"use the default (true)\".\n */\nexport function telemetryEnabled(vaultPath: string): boolean {\n try {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!existsSync(schemaPath)) return true;\n const parsed = parseYaml(readFileSync(schemaPath, \"utf-8\")) as { telemetry?: { enabled?: boolean } } | null;\n const e = parsed?.telemetry?.enabled;\n return typeof e === \"boolean\" ? e : true;\n } catch {\n return true;\n }\n}\n\n/**\n * Append one event. Fire-and-forget: respects the opt-out and NEVER throws — a\n * telemetry failure must not affect the tool call that triggered it.\n */\nexport async function appendEvent(vaultPath: string, event: TelemetryEvent, nowIso?: string): Promise<void> {\n try {\n if (!telemetryEnabled(vaultPath)) return;\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n // only the free-text query: note_path feeds usage-boost citation counts and must stay intact\n const safe = event.kind === \"search\" ? { ...event, query: redact(event.query, \"broad\").text } : event;\n const line = JSON.stringify({ ts: nowIso ?? new Date().toISOString(), ...safe });\n await appendFile(selectionLogPath(vaultPath), line + \"\\n\", \"utf-8\");\n } catch {\n // fire-and-forget\n }\n}\n\n// --- In-process correlation ring ---\n// Remembers the most recent search's top-k paths per vault so a subsequent\n// read/synthesize can be attributed to it (best-effort, time-bounded).\n\nconst recentSearch = new Map<string, { paths: Set<string>; atMs: number }>();\n\nexport function recordSearchForCorrelation(vaultPath: string, paths: string[], nowMs?: number): void {\n recentSearch.set(vaultPath, { paths: new Set(paths), atMs: nowMs ?? Date.now() });\n}\n\nexport function wasRecentlySearched(vaultPath: string, notePath: string, nowMs?: number): boolean {\n const r = recentSearch.get(vaultPath);\n if (!r) return false;\n if ((nowMs ?? Date.now()) - r.atMs > CORRELATION_WINDOW_MS) return false;\n return r.paths.has(notePath);\n}\n\n/** Read the selection log (oldest→newest). Returns [] if absent/unreadable. */\nexport async function readSelectionLog(vaultPath: string, opts: { limit?: number } = {}): Promise<TelemetryEvent[]> {\n try {\n const p = selectionLogPath(vaultPath);\n if (!existsSync(p)) return [];\n const raw = await readFile(p, \"utf-8\");\n const events = raw\n .split(\"\\n\")\n .filter((l) => l.trim())\n .map((l) => {\n try {\n return JSON.parse(l) as TelemetryEvent;\n } catch {\n return null;\n }\n })\n .filter((e): e is TelemetryEvent => e !== null);\n return opts.limit ? events.slice(-opts.limit) : events;\n } catch {\n return [];\n }\n}\n\n/** For CLI/analysis: cited-vs-retrieved rollup over the log. */\nexport interface SelectionStats {\n searches: number;\n selections: number;\n citedPaths: Record<string, number>;\n retrievedNeverCited: string[];\n}\n\nexport async function computeSelectionStats(vaultPath: string): Promise<SelectionStats> {\n const events = await readSelectionLog(vaultPath);\n const citedPaths: Record<string, number> = {};\n const retrieved = new Set<string>();\n let searches = 0;\n let selections = 0;\n for (const e of events) {\n if (e.kind === \"search\") {\n searches++;\n for (const r of e.results) retrieved.add(r.path);\n } else if (e.kind === \"selection\") {\n selections++;\n citedPaths[e.note_path] = (citedPaths[e.note_path] ?? 0) + 1;\n }\n }\n const retrievedNeverCited = [...retrieved].filter((p) => !(p in citedPaths)).sort();\n return { searches, selections, citedPaths, retrievedNeverCited };\n}\n"],"mappings":"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","names":[]}
|