r2mcp 0.2.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 (138) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +532 -0
  4. package/dist/breadcrumbs.d.ts +123 -0
  5. package/dist/breadcrumbs.js +135 -0
  6. package/dist/cli/classify-edges.d.ts +2 -0
  7. package/dist/cli/classify-edges.js +130 -0
  8. package/dist/cli/compile-wiki.d.ts +2 -0
  9. package/dist/cli/compile-wiki.js +173 -0
  10. package/dist/cli/dump-edges-json.d.ts +2 -0
  11. package/dist/cli/dump-edges-json.js +21 -0
  12. package/dist/cli/extract-entities.d.ts +17 -0
  13. package/dist/cli/extract-entities.js +166 -0
  14. package/dist/cli/lint-memory.d.ts +16 -0
  15. package/dist/cli/lint-memory.js +94 -0
  16. package/dist/cli/migrate.d.ts +17 -0
  17. package/dist/cli/migrate.js +146 -0
  18. package/dist/cli/setup-helpers.d.ts +7 -0
  19. package/dist/cli/setup-helpers.js +72 -0
  20. package/dist/cli/setup.d.ts +15 -0
  21. package/dist/cli/setup.js +95 -0
  22. package/dist/compiler/clustering.d.ts +29 -0
  23. package/dist/compiler/clustering.js +66 -0
  24. package/dist/compiler/frontmatter.d.ts +35 -0
  25. package/dist/compiler/frontmatter.js +168 -0
  26. package/dist/compiler/manifest.d.ts +32 -0
  27. package/dist/compiler/manifest.js +82 -0
  28. package/dist/compiler/prompts.d.ts +17 -0
  29. package/dist/compiler/prompts.js +82 -0
  30. package/dist/compiler/run.d.ts +52 -0
  31. package/dist/compiler/run.js +186 -0
  32. package/dist/compiler/tier.d.ts +10 -0
  33. package/dist/compiler/tier.js +85 -0
  34. package/dist/compiler/topic.d.ts +16 -0
  35. package/dist/compiler/topic.js +105 -0
  36. package/dist/compiler/types.d.ts +101 -0
  37. package/dist/compiler/types.js +4 -0
  38. package/dist/db.d.ts +10 -0
  39. package/dist/db.js +46 -0
  40. package/dist/edges/candidate-pairs.d.ts +24 -0
  41. package/dist/edges/candidate-pairs.js +35 -0
  42. package/dist/edges/classifier.d.ts +45 -0
  43. package/dist/edges/classifier.js +172 -0
  44. package/dist/edges/signals.d.ts +13 -0
  45. package/dist/edges/signals.js +45 -0
  46. package/dist/edges/stage1-haiku.d.ts +21 -0
  47. package/dist/edges/stage1-haiku.js +33 -0
  48. package/dist/edges/stage2-opus.d.ts +41 -0
  49. package/dist/edges/stage2-opus.js +101 -0
  50. package/dist/edges/state.d.ts +44 -0
  51. package/dist/edges/state.js +79 -0
  52. package/dist/edges/types.d.ts +20 -0
  53. package/dist/edges/types.js +1 -0
  54. package/dist/embeddings.d.ts +13 -0
  55. package/dist/embeddings.js +54 -0
  56. package/dist/entities/db.d.ts +49 -0
  57. package/dist/entities/db.js +109 -0
  58. package/dist/entities/extractor.d.ts +14 -0
  59. package/dist/entities/extractor.js +154 -0
  60. package/dist/entities/normalize.d.ts +5 -0
  61. package/dist/entities/normalize.js +7 -0
  62. package/dist/entities/prompt.d.ts +19 -0
  63. package/dist/entities/prompt.js +100 -0
  64. package/dist/entities/state.d.ts +44 -0
  65. package/dist/entities/state.js +99 -0
  66. package/dist/entities/types.d.ts +62 -0
  67. package/dist/entities/types.js +6 -0
  68. package/dist/env.d.ts +13 -0
  69. package/dist/env.js +32 -0
  70. package/dist/fingerprint.d.ts +2 -0
  71. package/dist/fingerprint.js +12 -0
  72. package/dist/graph-rebuild.d.ts +6 -0
  73. package/dist/graph-rebuild.js +20 -0
  74. package/dist/index.d.ts +4 -0
  75. package/dist/index.js +403 -0
  76. package/dist/instrumentation.d.ts +10 -0
  77. package/dist/instrumentation.js +37 -0
  78. package/dist/lint/checks/contradictions.d.ts +30 -0
  79. package/dist/lint/checks/contradictions.js +52 -0
  80. package/dist/lint/checks/drift.d.ts +5 -0
  81. package/dist/lint/checks/drift.js +34 -0
  82. package/dist/lint/checks/orphans.d.ts +5 -0
  83. package/dist/lint/checks/orphans.js +25 -0
  84. package/dist/lint/checks/stale.d.ts +6 -0
  85. package/dist/lint/checks/stale.js +29 -0
  86. package/dist/lint/checks/superseded-unflagged.d.ts +5 -0
  87. package/dist/lint/checks/superseded-unflagged.js +47 -0
  88. package/dist/lint/run.d.ts +11 -0
  89. package/dist/lint/run.js +95 -0
  90. package/dist/lint/types.d.ts +60 -0
  91. package/dist/lint/types.js +13 -0
  92. package/dist/mcp-response.d.ts +7 -0
  93. package/dist/mcp-response.js +13 -0
  94. package/dist/providers/anthropic.d.ts +13 -0
  95. package/dist/providers/anthropic.js +56 -0
  96. package/dist/providers/claude-code.d.ts +35 -0
  97. package/dist/providers/claude-code.js +175 -0
  98. package/dist/providers/errors.d.ts +12 -0
  99. package/dist/providers/errors.js +19 -0
  100. package/dist/providers/index.d.ts +30 -0
  101. package/dist/providers/index.js +71 -0
  102. package/dist/providers/openrouter.d.ts +19 -0
  103. package/dist/providers/openrouter.js +76 -0
  104. package/dist/providers/semaphore.d.ts +19 -0
  105. package/dist/providers/semaphore.js +51 -0
  106. package/dist/providers/types.d.ts +27 -0
  107. package/dist/providers/types.js +7 -0
  108. package/dist/schema.sql +116 -0
  109. package/dist/server-instructions.d.ts +9 -0
  110. package/dist/server-instructions.js +20 -0
  111. package/dist/telemetry.d.ts +39 -0
  112. package/dist/telemetry.js +130 -0
  113. package/dist/tools/classify.d.ts +44 -0
  114. package/dist/tools/classify.js +121 -0
  115. package/dist/tools/compile.d.ts +31 -0
  116. package/dist/tools/compile.js +132 -0
  117. package/dist/tools/dump-edges-sidecar.d.ts +37 -0
  118. package/dist/tools/dump-edges-sidecar.js +80 -0
  119. package/dist/tools/extract-entities.d.ts +53 -0
  120. package/dist/tools/extract-entities.js +169 -0
  121. package/dist/tools/lint.d.ts +10 -0
  122. package/dist/tools/lint.js +13 -0
  123. package/dist/tools/meditate.d.ts +25 -0
  124. package/dist/tools/meditate.js +128 -0
  125. package/dist/tools/recall.d.ts +66 -0
  126. package/dist/tools/recall.js +409 -0
  127. package/dist/tools/reject.d.ts +10 -0
  128. package/dist/tools/reject.js +24 -0
  129. package/dist/tools/remember.d.ts +26 -0
  130. package/dist/tools/remember.js +140 -0
  131. package/dist/tools/search.d.ts +30 -0
  132. package/dist/tools/search.js +69 -0
  133. package/dist/tools/spawn-cli.d.ts +14 -0
  134. package/dist/tools/spawn-cli.js +41 -0
  135. package/dist/tools/stats.d.ts +31 -0
  136. package/dist/tools/stats.js +88 -0
  137. package/package.json +86 -0
  138. package/skills/remember/SKILL.md +357 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Deterministic memory grouping — used by both tier and topic compile to
3
+ * keep section structure stable across runs (B.R5 / B.AC3).
4
+ *
5
+ * Clustering is computed from input only — the LLM never sees an
6
+ * unsorted/unstable list, so its outputs cite the same memory IDs and
7
+ * surface the same headers run-to-run.
8
+ */
9
+ /**
10
+ * Group memories by their first-listed topic, then sort:
11
+ * - clusters by topic name (alphabetical)
12
+ * - memories within each cluster by `id` (alphabetical, stable)
13
+ *
14
+ * Memories with no topics are grouped under "uncategorized".
15
+ */
16
+ export function clusterByTopic(memories) {
17
+ const buckets = new Map();
18
+ for (const m of memories) {
19
+ const topic = pickPrimaryTopic(m);
20
+ const bucket = buckets.get(topic) ?? [];
21
+ bucket.push(m);
22
+ buckets.set(topic, bucket);
23
+ }
24
+ const clusters = [];
25
+ for (const [topic, mems] of buckets) {
26
+ clusters.push({
27
+ topic,
28
+ topic_slug: topicToSlug(topic),
29
+ memories: [...mems].sort((a, b) => a.id.localeCompare(b.id)),
30
+ });
31
+ }
32
+ return clusters.sort((a, b) => a.topic.localeCompare(b.topic));
33
+ }
34
+ /**
35
+ * For topic-mode compile: filter to memories tagged with `topic`, sort by
36
+ * `created_at` ascending (so Timeline section flows naturally), tiebreak by id.
37
+ */
38
+ export function memoriesForTopic(memories, topic) {
39
+ return memories
40
+ .filter((m) => m.topics.includes(topic))
41
+ .sort((a, b) => {
42
+ if (a.created_at !== b.created_at)
43
+ return a.created_at.localeCompare(b.created_at);
44
+ return a.id.localeCompare(b.id);
45
+ });
46
+ }
47
+ function pickPrimaryTopic(m) {
48
+ if (!m.topics || m.topics.length === 0)
49
+ return 'uncategorized';
50
+ // Pick the lexicographically smallest topic so the bucket is stable
51
+ // even if topic order in the array changes.
52
+ return [...m.topics].sort()[0];
53
+ }
54
+ export function topicToSlug(topic) {
55
+ return topic
56
+ .toLowerCase()
57
+ .replace(/[^a-z0-9]+/g, '-')
58
+ .replace(/^-+|-+$/g, '');
59
+ }
60
+ export function topicTitle(topic) {
61
+ return topic
62
+ .split(/[-_\s]+/)
63
+ .filter(Boolean)
64
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
65
+ .join(' ');
66
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * YAML frontmatter generation + body comparison helpers for compile output
3
+ * (SPEC-044 B.R3, B.R5).
4
+ *
5
+ * Kept dependency-free: a compact YAML emitter is enough for our schema.
6
+ * We only emit strings, numbers, nulls, and arrays of strings — no nested
7
+ * objects — so a hand-rolled emitter sidesteps the js-yaml dependency.
8
+ */
9
+ import type { CompileFrontmatter } from './types.js';
10
+ export declare function emitFrontmatter(data: CompileFrontmatter): string;
11
+ /**
12
+ * Parse the leading `---\n...\n---\n` frontmatter block from a markdown file.
13
+ * Returns the parsed fields (only the ones we emit) and the body that follows.
14
+ * Throws if the file does not start with frontmatter.
15
+ */
16
+ export declare function parseFrontmatter(text: string): {
17
+ frontmatter: Partial<CompileFrontmatter>;
18
+ body: string;
19
+ };
20
+ /**
21
+ * Extract `## H2` and `### H3` headers from a markdown body. Used by B.R5/B.AC3
22
+ * stability check: header set across two runs must be identical.
23
+ */
24
+ export declare function extractHeaders(body: string): string[];
25
+ /**
26
+ * Strip frontmatter, headers, and inline citation tags (`<m:abc-123>`,
27
+ * `[m:abc-123]`) so the remaining body text can be Levenshtein-compared
28
+ * for prose-level variance (B.R5/B.AC3 third clause).
29
+ */
30
+ export declare function stripForBodyComparison(text: string): string;
31
+ /**
32
+ * Levenshtein distance ratio (1.0 = identical, 0.0 = totally different).
33
+ * Used by B.R5/B.AC3 to bound prose-level variance.
34
+ */
35
+ export declare function levenshteinRatio(a: string, b: string): number;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * YAML frontmatter generation + body comparison helpers for compile output
3
+ * (SPEC-044 B.R3, B.R5).
4
+ *
5
+ * Kept dependency-free: a compact YAML emitter is enough for our schema.
6
+ * We only emit strings, numbers, nulls, and arrays of strings — no nested
7
+ * objects — so a hand-rolled emitter sidesteps the js-yaml dependency.
8
+ */
9
+ const FRONTMATTER_DELIM = '---';
10
+ export function emitFrontmatter(data) {
11
+ const lines = [FRONTMATTER_DELIM];
12
+ lines.push(`generated_at: ${quote(data.generated_at)}`);
13
+ lines.push(`compile_run_id: ${quote(data.compile_run_id)}`);
14
+ lines.push(`source_count: ${data.source_count}`);
15
+ lines.push(`provider: ${quote(data.provider)}`);
16
+ if (data.source_git_sha === null) {
17
+ lines.push('source_git_sha: null');
18
+ }
19
+ else {
20
+ lines.push(`source_git_sha: ${quote(data.source_git_sha)}`);
21
+ }
22
+ if (data.tier)
23
+ lines.push(`tier: ${quote(data.tier)}`);
24
+ if (data.topic)
25
+ lines.push(`topic: ${quote(data.topic)}`);
26
+ lines.push('source_memory_ids:');
27
+ for (const id of data.source_memory_ids) {
28
+ lines.push(` - ${quote(id)}`);
29
+ }
30
+ lines.push(FRONTMATTER_DELIM);
31
+ return lines.join('\n') + '\n';
32
+ }
33
+ /**
34
+ * Parse the leading `---\n...\n---\n` frontmatter block from a markdown file.
35
+ * Returns the parsed fields (only the ones we emit) and the body that follows.
36
+ * Throws if the file does not start with frontmatter.
37
+ */
38
+ export function parseFrontmatter(text) {
39
+ if (!text.startsWith(FRONTMATTER_DELIM + '\n')) {
40
+ throw new Error('File does not start with YAML frontmatter');
41
+ }
42
+ const rest = text.slice(FRONTMATTER_DELIM.length + 1);
43
+ const endIdx = rest.indexOf('\n' + FRONTMATTER_DELIM + '\n');
44
+ if (endIdx === -1) {
45
+ throw new Error('Frontmatter block is not closed');
46
+ }
47
+ const yaml = rest.slice(0, endIdx);
48
+ const body = rest.slice(endIdx + ('\n' + FRONTMATTER_DELIM + '\n').length);
49
+ const fm = {};
50
+ const ids = [];
51
+ let inIds = false;
52
+ for (const rawLine of yaml.split('\n')) {
53
+ if (rawLine === '')
54
+ continue;
55
+ if (rawLine.startsWith(' - ')) {
56
+ if (inIds)
57
+ ids.push(unquote(rawLine.slice(4)));
58
+ continue;
59
+ }
60
+ inIds = false;
61
+ const colonIdx = rawLine.indexOf(':');
62
+ if (colonIdx === -1)
63
+ continue;
64
+ const key = rawLine.slice(0, colonIdx).trim();
65
+ const value = rawLine.slice(colonIdx + 1).trim();
66
+ if (key === 'source_memory_ids') {
67
+ inIds = true;
68
+ continue;
69
+ }
70
+ if (key === 'source_count') {
71
+ fm.source_count = Number(value);
72
+ }
73
+ else if (key === 'source_git_sha') {
74
+ fm.source_git_sha = value === 'null' ? null : unquote(value);
75
+ }
76
+ else if (key === 'generated_at')
77
+ fm.generated_at = unquote(value);
78
+ else if (key === 'compile_run_id')
79
+ fm.compile_run_id = unquote(value);
80
+ else if (key === 'provider')
81
+ fm.provider = unquote(value);
82
+ else if (key === 'tier')
83
+ fm.tier = unquote(value);
84
+ else if (key === 'topic')
85
+ fm.topic = unquote(value);
86
+ }
87
+ fm.source_memory_ids = ids;
88
+ return { frontmatter: fm, body };
89
+ }
90
+ function quote(s) {
91
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
92
+ }
93
+ function unquote(s) {
94
+ if (s.startsWith('"') && s.endsWith('"')) {
95
+ return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
96
+ }
97
+ return s;
98
+ }
99
+ /**
100
+ * Extract `## H2` and `### H3` headers from a markdown body. Used by B.R5/B.AC3
101
+ * stability check: header set across two runs must be identical.
102
+ */
103
+ export function extractHeaders(body) {
104
+ const out = [];
105
+ for (const line of body.split('\n')) {
106
+ const m = line.match(/^(##|###)\s+(.+?)\s*$/);
107
+ if (m)
108
+ out.push(`${m[1]} ${m[2].trim()}`);
109
+ }
110
+ return out;
111
+ }
112
+ /**
113
+ * Strip frontmatter, headers, and inline citation tags (`<m:abc-123>`,
114
+ * `[m:abc-123]`) so the remaining body text can be Levenshtein-compared
115
+ * for prose-level variance (B.R5/B.AC3 third clause).
116
+ */
117
+ export function stripForBodyComparison(text) {
118
+ // Remove frontmatter if present
119
+ let body = text;
120
+ if (body.startsWith(FRONTMATTER_DELIM + '\n')) {
121
+ const rest = body.slice(FRONTMATTER_DELIM.length + 1);
122
+ const endIdx = rest.indexOf('\n' + FRONTMATTER_DELIM + '\n');
123
+ if (endIdx !== -1)
124
+ body = rest.slice(endIdx + ('\n' + FRONTMATTER_DELIM + '\n').length);
125
+ }
126
+ // Remove markdown header lines
127
+ body = body
128
+ .split('\n')
129
+ .filter((l) => !l.match(/^#{1,6}\s/))
130
+ .join('\n');
131
+ // Remove citation tags
132
+ body = body.replace(/<m:[a-zA-Z0-9-]+>/g, '');
133
+ body = body.replace(/\[m:[a-zA-Z0-9-]+\]/g, '');
134
+ // Collapse whitespace
135
+ return body.replace(/\s+/g, ' ').trim();
136
+ }
137
+ /**
138
+ * Levenshtein distance ratio (1.0 = identical, 0.0 = totally different).
139
+ * Used by B.R5/B.AC3 to bound prose-level variance.
140
+ */
141
+ export function levenshteinRatio(a, b) {
142
+ if (a === b)
143
+ return 1.0;
144
+ const maxLen = Math.max(a.length, b.length);
145
+ if (maxLen === 0)
146
+ return 1.0;
147
+ return 1 - levenshtein(a, b) / maxLen;
148
+ }
149
+ function levenshtein(a, b) {
150
+ if (a.length === 0)
151
+ return b.length;
152
+ if (b.length === 0)
153
+ return a.length;
154
+ // Two-row DP for memory efficiency
155
+ let prev = new Array(b.length + 1);
156
+ let curr = new Array(b.length + 1);
157
+ for (let j = 0; j <= b.length; j++)
158
+ prev[j] = j;
159
+ for (let i = 1; i <= a.length; i++) {
160
+ curr[0] = i;
161
+ for (let j = 1; j <= b.length; j++) {
162
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
163
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
164
+ }
165
+ [prev, curr] = [curr, prev];
166
+ }
167
+ return prev[b.length];
168
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Compile manifest — written each run. Stale-cleanup logic uses it to delete
3
+ * compiled files that are no longer covered by the latest manifest (B.R8).
4
+ *
5
+ * Per-tier and per-topic invocations only update their own scoped entries.
6
+ * A full `compile({all: true})` rewrites everything in scope.
7
+ */
8
+ import type { CompileManifest, Tier } from './types.js';
9
+ export declare function manifestPath(compiledDir: string): string;
10
+ export declare function readManifest(compiledDir: string): Promise<CompileManifest | null>;
11
+ export declare function writeManifest(compiledDir: string, manifest: CompileManifest): Promise<string>;
12
+ /**
13
+ * Compute the set of files in `prev` that are NOT in `next` and should be
14
+ * deleted (B.AC8). Honors per-tier / per-topic scope: if `next` only updates
15
+ * the `preferences` tier and the `wiki-mode` topic, don't delete entries
16
+ * for other tiers or topics.
17
+ *
18
+ * `scope` lists what was updated this run:
19
+ * - `tiers`: which tier paths were rewritten
20
+ * - `topics`: which topic slugs were rewritten
21
+ * - `allTiers`: true if this was a full compile and ALL tiers should be
22
+ * considered in scope (so dropped tiers also get cleaned up — but in
23
+ * practice the three tier names are fixed)
24
+ * - `allTopics`: true if topic-set was rewritten as a whole; tiers/topics
25
+ * not in `next` and present in `prev` get deleted
26
+ */
27
+ export declare function computeStaleFiles(prev: CompileManifest | null, next: CompileManifest, scope: {
28
+ tiers: Tier[];
29
+ topics: string[];
30
+ allTopics: boolean;
31
+ }): string[];
32
+ export declare function deleteStaleFiles(compiledDir: string, paths: string[]): Promise<void>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Compile manifest — written each run. Stale-cleanup logic uses it to delete
3
+ * compiled files that are no longer covered by the latest manifest (B.R8).
4
+ *
5
+ * Per-tier and per-topic invocations only update their own scoped entries.
6
+ * A full `compile({all: true})` rewrites everything in scope.
7
+ */
8
+ import { existsSync } from 'node:fs';
9
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
10
+ import { dirname, join, resolve } from 'node:path';
11
+ const MANIFEST_FILENAME = 'manifest.json';
12
+ export function manifestPath(compiledDir) {
13
+ return join(compiledDir, MANIFEST_FILENAME);
14
+ }
15
+ export async function readManifest(compiledDir) {
16
+ const p = manifestPath(compiledDir);
17
+ if (!existsSync(p))
18
+ return null;
19
+ try {
20
+ const raw = await readFile(p, 'utf-8');
21
+ return JSON.parse(raw);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export async function writeManifest(compiledDir, manifest) {
28
+ const p = manifestPath(compiledDir);
29
+ await mkdir(dirname(p), { recursive: true });
30
+ await writeFile(p, JSON.stringify(manifest, null, 2), 'utf-8');
31
+ return p;
32
+ }
33
+ /**
34
+ * Compute the set of files in `prev` that are NOT in `next` and should be
35
+ * deleted (B.AC8). Honors per-tier / per-topic scope: if `next` only updates
36
+ * the `preferences` tier and the `wiki-mode` topic, don't delete entries
37
+ * for other tiers or topics.
38
+ *
39
+ * `scope` lists what was updated this run:
40
+ * - `tiers`: which tier paths were rewritten
41
+ * - `topics`: which topic slugs were rewritten
42
+ * - `allTiers`: true if this was a full compile and ALL tiers should be
43
+ * considered in scope (so dropped tiers also get cleaned up — but in
44
+ * practice the three tier names are fixed)
45
+ * - `allTopics`: true if topic-set was rewritten as a whole; tiers/topics
46
+ * not in `next` and present in `prev` get deleted
47
+ */
48
+ export function computeStaleFiles(prev, next, scope) {
49
+ if (!prev)
50
+ return [];
51
+ const stale = new Set();
52
+ const nextTierPaths = new Set(next.tiers.map((t) => t.path));
53
+ const nextTopicPaths = new Set(next.topics.map((t) => t.path));
54
+ // Tier files: only candidates within our scope.
55
+ for (const t of prev.tiers) {
56
+ if (!scope.tiers.includes(t.tier))
57
+ continue;
58
+ if (!nextTierPaths.has(t.path))
59
+ stale.add(t.path);
60
+ }
61
+ // Topic files: if allTopics, every prev topic that's not in next is stale.
62
+ // Otherwise, only the topics named in scope.topics.
63
+ for (const t of prev.topics) {
64
+ if (scope.allTopics) {
65
+ if (!nextTopicPaths.has(t.path))
66
+ stale.add(t.path);
67
+ }
68
+ else if (scope.topics.includes(t.topic)) {
69
+ if (!nextTopicPaths.has(t.path))
70
+ stale.add(t.path);
71
+ }
72
+ }
73
+ return [...stale];
74
+ }
75
+ export async function deleteStaleFiles(compiledDir, paths) {
76
+ for (const rel of paths) {
77
+ const abs = resolve(compiledDir, rel);
78
+ if (existsSync(abs)) {
79
+ await rm(abs, { force: true });
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Synthesis prompts for tier and topic compile. Kept in their own module so
3
+ * tests can snapshot them and developers can iterate on prompt language
4
+ * without scrolling through synthesis logic.
5
+ *
6
+ * Design contract (B.R5 stability):
7
+ * - The compiler controls structure (headers, citation placeholders).
8
+ * - The LLM produces only the prose paragraphs.
9
+ * - Citation tags are inserted by the compiler, not the LLM, so the set
10
+ * of cited memory IDs is identical across runs.
11
+ */
12
+ import type { MemoryForCompile, Tier } from './types.js';
13
+ export declare function tierSystemPrompt(tier: Tier): string;
14
+ export declare function topicSystemPrompt(): string;
15
+ export declare function memoryListPromptFragment(memories: MemoryForCompile[]): string;
16
+ export declare function tierClusterUserPrompt(topic: string, memories: MemoryForCompile[]): string;
17
+ export declare function topicSectionUserPrompt(topic: string, section: 'Summary' | 'Key Decisions' | 'Open Questions', memories: MemoryForCompile[]): string;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Synthesis prompts for tier and topic compile. Kept in their own module so
3
+ * tests can snapshot them and developers can iterate on prompt language
4
+ * without scrolling through synthesis logic.
5
+ *
6
+ * Design contract (B.R5 stability):
7
+ * - The compiler controls structure (headers, citation placeholders).
8
+ * - The LLM produces only the prose paragraphs.
9
+ * - Citation tags are inserted by the compiler, not the LLM, so the set
10
+ * of cited memory IDs is identical across runs.
11
+ */
12
+ const TIER_INTENT = {
13
+ preferences: 'a browsable summary of preferences and decisions',
14
+ 'project-context': 'a browsable summary of architecture and project state',
15
+ conversations: 'a browsable summary of relationship continuity and recurring threads',
16
+ };
17
+ export function tierSystemPrompt(tier) {
18
+ return `You produce ${TIER_INTENT[tier]} for a personal AI memory store.
19
+
20
+ Write a single concise paragraph (2-4 sentences) per cluster. Do NOT add headers, lists, or citation tags — those are inserted by the compiler around your output. Speak in present tense. Stay grounded in the cluster content; do not invent decisions or context.
21
+
22
+ Reply with ONLY the paragraph text — no preamble, no closing remarks.`;
23
+ }
24
+ export function topicSystemPrompt() {
25
+ return `You write a single section of an LLM wiki page about a topic. The section name and the source memories are given. Write a concise paragraph (2-5 sentences) capturing the section's content.
26
+
27
+ Do NOT add headers, lists, or citation tags — the compiler inserts those. Stay grounded; do not speculate beyond the source memories. If the section is empty (no relevant content), reply with the literal string: NONE`;
28
+ }
29
+ export function memoryListPromptFragment(memories) {
30
+ return memories.map((m) => `- (id=${m.id}, type=${m.type}) ${m.content}`).join('\n');
31
+ }
32
+ export function tierClusterUserPrompt(topic, memories) {
33
+ const edgeNote = describeEdges(memories);
34
+ return [
35
+ `Cluster topic: ${topic}`,
36
+ 'Source memories:',
37
+ memoryListPromptFragment(memories),
38
+ edgeNote ? `\nKnown structural relationships:\n${edgeNote}` : '',
39
+ '\nWrite the paragraph.',
40
+ ]
41
+ .filter(Boolean)
42
+ .join('\n');
43
+ }
44
+ export function topicSectionUserPrompt(topic, section, memories) {
45
+ const edgeNote = describeEdges(memories);
46
+ return [
47
+ `Topic: ${topic}`,
48
+ `Section: ${section}`,
49
+ 'Source memories:',
50
+ memoryListPromptFragment(memories),
51
+ edgeNote ? `\nKnown structural relationships:\n${edgeNote}` : '',
52
+ '\nWrite the section paragraph (or NONE if no content fits this section).',
53
+ ]
54
+ .filter(Boolean)
55
+ .join('\n');
56
+ }
57
+ /**
58
+ * Surface superseded / contradicted relationships so the LLM frames history
59
+ * accurately (B.AC7). Only mentions relations that affect prose framing.
60
+ */
61
+ function describeEdges(memories) {
62
+ const lines = [];
63
+ const ids = new Set(memories.map((m) => m.id));
64
+ for (const m of memories) {
65
+ if (!m.edges)
66
+ continue;
67
+ for (const e of m.edges) {
68
+ if (!ids.has(e.from_memory_id) || !ids.has(e.to_memory_id))
69
+ continue;
70
+ if (e.relation === 'supersedes') {
71
+ lines.push(`- ${e.from_memory_id} supersedes ${e.to_memory_id} — ${e.rationale}`);
72
+ }
73
+ else if (e.relation === 'contradicts') {
74
+ lines.push(`- ${e.from_memory_id} contradicts ${e.to_memory_id} — ${e.rationale}`);
75
+ }
76
+ else if (e.relation === 'evolved_into') {
77
+ lines.push(`- ${e.from_memory_id} evolved into ${e.to_memory_id} — ${e.rationale}`);
78
+ }
79
+ }
80
+ }
81
+ return lines.join('\n');
82
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Compile orchestrator — composes tier and topic synthesis, writes files,
3
+ * cleans stale outputs, and produces the run summary.
4
+ *
5
+ * `runCompile()` is dependency-injected so tests can run it without a DB:
6
+ * pass in the memory list and a mocked LLMProvider, get back the summary
7
+ * plus an in-memory file map for assertions.
8
+ */
9
+ import type { CompileManifest, CompileSummary, MemoryForCompile, Tier } from './types.js';
10
+ import type { LLMProvider } from '../providers/types.js';
11
+ export interface RunCompileOptions {
12
+ /** Compile a single tier. Mutually exclusive with `all` and `topic`. */
13
+ tier?: Tier;
14
+ /** Compile all three tiers. */
15
+ all?: boolean;
16
+ /** Compile a single topic page. Mutually exclusive with `tier` and `all`. */
17
+ topic?: string;
18
+ /** When true, emit preview to stdout and write no files. */
19
+ dryRun?: boolean;
20
+ /** Maximum total cost across all sections in the run. */
21
+ maxCostUsd: number;
22
+ runId: string;
23
+ startedAt: string;
24
+ /** git rev-parse HEAD of the host repo, or null. */
25
+ sourceGitSha: string | null;
26
+ /** Compiled output directory. Default: `<projectRoot>/memory/compiled/`. */
27
+ compiledDir: string;
28
+ }
29
+ export interface RunCompileDeps {
30
+ /**
31
+ * Returns the memories visible to compile. CLI implementations pass a DB
32
+ * query; tests inject synthetic data.
33
+ */
34
+ loadMemories: () => Promise<MemoryForCompile[]>;
35
+ provider: LLMProvider;
36
+ /**
37
+ * Test seam — replaces `writeFile`/`rm` calls with in-memory recording.
38
+ * Default: real filesystem.
39
+ */
40
+ fs?: CompileFs;
41
+ /** Test seam for stdout (dry-run preview). Default: process.stdout.write. */
42
+ stdout?: (chunk: string) => void;
43
+ }
44
+ export interface CompileFs {
45
+ ensureDir: (path: string) => Promise<void>;
46
+ writeFile: (path: string, content: string) => Promise<void>;
47
+ deleteFile: (path: string) => Promise<void>;
48
+ readManifest: (compiledDir: string) => Promise<CompileManifest | null>;
49
+ writeManifest: (compiledDir: string, manifest: CompileManifest) => Promise<string>;
50
+ exists: (path: string) => boolean;
51
+ }
52
+ export declare function runCompile(opts: RunCompileOptions, deps: RunCompileDeps): Promise<CompileSummary>;