@theglitchking/semantic-memory 1.2.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +137 -0
  4. package/ROADMAP.md +5 -2
  5. package/commands/healthcheck.md +13 -11
  6. package/dist/{chunk-UQSASB7Q.js → chunk-2EELGTLX.js} +32 -3
  7. package/dist/chunk-2EELGTLX.js.map +1 -0
  8. package/dist/chunk-5ZKNO6ZD.js +96 -0
  9. package/dist/chunk-5ZKNO6ZD.js.map +1 -0
  10. package/dist/chunk-7M3JZOYH.js +105 -0
  11. package/dist/chunk-7M3JZOYH.js.map +1 -0
  12. package/dist/chunk-A3KYHKB5.js +176 -0
  13. package/dist/chunk-A3KYHKB5.js.map +1 -0
  14. package/dist/{chunk-CYWPDFNI.js → chunk-CSXZ7QUU.js} +4 -203
  15. package/dist/chunk-CSXZ7QUU.js.map +1 -0
  16. package/dist/chunk-FM26NFTR.js +272 -0
  17. package/dist/chunk-FM26NFTR.js.map +1 -0
  18. package/dist/chunk-NTYYS53O.js +91 -0
  19. package/dist/chunk-NTYYS53O.js.map +1 -0
  20. package/dist/{chunk-E2Z4J4MK.js → chunk-OWZWMB4P.js} +70 -2
  21. package/dist/chunk-OWZWMB4P.js.map +1 -0
  22. package/dist/chunk-S4KW7VH4.js +203 -0
  23. package/dist/chunk-S4KW7VH4.js.map +1 -0
  24. package/dist/{chunk-HODDRXJK.js → chunk-V45NJIHI.js} +4 -2
  25. package/dist/chunk-V45NJIHI.js.map +1 -0
  26. package/dist/chunk-WSU4QIPB.js +311 -0
  27. package/dist/chunk-WSU4QIPB.js.map +1 -0
  28. package/dist/cli/index.js +265 -65
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/core/index.d.ts +39 -2
  31. package/dist/core/index.js +17 -11
  32. package/dist/decay-ELJUKVQG.js +14 -0
  33. package/dist/dossier-2UG724LB.js +39 -0
  34. package/dist/healthcheck-IMB47LZE.js +19 -0
  35. package/dist/indexer-YW4O3VVM.js +8 -0
  36. package/dist/lexicon-5PXQEMUF.js +26 -0
  37. package/dist/lexicon-5PXQEMUF.js.map +1 -0
  38. package/dist/lint-XUQ7JDYX.js +16 -0
  39. package/dist/lint-XUQ7JDYX.js.map +1 -0
  40. package/dist/mcp/server.js +478 -40
  41. package/dist/mcp/server.js.map +1 -1
  42. package/dist/profile-LXUEF7TX.js +20 -0
  43. package/dist/profile-LXUEF7TX.js.map +1 -0
  44. package/dist/{schema-F3RTY6SQ.js → schema-ATFCLC6Y.js} +2 -2
  45. package/dist/schema-ATFCLC6Y.js.map +1 -0
  46. package/dist/{search-27WYAI24.js → search-AHKEFI2B.js} +2 -2
  47. package/dist/telemetry-GB2TEHVE.js +21 -0
  48. package/dist/telemetry-GB2TEHVE.js.map +1 -0
  49. package/hooks/vault-context.js +270 -11
  50. package/package.json +2 -1
  51. package/skills/vault-first/SKILL.md +18 -0
  52. package/dist/chunk-CAN2GWUG.js +0 -88
  53. package/dist/chunk-CAN2GWUG.js.map +0 -1
  54. package/dist/chunk-CYWPDFNI.js.map +0 -1
  55. package/dist/chunk-E2Z4J4MK.js.map +0 -1
  56. package/dist/chunk-HODDRXJK.js.map +0 -1
  57. package/dist/chunk-UQSASB7Q.js.map +0 -1
  58. package/dist/healthcheck-E4GI5RMZ.js +0 -14
  59. package/dist/indexer-KD6UTXZB.js +0 -8
  60. package/dist/lint-GMXQF4RW.js +0 -11
  61. /package/dist/{healthcheck-E4GI5RMZ.js.map → decay-ELJUKVQG.js.map} +0 -0
  62. /package/dist/{indexer-KD6UTXZB.js.map → dossier-2UG724LB.js.map} +0 -0
  63. /package/dist/{lint-GMXQF4RW.js.map → healthcheck-IMB47LZE.js.map} +0 -0
  64. /package/dist/{schema-F3RTY6SQ.js.map → indexer-YW4O3VVM.js.map} +0 -0
  65. /package/dist/{search-27WYAI24.js.map → search-AHKEFI2B.js.map} +0 -0
@@ -0,0 +1,203 @@
1
+ // src/core/session.ts
2
+ import { mkdir, readFile, writeFile, rm } from "fs/promises";
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { spawn } from "child_process";
6
+ var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
7
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
8
+ var TAIL_BYTES = 4096;
9
+ var SessionManager = class {
10
+ constructor(sessionDir) {
11
+ this.sessionDir = sessionDir;
12
+ }
13
+ sessionDir;
14
+ get sessionPath() {
15
+ return join(this.sessionDir, "session.json");
16
+ }
17
+ async start(task, opts = {}) {
18
+ if (!task || task.trim().length === 0) {
19
+ return { ok: false, error: "session_start requires a non-empty task." };
20
+ }
21
+ await mkdir(this.sessionDir, { recursive: true });
22
+ const existing = await this.read();
23
+ if (existing && !existing.closed_at) {
24
+ const reuseSameTask = opts.reuseSameTask !== false;
25
+ if (reuseSameTask && existing.task === task) {
26
+ return { ok: true, id: existing.id, reused: true };
27
+ }
28
+ return {
29
+ ok: false,
30
+ error: `Session already open: id=${existing.id}, task=${JSON.stringify(existing.task)}. Close it with session_finish first.`
31
+ };
32
+ }
33
+ const now = (/* @__PURE__ */ new Date()).toISOString();
34
+ const id = `ses_${now.replace(/[:.]/g, "-")}`;
35
+ const state = {
36
+ id,
37
+ task: task.trim(),
38
+ started_at: now,
39
+ last_activity_at: now,
40
+ verifications: [],
41
+ notes_touched: []
42
+ };
43
+ await writeFile(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
44
+ return { ok: true, id };
45
+ }
46
+ async status() {
47
+ const state = await this.read();
48
+ if (!state || state.closed_at) return { state: "no_session" };
49
+ const age = Date.now() - new Date(state.last_activity_at).getTime();
50
+ const label = age > STALE_THRESHOLD_MS ? "stale" : "active";
51
+ return { ...state, state: label };
52
+ }
53
+ async run(cmd, timeoutMs) {
54
+ const state = await this.read();
55
+ if (!state || state.closed_at) {
56
+ return { ok: false, error: "No active session. Run session_start first." };
57
+ }
58
+ if (!cmd || cmd.trim().length === 0) {
59
+ return { ok: false, error: "session_run requires a non-empty command." };
60
+ }
61
+ const start = Date.now();
62
+ const record = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);
63
+ record.duration_ms = Date.now() - start;
64
+ state.verifications.push(record);
65
+ state.last_activity_at = (/* @__PURE__ */ new Date()).toISOString();
66
+ await writeFile(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
67
+ return { ok: true, record };
68
+ }
69
+ async finish(input) {
70
+ const state = await this.read();
71
+ if (!state || state.closed_at) {
72
+ return { ok: false, error: "No active session to finish." };
73
+ }
74
+ const explicitWaiver = input.verified === false;
75
+ if (!explicitWaiver && state.verifications.length === 0) {
76
+ return {
77
+ ok: false,
78
+ error: "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)."
79
+ };
80
+ }
81
+ if (explicitWaiver && (!input.reason || input.reason.trim().length === 0)) {
82
+ return {
83
+ ok: false,
84
+ error: "session_finish refused: verified=false requires a non-empty `reason` explaining why verification is not applicable to this task."
85
+ };
86
+ }
87
+ if (!input.summary || input.summary.trim().length === 0) {
88
+ return { ok: false, error: "session_finish requires a non-empty `summary`." };
89
+ }
90
+ const closed = {
91
+ ...state,
92
+ closed_at: (/* @__PURE__ */ new Date()).toISOString(),
93
+ closed_summary: input.summary.trim(),
94
+ ...explicitWaiver && {
95
+ closed_verified_waived: true,
96
+ closed_waiver_reason: input.reason.trim()
97
+ }
98
+ };
99
+ await rm(this.sessionPath, { force: true });
100
+ return { ok: true, closed };
101
+ }
102
+ /**
103
+ * Append paths to notes_touched on the active session, deduplicating. No-op when no
104
+ * session is active. Used by patch tools to record what the session edited.
105
+ */
106
+ async recordNotesTouched(paths) {
107
+ if (paths.length === 0) return;
108
+ const state = await this.read();
109
+ if (!state || state.closed_at) return;
110
+ const set = new Set(state.notes_touched);
111
+ for (const p of paths) set.add(p);
112
+ state.notes_touched = Array.from(set);
113
+ state.last_activity_at = (/* @__PURE__ */ new Date()).toISOString();
114
+ await writeFile(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
115
+ }
116
+ /**
117
+ * Read the active session WITHOUT computing the stale label. Returns null when no
118
+ * session is active or when the file is unparseable.
119
+ */
120
+ async readActive() {
121
+ const state = await this.read();
122
+ if (!state || state.closed_at) return null;
123
+ return state;
124
+ }
125
+ async read() {
126
+ if (!existsSync(this.sessionPath)) return null;
127
+ try {
128
+ const raw = await readFile(this.sessionPath, "utf-8");
129
+ const parsed = JSON.parse(raw);
130
+ if (typeof parsed?.id !== "string") return null;
131
+ return parsed;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ spawnAndCapture(cmd, timeoutMs) {
137
+ return new Promise((resolve) => {
138
+ const proc = spawn(cmd, { shell: true });
139
+ let buf = "";
140
+ let killedByTimeout = false;
141
+ const timer = setTimeout(() => {
142
+ killedByTimeout = true;
143
+ proc.kill("SIGTERM");
144
+ setTimeout(() => {
145
+ try {
146
+ proc.kill("SIGKILL");
147
+ } catch {
148
+ }
149
+ }, 1e3).unref();
150
+ }, timeoutMs);
151
+ const onData = (chunk) => {
152
+ buf += chunk.toString("utf-8");
153
+ if (buf.length > TAIL_BYTES * 4) {
154
+ buf = buf.slice(buf.length - TAIL_BYTES * 2);
155
+ }
156
+ };
157
+ proc.stdout?.on("data", onData);
158
+ proc.stderr?.on("data", onData);
159
+ proc.on("close", (code, signal) => {
160
+ clearTimeout(timer);
161
+ const tail = buf.slice(-TAIL_BYTES);
162
+ resolve({
163
+ cmd,
164
+ exit: killedByTimeout ? null : code,
165
+ signal: killedByTimeout ? "SIGTERM (timeout)" : signal ?? null,
166
+ duration_ms: 0,
167
+ tail,
168
+ at: (/* @__PURE__ */ new Date()).toISOString()
169
+ });
170
+ });
171
+ proc.on("error", (err) => {
172
+ clearTimeout(timer);
173
+ const tail = buf + `
174
+ [spawn error: ${err.message}]`;
175
+ resolve({
176
+ cmd,
177
+ exit: null,
178
+ signal: null,
179
+ duration_ms: 0,
180
+ tail: tail.slice(-TAIL_BYTES),
181
+ at: (/* @__PURE__ */ new Date()).toISOString()
182
+ });
183
+ });
184
+ });
185
+ }
186
+ };
187
+ function deriveProjectRoot(notesPath) {
188
+ const norm = notesPath.replace(/[\\/]+$/, "");
189
+ if (norm.endsWith("/.claude/.vault") || norm.endsWith("\\.claude\\.vault")) {
190
+ return norm.slice(0, -".claude/.vault".length).replace(/[\\/]+$/, "");
191
+ }
192
+ return norm;
193
+ }
194
+ function deriveSessionDir(notesPath) {
195
+ return join(deriveProjectRoot(notesPath), ".claude", ".semantic-memory");
196
+ }
197
+
198
+ export {
199
+ SessionManager,
200
+ deriveProjectRoot,
201
+ deriveSessionDir
202
+ };
203
+ //# sourceMappingURL=chunk-S4KW7VH4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/session.ts"],"sourcesContent":["import { mkdir, readFile, writeFile, rm } from \"node:fs/promises\";\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: task.trim(),\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 record = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);\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":[]}
@@ -58,6 +58,8 @@ var Indexer = class {
58
58
  const headers = this.extractHeaders(tree);
59
59
  const plainText = this.stripMarkdown(content);
60
60
  const chunks = this.chunkText(plainText);
61
+ const symptoms = Array.isArray(frontmatter.symptoms) ? frontmatter.symptoms.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => s.trim()) : [];
62
+ const allChunks = symptoms.length > 0 ? [...chunks, ...symptoms] : chunks;
61
63
  const title = frontmatter.title || headers[0] || basename(relativePath, ".md");
62
64
  const mtime = this.resolveMtime(frontmatter, fileStat.mtime);
63
65
  const loadPriority = typeof frontmatter.load_priority === "number" ? Math.min(10, Math.max(1, frontmatter.load_priority)) : void 0;
@@ -74,7 +76,7 @@ var Indexer = class {
74
76
  wikilinks,
75
77
  tags,
76
78
  headers,
77
- chunks,
79
+ chunks: allChunks,
78
80
  mtime,
79
81
  ...loadPriority !== void 0 && { loadPriority },
80
82
  ...status !== void 0 && { status },
@@ -166,4 +168,4 @@ var Indexer = class {
166
168
  export {
167
169
  Indexer
168
170
  };
169
- //# sourceMappingURL=chunk-HODDRXJK.js.map
171
+ //# sourceMappingURL=chunk-V45NJIHI.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\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 const docs = await Promise.all(\n files.map((file) => this.indexFile(join(this.notesPath, file), file))\n );\n return docs;\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 } = matter(raw);\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":"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","names":[]}
@@ -0,0 +1,311 @@
1
+ import {
2
+ deriveSessionDir
3
+ } from "./chunk-S4KW7VH4.js";
4
+
5
+ // src/core/dossier.ts
6
+ import { readFile, writeFile, mkdir } from "fs/promises";
7
+ import { existsSync } from "fs";
8
+ import { join } from "path";
9
+ import { glob } from "glob";
10
+ import matter from "gray-matter";
11
+
12
+ // src/core/section.ts
13
+ function extractSection(markdown, heading) {
14
+ const target = heading.trim().toLowerCase();
15
+ const lines = markdown.split("\n");
16
+ let startIdx = -1;
17
+ let startLevel = 0;
18
+ const headingAt = (line) => {
19
+ const m = /^(#{1,6})\s+(.*?)\s*$/.exec(line);
20
+ return m ? { level: m[1].length, text: m[2].trim().toLowerCase() } : null;
21
+ };
22
+ for (let i = 0; i < lines.length; i++) {
23
+ const h = headingAt(lines[i]);
24
+ if (h && h.text === target) {
25
+ startIdx = i;
26
+ startLevel = h.level;
27
+ break;
28
+ }
29
+ }
30
+ if (startIdx === -1) return null;
31
+ let endIdx = lines.length;
32
+ for (let i = startIdx + 1; i < lines.length; i++) {
33
+ const h = headingAt(lines[i]);
34
+ if (h && h.level <= startLevel) {
35
+ endIdx = i;
36
+ break;
37
+ }
38
+ }
39
+ return lines.slice(startIdx, endIdx).join("\n").trimEnd();
40
+ }
41
+
42
+ // src/core/dossier.ts
43
+ var DOSSIER_DIR = "dossiers";
44
+ var CACHE_FILE = "dossier-cache.json";
45
+ var DOSSIER_SECTIONS = [
46
+ "Purpose",
47
+ "Failure modes",
48
+ "Knobs & commands",
49
+ "Incident log",
50
+ "Current state"
51
+ ];
52
+ function slugifyEntity(entity) {
53
+ return entity.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "entity";
54
+ }
55
+ function normalizeEntity(s) {
56
+ return s.toLowerCase().trim().replace(/\s+/g, " ");
57
+ }
58
+ function sectionBody(markdown, heading) {
59
+ const section = extractSection(markdown, heading);
60
+ if (!section) return "";
61
+ return section.split("\n").slice(1).join("\n").trim();
62
+ }
63
+ function firstParagraph(body) {
64
+ const para = body.split(/\n\s*\n/).map((p) => p.trim()).find(Boolean);
65
+ return para ?? "";
66
+ }
67
+ function dossierTemplate(entity, opts = {}) {
68
+ const purpose = opts.purpose?.trim() || `_What ${entity} is and why it exists._`;
69
+ const seedNote = opts.seeded_from ? `
70
+ <!-- seeded from ${opts.seeded_from} -->
71
+ ` : "";
72
+ return `# ${entity}
73
+ ${seedNote}
74
+ ## Purpose
75
+
76
+ ${purpose}
77
+
78
+ ## Failure modes
79
+
80
+ _How it breaks, and the tell-tale symptoms._
81
+
82
+ ## Knobs & commands
83
+
84
+ _The buttons to push: commands, config, flags, dashboards._
85
+
86
+ ## Incident log
87
+
88
+ _Dated entries append here (newest last). Each: what happened \u2192 cause \u2192 fix._
89
+
90
+ ## Current state
91
+
92
+ _The single freshest sentence about where this entity stands right now._
93
+ `;
94
+ }
95
+ async function listDossiers(vaultPath) {
96
+ const files = await glob(`${DOSSIER_DIR}/**/*.md`, { cwd: vaultPath });
97
+ const entries = [];
98
+ for (const rel of files) {
99
+ try {
100
+ const raw = await readFile(join(vaultPath, rel), "utf-8");
101
+ const { data, content } = matter(raw);
102
+ if (data.type !== "dossier") continue;
103
+ const entity = typeof data.entity === "string" && data.entity.trim() ? data.entity.trim() : rel.replace(/^dossiers\//, "").replace(/\.md$/, "");
104
+ const aliases = Array.isArray(data.aliases) ? data.aliases.map(String).map((a) => a.trim()).filter(Boolean) : [];
105
+ entries.push({
106
+ entity,
107
+ aliases,
108
+ path: rel,
109
+ purpose: firstParagraph(sectionBody(content, "Purpose")),
110
+ current_state: sectionBody(content, "Current state")
111
+ });
112
+ } catch {
113
+ }
114
+ }
115
+ return entries.sort((a, b) => a.entity.localeCompare(b.entity));
116
+ }
117
+ async function compileDossiers(vaultPath) {
118
+ const entries = await listDossiers(vaultPath);
119
+ try {
120
+ const dir = deriveSessionDir(vaultPath);
121
+ await mkdir(dir, { recursive: true });
122
+ await writeFile(
123
+ join(dir, CACHE_FILE),
124
+ JSON.stringify({ compiledAt: (/* @__PURE__ */ new Date()).toISOString(), dossiers: entries }, null, 2),
125
+ "utf-8"
126
+ );
127
+ } catch {
128
+ }
129
+ return entries;
130
+ }
131
+ async function loadDossierCache(vaultPath) {
132
+ const cachePath = join(deriveSessionDir(vaultPath), CACHE_FILE);
133
+ if (existsSync(cachePath)) {
134
+ try {
135
+ const data = JSON.parse(await readFile(cachePath, "utf-8"));
136
+ if (Array.isArray(data?.dossiers)) return data.dossiers;
137
+ } catch {
138
+ }
139
+ }
140
+ return compileDossiers(vaultPath);
141
+ }
142
+ function findDossier(entries, entityOrAlias) {
143
+ const n = normalizeEntity(entityOrAlias);
144
+ return entries.find(
145
+ (d) => normalizeEntity(d.entity) === n || d.aliases.some((a) => normalizeEntity(a) === n)
146
+ );
147
+ }
148
+ function resolveDossierForQuery(entries, query) {
149
+ const nq = normalizeEntity(query);
150
+ let best;
151
+ for (const d of entries) {
152
+ const keys = [d.entity, ...d.aliases].map(normalizeEntity).filter(Boolean);
153
+ for (const k of keys) {
154
+ if (nq.includes(k) && (!best || k.length > best.keyLen)) {
155
+ best = { entry: d, keyLen: k.length };
156
+ }
157
+ }
158
+ }
159
+ return best?.entry;
160
+ }
161
+ async function readDossierRaw(vaultPath, rel) {
162
+ const abs = join(vaultPath, rel);
163
+ if (!existsSync(abs)) return null;
164
+ try {
165
+ const raw = await readFile(abs, "utf-8");
166
+ const { data } = matter(raw);
167
+ if (data.type !== "dossier") return null;
168
+ const entity = typeof data.entity === "string" ? data.entity : rel;
169
+ return { raw, entity };
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+ async function resolveDossierPath(vaultPath, entityOrPath) {
175
+ if (entityOrPath.endsWith(".md") && existsSync(join(vaultPath, entityOrPath))) return entityOrPath;
176
+ const entries = await listDossiers(vaultPath);
177
+ const match = findDossier(entries, entityOrPath);
178
+ if (match) return match.path;
179
+ return null;
180
+ }
181
+ async function initDossier(vaultPath, entity, opts = {}) {
182
+ const existing = await listDossiers(vaultPath);
183
+ const already = findDossier(existing, entity);
184
+ if (already) return { path: already.path, created: false, entity: already.entity };
185
+ const rel = join(DOSSIER_DIR, `${slugifyEntity(entity)}.md`);
186
+ const abs = join(vaultPath, rel);
187
+ if (existsSync(abs)) return { path: rel, created: false, entity };
188
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
189
+ const fm = {
190
+ title: entity,
191
+ type: "dossier",
192
+ entity,
193
+ status: "active",
194
+ last_verified: today,
195
+ confidence: "medium",
196
+ ...opts.aliases && opts.aliases.length > 0 ? { aliases: opts.aliases } : {},
197
+ ...opts.seeded_from ? { sources: [opts.seeded_from] } : {}
198
+ };
199
+ const body = dossierTemplate(entity, { purpose: opts.purpose, seeded_from: opts.seeded_from });
200
+ await mkdir(join(vaultPath, DOSSIER_DIR), { recursive: true });
201
+ await writeFile(abs, matter.stringify(body, fm), "utf-8");
202
+ await compileDossiers(vaultPath);
203
+ return { path: rel, created: true, entity };
204
+ }
205
+ async function appendIncident(vaultPath, entityOrPath, incident, opts = {}) {
206
+ const rel = await resolveDossierPath(vaultPath, entityOrPath);
207
+ if (!rel) return null;
208
+ const doc = await readDossierRaw(vaultPath, rel);
209
+ if (!doc) return null;
210
+ const today = opts.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
211
+ const line = `- ${today} \u2014 ${incident.trim()}`;
212
+ const patched = appendToSection(doc.raw, "Incident log", line);
213
+ await writeFile(join(vaultPath, rel), patched, "utf-8");
214
+ await compileDossiers(vaultPath);
215
+ return { path: rel };
216
+ }
217
+ async function setCurrentState(vaultPath, entityOrPath, state) {
218
+ const rel = await resolveDossierPath(vaultPath, entityOrPath);
219
+ if (!rel) return null;
220
+ const doc = await readDossierRaw(vaultPath, rel);
221
+ if (!doc) return null;
222
+ const patched = replaceSectionBody(doc.raw, "Current state", state.trim());
223
+ await writeFile(join(vaultPath, rel), patched, "utf-8");
224
+ await compileDossiers(vaultPath);
225
+ return { path: rel };
226
+ }
227
+ function locateSection(markdown, heading) {
228
+ const lines = markdown.split("\n");
229
+ const want = heading.trim().toLowerCase();
230
+ let start = -1;
231
+ let level = 0;
232
+ for (let i = 0; i < lines.length; i++) {
233
+ const m = lines[i].match(/^(#{1,6})\s+(.*?)\s*$/);
234
+ if (m && m[2].trim().toLowerCase() === want) {
235
+ start = i;
236
+ level = m[1].length;
237
+ break;
238
+ }
239
+ }
240
+ if (start === -1) return null;
241
+ let end = lines.length;
242
+ for (let i = start + 1; i < lines.length; i++) {
243
+ const m = lines[i].match(/^(#{1,6})\s+/);
244
+ if (m && m[1].length <= level) {
245
+ end = i;
246
+ break;
247
+ }
248
+ }
249
+ return { start, level, end, lines };
250
+ }
251
+ function isPlaceholderBody(body) {
252
+ const t = body.trim();
253
+ return t === "" || /^_.*_$/.test(t) && !t.includes("\n");
254
+ }
255
+ function appendToSection(markdown, heading, line) {
256
+ const loc = locateSection(markdown, heading);
257
+ if (!loc) {
258
+ const sep = markdown.endsWith("\n") ? "" : "\n";
259
+ return `${markdown}${sep}
260
+ ## ${heading}
261
+
262
+ ${line}
263
+ `;
264
+ }
265
+ const { lines, start, end } = loc;
266
+ const body = lines.slice(start + 1, end).join("\n");
267
+ const bodyStripped = body.trim();
268
+ const replacePlaceholder = isPlaceholderBody(bodyStripped);
269
+ let insertAt = end;
270
+ while (insertAt - 1 > start && lines[insertAt - 1].trim() === "") insertAt--;
271
+ const before = lines.slice(0, replacePlaceholder ? start + 1 : insertAt);
272
+ const after = lines.slice(replacePlaceholder ? end : insertAt);
273
+ const injected = replacePlaceholder ? ["", line] : [line];
274
+ return [...before, ...injected, ...after].join("\n");
275
+ }
276
+ function replaceSectionBody(markdown, heading, newBody) {
277
+ const loc = locateSection(markdown, heading);
278
+ if (!loc) {
279
+ const sep = markdown.endsWith("\n") ? "" : "\n";
280
+ return `${markdown}${sep}
281
+ ## ${heading}
282
+
283
+ ${newBody}
284
+ `;
285
+ }
286
+ const { lines, start, end } = loc;
287
+ const before = lines.slice(0, start + 1);
288
+ const after = lines.slice(end);
289
+ return [...before, "", newBody, "", ...after].join("\n").replace(/\n{3,}/g, "\n\n");
290
+ }
291
+
292
+ export {
293
+ extractSection,
294
+ DOSSIER_DIR,
295
+ DOSSIER_SECTIONS,
296
+ slugifyEntity,
297
+ normalizeEntity,
298
+ dossierTemplate,
299
+ listDossiers,
300
+ compileDossiers,
301
+ loadDossierCache,
302
+ findDossier,
303
+ resolveDossierForQuery,
304
+ resolveDossierPath,
305
+ initDossier,
306
+ appendIncident,
307
+ setCurrentState,
308
+ appendToSection,
309
+ replaceSectionBody
310
+ };
311
+ //# sourceMappingURL=chunk-WSU4QIPB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/dossier.ts","../src/core/section.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"gray-matter\";\nimport { deriveSessionDir } from \"./session.js\";\nimport { extractSection } from \"./section.js\";\n\n/**\n * Entity dossiers (v1.5 Phase 8) — the organizing unit of the resident expert.\n *\n * A dossier is a living note about one critical component/entity: what it is, how\n * it fails, which knobs to turn, its incident history, and its current state.\n * Knowledge accretes IN PLACE (incidents append to a log section; current-state is\n * revised) instead of spawning a new note per event — so the entity, not the file,\n * is the memory's unit. Canonical storage is markdown under <vault>/dossiers/\n * (type `dossier`); a derived JSON cache under .claude/.semantic-memory/ lets the\n * hot-path hook resolve an utterance to a dossier head for two-hop retrieval.\n */\n\nexport const DOSSIER_DIR = \"dossiers\";\nconst CACHE_FILE = \"dossier-cache.json\";\n\n/** The fixed section skeleton every dossier carries (order is meaningful). */\nexport const DOSSIER_SECTIONS = [\n \"Purpose\",\n \"Failure modes\",\n \"Knobs & commands\",\n \"Incident log\",\n \"Current state\",\n] as const;\n\nexport type DossierSection = (typeof DOSSIER_SECTIONS)[number];\n\nexport interface DossierEntry {\n /** The canonical entity name (e.g. \"payment-gateway\"). */\n entity: string;\n /** The human's phrases for this entity (feed the lexicon compiler). */\n aliases: string[];\n /** Dossier note path relative to vault root. */\n path: string;\n /** First paragraph of the Purpose section — the head injected on two-hop. */\n purpose: string;\n /** The Current state section body — the freshest one-liner about the entity. */\n current_state: string;\n}\n\nexport function slugifyEntity(entity: string): string {\n return (\n entity\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"entity\"\n );\n}\n\nexport function normalizeEntity(s: string): string {\n return s.toLowerCase().trim().replace(/\\s+/g, \" \");\n}\n\n/** Body-only text of a section (heading + leading/trailing blank lines stripped). */\nfunction sectionBody(markdown: string, heading: string): string {\n const section = extractSection(markdown, heading);\n if (!section) return \"\";\n // Drop the heading line, collapse to a trimmed body.\n return section\n .split(\"\\n\")\n .slice(1)\n .join(\"\\n\")\n .trim();\n}\n\n/** First non-empty paragraph of a section body (the \"head\" for injection). */\nfunction firstParagraph(body: string): string {\n const para = body.split(/\\n\\s*\\n/).map((p) => p.trim()).find(Boolean);\n return para ?? \"\";\n}\n\n/**\n * The dossier template — the fixed skeleton `dossier init` scaffolds and every\n * section edit patches into. Placeholders are intentionally terse prompts, not\n * prose, so an empty dossier is cheap to read and obvious to fill.\n */\nexport function dossierTemplate(entity: string, opts: { purpose?: string; seeded_from?: string } = {}): string {\n const purpose = opts.purpose?.trim() || `_What ${entity} is and why it exists._`;\n const seedNote = opts.seeded_from ? `\\n<!-- seeded from ${opts.seeded_from} -->\\n` : \"\";\n return `# ${entity}\n${seedNote}\n## Purpose\n\n${purpose}\n\n## Failure modes\n\n_How it breaks, and the tell-tale symptoms._\n\n## Knobs & commands\n\n_The buttons to push: commands, config, flags, dashboards._\n\n## Incident log\n\n_Dated entries append here (newest last). Each: what happened → cause → fix._\n\n## Current state\n\n_The single freshest sentence about where this entity stands right now._\n`;\n}\n\n/** Read every dossier note into an entry (best-effort; skips unreadable files). */\nexport async function listDossiers(vaultPath: string): Promise<DossierEntry[]> {\n const files = await glob(`${DOSSIER_DIR}/**/*.md`, { cwd: vaultPath });\n const entries: DossierEntry[] = [];\n for (const rel of files) {\n try {\n const raw = await readFile(join(vaultPath, rel), \"utf-8\");\n const { data, content } = matter(raw);\n if (data.type !== \"dossier\") continue;\n const entity = typeof data.entity === \"string\" && data.entity.trim() ? data.entity.trim() : rel.replace(/^dossiers\\//, \"\").replace(/\\.md$/, \"\");\n const aliases = Array.isArray(data.aliases) ? data.aliases.map(String).map((a) => a.trim()).filter(Boolean) : [];\n entries.push({\n entity,\n aliases,\n path: rel,\n purpose: firstParagraph(sectionBody(content, \"Purpose\")),\n current_state: sectionBody(content, \"Current state\"),\n });\n } catch {\n /* skip unreadable dossier */\n }\n }\n return entries.sort((a, b) => a.entity.localeCompare(b.entity));\n}\n\n/** Compile all dossiers into the derived cache the hook reads for two-hop routing. */\nexport async function compileDossiers(vaultPath: string): Promise<DossierEntry[]> {\n const entries = await listDossiers(vaultPath);\n try {\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n await writeFile(\n join(dir, CACHE_FILE),\n JSON.stringify({ compiledAt: new Date().toISOString(), dossiers: entries }, null, 2),\n \"utf-8\"\n );\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 loadDossierCache(vaultPath: string): Promise<DossierEntry[]> {\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?.dossiers)) return data.dossiers as DossierEntry[];\n } catch {\n /* fall through to recompile */\n }\n }\n return compileDossiers(vaultPath);\n}\n\n/** Find a dossier by exact entity name or any of its aliases (normalized). */\nexport function findDossier(entries: DossierEntry[], entityOrAlias: string): DossierEntry | undefined {\n const n = normalizeEntity(entityOrAlias);\n return entries.find(\n (d) => normalizeEntity(d.entity) === n || d.aliases.some((a) => normalizeEntity(a) === n)\n );\n}\n\n/**\n * Resolve an utterance to a dossier for two-hop retrieval: the query mentions the\n * entity name or one of its aliases. Returns the best (longest-alias) match so a\n * specific phrase wins over a generic one. Deterministic — no LLM, no embedding.\n */\nexport function resolveDossierForQuery(entries: DossierEntry[], query: string): DossierEntry | undefined {\n const nq = normalizeEntity(query);\n let best: { entry: DossierEntry; keyLen: number } | undefined;\n for (const d of entries) {\n const keys = [d.entity, ...d.aliases].map(normalizeEntity).filter(Boolean);\n for (const k of keys) {\n // Word-ish containment: the key appears in the query.\n if (nq.includes(k) && (!best || k.length > best.keyLen)) {\n best = { entry: d, keyLen: k.length };\n }\n }\n }\n return best?.entry;\n}\n\nasync function readDossierRaw(vaultPath: string, rel: string): Promise<{ raw: string; entity: string } | null> {\n const abs = join(vaultPath, rel);\n if (!existsSync(abs)) return null;\n try {\n const raw = await readFile(abs, \"utf-8\");\n const { data } = matter(raw);\n if (data.type !== \"dossier\") return null;\n const entity = typeof data.entity === \"string\" ? data.entity : rel;\n return { raw, entity };\n } catch {\n return null;\n }\n}\n\n/** Resolve an entity/alias/path argument to a concrete dossier note path. */\nexport async function resolveDossierPath(vaultPath: string, entityOrPath: string): Promise<string | null> {\n // Direct path?\n if (entityOrPath.endsWith(\".md\") && existsSync(join(vaultPath, entityOrPath))) return entityOrPath;\n const entries = await listDossiers(vaultPath);\n const match = findDossier(entries, entityOrPath);\n if (match) return match.path;\n // Fall back to the slug path even if not yet created.\n return null;\n}\n\nexport interface InitDossierResult {\n path: string;\n created: boolean;\n entity: string;\n}\n\n/**\n * Scaffold a dossier for an entity. No-op (created:false) if one already exists for\n * that entity/slug. `seeded_from` records provenance (e.g. a babel-fish project-map\n * entry) without failing when that source is absent — same fail-open posture as the\n * v1.2 code-symbol drift seed.\n */\nexport async function initDossier(\n vaultPath: string,\n entity: string,\n opts: { aliases?: string[]; purpose?: string; seeded_from?: string } = {}\n): Promise<InitDossierResult> {\n const existing = await listDossiers(vaultPath);\n const already = findDossier(existing, entity);\n if (already) return { path: already.path, created: false, entity: already.entity };\n\n const rel = join(DOSSIER_DIR, `${slugifyEntity(entity)}.md`);\n const abs = join(vaultPath, rel);\n if (existsSync(abs)) return { path: rel, created: false, entity };\n\n const today = new Date().toISOString().slice(0, 10);\n const fm: Record<string, unknown> = {\n title: entity,\n type: \"dossier\",\n entity,\n status: \"active\",\n last_verified: today,\n confidence: \"medium\",\n ...(opts.aliases && opts.aliases.length > 0 ? { aliases: opts.aliases } : {}),\n ...(opts.seeded_from ? { sources: [opts.seeded_from] } : {}),\n };\n const body = dossierTemplate(entity, { purpose: opts.purpose, seeded_from: opts.seeded_from });\n await mkdir(join(vaultPath, DOSSIER_DIR), { recursive: true });\n await writeFile(abs, matter.stringify(body, fm), \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel, created: true, entity };\n}\n\n/**\n * Append a dated incident line to a dossier's Incident log (accretion, never a new\n * file). Returns the resolved path, or null if no dossier exists for the entity.\n */\nexport async function appendIncident(\n vaultPath: string,\n entityOrPath: string,\n incident: string,\n opts: { today?: string } = {}\n): Promise<{ path: string } | null> {\n const rel = await resolveDossierPath(vaultPath, entityOrPath);\n if (!rel) return null;\n const doc = await readDossierRaw(vaultPath, rel);\n if (!doc) return null;\n const today = opts.today ?? new Date().toISOString().slice(0, 10);\n const line = `- ${today} — ${incident.trim()}`;\n const patched = appendToSection(doc.raw, \"Incident log\", line);\n await writeFile(join(vaultPath, rel), patched, \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel };\n}\n\n/**\n * Replace the body of a dossier's Current state section (the freshest one-liner).\n * Returns the resolved path, or null if no dossier exists for the entity.\n */\nexport async function setCurrentState(\n vaultPath: string,\n entityOrPath: string,\n state: string\n): Promise<{ path: string } | null> {\n const rel = await resolveDossierPath(vaultPath, entityOrPath);\n if (!rel) return null;\n const doc = await readDossierRaw(vaultPath, rel);\n if (!doc) return null;\n const patched = replaceSectionBody(doc.raw, \"Current state\", state.trim());\n await writeFile(join(vaultPath, rel), patched, \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel };\n}\n\n// --- section-level markdown editing (heading-scoped, format-preserving) ---\n\n/** Split into lines, find a `## <heading>` (case-insensitive), return its bounds. */\nfunction locateSection(markdown: string, heading: string): { start: number; level: number; end: number; lines: string[] } | null {\n const lines = markdown.split(\"\\n\");\n const want = heading.trim().toLowerCase();\n let start = -1;\n let level = 0;\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].match(/^(#{1,6})\\s+(.*?)\\s*$/);\n if (m && m[2].trim().toLowerCase() === want) {\n start = i;\n level = m[1].length;\n break;\n }\n }\n if (start === -1) return null;\n let end = lines.length;\n for (let i = start + 1; i < lines.length; i++) {\n const m = lines[i].match(/^(#{1,6})\\s+/);\n if (m && m[1].length <= level) {\n end = i;\n break;\n }\n }\n return { start, level, end, lines };\n}\n\n/** True if a section body is only the template placeholder (an italic prompt). */\nfunction isPlaceholderBody(body: string): boolean {\n const t = body.trim();\n return t === \"\" || (/^_.*_$/.test(t) && !t.includes(\"\\n\"));\n}\n\n/** Append a line to the end of a section's body (before the next heading/EOF). */\nexport function appendToSection(markdown: string, heading: string, line: string): string {\n const loc = locateSection(markdown, heading);\n if (!loc) {\n // Section absent — append a fresh one at EOF.\n const sep = markdown.endsWith(\"\\n\") ? \"\" : \"\\n\";\n return `${markdown}${sep}\\n## ${heading}\\n\\n${line}\\n`;\n }\n const { lines, start, end } = loc;\n const body = lines.slice(start + 1, end).join(\"\\n\");\n // If the body is just the placeholder prompt, replace it with the first real line.\n const bodyStripped = body.trim();\n const replacePlaceholder = isPlaceholderBody(bodyStripped);\n // Find the last non-empty line index within the section body.\n let insertAt = end;\n while (insertAt - 1 > start && lines[insertAt - 1].trim() === \"\") insertAt--;\n const before = lines.slice(0, replacePlaceholder ? start + 1 : insertAt);\n const after = lines.slice(replacePlaceholder ? end : insertAt);\n const injected = replacePlaceholder ? [\"\", line] : [line];\n return [...before, ...injected, ...after].join(\"\\n\");\n}\n\n/** Replace a section's entire body (keeps the heading), preserving surrounding doc. */\nexport function replaceSectionBody(markdown: string, heading: string, newBody: string): string {\n const loc = locateSection(markdown, heading);\n if (!loc) {\n const sep = markdown.endsWith(\"\\n\") ? \"\" : \"\\n\";\n return `${markdown}${sep}\\n## ${heading}\\n\\n${newBody}\\n`;\n }\n const { lines, start, end } = loc;\n const before = lines.slice(0, start + 1);\n const after = lines.slice(end);\n return [...before, \"\", newBody, \"\", ...after].join(\"\\n\").replace(/\\n{3,}/g, \"\\n\\n\");\n}\n","/**\n * Section-targeted read (v1.4 Phase 7). Extract one heading's section from a\n * markdown document so pulling a dossier's *Knobs* section doesn't cost the whole\n * file. Matches a heading by text (case-insensitive), returns from that heading\n * through to the next heading of the same or higher level.\n */\nexport function extractSection(markdown: string, heading: string): string | null {\n const target = heading.trim().toLowerCase();\n const lines = markdown.split(\"\\n\");\n let startIdx = -1;\n let startLevel = 0;\n\n const headingAt = (line: string): { level: number; text: string } | null => {\n const m = /^(#{1,6})\\s+(.*?)\\s*$/.exec(line);\n return m ? { level: m[1].length, text: m[2].trim().toLowerCase() } : null;\n };\n\n for (let i = 0; i < lines.length; i++) {\n const h = headingAt(lines[i]);\n if (h && h.text === target) {\n startIdx = i;\n startLevel = h.level;\n break;\n }\n }\n if (startIdx === -1) return null;\n\n let endIdx = lines.length;\n for (let i = startIdx + 1; i < lines.length; i++) {\n const h = headingAt(lines[i]);\n if (h && h.level <= startLevel) {\n endIdx = i;\n break;\n }\n }\n return lines.slice(startIdx, endIdx).join(\"\\n\").trimEnd();\n}\n"],"mappings":"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","names":[]}