@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/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@rarusoft/dendrite-wiki",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "The markdown-wiki adapter for @rarusoft/dendrite-memory: implements the CanonicalTarget interface against VitePress-rendered docs/wiki/, owns the wiki page store + lint + search + synthesis + maintenance review surface + browser-side review bridge. Pair with @rarusoft/dendrite-memory for the full Dendrite living-wiki experience.",
5
+ "type": "module",
6
+ "private": false,
7
+ "main": "./dist/index.js",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.ts",
12
+ "source": "./src/index.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "@rarusoft/dendrite-memory": "0.1.0-alpha.0"
29
+ },
30
+ "license": "Apache-2.0",
31
+ "author": {
32
+ "name": "Michael Fillalan",
33
+ "url": "https://x.com/MichaelFillalan"
34
+ }
35
+ }
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Extracts an `ApiFileReference` from a single TypeScript source file.
3
+ *
4
+ * Uses the TypeScript Compiler API directly (no typedoc, no extra dependencies — the
5
+ * `typescript` package is already a devDep). Parses the file with `ts.createSourceFile`
6
+ * and `setParentNodes: true` so `Node.getText()`, the printer, and parent-walking helpers
7
+ * all work without a full Program. JSDoc is read off `node.jsDoc[last]` directly because
8
+ * Program-loaded source files don't set parent pointers in a way `getJSDocCommentsAndTags`
9
+ * can use; the last entry of the array is the immediately-preceding doc block.
10
+ *
11
+ * Each top-level exported declaration becomes one `ApiSymbol`. Function default values
12
+ * are stripped from rendered signatures (implementation detail, not type contract).
13
+ * Interfaces render with their full member body via the TS printer. `@internal`-tagged
14
+ * exports are filtered. The renderer in `./render.ts` formats the result as markdown.
15
+ */
16
+
17
+ import { readFileSync } from 'node:fs';
18
+ import path from 'node:path';
19
+ import ts from 'typescript';
20
+ import type { ApiDocTag, ApiFileReference, ApiSymbol, ApiSymbolKind } from './types.js';
21
+
22
+ export interface ExtractOptions {
23
+ // Project root used to derive `sourcePath` (project-relative, forward slashes).
24
+ rootDir?: string;
25
+ }
26
+
27
+ export function extractApiFileReference(
28
+ sourcePath: string,
29
+ options: ExtractOptions = {}
30
+ ): ApiFileReference {
31
+ const rootDir = options.rootDir ?? process.cwd();
32
+ const absolute = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(rootDir, sourcePath);
33
+ const relative = toForwardSlash(path.relative(rootDir, absolute));
34
+
35
+ // Parse the file directly with parent pointers enabled. We don't need a Program for A1 —
36
+ // there's no type-checker work, just AST traversal of declarations and JSDoc. Setting parent
37
+ // pointers makes ts.Node.getText() and printNode work without a separate setup step.
38
+ const text = readFileSync(absolute, 'utf8');
39
+ const sourceFile = ts.createSourceFile(absolute, text, ts.ScriptTarget.ES2022, /*setParentNodes*/ true, ts.ScriptKind.TS);
40
+
41
+ const symbols: ApiSymbol[] = [];
42
+ for (const statement of sourceFile.statements) {
43
+ const extracted = extractSymbolsFromStatement(statement, sourceFile);
44
+ for (const symbol of extracted) {
45
+ if (isInternal(symbol)) {
46
+ continue;
47
+ }
48
+ symbols.push(symbol);
49
+ }
50
+ }
51
+
52
+ symbols.sort((left, right) => left.sourceLine - right.sourceLine);
53
+
54
+ return {
55
+ sourcePath: relative,
56
+ moduleSlug: deriveModuleSlug(relative),
57
+ symbols,
58
+ fileDocComment: extractFileDocComment(sourceFile)
59
+ };
60
+ }
61
+
62
+ function extractSymbolsFromStatement(
63
+ statement: ts.Statement,
64
+ sourceFile: ts.SourceFile
65
+ ): ApiSymbol[] {
66
+ if (!hasExportModifier(statement)) {
67
+ return [];
68
+ }
69
+
70
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
71
+ return [buildSymbol(statement, statement.name.text, 'function', renderFunctionSignature(statement), sourceFile)];
72
+ }
73
+
74
+ if (ts.isClassDeclaration(statement) && statement.name) {
75
+ return [buildSymbol(statement, statement.name.text, 'class', renderClassSignature(statement), sourceFile)];
76
+ }
77
+
78
+ if (ts.isInterfaceDeclaration(statement)) {
79
+ return [buildSymbol(statement, statement.name.text, 'interface', renderInterfaceSignature(statement), sourceFile)];
80
+ }
81
+
82
+ if (ts.isTypeAliasDeclaration(statement)) {
83
+ return [buildSymbol(statement, statement.name.text, 'type-alias', renderTypeAliasSignature(statement), sourceFile)];
84
+ }
85
+
86
+ if (ts.isEnumDeclaration(statement)) {
87
+ return [buildSymbol(statement, statement.name.text, 'enum', renderEnumSignature(statement), sourceFile)];
88
+ }
89
+
90
+ if (ts.isVariableStatement(statement)) {
91
+ const out: ApiSymbol[] = [];
92
+ for (const decl of statement.declarationList.declarations) {
93
+ if (!ts.isIdentifier(decl.name)) {
94
+ continue;
95
+ }
96
+ out.push(
97
+ buildSymbol(statement, decl.name.text, 'variable', renderVariableSignature(statement, decl), sourceFile)
98
+ );
99
+ }
100
+ return out;
101
+ }
102
+
103
+ return [];
104
+ }
105
+
106
+ function buildSymbol(
107
+ jsDocCarrier: ts.Node,
108
+ name: string,
109
+ kind: ApiSymbolKind,
110
+ signature: string,
111
+ sourceFile: ts.SourceFile
112
+ ): ApiSymbol {
113
+ const { docComment, tags } = parseJSDoc(jsDocCarrier);
114
+ const sourceLine = sourceFile.getLineAndCharacterOfPosition(jsDocCarrier.getStart(sourceFile)).line + 1;
115
+ const isDeprecated = tags.some((tag) => tag.name === 'deprecated');
116
+ return {
117
+ name,
118
+ kind,
119
+ signature,
120
+ docComment,
121
+ tags,
122
+ sourceLine,
123
+ isDeprecated
124
+ };
125
+ }
126
+
127
+ function hasExportModifier(node: ts.Node): boolean {
128
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
129
+ if (!modifiers) {
130
+ return false;
131
+ }
132
+ return modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
133
+ }
134
+
135
+ function parseJSDoc(node: ts.Node): { docComment: string | null; tags: ApiDocTag[] } {
136
+ // ts.getJSDocCommentsAndTags walks the parent chain, but Program-created source files do
137
+ // not have parent pointers set, so it returns nothing. Read .jsDoc directly instead. The
138
+ // parser attaches one JSDoc node per JSDoc block preceding the declaration; the LAST entry
139
+ // is the immediately-preceding one, which is the "owning" doc for the declaration.
140
+ const jsDocList = (node as { jsDoc?: ts.JSDoc[] }).jsDoc;
141
+ if (!jsDocList || jsDocList.length === 0) {
142
+ return { docComment: null, tags: [] };
143
+ }
144
+
145
+ const owning = jsDocList[jsDocList.length - 1];
146
+ const body = renderJSDocComment(owning.comment).trim();
147
+ const tags: ApiDocTag[] = [];
148
+ if (owning.tags) {
149
+ for (const tag of owning.tags) {
150
+ tags.push(parseTag(tag));
151
+ }
152
+ }
153
+ return { docComment: body.length > 0 ? body : null, tags };
154
+ }
155
+
156
+ function parseTag(tag: ts.JSDocTag): ApiDocTag {
157
+ const name = tag.tagName.text;
158
+ const text = renderJSDocComment(tag.comment).trim();
159
+
160
+ if (ts.isJSDocParameterTag(tag)) {
161
+ return {
162
+ name,
163
+ text,
164
+ paramName: ts.isIdentifier(tag.name) ? tag.name.text : tag.name.getText()
165
+ };
166
+ }
167
+
168
+ return { name, text };
169
+ }
170
+
171
+ function renderJSDocComment(comment: string | ts.NodeArray<ts.JSDocComment> | undefined): string {
172
+ if (!comment) {
173
+ return '';
174
+ }
175
+ if (typeof comment === 'string') {
176
+ return normalizeLineEndings(comment);
177
+ }
178
+ const joined = comment
179
+ .map((part) => {
180
+ if (part.kind === ts.SyntaxKind.JSDocText) {
181
+ return (part as ts.JSDocText).text;
182
+ }
183
+ if (part.kind === ts.SyntaxKind.JSDocLink || part.kind === ts.SyntaxKind.JSDocLinkCode || part.kind === ts.SyntaxKind.JSDocLinkPlain) {
184
+ const link = part as ts.JSDocLink | ts.JSDocLinkCode | ts.JSDocLinkPlain;
185
+ const targetName = link.name ? link.name.getText() : '';
186
+ const linkText = link.text ?? '';
187
+ const label = linkText.trim().length > 0 ? `${targetName} ${linkText.trim()}`.trim() : targetName;
188
+ return `{@link ${label}}`;
189
+ }
190
+ return '';
191
+ })
192
+ .join('');
193
+ return normalizeLineEndings(joined);
194
+ }
195
+
196
+ /**
197
+ * Normalize CRLF / lone CR to LF in extracted text. Source files checked out on Windows
198
+ * with `core.autocrlf=true` come back from disk with CRLF, which would otherwise leak
199
+ * into the extracted JSDoc/TSDoc text fields and produce platform-dependent fixtures
200
+ * and pages. Normalizing at the extraction boundary keeps every downstream artifact
201
+ * (`ApiFileReference`, rendered markdown, manifest content hash) byte-identical across
202
+ * Windows / macOS / Linux checkouts.
203
+ */
204
+ function normalizeLineEndings(text: string): string {
205
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
206
+ }
207
+
208
+ function isInternal(symbol: ApiSymbol): boolean {
209
+ return symbol.tags.some((tag) => tag.name === 'internal');
210
+ }
211
+
212
+ function extractFileDocComment(sourceFile: ts.SourceFile): string | null {
213
+ const fullText = sourceFile.getFullText();
214
+ const ranges = ts.getLeadingCommentRanges(fullText, 0);
215
+ if (!ranges) {
216
+ return null;
217
+ }
218
+
219
+ for (const range of ranges) {
220
+ if (range.kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
221
+ continue;
222
+ }
223
+ const raw = fullText.slice(range.pos, range.end);
224
+ if (!raw.startsWith('/**')) {
225
+ continue;
226
+ }
227
+ return cleanBlockComment(raw);
228
+ }
229
+
230
+ return null;
231
+ }
232
+
233
+ function cleanBlockComment(raw: string): string {
234
+ const inner = raw.replace(/^\/\*\*/, '').replace(/\*\/$/, '');
235
+ const lines = inner.split(/\r?\n/).map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
236
+ while (lines.length > 0 && lines[0].trim() === '') {
237
+ lines.shift();
238
+ }
239
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
240
+ lines.pop();
241
+ }
242
+ return lines.join('\n').trim();
243
+ }
244
+
245
+ // --- signature renderers -----------------------------------------------------
246
+
247
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: true, omitTrailingSemicolon: true });
248
+
249
+ function renderFunctionSignature(node: ts.FunctionDeclaration): string {
250
+ const typeParams = renderTypeParameters(node.typeParameters);
251
+ const params = (node.parameters ?? []).map((p) => renderParameter(p)).join(', ');
252
+ const returnType = node.type ? `: ${printNode(node.type)}` : '';
253
+ return `function ${node.name?.text ?? ''}${typeParams}(${params})${returnType}`;
254
+ }
255
+
256
+ function renderParameter(node: ts.ParameterDeclaration): string {
257
+ // Strip default values from the rendered signature — they're implementation detail, not
258
+ // part of the public type contract, and they add visual noise to the API page. Preserve
259
+ // rest tokens, optionals, and the type annotation.
260
+ const dotDotDot = node.dotDotDotToken ? '...' : '';
261
+ const name = node.name.getText();
262
+ const question = node.questionToken ? '?' : '';
263
+ const typeAnnotation = node.type ? `: ${printNode(node.type)}` : '';
264
+ return `${dotDotDot}${name}${question}${typeAnnotation}`;
265
+ }
266
+
267
+ function renderClassSignature(node: ts.ClassDeclaration): string {
268
+ const typeParams = renderTypeParameters(node.typeParameters);
269
+ const heritage = (node.heritageClauses ?? [])
270
+ .map((clause) => printNode(clause).trim())
271
+ .join(' ');
272
+ const heritageSuffix = heritage.length > 0 ? ` ${heritage}` : '';
273
+ return `class ${node.name?.text ?? ''}${typeParams}${heritageSuffix}`;
274
+ }
275
+
276
+ function renderInterfaceSignature(node: ts.InterfaceDeclaration): string {
277
+ // Print the full interface including its members. An empty `interface Foo` line on its
278
+ // own is useless on the API page; readers care about the shape of the type, which is the
279
+ // member list. The TS printer handles formatting + indentation cleanly. Strip the leading
280
+ // `export` modifier — every symbol on an API reference page is exported by definition.
281
+ return printNode(node).replace(/^export\s+/, '');
282
+ }
283
+
284
+ function renderTypeAliasSignature(node: ts.TypeAliasDeclaration): string {
285
+ const typeParams = renderTypeParameters(node.typeParameters);
286
+ return `type ${node.name.text}${typeParams} = ${printNode(node.type)}`;
287
+ }
288
+
289
+ function renderEnumSignature(node: ts.EnumDeclaration): string {
290
+ const members = node.members.map((member) => ` ${printNode(member)}`).join(',\n');
291
+ return `enum ${node.name.text} {\n${members}\n}`;
292
+ }
293
+
294
+ function renderVariableSignature(
295
+ statement: ts.VariableStatement,
296
+ decl: ts.VariableDeclaration
297
+ ): string {
298
+ const flags = statement.declarationList.flags;
299
+ const keyword = flags & ts.NodeFlags.Const ? 'const' : flags & ts.NodeFlags.Let ? 'let' : 'var';
300
+ const name = ts.isIdentifier(decl.name) ? decl.name.text : decl.name.getText();
301
+ const typeAnnotation = decl.type ? `: ${printNode(decl.type)}` : '';
302
+ return `${keyword} ${name}${typeAnnotation}`;
303
+ }
304
+
305
+ function renderTypeParameters(params: ts.NodeArray<ts.TypeParameterDeclaration> | undefined): string {
306
+ if (!params || params.length === 0) {
307
+ return '';
308
+ }
309
+ return `<${params.map((p) => printNode(p)).join(', ')}>`;
310
+ }
311
+
312
+ function printNode(node: ts.Node): string {
313
+ const sourceFile = node.getSourceFile();
314
+ if (sourceFile) {
315
+ return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
316
+ }
317
+ // Fallback for synthesized nodes: build a transient SourceFile.
318
+ const transientFile = ts.createSourceFile('__transient__.ts', '', ts.ScriptTarget.ES2022, false, ts.ScriptKind.TS);
319
+ return printer.printNode(ts.EmitHint.Unspecified, node, transientFile);
320
+ }
321
+
322
+ function deriveModuleSlug(relativeSourcePath: string): string {
323
+ const trimmed = relativeSourcePath.replace(/\\/g, '/').replace(/^\.\//, '');
324
+ const withoutExt = trimmed.replace(/\.[cm]?tsx?$/i, '');
325
+ const stripped = withoutExt
326
+ .replace(/^src\//, '')
327
+ .replace(/^packages\/([^/]+)\/src\//, '$1/');
328
+ return `api/${stripped}`;
329
+ }
330
+
331
+ function toForwardSlash(value: string): string {
332
+ return value.replace(/\\/g, '/');
333
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Language pluggability surface for the API reference generator.
3
+ *
4
+ * The orchestrator (`refreshApiReference`) walks a registered list of `LanguageExtractor`
5
+ * implementations and dispatches to the first one whose `detect(rootDir)` returns true.
6
+ * TypeScript is the only built-in today (`./typescript-extractor.ts`); future Python/Rust/Go
7
+ * support is a drop-in module implementing this same interface — no orchestrator changes.
8
+ *
9
+ * The interface is deliberately small and async-friendly so a Python extractor that shells
10
+ * out to `pdoc --output json` or a Rust extractor wrapping `rustdoc --output-format json`
11
+ * can implement it without contortion. It is also free of TypeScript-specific shapes;
12
+ * everything the orchestrator needs is the language-agnostic `ApiFileReference` from
13
+ * `./types.ts`. Phase A7 of the API reference roadmap establishes this layering.
14
+ */
15
+
16
+ import type { ApiFileReference } from './types.js';
17
+ import type { WalkOptions } from './walk.js';
18
+
19
+ export interface LanguageExtractor {
20
+ // Stable identifier — 'typescript' | 'python' | 'rust' | etc. Used in diagnostics and
21
+ // (eventually) in per-language manifest entries when more than one extractor is active.
22
+ id: string;
23
+
24
+ // Returns true iff this extractor can meaningfully handle the project rooted at
25
+ // `rootDir`. Should be cheap (file-existence checks) and never throw — return false on
26
+ // any error.
27
+ detect(rootDir: string): Promise<boolean>;
28
+
29
+ // Returns the list of source files this extractor will operate on, project-relative,
30
+ // forward slashes, sorted.
31
+ walk(rootDir: string, options?: WalkOptions): Promise<string[]>;
32
+
33
+ // Parses one source file and returns its API surface. The orchestrator never calls
34
+ // `extract` for a path that did not come back from the same extractor's `walk`, so each
35
+ // extractor controls its own parser semantics end-to-end.
36
+ extract(sourcePath: string, options?: { rootDir?: string }): Promise<ApiFileReference>;
37
+ }