@rarusoft/dendrite-wiki 0.1.0-alpha.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/README.md +79 -0
- package/dist/api-extractor/extract.js +269 -0
- package/dist/api-extractor/language-extractor.js +15 -0
- package/dist/api-extractor/python-extractor.js +358 -0
- package/dist/api-extractor/render.js +195 -0
- package/dist/api-extractor/tree-sitter-extractor.js +1079 -0
- package/dist/api-extractor/types.js +11 -0
- package/dist/api-extractor/typescript-extractor.js +50 -0
- package/dist/api-extractor/walk.js +178 -0
- package/dist/api-reference.js +438 -0
- package/dist/benchmark-events.js +129 -0
- package/dist/benchmark.js +270 -0
- package/dist/binder-export.js +381 -0
- package/dist/canonical-target.js +168 -0
- package/dist/chart-insert.js +377 -0
- package/dist/chart-prompts.js +414 -0
- package/dist/context-cache.js +98 -0
- package/dist/contradicts-shipped-memory.js +232 -0
- package/dist/diff-context.js +142 -0
- package/dist/doctor.js +220 -0
- package/dist/generated-docs.js +219 -0
- package/dist/i18n.js +71 -0
- package/dist/index.js +49 -0
- package/dist/librarian.js +255 -0
- package/dist/maintenance-actions.js +244 -0
- package/dist/maintenance-inbox.js +842 -0
- package/dist/maintenance-runner.js +62 -0
- package/dist/page-drift.js +225 -0
- package/dist/page-inbox.js +168 -0
- package/dist/report-export.js +339 -0
- package/dist/review-bridge.js +1386 -0
- package/dist/search-index.js +199 -0
- package/dist/store.js +1617 -0
- package/dist/telemetry-defaults.js +44 -0
- package/dist/telemetry-report.js +263 -0
- package/dist/telemetry.js +544 -0
- package/dist/wiki-synthesis.js +901 -0
- package/package.json +35 -0
- package/src/api-extractor/extract.ts +333 -0
- package/src/api-extractor/language-extractor.ts +37 -0
- package/src/api-extractor/python-extractor.ts +380 -0
- package/src/api-extractor/render.ts +267 -0
- package/src/api-extractor/tree-sitter-extractor.ts +1210 -0
- package/src/api-extractor/types.ts +41 -0
- package/src/api-extractor/typescript-extractor.ts +56 -0
- package/src/api-extractor/walk.ts +209 -0
- package/src/api-reference.ts +552 -0
- package/src/benchmark-events.ts +216 -0
- package/src/benchmark.ts +376 -0
- package/src/binder-export.ts +437 -0
- package/src/canonical-target.ts +192 -0
- package/src/chart-insert.ts +478 -0
- package/src/chart-prompts.ts +417 -0
- package/src/context-cache.ts +129 -0
- package/src/contradicts-shipped-memory.ts +311 -0
- package/src/diff-context.ts +187 -0
- package/src/doctor.ts +260 -0
- package/src/generated-docs.ts +316 -0
- package/src/i18n.ts +106 -0
- package/src/index.ts +59 -0
- package/src/librarian.ts +331 -0
- package/src/maintenance-actions.ts +314 -0
- package/src/maintenance-inbox.ts +1132 -0
- package/src/maintenance-runner.ts +85 -0
- package/src/page-drift.ts +292 -0
- package/src/page-inbox.ts +254 -0
- package/src/report-export.ts +392 -0
- package/src/review-bridge.ts +1729 -0
- package/src/search-index.ts +266 -0
- package/src/store.ts +2171 -0
- package/src/telemetry-defaults.ts +50 -0
- package/src/telemetry-report.ts +365 -0
- package/src/telemetry.ts +757 -0
- package/src/wiki-synthesis.ts +1307 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-to-Binder export — R6 of the retro-editor experiment.
|
|
3
|
+
*
|
|
4
|
+
* Produces a single self-contained HTML file from selected wiki pages,
|
|
5
|
+
* styled to print well on paper: cover page with project name and
|
|
6
|
+
* timestamp, table of contents, page-break rules between sections, claim
|
|
7
|
+
* and source citations distinguished even in B/W. Open the output in a
|
|
8
|
+
* browser and File → Print → Save as PDF for the binder workflow.
|
|
9
|
+
*
|
|
10
|
+
* Driven by `dendrite-wiki binder:export [--all | --pages a,b,c]
|
|
11
|
+
* [--theme selectric|amber|wordperfect|modern] [--output path]
|
|
12
|
+
* [--title text]`. Default output: `docs/public/binder.html` (gitignored
|
|
13
|
+
* via `docs/public/*.html` patterns the operator may already have).
|
|
14
|
+
*
|
|
15
|
+
* Intentionally does NOT shell out to headless Chrome — Puppeteer adds
|
|
16
|
+
* ~150 MB to the install footprint, and the browser-as-print-engine path
|
|
17
|
+
* works on every machine without any new install. If a future R6.1 wants
|
|
18
|
+
* one-step PDF, it can layer Puppeteer on top of this HTML output.
|
|
19
|
+
*/
|
|
20
|
+
import { promises as fs } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import MarkdownIt from 'markdown-it';
|
|
23
|
+
import { listWikiPages, readWikiPage } from './store.js';
|
|
24
|
+
const DEFAULT_TITLE = 'Dendrite Wiki MCP — Binder';
|
|
25
|
+
export async function exportBinderHtml(options = {}) {
|
|
26
|
+
const root = path.resolve(options.root ?? process.cwd());
|
|
27
|
+
const outputPath = path.resolve(options.outputPath ?? path.join(root, 'docs', 'public', 'binder.html'));
|
|
28
|
+
const theme = options.theme ?? 'selectric';
|
|
29
|
+
const title = options.title ?? DEFAULT_TITLE;
|
|
30
|
+
const allPages = await listWikiPages();
|
|
31
|
+
const allBySlug = new Map(allPages.map((p) => [p.slug, p]));
|
|
32
|
+
let selectedSlugs;
|
|
33
|
+
if (options.all || !options.slugs || options.slugs.length === 0) {
|
|
34
|
+
// Default: every page except generated reference (api/*) — those
|
|
35
|
+
// are noisy in a binder and the operator can opt them in via --pages.
|
|
36
|
+
selectedSlugs = allPages
|
|
37
|
+
.filter((p) => !p.slug.startsWith('api/'))
|
|
38
|
+
.map((p) => p.slug)
|
|
39
|
+
.sort();
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
selectedSlugs = options.slugs;
|
|
43
|
+
}
|
|
44
|
+
const pages = [];
|
|
45
|
+
for (const slug of selectedSlugs) {
|
|
46
|
+
const summary = allBySlug.get(slug);
|
|
47
|
+
if (!summary) {
|
|
48
|
+
throw new Error(`Unknown wiki page slug: ${slug}`);
|
|
49
|
+
}
|
|
50
|
+
const raw = await readWikiPage(slug);
|
|
51
|
+
const stripped = stripFrontmatter(raw);
|
|
52
|
+
const html = renderMarkdown(stripped);
|
|
53
|
+
pages.push({ slug, title: summary.title, html });
|
|
54
|
+
}
|
|
55
|
+
const html = renderBinderHtml({ title, theme, pages });
|
|
56
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
57
|
+
await fs.writeFile(outputPath, html, 'utf8');
|
|
58
|
+
const stat = await fs.stat(outputPath);
|
|
59
|
+
return {
|
|
60
|
+
outputPath,
|
|
61
|
+
pageCount: pages.length,
|
|
62
|
+
bytesWritten: stat.size,
|
|
63
|
+
pages: pages.map(({ slug, title: t }) => ({ slug, title: t })),
|
|
64
|
+
theme
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function stripFrontmatter(content) {
|
|
68
|
+
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);
|
|
69
|
+
return match ? content.slice(match[0].length) : content;
|
|
70
|
+
}
|
|
71
|
+
function renderMarkdown(markdown) {
|
|
72
|
+
const md = new MarkdownIt({
|
|
73
|
+
html: false,
|
|
74
|
+
linkify: true,
|
|
75
|
+
typographer: true,
|
|
76
|
+
breaks: false
|
|
77
|
+
});
|
|
78
|
+
return md.render(markdown);
|
|
79
|
+
}
|
|
80
|
+
function renderBinderHtml({ title, theme, pages }) {
|
|
81
|
+
const generatedAt = new Date();
|
|
82
|
+
const generatedHuman = generatedAt.toLocaleString('en-US', {
|
|
83
|
+
weekday: 'long',
|
|
84
|
+
year: 'numeric',
|
|
85
|
+
month: 'long',
|
|
86
|
+
day: 'numeric',
|
|
87
|
+
hour: 'numeric',
|
|
88
|
+
minute: '2-digit'
|
|
89
|
+
});
|
|
90
|
+
const generatedIso = generatedAt.toISOString();
|
|
91
|
+
const palette = themePalette(theme);
|
|
92
|
+
const tocItems = pages
|
|
93
|
+
.map((p, idx) => ` <li><a href="#page-${idx + 1}"><span class="toc-num">${String(idx + 1).padStart(2, '0')}</span> <span class="toc-title">${escapeHtml(p.title)}</span> <span class="toc-slug">${escapeHtml(p.slug)}</span></a></li>`)
|
|
94
|
+
.join('\n');
|
|
95
|
+
const sections = pages
|
|
96
|
+
.map((p, idx) => ` <section class="binder-page" id="page-${idx + 1}">
|
|
97
|
+
<header class="binder-page-header">
|
|
98
|
+
<span class="binder-page-num">PAGE ${String(idx + 1).padStart(2, '0')} OF ${String(pages.length).padStart(2, '0')}</span>
|
|
99
|
+
<span class="binder-page-slug">${escapeHtml(p.slug)}.md</span>
|
|
100
|
+
</header>
|
|
101
|
+
<div class="binder-page-body">
|
|
102
|
+
${p.html}
|
|
103
|
+
</div>
|
|
104
|
+
</section>`)
|
|
105
|
+
.join('\n');
|
|
106
|
+
return `<!DOCTYPE html>
|
|
107
|
+
<html lang="en">
|
|
108
|
+
<head>
|
|
109
|
+
<meta charset="utf-8">
|
|
110
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
111
|
+
<title>${escapeHtml(title)}</title>
|
|
112
|
+
<style>
|
|
113
|
+
${binderStyles(palette)}
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body data-theme="${theme}">
|
|
117
|
+
<main class="binder">
|
|
118
|
+
<section class="binder-cover">
|
|
119
|
+
<div class="binder-cover-frame">
|
|
120
|
+
<p class="binder-cover-eyebrow">Dendrite Wiki MCP</p>
|
|
121
|
+
<h1 class="binder-cover-title">${escapeHtml(title)}</h1>
|
|
122
|
+
<p class="binder-cover-meta">
|
|
123
|
+
Compiled <strong>${escapeHtml(generatedHuman)}</strong>
|
|
124
|
+
· ${pages.length} page${pages.length === 1 ? '' : 's'}
|
|
125
|
+
· theme: <em>${escapeHtml(theme)}</em>
|
|
126
|
+
</p>
|
|
127
|
+
<p class="binder-cover-iso"><code>${escapeHtml(generatedIso)}</code></p>
|
|
128
|
+
</div>
|
|
129
|
+
</section>
|
|
130
|
+
|
|
131
|
+
<section class="binder-toc">
|
|
132
|
+
<h2 class="binder-toc-title">Contents</h2>
|
|
133
|
+
<ol class="binder-toc-list">
|
|
134
|
+
${tocItems}
|
|
135
|
+
</ol>
|
|
136
|
+
</section>
|
|
137
|
+
|
|
138
|
+
${sections}
|
|
139
|
+
|
|
140
|
+
<footer class="binder-foot">
|
|
141
|
+
<p>Generated by <strong>Dendrite Wiki MCP</strong> · binder:export · ${escapeHtml(theme)} theme · ${escapeHtml(generatedIso)}</p>
|
|
142
|
+
</footer>
|
|
143
|
+
</main>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
function themePalette(theme) {
|
|
149
|
+
switch (theme) {
|
|
150
|
+
case 'amber':
|
|
151
|
+
return {
|
|
152
|
+
bg: '#150a00',
|
|
153
|
+
fg: '#ffb000',
|
|
154
|
+
accent: '#ffd166',
|
|
155
|
+
muted: '#a06900',
|
|
156
|
+
divider: '#4a2a00',
|
|
157
|
+
bgAlt: '#1f0e00',
|
|
158
|
+
fontBody: "'VT323', 'Courier New', monospace",
|
|
159
|
+
fontMono: "'VT323', 'Courier New', monospace",
|
|
160
|
+
bodyJustify: false,
|
|
161
|
+
uppercaseHeadings: true
|
|
162
|
+
};
|
|
163
|
+
case 'wordperfect':
|
|
164
|
+
return {
|
|
165
|
+
bg: '#0000aa',
|
|
166
|
+
fg: '#f0f0f0',
|
|
167
|
+
accent: '#ffff55',
|
|
168
|
+
muted: '#a0a0c0',
|
|
169
|
+
divider: '#5555cc',
|
|
170
|
+
bgAlt: '#00008b',
|
|
171
|
+
fontBody: "'IBM Plex Mono', 'Courier New', monospace",
|
|
172
|
+
fontMono: "'IBM Plex Mono', 'Courier New', monospace",
|
|
173
|
+
bodyJustify: false,
|
|
174
|
+
uppercaseHeadings: false
|
|
175
|
+
};
|
|
176
|
+
case 'modern':
|
|
177
|
+
return {
|
|
178
|
+
bg: '#ffffff',
|
|
179
|
+
fg: '#1a1a1a',
|
|
180
|
+
accent: '#1f6feb',
|
|
181
|
+
muted: '#666666',
|
|
182
|
+
divider: '#e6e6e6',
|
|
183
|
+
bgAlt: '#fafafa',
|
|
184
|
+
fontBody: "'Inter', system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif",
|
|
185
|
+
fontMono: "'JetBrains Mono', 'Consolas', monospace",
|
|
186
|
+
bodyJustify: false,
|
|
187
|
+
uppercaseHeadings: false
|
|
188
|
+
};
|
|
189
|
+
case 'selectric':
|
|
190
|
+
default:
|
|
191
|
+
return {
|
|
192
|
+
bg: '#f5f0e1',
|
|
193
|
+
fg: '#1a1410',
|
|
194
|
+
accent: '#8b1a1a',
|
|
195
|
+
muted: '#5e4f40',
|
|
196
|
+
divider: '#c4b89e',
|
|
197
|
+
bgAlt: '#ede6d2',
|
|
198
|
+
fontBody: "'Special Elite', 'Cutive Mono', 'Courier New', monospace",
|
|
199
|
+
fontMono: "'Cutive Mono', 'Courier New', monospace",
|
|
200
|
+
bodyJustify: true,
|
|
201
|
+
uppercaseHeadings: true
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function binderStyles(p) {
|
|
206
|
+
return `
|
|
207
|
+
@import url('https://fonts.googleapis.com/css2?family=VT323&family=IBM+Plex+Mono:wght@400;500;600;700&family=Special+Elite&family=Cutive+Mono&family=Inter:wght@400;500;600;700&display=swap');
|
|
208
|
+
|
|
209
|
+
:root {
|
|
210
|
+
--bg: ${p.bg};
|
|
211
|
+
--fg: ${p.fg};
|
|
212
|
+
--accent: ${p.accent};
|
|
213
|
+
--muted: ${p.muted};
|
|
214
|
+
--divider: ${p.divider};
|
|
215
|
+
--bg-alt: ${p.bgAlt};
|
|
216
|
+
--font-body: ${p.fontBody};
|
|
217
|
+
--font-mono: ${p.fontMono};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
* { box-sizing: border-box; }
|
|
221
|
+
|
|
222
|
+
html, body {
|
|
223
|
+
margin: 0;
|
|
224
|
+
padding: 0;
|
|
225
|
+
background: var(--bg);
|
|
226
|
+
color: var(--fg);
|
|
227
|
+
font-family: var(--font-body);
|
|
228
|
+
line-height: 1.6;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.binder {
|
|
232
|
+
max-width: 7.5in;
|
|
233
|
+
margin: 0 auto;
|
|
234
|
+
padding: 0.6in 0.75in;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
238
|
+
code { font-family: var(--font-mono); background: var(--bg-alt); padding: 0.05rem 0.3rem; border-radius: 2px; font-size: 0.92em; }
|
|
239
|
+
pre { background: var(--bg-alt); border: 1px solid var(--divider); padding: 0.6rem 0.8rem; overflow-x: auto; border-radius: 3px; font-size: 0.85em; line-height: 1.5; }
|
|
240
|
+
pre code { background: transparent; padding: 0; }
|
|
241
|
+
blockquote { border-left: 3px solid var(--accent); margin: 1em 0; padding: 0.2em 1em; color: var(--muted); font-style: italic; }
|
|
242
|
+
hr { border: none; border-top: 1px solid var(--divider); margin: 1.5em 0; }
|
|
243
|
+
table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 0.92em; }
|
|
244
|
+
th, td { border: 1px solid var(--divider); padding: 0.4em 0.6em; text-align: left; }
|
|
245
|
+
th { background: var(--bg-alt); }
|
|
246
|
+
|
|
247
|
+
h1, h2, h3, h4, h5, h6 {
|
|
248
|
+
font-family: var(--font-body);
|
|
249
|
+
color: var(--fg);
|
|
250
|
+
${p.uppercaseHeadings ? 'text-transform: uppercase; letter-spacing: 0.06em; font-weight: 400;' : 'font-weight: 700;'}
|
|
251
|
+
}
|
|
252
|
+
h1 { font-size: 1.7em; border-bottom: 2px solid var(--fg); padding-bottom: 0.3em; margin-top: 0; }
|
|
253
|
+
h2 { font-size: 1.3em; border-bottom: 1px solid var(--divider); padding-bottom: 0.2em; margin-top: 1.6em; }
|
|
254
|
+
h3 { font-size: 1.1em; margin-top: 1.4em; }
|
|
255
|
+
|
|
256
|
+
${p.bodyJustify ? '.binder-page-body p, .binder-page-body li { text-align: justify; hyphens: auto; }' : ''}
|
|
257
|
+
|
|
258
|
+
/* Cover */
|
|
259
|
+
.binder-cover {
|
|
260
|
+
min-height: 9in;
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
justify-content: center;
|
|
264
|
+
page-break-after: always;
|
|
265
|
+
border-bottom: 1px solid var(--divider);
|
|
266
|
+
}
|
|
267
|
+
.binder-cover-frame {
|
|
268
|
+
border: 4px double var(--fg);
|
|
269
|
+
padding: 1.4in 1.1in;
|
|
270
|
+
text-align: center;
|
|
271
|
+
max-width: 6in;
|
|
272
|
+
}
|
|
273
|
+
.binder-cover-eyebrow {
|
|
274
|
+
margin: 0 0 0.6em 0;
|
|
275
|
+
font-size: 0.85em;
|
|
276
|
+
letter-spacing: 0.4em;
|
|
277
|
+
text-transform: uppercase;
|
|
278
|
+
color: var(--muted);
|
|
279
|
+
}
|
|
280
|
+
.binder-cover-title {
|
|
281
|
+
font-size: 2.2em;
|
|
282
|
+
margin: 0 0 1em 0;
|
|
283
|
+
border: none;
|
|
284
|
+
padding: 0;
|
|
285
|
+
${p.uppercaseHeadings ? '' : 'font-weight: 700;'}
|
|
286
|
+
}
|
|
287
|
+
.binder-cover-meta {
|
|
288
|
+
margin: 0.4em 0;
|
|
289
|
+
font-size: 0.95em;
|
|
290
|
+
color: var(--muted);
|
|
291
|
+
}
|
|
292
|
+
.binder-cover-meta strong { color: var(--fg); }
|
|
293
|
+
.binder-cover-meta em { color: var(--accent); font-style: normal; }
|
|
294
|
+
.binder-cover-iso {
|
|
295
|
+
margin: 1em 0 0 0;
|
|
296
|
+
font-size: 0.78em;
|
|
297
|
+
color: var(--muted);
|
|
298
|
+
}
|
|
299
|
+
.binder-cover-iso code { background: transparent; }
|
|
300
|
+
|
|
301
|
+
/* TOC */
|
|
302
|
+
.binder-toc {
|
|
303
|
+
page-break-after: always;
|
|
304
|
+
padding: 0 0 0.5in 0;
|
|
305
|
+
}
|
|
306
|
+
.binder-toc-title {
|
|
307
|
+
margin-top: 0;
|
|
308
|
+
}
|
|
309
|
+
.binder-toc-list {
|
|
310
|
+
list-style: none;
|
|
311
|
+
padding: 0;
|
|
312
|
+
margin: 1em 0;
|
|
313
|
+
font-family: var(--font-mono);
|
|
314
|
+
font-size: 0.95em;
|
|
315
|
+
}
|
|
316
|
+
.binder-toc-list li {
|
|
317
|
+
margin: 0.3em 0;
|
|
318
|
+
border-bottom: 1px dotted var(--divider);
|
|
319
|
+
padding: 0.2em 0;
|
|
320
|
+
}
|
|
321
|
+
.binder-toc-list a {
|
|
322
|
+
display: grid;
|
|
323
|
+
grid-template-columns: 2.5em 1fr auto;
|
|
324
|
+
gap: 0.5em;
|
|
325
|
+
text-decoration: none;
|
|
326
|
+
color: var(--fg);
|
|
327
|
+
}
|
|
328
|
+
.binder-toc-list a:hover { color: var(--accent); }
|
|
329
|
+
.toc-num { color: var(--muted); }
|
|
330
|
+
.toc-slug { color: var(--muted); font-size: 0.85em; }
|
|
331
|
+
|
|
332
|
+
/* Pages */
|
|
333
|
+
.binder-page {
|
|
334
|
+
page-break-before: always;
|
|
335
|
+
padding-top: 0.4in;
|
|
336
|
+
}
|
|
337
|
+
.binder-page-header {
|
|
338
|
+
display: flex;
|
|
339
|
+
justify-content: space-between;
|
|
340
|
+
font-family: var(--font-mono);
|
|
341
|
+
font-size: 0.78em;
|
|
342
|
+
letter-spacing: 0.08em;
|
|
343
|
+
color: var(--muted);
|
|
344
|
+
border-bottom: 1px solid var(--divider);
|
|
345
|
+
padding-bottom: 0.4em;
|
|
346
|
+
margin-bottom: 1em;
|
|
347
|
+
}
|
|
348
|
+
.binder-page-body h1 { margin-top: 0.2em; }
|
|
349
|
+
.binder-page-body img { max-width: 100%; }
|
|
350
|
+
|
|
351
|
+
.binder-foot {
|
|
352
|
+
margin-top: 2em;
|
|
353
|
+
padding-top: 1em;
|
|
354
|
+
border-top: 1px solid var(--divider);
|
|
355
|
+
color: var(--muted);
|
|
356
|
+
font-size: 0.78em;
|
|
357
|
+
text-align: center;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Print rules */
|
|
361
|
+
@media print {
|
|
362
|
+
html, body { background: #ffffff !important; color: #000 !important; }
|
|
363
|
+
body[data-theme="amber"], body[data-theme="amber"] * { background: #ffffff !important; color: #000000 !important; }
|
|
364
|
+
body[data-theme="wordperfect"], body[data-theme="wordperfect"] * { background: #ffffff !important; color: #000000 !important; }
|
|
365
|
+
a { color: #000 !important; text-decoration: underline; }
|
|
366
|
+
pre, table { page-break-inside: avoid; }
|
|
367
|
+
h1, h2, h3 { page-break-after: avoid; }
|
|
368
|
+
.binder { padding: 0; max-width: none; }
|
|
369
|
+
.binder-cover { min-height: 95vh; }
|
|
370
|
+
.binder-page { padding-top: 0; }
|
|
371
|
+
}
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
function escapeHtml(value) {
|
|
375
|
+
return value
|
|
376
|
+
.replace(/&/g, '&')
|
|
377
|
+
.replace(/</g, '<')
|
|
378
|
+
.replace(/>/g, '>')
|
|
379
|
+
.replace(/"/g, '"')
|
|
380
|
+
.replace(/'/g, ''');
|
|
381
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WikiCanonicalTarget — the markdown-wiki implementation of `CanonicalTarget`.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 slice B wave 3 of the Library Extraction Roadmap split this file. The
|
|
5
|
+
* `CanonicalTarget` interface itself lives in `@rarusoft/dendrite-memory` so the brain's
|
|
6
|
+
* promotion path is backend-agnostic; this file holds only the wiki-flavored
|
|
7
|
+
* implementation plus the wiki-specific defaults. The constant
|
|
8
|
+
* `DEFAULT_WIKI_PROMOTION_TARGET_SLUG` stays here (wiki-specific) and is also
|
|
9
|
+
* imported by `auto-promote.ts` and `consolidate.ts` for trust gating.
|
|
10
|
+
*
|
|
11
|
+
* The module registers `WikiCanonicalTarget` as the brain's default target at
|
|
12
|
+
* the bottom of this file via a top-level side effect, so any code path that
|
|
13
|
+
* loads the wiki tier (everything that goes through `src/server.ts` → `./store.js`
|
|
14
|
+
* → here) auto-wires the default. Tests that bypass the wiki tier and exercise
|
|
15
|
+
* brain promotion directly must either `setDefaultCanonicalTarget(...)` with a
|
|
16
|
+
* mock or `import './canonical-target.js'` for the side effect.
|
|
17
|
+
*/
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { setDefaultCanonicalTarget } from '@rarusoft/dendrite-memory';
|
|
20
|
+
import { appendProjectLog, listWikiPages, pagePathFromSlug, readWikiPage, writeWikiPage } from './store.js';
|
|
21
|
+
/**
|
|
22
|
+
* Default target id when the records don't suggest one and no caller-supplied id
|
|
23
|
+
* is provided. Wiki-specific.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_WIKI_PROMOTION_TARGET_SLUG = 'architecture';
|
|
26
|
+
/**
|
|
27
|
+
* The markdown-wiki implementation of `CanonicalTarget`. Wraps the existing
|
|
28
|
+
* `readWikiPage` / `writeWikiPage` / `appendProjectLog` plus the markdown
|
|
29
|
+
* formatting rules that used to live inline in `memory-promotion.ts`.
|
|
30
|
+
*/
|
|
31
|
+
export class WikiCanonicalTarget {
|
|
32
|
+
async readContent(targetId) {
|
|
33
|
+
return readWikiPage(targetId).catch(() => '');
|
|
34
|
+
}
|
|
35
|
+
async writeContent(targetId, content) {
|
|
36
|
+
await writeWikiPage(targetId, content);
|
|
37
|
+
}
|
|
38
|
+
async appendChangeLog(entry) {
|
|
39
|
+
await appendProjectLog(entry);
|
|
40
|
+
}
|
|
41
|
+
async listAvailableTargetIds() {
|
|
42
|
+
const pages = await listWikiPages();
|
|
43
|
+
return pages.map((page) => page.slug);
|
|
44
|
+
}
|
|
45
|
+
formatTargetPath(targetId) {
|
|
46
|
+
return `docs/wiki/${targetId}.md`;
|
|
47
|
+
}
|
|
48
|
+
resolveTitle(targetId, currentContent) {
|
|
49
|
+
const fromContent = currentContent.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? '';
|
|
50
|
+
if (fromContent)
|
|
51
|
+
return fromContent;
|
|
52
|
+
// Slug → Title Case fallback. Mirrors the legacy `titleFromSlug` exactly so the
|
|
53
|
+
// preview UI sees the same string before and after the Phase 2 refactor.
|
|
54
|
+
const slugTitle = targetId
|
|
55
|
+
.split('/')
|
|
56
|
+
.pop()
|
|
57
|
+
?.split('-')
|
|
58
|
+
.map((segment) => (segment ? segment[0].toUpperCase() + segment.slice(1) : segment))
|
|
59
|
+
.join(' ');
|
|
60
|
+
return slugTitle ?? path.basename(pagePathFromSlug(targetId), '.md');
|
|
61
|
+
}
|
|
62
|
+
resolveTargetId(records, requestedTargetId) {
|
|
63
|
+
const requested = requestedTargetId?.trim();
|
|
64
|
+
if (requested) {
|
|
65
|
+
return requested;
|
|
66
|
+
}
|
|
67
|
+
// Rank candidate target slugs by how many records mention them in relatedPages.
|
|
68
|
+
// Ties broken alphabetically for deterministic output. Mirrors the legacy
|
|
69
|
+
// `resolvePromotionTargetSlug` exactly.
|
|
70
|
+
const relatedPageCounts = new Map();
|
|
71
|
+
for (const record of records) {
|
|
72
|
+
for (const page of record.relatedPages) {
|
|
73
|
+
relatedPageCounts.set(page, (relatedPageCounts.get(page) ?? 0) + 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const rankedRelatedPage = [...relatedPageCounts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))[0]?.[0];
|
|
77
|
+
if (rankedRelatedPage) {
|
|
78
|
+
return rankedRelatedPage;
|
|
79
|
+
}
|
|
80
|
+
// Second-choice fallback: the first wiki-kinded source slug across all records.
|
|
81
|
+
const wikiSource = records
|
|
82
|
+
.flatMap((record) => record.sources)
|
|
83
|
+
.find((source) => source.kind === 'wiki')?.slug;
|
|
84
|
+
if (wikiSource) {
|
|
85
|
+
return wikiSource;
|
|
86
|
+
}
|
|
87
|
+
// Default to 'architecture' rather than 'project-log' — the project log is for
|
|
88
|
+
// chronological change history, not durable lessons. Architecture is the
|
|
89
|
+
// seeded canonical page in every dendrite-wiki project and is the right
|
|
90
|
+
// fallback for general project facts. The operator can always override by
|
|
91
|
+
// passing requestedTargetId explicitly.
|
|
92
|
+
return DEFAULT_WIKI_PROMOTION_TARGET_SLUG;
|
|
93
|
+
}
|
|
94
|
+
resolveSectionHeading(records) {
|
|
95
|
+
const kinds = new Set(records.map((record) => record.kind));
|
|
96
|
+
if (kinds.size === 1 && kinds.has('warning')) {
|
|
97
|
+
return '## Promoted Warnings';
|
|
98
|
+
}
|
|
99
|
+
if (kinds.size === 1 && kinds.has('handoff')) {
|
|
100
|
+
return '## Promoted Handoff Notes';
|
|
101
|
+
}
|
|
102
|
+
return '## Promoted Lessons';
|
|
103
|
+
}
|
|
104
|
+
formatPromotionBlock(sectionHeading, records) {
|
|
105
|
+
const lines = [sectionHeading, ''];
|
|
106
|
+
for (const record of records) {
|
|
107
|
+
const provenance = this.buildProvenanceLine(record);
|
|
108
|
+
lines.push(`- ${escapeMarkdownForVue(record.text)}`);
|
|
109
|
+
if (provenance) {
|
|
110
|
+
lines.push(` - ${provenance}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return `${lines.join('\n')}\n`;
|
|
114
|
+
}
|
|
115
|
+
composeNewContent(existingContent, proposedText, fallbackTitle) {
|
|
116
|
+
if (existingContent === '') {
|
|
117
|
+
return `# ${fallbackTitle}\n\n${proposedText.trim()}\n`;
|
|
118
|
+
}
|
|
119
|
+
const trimmed = existingContent.replace(/\s+$/g, '');
|
|
120
|
+
return `${trimmed}\n\n${proposedText.trim()}\n`;
|
|
121
|
+
}
|
|
122
|
+
isPromotionAlreadyApplied(existingContent, proposedText) {
|
|
123
|
+
return existingContent.includes(proposedText.trim());
|
|
124
|
+
}
|
|
125
|
+
anchorForHeading(heading) {
|
|
126
|
+
return heading
|
|
127
|
+
.replace(/^#+\s*/, '')
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
130
|
+
.trim()
|
|
131
|
+
.replace(/\s+/g, '-');
|
|
132
|
+
}
|
|
133
|
+
// ─── Wiki-specific internals ──────────────────────────────────────────────
|
|
134
|
+
buildProvenanceLine(record) {
|
|
135
|
+
const segments = [`kind: ${record.kind}`];
|
|
136
|
+
if (record.recallCount > 0) {
|
|
137
|
+
segments.push(`recalled ${record.recallCount}x`);
|
|
138
|
+
}
|
|
139
|
+
if (record.sources.length > 0) {
|
|
140
|
+
segments.push(`Sources: ${record.sources.map((source) => `${source.kind}:${source.slug}`).join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
segments.push('Sources: none');
|
|
144
|
+
}
|
|
145
|
+
return `_Provenance: ${segments.join(' · ')}_`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Factory: build a WikiCanonicalTarget. Mirrors the `createFilesystemMemoryStorage`
|
|
150
|
+
* pattern from Phase 1 so call sites in `memory-promotion.ts`, `auto-promote.ts`,
|
|
151
|
+
* and `consolidate.ts` look uniform.
|
|
152
|
+
*/
|
|
153
|
+
export function createWikiCanonicalTarget() {
|
|
154
|
+
return new WikiCanonicalTarget();
|
|
155
|
+
}
|
|
156
|
+
// VitePress parses every markdown page as a Vue SFC, so any literal `<word>` substring
|
|
157
|
+
// (e.g. `.github/agents/<name>.agent.md` from a memory body) trips the Vue tag parser
|
|
158
|
+
// with "Element is missing end tag" and breaks docs:build. Centralized here as a
|
|
159
|
+
// module-level helper rather than a method because the same rule applies to anything
|
|
160
|
+
// the wiki adapter emits into a VitePress-rendered page.
|
|
161
|
+
function escapeMarkdownForVue(value) {
|
|
162
|
+
return value.replace(/</g, '<').replace(/>/g, '>');
|
|
163
|
+
}
|
|
164
|
+
// Slice B wave 3: register WikiCanonicalTarget as the brain's default at module
|
|
165
|
+
// load. Any code path that imports this file (or any wiki-side module that
|
|
166
|
+
// transitively imports it) auto-wires the DI surface so brain promotion functions
|
|
167
|
+
// resolve to the wiki adapter.
|
|
168
|
+
setDefaultCanonicalTarget(createWikiCanonicalTarget());
|