@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.
Files changed (74) hide show
  1. package/README.md +79 -0
  2. package/dist/api-extractor/extract.js +269 -0
  3. package/dist/api-extractor/language-extractor.js +15 -0
  4. package/dist/api-extractor/python-extractor.js +358 -0
  5. package/dist/api-extractor/render.js +195 -0
  6. package/dist/api-extractor/tree-sitter-extractor.js +1079 -0
  7. package/dist/api-extractor/types.js +11 -0
  8. package/dist/api-extractor/typescript-extractor.js +50 -0
  9. package/dist/api-extractor/walk.js +178 -0
  10. package/dist/api-reference.js +438 -0
  11. package/dist/benchmark-events.js +129 -0
  12. package/dist/benchmark.js +270 -0
  13. package/dist/binder-export.js +381 -0
  14. package/dist/canonical-target.js +168 -0
  15. package/dist/chart-insert.js +377 -0
  16. package/dist/chart-prompts.js +414 -0
  17. package/dist/context-cache.js +98 -0
  18. package/dist/contradicts-shipped-memory.js +232 -0
  19. package/dist/diff-context.js +142 -0
  20. package/dist/doctor.js +220 -0
  21. package/dist/generated-docs.js +219 -0
  22. package/dist/i18n.js +71 -0
  23. package/dist/index.js +49 -0
  24. package/dist/librarian.js +255 -0
  25. package/dist/maintenance-actions.js +244 -0
  26. package/dist/maintenance-inbox.js +842 -0
  27. package/dist/maintenance-runner.js +62 -0
  28. package/dist/page-drift.js +225 -0
  29. package/dist/page-inbox.js +168 -0
  30. package/dist/report-export.js +339 -0
  31. package/dist/review-bridge.js +1386 -0
  32. package/dist/search-index.js +199 -0
  33. package/dist/store.js +1617 -0
  34. package/dist/telemetry-defaults.js +44 -0
  35. package/dist/telemetry-report.js +263 -0
  36. package/dist/telemetry.js +544 -0
  37. package/dist/wiki-synthesis.js +901 -0
  38. package/package.json +35 -0
  39. package/src/api-extractor/extract.ts +333 -0
  40. package/src/api-extractor/language-extractor.ts +37 -0
  41. package/src/api-extractor/python-extractor.ts +380 -0
  42. package/src/api-extractor/render.ts +267 -0
  43. package/src/api-extractor/tree-sitter-extractor.ts +1210 -0
  44. package/src/api-extractor/types.ts +41 -0
  45. package/src/api-extractor/typescript-extractor.ts +56 -0
  46. package/src/api-extractor/walk.ts +209 -0
  47. package/src/api-reference.ts +552 -0
  48. package/src/benchmark-events.ts +216 -0
  49. package/src/benchmark.ts +376 -0
  50. package/src/binder-export.ts +437 -0
  51. package/src/canonical-target.ts +192 -0
  52. package/src/chart-insert.ts +478 -0
  53. package/src/chart-prompts.ts +417 -0
  54. package/src/context-cache.ts +129 -0
  55. package/src/contradicts-shipped-memory.ts +311 -0
  56. package/src/diff-context.ts +187 -0
  57. package/src/doctor.ts +260 -0
  58. package/src/generated-docs.ts +316 -0
  59. package/src/i18n.ts +106 -0
  60. package/src/index.ts +59 -0
  61. package/src/librarian.ts +331 -0
  62. package/src/maintenance-actions.ts +314 -0
  63. package/src/maintenance-inbox.ts +1132 -0
  64. package/src/maintenance-runner.ts +85 -0
  65. package/src/page-drift.ts +292 -0
  66. package/src/page-inbox.ts +254 -0
  67. package/src/report-export.ts +392 -0
  68. package/src/review-bridge.ts +1729 -0
  69. package/src/search-index.ts +266 -0
  70. package/src/store.ts +2171 -0
  71. package/src/telemetry-defaults.ts +50 -0
  72. package/src/telemetry-report.ts +365 -0
  73. package/src/telemetry.ts +757 -0
  74. package/src/wiki-synthesis.ts +1307 -0
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ // Public surface of @rarusoft/dendrite-wiki.
2
+ //
3
+ // Phase 4 slice D wave 2 of the Library Extraction Roadmap. The markdown-wiki
4
+ // adapter for @rarusoft/dendrite-memory: implements `CanonicalTarget` against
5
+ // VitePress-rendered `docs/wiki/`, owns the wiki page store + lint + search +
6
+ // synthesis + maintenance review surface + browser-side review bridge.
7
+ //
8
+ // The canonical-target.ts module has a top-level side effect that registers
9
+ // `WikiCanonicalTarget` as the brain's default canonical target — any consumer
10
+ // that imports from `@rarusoft/dendrite-wiki` therefore auto-wires the wiki adapter for
11
+ // brain promotion functions.
12
+
13
+ // canonical-target.ts MUST be re-exported first so its top-level
14
+ // setDefaultCanonicalTarget side effect fires before anything else.
15
+ export * from './canonical-target.js';
16
+
17
+ // Core wiki page store + search + lint + context briefing surface.
18
+ export * from './store.js';
19
+ export * from './search-index.js';
20
+ export * from './context-cache.js';
21
+
22
+ // Maintenance review surface.
23
+ export * from './maintenance-actions.js';
24
+ export * from './maintenance-inbox.js';
25
+ export * from './maintenance-runner.js';
26
+
27
+ // Wiki page drift + contradicts-shipped-memory lint surface.
28
+ export * from './page-drift.js';
29
+ export * from './contradicts-shipped-memory.js';
30
+
31
+ // Per-page inbox + librarian audit (the multi-category maintenance aggregator).
32
+ export * from './page-inbox.js';
33
+ export * from './librarian.js';
34
+
35
+ // Browser-side review bridge.
36
+ export * from './review-bridge.js';
37
+
38
+ // API reference generator + chart insertion + chart prompts.
39
+ export * from './api-reference.js';
40
+ export * from './chart-insert.js';
41
+ export * from './chart-prompts.js';
42
+
43
+ // Synthesis provider (LLM-assisted wiki narration).
44
+ export * from './wiki-synthesis.js';
45
+
46
+ // Telemetry + benchmark + report/binder exports + doctor + diff-context.
47
+ export * from './telemetry.js';
48
+ export * from './telemetry-defaults.js';
49
+ export * from './telemetry-report.js';
50
+ export * from './benchmark.js';
51
+ export * from './benchmark-events.js';
52
+ export * from './report-export.js';
53
+ export * from './binder-export.js';
54
+ export * from './doctor.js';
55
+ export * from './diff-context.js';
56
+ export * from './generated-docs.js';
57
+
58
+ // i18n translation table.
59
+ export * from './i18n.js';
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Wiki Librarian audit — one-shot maintenance briefing for an agent that's been told
3
+ * "organize the wiki".
4
+ *
5
+ * Aggregates every open maintenance signal (lint findings, page-drift, contradicts-
6
+ * shipped-memory, promotion-ready memories) into a single structured payload with
7
+ * pre-gathered evidence and a per-item `recommendedAction` sentence. The agent reads
8
+ * this once, plans across categories, and then acts using the existing tool surface
9
+ * (`memory_promote`, `wiki_write`, `memory_forget`, etc.). Every change still flows
10
+ * through the audited write paths, so the operator's safety story stays exactly what
11
+ * it was for manual edits: git diff + project-log entry per change.
12
+ *
13
+ * This module is deliberately a projection — it doesn't write anything itself, it just
14
+ * gathers the evidence an LLM needs to make good organizing decisions in one tool call
15
+ * instead of forcing it to chain a dozen exploratory reads.
16
+ */
17
+ import {
18
+ detectContradictsShippedMemory,
19
+ type ContradictionSignal
20
+ } from './contradicts-shipped-memory.js';
21
+ // Side-effect import: registers WikiCanonicalTarget on the brain DI surface.
22
+ import './canonical-target.js';
23
+ import {
24
+ listProjectMemories,
25
+ previewProjectMemoryPromotion,
26
+ resolvePromotionTargetSlug,
27
+ reviewProjectMemories,
28
+ type ProjectMemoryRecord
29
+ } from '@rarusoft/dendrite-memory';
30
+ import {
31
+ detectPageDrift,
32
+ extractPageIntent,
33
+ extractRecentEntriesMentioningPage
34
+ } from './page-drift.js';
35
+ import { lintWikiPages, pagePathFromSlug, readWikiPage, type WikiLintFinding, type WikiLintRule } from './store.js';
36
+ import { promises as fs } from 'node:fs';
37
+
38
+ export type LibrarianCategory =
39
+ | 'page-drift'
40
+ | 'contradicts-shipped-memory'
41
+ | 'promotion-ready'
42
+ | 'unsupported-claim'
43
+ | 'stale-claim'
44
+ | 'orphan-page'
45
+ | 'missing-h1'
46
+ | 'missing-summary'
47
+ | 'other-lint';
48
+
49
+ export interface LibrarianAuditItem {
50
+ category: LibrarianCategory;
51
+ /** Wiki page slug this item targets (omitted for items not tied to a single page). */
52
+ slug?: string;
53
+ /** Short one-line description of what's wrong / pending. */
54
+ summary: string;
55
+ /** Pre-gathered evidence — the data the agent needs to act without further reads. */
56
+ evidence: Record<string, unknown>;
57
+ /** A natural-language instruction the agent should follow to resolve this item. */
58
+ recommendedAction: string;
59
+ /** MCP tools the agent should call to enact `recommendedAction`. */
60
+ recommendedTools: string[];
61
+ }
62
+
63
+ export interface LibrarianAudit {
64
+ totalItems: number;
65
+ byCategory: Record<LibrarianCategory, number>;
66
+ items: LibrarianAuditItem[];
67
+ playbook: string;
68
+ }
69
+
70
+ export interface BuildLibrarianAuditOptions {
71
+ /** Cap on items per category (default 25). */
72
+ maxPerCategory?: number;
73
+ /** When provided, restricts the audit to these categories only. */
74
+ categories?: LibrarianCategory[];
75
+ }
76
+
77
+ const DEFAULT_MAX_PER_CATEGORY = 25;
78
+
79
+ const PLAYBOOK_TEXT = [
80
+ 'Librarian mode: work down this list category by category, highest-leverage first.',
81
+ '',
82
+ '1. promotion-ready: call memory_promote(memoryIds, mode="apply", targetPage) for each item — applies the memory text into the target page and marks the memory superseded.',
83
+ '2. contradicts-shipped-memory: read the page with wiki_read, study the contradicting memories in evidence.contradictingMemoryIds, then rewrite the offending section with wiki_write so the prose matches shipped reality. Add `contradicts-shipped-memory: ignore` to frontmatter if the negation is intentional design language.',
84
+ '3. page-drift: read the page intent + recent activity in the evidence block, then rewrite the first paragraph with wiki_write so it reflects what the page is now about. If the drift is healthy (e.g., a roadmap mostly delivered), the new paragraph should say so.',
85
+ '4. unsupported-claim / stale-claim: read the page, either attach a source citation or mark the claim status, then wiki_write the updated page.',
86
+ '5. orphan-page / missing-h1 / missing-summary: structural fixes — add an H1, add a summary paragraph, or link the page from a canonical surface.',
87
+ '',
88
+ 'Every wiki_write call goes through the audited path — project-log entry is appended automatically and git diff is the operator review surface. Memory promotions mark the source memory superseded in the same operation.'
89
+ ].join('\n');
90
+
91
+ export async function buildLibrarianAudit(options: BuildLibrarianAuditOptions = {}): Promise<LibrarianAudit> {
92
+ const maxPerCategory = options.maxPerCategory ?? DEFAULT_MAX_PER_CATEGORY;
93
+ const allowCategory = (cat: LibrarianCategory): boolean =>
94
+ !options.categories || options.categories.includes(cat);
95
+
96
+ const [lintFindings, memoryReview, allMemories, projectLogContent] = await Promise.all([
97
+ lintWikiPages(),
98
+ reviewProjectMemories(),
99
+ listProjectMemories({ includeArchived: true }),
100
+ fs.readFile(pagePathFromSlug('project-log'), 'utf8').catch(() => '')
101
+ ]);
102
+
103
+ const activeMemories = allMemories.filter(
104
+ (record) => record.status === 'active' || record.status === 'superseded'
105
+ );
106
+ const memoriesById = new Map(allMemories.map((record) => [record.id, record]));
107
+
108
+ const items: LibrarianAuditItem[] = [];
109
+ const counts: Record<LibrarianCategory, number> = {
110
+ 'page-drift': 0,
111
+ 'contradicts-shipped-memory': 0,
112
+ 'promotion-ready': 0,
113
+ 'unsupported-claim': 0,
114
+ 'stale-claim': 0,
115
+ 'orphan-page': 0,
116
+ 'missing-h1': 0,
117
+ 'missing-summary': 0,
118
+ 'other-lint': 0
119
+ };
120
+
121
+ // 1) Promotion-ready memories — highest leverage, fully deterministic apply path.
122
+ if (allowCategory('promotion-ready')) {
123
+ for (const finding of memoryReview.findings) {
124
+ if (finding.kind !== 'promotion-ready') continue;
125
+ if (counts['promotion-ready'] >= maxPerCategory) break;
126
+ const targetSlug = resolvePromotionTargetSlug(finding.records);
127
+ let proposedTextPreview = '';
128
+ let proposedHeading = '## Promoted Lessons';
129
+ try {
130
+ const preview = await previewProjectMemoryPromotion(finding.memoryIds, { targetPage: targetSlug });
131
+ proposedTextPreview = truncate(preview.proposedText, 400);
132
+ proposedHeading = preview.sectionHeading;
133
+ } catch {
134
+ // preview may fail if a memory was archived between review and preview — skip evidence.
135
+ }
136
+ items.push({
137
+ category: 'promotion-ready',
138
+ slug: targetSlug,
139
+ summary: finding.summary,
140
+ evidence: {
141
+ memoryIds: finding.memoryIds,
142
+ recallCount: finding.records[0]?.recallCount ?? 0,
143
+ sourceRefs: finding.records.flatMap((record) =>
144
+ record.sources.map((source) => `${source.kind}:${source.slug}`)
145
+ ),
146
+ targetSlug,
147
+ proposedHeading,
148
+ proposedTextPreview
149
+ },
150
+ recommendedAction: `Call memory_promote(memoryIds=${JSON.stringify(finding.memoryIds)}, mode="apply", targetPage="${targetSlug}"). The memory becomes a "Promoted Lessons" bullet on the page and is marked superseded so the inbox stops surfacing it.`,
151
+ recommendedTools: ['memory_promote']
152
+ });
153
+ counts['promotion-ready'] += 1;
154
+ }
155
+ }
156
+
157
+ // 2) Contradicts-shipped-memory + page-drift findings need page-level evidence,
158
+ // so we read each affected page once and produce both kinds of items from it.
159
+ const driftableSlugs = new Set(
160
+ lintFindings
161
+ .filter((finding) =>
162
+ finding.rule === 'contradicts-shipped-memory' || finding.rule === 'page-drift'
163
+ )
164
+ .map((finding) => finding.slug)
165
+ );
166
+
167
+ for (const slug of driftableSlugs) {
168
+ const content = await readWikiPage(slug).catch(() => '');
169
+ if (!content) continue;
170
+
171
+ if (allowCategory('contradicts-shipped-memory') && counts['contradicts-shipped-memory'] < maxPerCategory) {
172
+ const signals = detectContradictsShippedMemory(content, activeMemories, projectLogContent);
173
+ for (const signal of signals) {
174
+ if (counts['contradicts-shipped-memory'] >= maxPerCategory) break;
175
+ const contradictingTexts = signal.contradictingMemoryIds.map((id) => {
176
+ const record = memoriesById.get(id);
177
+ return record ? { id, summary: record.summary, kind: record.kind } : { id };
178
+ });
179
+ items.push({
180
+ category: 'contradicts-shipped-memory',
181
+ slug,
182
+ summary: `${slug}: section "${signal.sectionHeading}" denies a feature that shipped`,
183
+ evidence: {
184
+ sectionHeading: signal.sectionHeading,
185
+ matchedNegation: signal.matchedNegation,
186
+ objectTokens: signal.objectTokens,
187
+ contradictingMemories: contradictingTexts,
188
+ affirmingSnippets: signal.affirmingSnippets
189
+ },
190
+ recommendedAction: `Read the page with wiki_read(slug="${slug}"). Locate the section "${signal.sectionHeading}". Rewrite its prose so the negation "${signal.matchedNegation}" is replaced with current shipped state — the contradicting memories prove the feature exists. Apply with wiki_write. If the negation is genuinely intentional design language (e.g., privacy boundary), instead add \`contradicts-shipped-memory: ignore\` to the page frontmatter.`,
191
+ recommendedTools: ['wiki_read', 'wiki_write']
192
+ });
193
+ counts['contradicts-shipped-memory'] += 1;
194
+ }
195
+ }
196
+
197
+ if (allowCategory('page-drift') && counts['page-drift'] < maxPerCategory) {
198
+ const hasDriftFinding = lintFindings.some(
199
+ (finding) => finding.slug === slug && finding.rule === 'page-drift'
200
+ );
201
+ if (hasDriftFinding) {
202
+ const drift = detectPageDrift(content, slug, projectLogContent);
203
+ const intent = extractPageIntent(content);
204
+ const activityMatch = extractRecentEntriesMentioningPage(projectLogContent, slug, 8, 7);
205
+ items.push({
206
+ category: 'page-drift',
207
+ slug,
208
+ summary: `${slug}: page intent diverged from recent activity (~${drift ? Math.round(drift.similarity * 100) : 0}% overlap across ${activityMatch.distinctDays} day${activityMatch.distinctDays === 1 ? '' : 's'})`,
209
+ evidence: {
210
+ currentIntent: intent,
211
+ recentActivityEntries: activityMatch.entries,
212
+ matchedDistinctDays: activityMatch.distinctDays,
213
+ similarityPercent: drift ? Math.round(drift.similarity * 100) : 0
214
+ },
215
+ recommendedAction: `Read the page with wiki_read(slug="${slug}"). Read the recent activity in evidence.recentActivityEntries. Rewrite the first paragraph (right after the H1) so it reflects what the page is NOW about. If the drift is healthy — e.g., a roadmap that's mostly delivered — the new paragraph should say so explicitly. Apply with wiki_write.`,
216
+ recommendedTools: ['wiki_read', 'wiki_write']
217
+ });
218
+ counts['page-drift'] += 1;
219
+ }
220
+ }
221
+ }
222
+
223
+ // 3) Remaining lint findings — surface in their own categories so the agent can
224
+ // triage them with appropriate care. Skipping ones we already processed above.
225
+ for (const finding of lintFindings) {
226
+ if (finding.rule === 'contradicts-shipped-memory' || finding.rule === 'page-drift') {
227
+ continue;
228
+ }
229
+ const category = mapLintRuleToCategory(finding.rule);
230
+ if (!allowCategory(category)) continue;
231
+ if (counts[category] >= maxPerCategory) continue;
232
+ items.push(buildLintItem(category, finding));
233
+ counts[category] += 1;
234
+ }
235
+
236
+ // Stable sort: promotion-ready first (highest deterministic safety), then drift,
237
+ // then contradicts, then everything else alphabetically by slug.
238
+ const categoryOrder: Record<LibrarianCategory, number> = {
239
+ 'promotion-ready': 0,
240
+ 'contradicts-shipped-memory': 1,
241
+ 'page-drift': 2,
242
+ 'stale-claim': 3,
243
+ 'unsupported-claim': 4,
244
+ 'orphan-page': 5,
245
+ 'missing-h1': 6,
246
+ 'missing-summary': 7,
247
+ 'other-lint': 8
248
+ };
249
+ items.sort((left, right) => {
250
+ const delta = categoryOrder[left.category] - categoryOrder[right.category];
251
+ if (delta !== 0) return delta;
252
+ return (left.slug ?? '').localeCompare(right.slug ?? '');
253
+ });
254
+
255
+ return {
256
+ totalItems: items.length,
257
+ byCategory: counts,
258
+ items,
259
+ playbook: PLAYBOOK_TEXT
260
+ };
261
+ }
262
+
263
+ function mapLintRuleToCategory(rule: WikiLintRule): LibrarianCategory {
264
+ switch (rule) {
265
+ case 'stale-claim':
266
+ return 'stale-claim';
267
+ case 'unsupported-claim':
268
+ return 'unsupported-claim';
269
+ case 'orphan-page':
270
+ return 'orphan-page';
271
+ case 'missing-h1':
272
+ return 'missing-h1';
273
+ case 'missing-summary':
274
+ return 'missing-summary';
275
+ default:
276
+ return 'other-lint';
277
+ }
278
+ }
279
+
280
+ function buildLintItem(category: LibrarianCategory, finding: WikiLintFinding): LibrarianAuditItem {
281
+ const summary = `${finding.slug}: ${finding.rule} — ${finding.message}`;
282
+ let recommendedAction = '';
283
+ const recommendedTools: string[] = [];
284
+
285
+ switch (category) {
286
+ case 'unsupported-claim':
287
+ recommendedAction = `Read the page with wiki_read(slug="${finding.slug}"). Locate the unsupported claim. Either attach a source citation (file, wiki page, decision) and write it back with wiki_write, or downgrade the claim status from [current] to [stale] if it's no longer accurate.`;
288
+ recommendedTools.push('wiki_read', 'wiki_write');
289
+ break;
290
+ case 'stale-claim':
291
+ recommendedAction = `Read the page with wiki_read(slug="${finding.slug}"). Locate the stale claim. Either update it to current truth (and flip status to [current]) or remove it. Apply with wiki_write.`;
292
+ recommendedTools.push('wiki_read', 'wiki_write');
293
+ break;
294
+ case 'orphan-page':
295
+ recommendedAction = `Page is not linked from anywhere. Either link it from a canonical surface (project plan, architecture, an index page) — wiki_read + wiki_write — or, if the page is no longer relevant, archive/delete it.`;
296
+ recommendedTools.push('wiki_read', 'wiki_write');
297
+ break;
298
+ case 'missing-h1':
299
+ recommendedAction = `Add a top-level H1 heading to the page. wiki_read + wiki_write.`;
300
+ recommendedTools.push('wiki_read', 'wiki_write');
301
+ break;
302
+ case 'missing-summary':
303
+ recommendedAction = `Add a short summary paragraph immediately after the H1 explaining what the page is about. wiki_read + wiki_write.`;
304
+ recommendedTools.push('wiki_read', 'wiki_write');
305
+ break;
306
+ default:
307
+ recommendedAction = `Open the finding in the central Review Board — this lint rule has a specialized action there (snooze, archive guidance, insert H1, etc.). Or read the page and resolve manually with wiki_write.`;
308
+ recommendedTools.push('wiki_read', 'wiki_write');
309
+ }
310
+
311
+ return {
312
+ category,
313
+ slug: finding.slug,
314
+ summary,
315
+ evidence: {
316
+ rule: finding.rule,
317
+ message: finding.message,
318
+ path: finding.path
319
+ },
320
+ recommendedAction,
321
+ recommendedTools
322
+ };
323
+ }
324
+
325
+ function truncate(text: string, max: number): string {
326
+ if (text.length <= max) return text;
327
+ return `${text.slice(0, max - 1).trimEnd()}…`;
328
+ }
329
+
330
+ // Re-exported for tests so they can assert the shape of pre-gathered evidence.
331
+ export type { ProjectMemoryRecord, ContradictionSignal };