aidimag 1.0.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 (91) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +113 -0
  3. package/dist/capture/bootstrap.d.ts +25 -0
  4. package/dist/capture/bootstrap.js +188 -0
  5. package/dist/capture/commit-miner.d.ts +59 -0
  6. package/dist/capture/commit-miner.js +381 -0
  7. package/dist/capture/harvest.d.ts +50 -0
  8. package/dist/capture/harvest.js +207 -0
  9. package/dist/capture/pr-miner.d.ts +57 -0
  10. package/dist/capture/pr-miner.js +185 -0
  11. package/dist/capture/session-briefing.d.ts +36 -0
  12. package/dist/capture/session-briefing.js +150 -0
  13. package/dist/capture/session-extraction.d.ts +23 -0
  14. package/dist/capture/session-extraction.js +54 -0
  15. package/dist/capture/triage.d.ts +30 -0
  16. package/dist/capture/triage.js +108 -0
  17. package/dist/cli/commands/capture.d.ts +5 -0
  18. package/dist/cli/commands/capture.js +357 -0
  19. package/dist/cli/commands/hosts.d.ts +5 -0
  20. package/dist/cli/commands/hosts.js +98 -0
  21. package/dist/cli/commands/knowledge.d.ts +5 -0
  22. package/dist/cli/commands/knowledge.js +121 -0
  23. package/dist/cli/commands/memory.d.ts +6 -0
  24. package/dist/cli/commands/memory.js +392 -0
  25. package/dist/cli/commands/sync.d.ts +5 -0
  26. package/dist/cli/commands/sync.js +307 -0
  27. package/dist/cli/commands/tickets.d.ts +6 -0
  28. package/dist/cli/commands/tickets.js +328 -0
  29. package/dist/cli/commands/verify.d.ts +5 -0
  30. package/dist/cli/commands/verify.js +136 -0
  31. package/dist/cli/index.d.ts +19 -0
  32. package/dist/cli/index.js +46 -0
  33. package/dist/cli/shared.d.ts +37 -0
  34. package/dist/cli/shared.js +175 -0
  35. package/dist/config.d.ts +55 -0
  36. package/dist/config.js +51 -0
  37. package/dist/context/generate.d.ts +24 -0
  38. package/dist/context/generate.js +115 -0
  39. package/dist/critique/critique.d.ts +40 -0
  40. package/dist/critique/critique.js +110 -0
  41. package/dist/db/schema.d.ts +28 -0
  42. package/dist/db/schema.js +269 -0
  43. package/dist/db/store.d.ts +182 -0
  44. package/dist/db/store.js +906 -0
  45. package/dist/debug.d.ts +14 -0
  46. package/dist/debug.js +25 -0
  47. package/dist/embeddings/provider.d.ts +16 -0
  48. package/dist/embeddings/provider.js +100 -0
  49. package/dist/embeddings/search.d.ts +22 -0
  50. package/dist/embeddings/search.js +95 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +12 -0
  53. package/dist/knowledge/chunk.d.ts +9 -0
  54. package/dist/knowledge/chunk.js +78 -0
  55. package/dist/knowledge/extract.d.ts +34 -0
  56. package/dist/knowledge/extract.js +113 -0
  57. package/dist/knowledge/ingest.d.ts +79 -0
  58. package/dist/knowledge/ingest.js +305 -0
  59. package/dist/knowledge/llm.d.ts +21 -0
  60. package/dist/knowledge/llm.js +110 -0
  61. package/dist/mcp/server.d.ts +11 -0
  62. package/dist/mcp/server.js +532 -0
  63. package/dist/security/evidence.d.ts +11 -0
  64. package/dist/security/evidence.js +15 -0
  65. package/dist/security/seal.d.ts +3 -0
  66. package/dist/security/seal.js +28 -0
  67. package/dist/security/sync-push.d.ts +2 -0
  68. package/dist/security/sync-push.js +37 -0
  69. package/dist/security/url.d.ts +6 -0
  70. package/dist/security/url.js +67 -0
  71. package/dist/sync/client.d.ts +84 -0
  72. package/dist/sync/client.js +391 -0
  73. package/dist/sync/server.d.ts +75 -0
  74. package/dist/sync/server.js +659 -0
  75. package/dist/tickets/provider.d.ts +80 -0
  76. package/dist/tickets/provider.js +375 -0
  77. package/dist/types.d.ts +133 -0
  78. package/dist/types.js +5 -0
  79. package/dist/ui/page.d.ts +5 -0
  80. package/dist/ui/page.js +841 -0
  81. package/dist/ui/server.d.ts +10 -0
  82. package/dist/ui/server.js +437 -0
  83. package/dist/verify/check.d.ts +38 -0
  84. package/dist/verify/check.js +121 -0
  85. package/dist/verify/engine.d.ts +39 -0
  86. package/dist/verify/engine.js +178 -0
  87. package/dist/verify/hooks.d.ts +24 -0
  88. package/dist/verify/hooks.js +125 -0
  89. package/dist/verify/runners.d.ts +26 -0
  90. package/dist/verify/runners.js +193 -0
  91. package/package.json +78 -0
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Debug channel for best-effort code paths.
3
+ *
4
+ * aidimag deliberately swallows errors in advisory paths (auto-sync, gap
5
+ * logging, embeddings, context regeneration) so they can never break a user
6
+ * command. The cost was observability: "why didn't X happen?" had no answer.
7
+ *
8
+ * `AIDIMAG_DEBUG=1 dim <cmd>` makes every swallowed error visible on stderr
9
+ * without changing behavior. Use `debugLog(scope, err)` in any catch that
10
+ * intentionally continues.
11
+ */
12
+ export declare const DEBUG_ENABLED: boolean;
13
+ /** Report a swallowed error when AIDIMAG_DEBUG is on. Never throws. */
14
+ export declare function debugLog(scope: string, err: unknown): void;
package/dist/debug.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Debug channel for best-effort code paths.
3
+ *
4
+ * aidimag deliberately swallows errors in advisory paths (auto-sync, gap
5
+ * logging, embeddings, context regeneration) so they can never break a user
6
+ * command. The cost was observability: "why didn't X happen?" had no answer.
7
+ *
8
+ * `AIDIMAG_DEBUG=1 dim <cmd>` makes every swallowed error visible on stderr
9
+ * without changing behavior. Use `debugLog(scope, err)` in any catch that
10
+ * intentionally continues.
11
+ */
12
+ export const DEBUG_ENABLED = process.env.AIDIMAG_DEBUG === "1" || process.env.AIDIMAG_DEBUG === "true";
13
+ /** Report a swallowed error when AIDIMAG_DEBUG is on. Never throws. */
14
+ export function debugLog(scope, err) {
15
+ if (!DEBUG_ENABLED)
16
+ return;
17
+ try {
18
+ const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
19
+ console.error(`[aidimag:debug] ${scope}: ${msg}`);
20
+ }
21
+ catch {
22
+ /* the debug channel itself must never throw */
23
+ }
24
+ }
25
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Embedding providers — pluggable, zero-config auto-detection.
3
+ *
4
+ * AIDIMAG_EMBEDDINGS = auto (default) | openai | ollama | off
5
+ *
6
+ * auto: OpenAI if OPENAI_API_KEY is set, else Ollama if reachable, else off
7
+ * (search degrades gracefully to FTS-only — aidimag never *requires* embeddings).
8
+ */
9
+ export interface EmbeddingProvider {
10
+ readonly name: string;
11
+ readonly model: string;
12
+ readonly dim: number;
13
+ embed(texts: string[]): Promise<number[][]>;
14
+ }
15
+ /** Resolve the configured/auto-detected provider (cached per process). */
16
+ export declare function getEmbeddingProvider(): Promise<EmbeddingProvider | null>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Embedding providers — pluggable, zero-config auto-detection.
3
+ *
4
+ * AIDIMAG_EMBEDDINGS = auto (default) | openai | ollama | off
5
+ *
6
+ * auto: OpenAI if OPENAI_API_KEY is set, else Ollama if reachable, else off
7
+ * (search degrades gracefully to FTS-only — aidimag never *requires* embeddings).
8
+ */
9
+ const OLLAMA_URL = process.env.AIDIMAG_OLLAMA_URL ?? "http://localhost:11434";
10
+ const OLLAMA_MODEL = process.env.AIDIMAG_OLLAMA_MODEL ?? "nomic-embed-text";
11
+ const OPENAI_MODEL = process.env.AIDIMAG_OPENAI_MODEL ?? "text-embedding-3-small";
12
+ class OpenAiProvider {
13
+ name = "openai";
14
+ model = OPENAI_MODEL;
15
+ dim = 1536;
16
+ async embed(texts) {
17
+ const res = await fetch("https://api.openai.com/v1/embeddings", {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
22
+ },
23
+ body: JSON.stringify({ model: this.model, input: texts }),
24
+ });
25
+ if (!res.ok)
26
+ throw new Error(`OpenAI embeddings: HTTP ${res.status} ${await res.text()}`);
27
+ const body = (await res.json());
28
+ return body.data.map((d) => d.embedding);
29
+ }
30
+ }
31
+ class OllamaProvider {
32
+ name = "ollama";
33
+ model = OLLAMA_MODEL;
34
+ dim;
35
+ constructor(dim) {
36
+ this.dim = dim;
37
+ }
38
+ async embed(texts) {
39
+ const out = [];
40
+ for (const text of texts) {
41
+ const res = await fetch(`${OLLAMA_URL}/api/embeddings`, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify({ model: this.model, prompt: text }),
45
+ });
46
+ if (!res.ok)
47
+ throw new Error(`Ollama embeddings: HTTP ${res.status}`);
48
+ const body = (await res.json());
49
+ out.push(body.embedding);
50
+ }
51
+ return out;
52
+ }
53
+ static async detect() {
54
+ try {
55
+ const ctl = new AbortController();
56
+ const t = setTimeout(() => ctl.abort(), 1500);
57
+ const res = await fetch(`${OLLAMA_URL}/api/embeddings`, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({ model: OLLAMA_MODEL, prompt: "probe" }),
61
+ signal: ctl.signal,
62
+ });
63
+ clearTimeout(t);
64
+ if (!res.ok)
65
+ return null;
66
+ const body = (await res.json());
67
+ if (!body.embedding?.length)
68
+ return null;
69
+ return new OllamaProvider(body.embedding.length);
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ }
76
+ let cached;
77
+ /** Resolve the configured/auto-detected provider (cached per process). */
78
+ export async function getEmbeddingProvider() {
79
+ if (cached !== undefined)
80
+ return cached;
81
+ const mode = (process.env.AIDIMAG_EMBEDDINGS ?? "auto").toLowerCase();
82
+ if (mode === "off")
83
+ return (cached = null);
84
+ if (mode === "openai") {
85
+ if (!process.env.OPENAI_API_KEY)
86
+ throw new Error("AIDIMAG_EMBEDDINGS=openai but OPENAI_API_KEY is not set");
87
+ return (cached = new OpenAiProvider());
88
+ }
89
+ if (mode === "ollama") {
90
+ const p = await OllamaProvider.detect();
91
+ if (!p)
92
+ throw new Error(`AIDIMAG_EMBEDDINGS=ollama but Ollama is not reachable at ${OLLAMA_URL} (model: ${OLLAMA_MODEL})`);
93
+ return (cached = p);
94
+ }
95
+ // auto
96
+ if (process.env.OPENAI_API_KEY)
97
+ return (cached = new OpenAiProvider());
98
+ return (cached = await OllamaProvider.detect());
99
+ }
100
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hybrid semantic recall — merges FTS keyword results with vector KNN using
3
+ * reciprocal-rank fusion, then applies the trust (status) penalty so VERIFIED
4
+ * memories outrank STALE ones regardless of similarity.
5
+ *
6
+ * Degrades gracefully: no provider / no vec extension → plain FTS search.
7
+ */
8
+ import type { MemoryStore } from "../db/store.js";
9
+ import type { MemoryEntry, MemorySearchOptions } from "../types.js";
10
+ import { type EmbeddingProvider } from "./provider.js";
11
+ /** Embed and index one memory (call after write/approve). No-op without a provider. */
12
+ export declare function indexMemory(store: MemoryStore, entry: MemoryEntry): Promise<boolean>;
13
+ /** Backfill embeddings for all memories missing one. Returns count indexed. */
14
+ export declare function reindexAll(store: MemoryStore): Promise<{
15
+ indexed: number;
16
+ provider: EmbeddingProvider | null;
17
+ }>;
18
+ /** Hybrid search: FTS + KNN fused, status-aware. Falls back to FTS-only. */
19
+ export declare function hybridSearch(store: MemoryStore, opts: MemorySearchOptions): Promise<{
20
+ results: MemoryEntry[];
21
+ semantic: boolean;
22
+ }>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Hybrid semantic recall — merges FTS keyword results with vector KNN using
3
+ * reciprocal-rank fusion, then applies the trust (status) penalty so VERIFIED
4
+ * memories outrank STALE ones regardless of similarity.
5
+ *
6
+ * Degrades gracefully: no provider / no vec extension → plain FTS search.
7
+ */
8
+ import { getEmbeddingProvider } from "./provider.js";
9
+ import { debugLog } from "../debug.js";
10
+ const RRF_K = 60;
11
+ const STATUS_PENALTY = { VERIFIED: 0, UNVERIFIED: 0.004, STALE: 0.012, REFUTED: 0.02 };
12
+ /** Embed and index one memory (call after write/approve). No-op without a provider. */
13
+ export async function indexMemory(store, entry) {
14
+ const provider = await getEmbeddingProvider();
15
+ if (!provider || !store.vecAvailable)
16
+ return false;
17
+ if (!store.ensureVecTable(provider.model, provider.dim))
18
+ return false;
19
+ const [vec] = await provider.embed([embeddingText(entry)]);
20
+ store.upsertEmbedding(entry.id, vec);
21
+ return true;
22
+ }
23
+ /** Backfill embeddings for all memories missing one. Returns count indexed. */
24
+ export async function reindexAll(store) {
25
+ const provider = await getEmbeddingProvider();
26
+ if (!provider || !store.vecAvailable)
27
+ return { indexed: 0, provider: null };
28
+ store.ensureVecTable(provider.model, provider.dim);
29
+ const ids = store.unembeddedIds();
30
+ let indexed = 0;
31
+ const BATCH = 16;
32
+ for (let i = 0; i < ids.length; i += BATCH) {
33
+ const batch = ids.slice(i, i + BATCH).map((id) => store.get(id)).filter((m) => !!m);
34
+ if (batch.length === 0)
35
+ continue;
36
+ const vectors = await provider.embed(batch.map(embeddingText));
37
+ batch.forEach((m, j) => store.upsertEmbedding(m.id, vectors[j]));
38
+ indexed += batch.length;
39
+ }
40
+ return { indexed, provider };
41
+ }
42
+ /** Hybrid search: FTS + KNN fused, status-aware. Falls back to FTS-only. */
43
+ export async function hybridSearch(store, opts) {
44
+ const ftsResults = store.search(opts);
45
+ const provider = opts.query.trim() ? await getEmbeddingProvider().catch(() => null) : null;
46
+ if (!provider || !store.vecAvailable)
47
+ return { results: ftsResults, semantic: false };
48
+ let knnIds = [];
49
+ try {
50
+ const [qvec] = await provider.embed([opts.query]);
51
+ knnIds = store.knn(qvec, Math.max(opts.limit ?? 10, 10) * 2);
52
+ }
53
+ catch (err) {
54
+ // embedding/KNN failure → keyword-only results, never a broken search
55
+ debugLog("semantic search (fell back to keyword-only)", err);
56
+ return { results: ftsResults, semantic: false };
57
+ }
58
+ if (knnIds.length === 0)
59
+ return { results: ftsResults, semantic: false };
60
+ // reciprocal-rank fusion
61
+ const scores = new Map();
62
+ ftsResults.forEach((m, rank) => scores.set(m.id, (scores.get(m.id) ?? 0) + 1 / (RRF_K + rank + 1)));
63
+ knnIds.forEach(({ id }, rank) => scores.set(id, (scores.get(id) ?? 0) + 1 / (RRF_K + rank + 1)));
64
+ const byId = new Map(ftsResults.map((m) => [m.id, m]));
65
+ for (const { id } of knnIds) {
66
+ if (!byId.has(id)) {
67
+ const m = store.get(id);
68
+ if (m)
69
+ byId.set(id, m);
70
+ }
71
+ }
72
+ let candidates = [...byId.values()];
73
+ // re-apply filters that FTS applied but KNN bypassed
74
+ if (opts.kind)
75
+ candidates = candidates.filter((m) => m.kind === opts.kind);
76
+ if (opts.status)
77
+ candidates = candidates.filter((m) => m.status === opts.status);
78
+ else if (!opts.includeRefuted)
79
+ candidates = candidates.filter((m) => m.status !== "REFUTED");
80
+ if (opts.paths?.length) {
81
+ candidates = candidates.filter((m) => m.scope.paths.length === 0 ||
82
+ m.scope.paths.some((sp) => opts.paths.some((p) => p.startsWith(sp) || sp.startsWith(p))));
83
+ }
84
+ candidates.sort((a, b) => {
85
+ const sa = (scores.get(a.id) ?? 0) - (STATUS_PENALTY[a.status] ?? 0);
86
+ const sb = (scores.get(b.id) ?? 0) - (STATUS_PENALTY[b.status] ?? 0);
87
+ return sb - sa || b.confidence - a.confidence;
88
+ });
89
+ return { results: candidates.slice(0, Math.min(opts.limit ?? 10, 50)), semantic: true };
90
+ }
91
+ function embeddingText(m) {
92
+ const scope = [...m.scope.paths, ...m.scope.symbols].join(" ");
93
+ return `${m.kind}: ${m.claim}${scope ? `\nscope: ${scope}` : ""}`;
94
+ }
95
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,11 @@
1
+ export * from "./types.js";
2
+ export { MemoryStore, findRepoRoot, dbPathFor, AIDIMAG_DIR, DB_FILE } from "./db/store.js";
3
+ export { mineCommits, readCommits, classifyCommit, scopeFromFiles } from "./capture/commit-miner.js";
4
+ export { SESSION_END_PROMPT, proposalSummaryLine } from "./capture/session-extraction.js";
5
+ export { verifyAll, verifyMemory, decayedConfidence } from "./verify/engine.js";
6
+ export { runEvidence } from "./verify/runners.js";
7
+ export { installGitHooks } from "./verify/hooks.js";
8
+ export { getEmbeddingProvider } from "./embeddings/provider.js";
9
+ export { hybridSearch, indexMemory, reindexAll } from "./embeddings/search.js";
10
+ export { startSyncServer } from "./sync/server.js";
11
+ export { sync, readCloudConfig, writeCloudConfig } from "./sync/client.js";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export * from "./types.js";
2
+ export { MemoryStore, findRepoRoot, dbPathFor, AIDIMAG_DIR, DB_FILE } from "./db/store.js";
3
+ export { mineCommits, readCommits, classifyCommit, scopeFromFiles } from "./capture/commit-miner.js";
4
+ export { SESSION_END_PROMPT, proposalSummaryLine } from "./capture/session-extraction.js";
5
+ export { verifyAll, verifyMemory, decayedConfidence } from "./verify/engine.js";
6
+ export { runEvidence } from "./verify/runners.js";
7
+ export { installGitHooks } from "./verify/hooks.js";
8
+ export { getEmbeddingProvider } from "./embeddings/provider.js";
9
+ export { hybridSearch, indexMemory, reindexAll } from "./embeddings/search.js";
10
+ export { startSyncServer } from "./sync/server.js";
11
+ export { sync, readCloudConfig, writeCloudConfig } from "./sync/client.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Structure-aware chunking for large knowledge documents. We split on natural
3
+ * boundaries (Markdown headings first, then blank-line paragraphs) and pack
4
+ * sections up to ~chunkBytes, so a chunk is a coherent unit rather than a
5
+ * byte-count cut mid-sentence. Each chunk is summarized independently and the
6
+ * resulting claims are deduplicated across the whole document.
7
+ */
8
+ /** Split text into coherent chunks no larger than ~chunkBytes (best effort). */
9
+ export declare function chunkText(content: string, chunkBytes: number): string[];
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Structure-aware chunking for large knowledge documents. We split on natural
3
+ * boundaries (Markdown headings first, then blank-line paragraphs) and pack
4
+ * sections up to ~chunkBytes, so a chunk is a coherent unit rather than a
5
+ * byte-count cut mid-sentence. Each chunk is summarized independently and the
6
+ * resulting claims are deduplicated across the whole document.
7
+ */
8
+ /** Split text into coherent chunks no larger than ~chunkBytes (best effort). */
9
+ export function chunkText(content, chunkBytes) {
10
+ if (Buffer.byteLength(content, "utf8") <= chunkBytes)
11
+ return [content];
12
+ // First pass: break at Markdown headings (keep the heading with its body).
13
+ const sections = splitOnHeadings(content);
14
+ const chunks = [];
15
+ let buf = "";
16
+ const flush = () => {
17
+ if (buf.trim())
18
+ chunks.push(buf.trim());
19
+ buf = "";
20
+ };
21
+ for (const section of sections) {
22
+ // A single section bigger than the budget → split it further on paragraphs.
23
+ if (Buffer.byteLength(section, "utf8") > chunkBytes) {
24
+ flush();
25
+ for (const part of splitOnParagraphs(section, chunkBytes))
26
+ chunks.push(part);
27
+ continue;
28
+ }
29
+ if (buf && Buffer.byteLength(buf + "\n\n" + section, "utf8") > chunkBytes)
30
+ flush();
31
+ buf = buf ? buf + "\n\n" + section : section;
32
+ }
33
+ flush();
34
+ return chunks.length ? chunks : [content];
35
+ }
36
+ /** Group lines into sections that each start at a Markdown heading. */
37
+ function splitOnHeadings(content) {
38
+ const lines = content.split("\n");
39
+ const sections = [];
40
+ let current = [];
41
+ for (const line of lines) {
42
+ if (/^#{1,6}\s/.test(line) && current.some((l) => l.trim())) {
43
+ sections.push(current.join("\n"));
44
+ current = [];
45
+ }
46
+ current.push(line);
47
+ }
48
+ if (current.length)
49
+ sections.push(current.join("\n"));
50
+ return sections;
51
+ }
52
+ /** Pack blank-line-separated paragraphs up to the byte budget. */
53
+ function splitOnParagraphs(text, chunkBytes) {
54
+ const paras = text.split(/\n\s*\n/);
55
+ const out = [];
56
+ let buf = "";
57
+ for (const para of paras) {
58
+ if (buf && Buffer.byteLength(buf + "\n\n" + para, "utf8") > chunkBytes) {
59
+ out.push(buf.trim());
60
+ buf = "";
61
+ }
62
+ // A single paragraph over budget: hard-wrap by characters as a last resort.
63
+ if (Buffer.byteLength(para, "utf8") > chunkBytes) {
64
+ if (buf.trim()) {
65
+ out.push(buf.trim());
66
+ buf = "";
67
+ }
68
+ for (let i = 0; i < para.length; i += chunkBytes)
69
+ out.push(para.slice(i, i + chunkBytes));
70
+ continue;
71
+ }
72
+ buf = buf ? buf + "\n\n" + para : para;
73
+ }
74
+ if (buf.trim())
75
+ out.push(buf.trim());
76
+ return out;
77
+ }
78
+ //# sourceMappingURL=chunk.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared claim-extraction contract for knowledge ingestion. Both summarizer paths
3
+ * use this so they produce identical, reviewable output:
4
+ * - the LLM fallback (`dim knowledge sync`) feeds these instructions to a model;
5
+ * - the MCP `knowledge_ingest` prompt feeds them to the connected agent.
6
+ *
7
+ * The extractor turns a project document into typed, scoped, FALSIFIABLE claims —
8
+ * never a single blob. Claims become proposals (source `knowledge:<doc>`) and, once
9
+ * approved with `dim review`, PINNED memories.
10
+ */
11
+ import type { GuardrailLevel, MemoryKind } from "../types.js";
12
+ export interface ExtractedClaim {
13
+ kind: MemoryKind;
14
+ claim: string;
15
+ paths?: string[];
16
+ symbols?: string[];
17
+ /** only for kind === "GUARDRAIL" */
18
+ guardrailLevel?: GuardrailLevel;
19
+ /** short note on why this is durable / where in the doc it came from */
20
+ rationale?: string;
21
+ /** optional STATIC_CHECK shell command that passes iff the claim holds */
22
+ staticCheck?: string;
23
+ }
24
+ export declare const KNOWLEDGE_EXTRACT_INSTRUCTIONS = "You are extracting durable, project-specific knowledge from a document a developer dropped into the knowledge inbox (a design doc, ADR, style guide, runbook, or notes).\n\nTurn it into a small set of FALSIFIABLE claims. Rules:\n\n1. Only DURABLE, NON-OBVIOUS facts about THIS project \u2014 rules, decisions, architecture, invariants, guardrails, procedures. Skip generic advice, tutorials, and anything a quick file read already shows.\n2. Write each claim as a checkable statement. Bad: \"auth is complex\". Good: \"JWT refresh in src/auth/refresh.ts must run before the middleware chain; reordering breaks session renewal\".\n3. Pick the right kind:\n - DECISION: a choice + the rejected alternative\n - CONVENTION: a rule the repo follows\n - GOTCHA: surprising behavior that costs time\n - FAILED_APPROACH: something tried that did NOT work\n - ARCHITECTURE: how components fit together\n - INVARIANT: something that must always/never hold\n - GUARDRAIL: a behavioral rule for future agents \u2014 set guardrail_level to \"never\" (refuse), \"ask-first\" (confirm), or \"always\" (do automatically)\n - SKILL: a reusable step-by-step procedure (write the steps as \"1) ... 2) ...\")\n - TODO_CONTEXT: unfinished work + the context to resume it\n4. Scope each claim with paths/symbols it applies to when the document names them; otherwise leave them empty (repo-wide).\n5. Extract 0\u201312 claims. Zero is fine. Do NOT pad, and do NOT invent rules the document doesn't support.\n\nRespond with ONLY a JSON object of this exact shape:\n{\"claims\":[{\"kind\":\"CONVENTION\",\"claim\":\"...\",\"paths\":[\"src/x\"],\"symbols\":[],\"guardrail_level\":null,\"rationale\":\"...\"}]}";
25
+ /** Build the user message for the LLM: instructions + the document. */
26
+ export declare function buildExtractionUser(filename: string, content: string): string;
27
+ /**
28
+ * Tolerantly parse a model/agent response into validated claims. Accepts a bare
29
+ * array or an object with a `claims` array, and ignores anything malformed rather
30
+ * than throwing — a bad doc should never crash ingestion.
31
+ */
32
+ export declare function parseClaims(raw: string): ExtractedClaim[];
33
+ /** Collapse claims repeated across chunks (normalized text match). */
34
+ export declare function dedupeClaims(claims: ExtractedClaim[]): ExtractedClaim[];
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared claim-extraction contract for knowledge ingestion. Both summarizer paths
3
+ * use this so they produce identical, reviewable output:
4
+ * - the LLM fallback (`dim knowledge sync`) feeds these instructions to a model;
5
+ * - the MCP `knowledge_ingest` prompt feeds them to the connected agent.
6
+ *
7
+ * The extractor turns a project document into typed, scoped, FALSIFIABLE claims —
8
+ * never a single blob. Claims become proposals (source `knowledge:<doc>`) and, once
9
+ * approved with `dim review`, PINNED memories.
10
+ */
11
+ const KINDS = [
12
+ "DECISION", "CONVENTION", "GOTCHA", "FAILED_APPROACH",
13
+ "ARCHITECTURE", "INVARIANT", "TODO_CONTEXT", "GUARDRAIL", "SKILL",
14
+ ];
15
+ const GUARDRAIL_LEVELS = ["never", "ask-first", "always"];
16
+ export const KNOWLEDGE_EXTRACT_INSTRUCTIONS = `You are extracting durable, project-specific knowledge from a document a developer dropped into the knowledge inbox (a design doc, ADR, style guide, runbook, or notes).
17
+
18
+ Turn it into a small set of FALSIFIABLE claims. Rules:
19
+
20
+ 1. Only DURABLE, NON-OBVIOUS facts about THIS project — rules, decisions, architecture, invariants, guardrails, procedures. Skip generic advice, tutorials, and anything a quick file read already shows.
21
+ 2. Write each claim as a checkable statement. Bad: "auth is complex". Good: "JWT refresh in src/auth/refresh.ts must run before the middleware chain; reordering breaks session renewal".
22
+ 3. Pick the right kind:
23
+ - DECISION: a choice + the rejected alternative
24
+ - CONVENTION: a rule the repo follows
25
+ - GOTCHA: surprising behavior that costs time
26
+ - FAILED_APPROACH: something tried that did NOT work
27
+ - ARCHITECTURE: how components fit together
28
+ - INVARIANT: something that must always/never hold
29
+ - GUARDRAIL: a behavioral rule for future agents — set guardrail_level to "never" (refuse), "ask-first" (confirm), or "always" (do automatically)
30
+ - SKILL: a reusable step-by-step procedure (write the steps as "1) ... 2) ...")
31
+ - TODO_CONTEXT: unfinished work + the context to resume it
32
+ 4. Scope each claim with paths/symbols it applies to when the document names them; otherwise leave them empty (repo-wide).
33
+ 5. Extract 0–12 claims. Zero is fine. Do NOT pad, and do NOT invent rules the document doesn't support.
34
+
35
+ Respond with ONLY a JSON object of this exact shape:
36
+ {"claims":[{"kind":"CONVENTION","claim":"...","paths":["src/x"],"symbols":[],"guardrail_level":null,"rationale":"..."}]}`;
37
+ /** Build the user message for the LLM: instructions + the document. */
38
+ export function buildExtractionUser(filename, content) {
39
+ return `Document: ${filename}\n\n----- BEGIN DOCUMENT -----\n${content}\n----- END DOCUMENT -----`;
40
+ }
41
+ function asStringArray(v) {
42
+ if (!Array.isArray(v))
43
+ return undefined;
44
+ const out = v.filter((x) => typeof x === "string" && x.trim().length > 0);
45
+ return out.length ? out : undefined;
46
+ }
47
+ /**
48
+ * Tolerantly parse a model/agent response into validated claims. Accepts a bare
49
+ * array or an object with a `claims` array, and ignores anything malformed rather
50
+ * than throwing — a bad doc should never crash ingestion.
51
+ */
52
+ export function parseClaims(raw) {
53
+ let data;
54
+ try {
55
+ data = JSON.parse(raw);
56
+ }
57
+ catch {
58
+ // last resort: pull the first {...} or [...] block out of chatty output
59
+ const m = raw.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
60
+ if (!m)
61
+ return [];
62
+ try {
63
+ data = JSON.parse(m[0]);
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ }
69
+ const list = Array.isArray(data)
70
+ ? data
71
+ : data?.claims;
72
+ if (!Array.isArray(list))
73
+ return [];
74
+ const claims = [];
75
+ for (const item of list) {
76
+ const kind = String(item?.kind ?? "").toUpperCase();
77
+ const claim = typeof item?.claim === "string" ? item.claim.trim() : "";
78
+ if (!claim || !KINDS.includes(kind))
79
+ continue;
80
+ const out = { kind, claim };
81
+ const paths = asStringArray(item.paths);
82
+ const symbols = asStringArray(item.symbols);
83
+ if (paths)
84
+ out.paths = paths;
85
+ if (symbols)
86
+ out.symbols = symbols;
87
+ if (typeof item.rationale === "string" && item.rationale.trim())
88
+ out.rationale = item.rationale.trim();
89
+ const sc = item.static_check ?? item.staticCheck;
90
+ if (typeof sc === "string" && sc.trim())
91
+ out.staticCheck = sc.trim();
92
+ if (kind === "GUARDRAIL") {
93
+ const lvl = String(item.guardrail_level ?? item.guardrailLevel ?? "ask-first").toLowerCase();
94
+ out.guardrailLevel = GUARDRAIL_LEVELS.includes(lvl) ? lvl : "ask-first";
95
+ }
96
+ claims.push(out);
97
+ }
98
+ return dedupeClaims(claims);
99
+ }
100
+ /** Collapse claims repeated across chunks (normalized text match). */
101
+ export function dedupeClaims(claims) {
102
+ const seen = new Set();
103
+ const out = [];
104
+ for (const c of claims) {
105
+ const key = c.claim.toLowerCase().replace(/\s+/g, " ").trim();
106
+ if (seen.has(key))
107
+ continue;
108
+ seen.add(key);
109
+ out.push(c);
110
+ }
111
+ return out;
112
+ }
113
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Knowledge ingestion pipeline (KNOWLEDGEBASE_DESIGN.md).
3
+ *
4
+ * Drop docs into the `knowledge/` inbox → they're classified, (chunked and)
5
+ * summarized into typed claims → queued as proposals (source `knowledge:<doc>`,
6
+ * pin-on-approve) → a plain-text summary is written and the original is backed up
7
+ * to .aidimag/knowledge/processed/ before the inbox copy is removed.
8
+ * PDF and DOCX files have their text extracted (pdf-parse / mammoth) first.
9
+ *
10
+ * Trust gate: claims become PINNED memories only after `dim review` (unless the
11
+ * repo opts out with knowledge.requireReview = false). Nothing is ever deleted —
12
+ * unsupported files move to .aidimag/knowledge/skipped/ with a reason.
13
+ */
14
+ import type { MemoryStore } from "../db/store.js";
15
+ import type { ResolvedKnowledgeConfig } from "../config.js";
16
+ import type { ExtractedClaim } from "./extract.js";
17
+ export interface PendingDoc {
18
+ file: string;
19
+ abs: string;
20
+ content: string;
21
+ hash: string;
22
+ bytes: number;
23
+ }
24
+ export interface SkipCandidate {
25
+ file: string;
26
+ reason: string;
27
+ }
28
+ export interface DocResult {
29
+ file: string;
30
+ hash: string;
31
+ claimCount: number;
32
+ proposalIds: string[];
33
+ memoryIds: string[];
34
+ pinned: boolean;
35
+ }
36
+ export interface IngestReport {
37
+ processed: DocResult[];
38
+ pendingNoSummarizer: string[];
39
+ duplicates: string[];
40
+ skipped: SkipCandidate[];
41
+ summarizer: string | null;
42
+ }
43
+ interface ManifestEntry {
44
+ file: string;
45
+ hash: string;
46
+ date: string;
47
+ via: string;
48
+ claimCount: number;
49
+ proposalIds: string[];
50
+ memoryIds: string[];
51
+ }
52
+ interface Manifest {
53
+ version: number;
54
+ docs: ManifestEntry[];
55
+ }
56
+ export declare function readManifest(root: string): Manifest;
57
+ export declare function classifyInbox(root: string, cfg: ResolvedKnowledgeConfig): Promise<{
58
+ pending: PendingDoc[];
59
+ toSkip: SkipCandidate[];
60
+ }>;
61
+ /**
62
+ * Turn extracted claims into proposals and retire the inbox file safely.
63
+ * Used by both the LLM path and the MCP agent path (which supplies its own claims).
64
+ */
65
+ export declare function finalizeDoc(store: MemoryStore, root: string, cfg: ResolvedKnowledgeConfig, doc: {
66
+ file: string;
67
+ hash: string;
68
+ abs?: string;
69
+ }, claims: ExtractedClaim[], via: string): DocResult;
70
+ export declare function ingestAll(store: MemoryStore, root: string, cfg: ResolvedKnowledgeConfig): Promise<IngestReport>;
71
+ export interface KnowledgeStatus {
72
+ folder: string;
73
+ pending: PendingDoc[];
74
+ unsupported: SkipCandidate[];
75
+ skippedOnDisk: string[];
76
+ processed: ManifestEntry[];
77
+ }
78
+ export declare function knowledgeStatus(root: string, cfg: ResolvedKnowledgeConfig): Promise<KnowledgeStatus>;
79
+ export {};