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.
- package/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/dist/breadcrumbs.d.ts +123 -0
- package/dist/breadcrumbs.js +135 -0
- package/dist/cli/classify-edges.d.ts +2 -0
- package/dist/cli/classify-edges.js +130 -0
- package/dist/cli/compile-wiki.d.ts +2 -0
- package/dist/cli/compile-wiki.js +173 -0
- package/dist/cli/dump-edges-json.d.ts +2 -0
- package/dist/cli/dump-edges-json.js +21 -0
- package/dist/cli/extract-entities.d.ts +17 -0
- package/dist/cli/extract-entities.js +166 -0
- package/dist/cli/lint-memory.d.ts +16 -0
- package/dist/cli/lint-memory.js +94 -0
- package/dist/cli/migrate.d.ts +17 -0
- package/dist/cli/migrate.js +146 -0
- package/dist/cli/setup-helpers.d.ts +7 -0
- package/dist/cli/setup-helpers.js +72 -0
- package/dist/cli/setup.d.ts +15 -0
- package/dist/cli/setup.js +95 -0
- package/dist/compiler/clustering.d.ts +29 -0
- package/dist/compiler/clustering.js +66 -0
- package/dist/compiler/frontmatter.d.ts +35 -0
- package/dist/compiler/frontmatter.js +168 -0
- package/dist/compiler/manifest.d.ts +32 -0
- package/dist/compiler/manifest.js +82 -0
- package/dist/compiler/prompts.d.ts +17 -0
- package/dist/compiler/prompts.js +82 -0
- package/dist/compiler/run.d.ts +52 -0
- package/dist/compiler/run.js +186 -0
- package/dist/compiler/tier.d.ts +10 -0
- package/dist/compiler/tier.js +85 -0
- package/dist/compiler/topic.d.ts +16 -0
- package/dist/compiler/topic.js +105 -0
- package/dist/compiler/types.d.ts +101 -0
- package/dist/compiler/types.js +4 -0
- package/dist/db.d.ts +10 -0
- package/dist/db.js +46 -0
- package/dist/edges/candidate-pairs.d.ts +24 -0
- package/dist/edges/candidate-pairs.js +35 -0
- package/dist/edges/classifier.d.ts +45 -0
- package/dist/edges/classifier.js +172 -0
- package/dist/edges/signals.d.ts +13 -0
- package/dist/edges/signals.js +45 -0
- package/dist/edges/stage1-haiku.d.ts +21 -0
- package/dist/edges/stage1-haiku.js +33 -0
- package/dist/edges/stage2-opus.d.ts +41 -0
- package/dist/edges/stage2-opus.js +101 -0
- package/dist/edges/state.d.ts +44 -0
- package/dist/edges/state.js +79 -0
- package/dist/edges/types.d.ts +20 -0
- package/dist/edges/types.js +1 -0
- package/dist/embeddings.d.ts +13 -0
- package/dist/embeddings.js +54 -0
- package/dist/entities/db.d.ts +49 -0
- package/dist/entities/db.js +109 -0
- package/dist/entities/extractor.d.ts +14 -0
- package/dist/entities/extractor.js +154 -0
- package/dist/entities/normalize.d.ts +5 -0
- package/dist/entities/normalize.js +7 -0
- package/dist/entities/prompt.d.ts +19 -0
- package/dist/entities/prompt.js +100 -0
- package/dist/entities/state.d.ts +44 -0
- package/dist/entities/state.js +99 -0
- package/dist/entities/types.d.ts +62 -0
- package/dist/entities/types.js +6 -0
- package/dist/env.d.ts +13 -0
- package/dist/env.js +32 -0
- package/dist/fingerprint.d.ts +2 -0
- package/dist/fingerprint.js +12 -0
- package/dist/graph-rebuild.d.ts +6 -0
- package/dist/graph-rebuild.js +20 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +403 -0
- package/dist/instrumentation.d.ts +10 -0
- package/dist/instrumentation.js +37 -0
- package/dist/lint/checks/contradictions.d.ts +30 -0
- package/dist/lint/checks/contradictions.js +52 -0
- package/dist/lint/checks/drift.d.ts +5 -0
- package/dist/lint/checks/drift.js +34 -0
- package/dist/lint/checks/orphans.d.ts +5 -0
- package/dist/lint/checks/orphans.js +25 -0
- package/dist/lint/checks/stale.d.ts +6 -0
- package/dist/lint/checks/stale.js +29 -0
- package/dist/lint/checks/superseded-unflagged.d.ts +5 -0
- package/dist/lint/checks/superseded-unflagged.js +47 -0
- package/dist/lint/run.d.ts +11 -0
- package/dist/lint/run.js +95 -0
- package/dist/lint/types.d.ts +60 -0
- package/dist/lint/types.js +13 -0
- package/dist/mcp-response.d.ts +7 -0
- package/dist/mcp-response.js +13 -0
- package/dist/providers/anthropic.d.ts +13 -0
- package/dist/providers/anthropic.js +56 -0
- package/dist/providers/claude-code.d.ts +35 -0
- package/dist/providers/claude-code.js +175 -0
- package/dist/providers/errors.d.ts +12 -0
- package/dist/providers/errors.js +19 -0
- package/dist/providers/index.d.ts +30 -0
- package/dist/providers/index.js +71 -0
- package/dist/providers/openrouter.d.ts +19 -0
- package/dist/providers/openrouter.js +76 -0
- package/dist/providers/semaphore.d.ts +19 -0
- package/dist/providers/semaphore.js +51 -0
- package/dist/providers/types.d.ts +27 -0
- package/dist/providers/types.js +7 -0
- package/dist/schema.sql +116 -0
- package/dist/server-instructions.d.ts +9 -0
- package/dist/server-instructions.js +20 -0
- package/dist/telemetry.d.ts +39 -0
- package/dist/telemetry.js +130 -0
- package/dist/tools/classify.d.ts +44 -0
- package/dist/tools/classify.js +121 -0
- package/dist/tools/compile.d.ts +31 -0
- package/dist/tools/compile.js +132 -0
- package/dist/tools/dump-edges-sidecar.d.ts +37 -0
- package/dist/tools/dump-edges-sidecar.js +80 -0
- package/dist/tools/extract-entities.d.ts +53 -0
- package/dist/tools/extract-entities.js +169 -0
- package/dist/tools/lint.d.ts +10 -0
- package/dist/tools/lint.js +13 -0
- package/dist/tools/meditate.d.ts +25 -0
- package/dist/tools/meditate.js +128 -0
- package/dist/tools/recall.d.ts +66 -0
- package/dist/tools/recall.js +409 -0
- package/dist/tools/reject.d.ts +10 -0
- package/dist/tools/reject.js +24 -0
- package/dist/tools/remember.d.ts +26 -0
- package/dist/tools/remember.js +140 -0
- package/dist/tools/search.d.ts +30 -0
- package/dist/tools/search.js +69 -0
- package/dist/tools/spawn-cli.d.ts +14 -0
- package/dist/tools/spawn-cli.js +41 -0
- package/dist/tools/stats.d.ts +31 -0
- package/dist/tools/stats.js +88 -0
- package/package.json +86 -0
- 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>;
|