@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
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared types for the API reference extractor.
3
+ *
4
+ * Defines `ApiSymbol`, `ApiDocTag`, and `ApiFileReference` — the structured shape every
5
+ * `LanguageExtractor` must produce. The renderer in `./render.ts` and the orchestrator in
6
+ * `../api-reference.ts` consume this shape; the TypeScript implementation in
7
+ * `./extract.ts` and `./typescript-extractor.ts` produces it. Future Python/Rust/Go
8
+ * extractors will produce the same shape, which is what makes language pluggability work
9
+ * without orchestrator changes. Phase A1 of the API reference roadmap defines the contract.
10
+ */
11
+
12
+ export type ApiSymbolKind =
13
+ | 'function'
14
+ | 'class'
15
+ | 'interface'
16
+ | 'type-alias'
17
+ | 'enum'
18
+ | 'variable';
19
+
20
+ export interface ApiDocTag {
21
+ name: string;
22
+ text: string;
23
+ paramName?: string;
24
+ }
25
+
26
+ export interface ApiSymbol {
27
+ name: string;
28
+ kind: ApiSymbolKind;
29
+ signature: string;
30
+ docComment: string | null;
31
+ tags: ApiDocTag[];
32
+ sourceLine: number;
33
+ isDeprecated: boolean;
34
+ }
35
+
36
+ export interface ApiFileReference {
37
+ sourcePath: string;
38
+ moduleSlug: string;
39
+ symbols: ApiSymbol[];
40
+ fileDocComment: string | null;
41
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The built-in TypeScript `LanguageExtractor`.
3
+ *
4
+ * Thin adapter that wraps `extractApiFileReference` (from `./extract.ts`) and
5
+ * `walkProjectSources` (from `./walk.ts`) behind the language-agnostic interface, so the
6
+ * orchestrator's dispatch loop is uniform across languages. `detect()` is intentionally
7
+ * high-recall: returns true on `tsconfig.json`, `package.json`, OR a bare `src/` directory
8
+ * — any of those is a strong "this is a Node/TypeScript project" signal. When future
9
+ * extractors are added, registration order in `../api-reference.ts` decides which one
10
+ * claims a project where multiple `detect()` would match.
11
+ */
12
+
13
+ import { promises as fs } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { extractApiFileReference } from './extract.js';
16
+ import type { LanguageExtractor } from './language-extractor.js';
17
+ import { walkProjectSources, type WalkOptions } from './walk.js';
18
+
19
+ async function exists(filePath: string): Promise<boolean> {
20
+ try {
21
+ await fs.access(filePath);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ export const typeScriptExtractor: LanguageExtractor = {
29
+ id: 'typescript',
30
+
31
+ async detect(rootDir: string): Promise<boolean> {
32
+ // High-recall detection: any of the conventional Node/TS signals counts. For projects
33
+ // that have just `src/` with .ts files (e.g., test fixtures) this still resolves true.
34
+ // When a second language extractor is added, its detect() runs in registration order,
35
+ // so the orchestrator can prefer (say) Python over TypeScript by registering python
36
+ // first.
37
+ if (await exists(path.join(rootDir, 'tsconfig.json'))) {
38
+ return true;
39
+ }
40
+ if (await exists(path.join(rootDir, 'package.json'))) {
41
+ return true;
42
+ }
43
+ if (await exists(path.join(rootDir, 'src'))) {
44
+ return true;
45
+ }
46
+ return false;
47
+ },
48
+
49
+ async walk(rootDir: string, options?: WalkOptions): Promise<string[]> {
50
+ return walkProjectSources(rootDir, options);
51
+ },
52
+
53
+ async extract(sourcePath: string, options?: { rootDir?: string }) {
54
+ return extractApiFileReference(sourcePath, { rootDir: options?.rootDir });
55
+ }
56
+ };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Walk a project directory and return source paths the API reference generator should
3
+ * extract — project-relative, forward slashes, alphabetically sorted.
4
+ *
5
+ * Pure Node 20+ — no glob library. The matcher is a small custom converter from glob to
6
+ * regex covering double-star, single-star, single-char, and literal segments. That covers
7
+ * the patterns the generator's defaults pass in: source globs under the root source tree
8
+ * plus workspace package source trees, test-file exclusions, internal-convention directory
9
+ * exclusions, and `node_modules` pruning.
10
+ *
11
+ * A second filter respects file-level `@internal` JSDoc on the source itself: when
12
+ * `respectInternalConvention` is true (default), each candidate's first 2KB is read and
13
+ * the file is skipped if a top-of-file JSDoc block contains an `@internal` tag. That
14
+ * mirrors how individual symbols are filtered in `./extract.ts` and lets a whole module
15
+ * opt out of the API reference without moving it into an `internal/` directory.
16
+ */
17
+
18
+ import { promises as fs } from 'node:fs';
19
+ import path from 'node:path';
20
+
21
+ export interface WalkOptions {
22
+ include?: string[];
23
+ exclude?: string[];
24
+ respectInternalConvention?: boolean;
25
+ // Short-circuit after collecting this many matches. Useful for "is there at least one
26
+ // file of language X here?" content-based detect checks (Bash uses `limit: 1`) where a
27
+ // full project scan would be wasteful. Returns up to `limit` matches; subsequent
28
+ // directory recursion is suppressed once the cap is hit. When omitted, the walker
29
+ // collects every match.
30
+ limit?: number;
31
+ }
32
+
33
+ // TypeScript's modern file extensions: `.ts` is the canonical, `.tsx` covers JSX/React,
34
+ // `.cts`/`.mts` are the ESM-compat node variants. The slug regex in extract.ts already
35
+ // strips all four; the walker now lists them explicitly because brace expansion isn't
36
+ // supported in the matcher (see `toMatcher`).
37
+ const DEFAULT_INCLUDE = [
38
+ 'src/**/*.ts',
39
+ 'src/**/*.tsx',
40
+ 'src/**/*.cts',
41
+ 'src/**/*.mts',
42
+ 'packages/*/src/**/*.ts',
43
+ 'packages/*/src/**/*.tsx',
44
+ 'packages/*/src/**/*.cts',
45
+ 'packages/*/src/**/*.mts'
46
+ ];
47
+ const DEFAULT_EXCLUDE = [
48
+ '**/*.test.ts',
49
+ '**/*.test.tsx',
50
+ '**/*.d.ts',
51
+ '**/internal/**',
52
+ '**/_internal/**',
53
+ '**/node_modules/**'
54
+ ];
55
+
56
+ export async function walkProjectSources(
57
+ rootDir: string,
58
+ options: WalkOptions = {}
59
+ ): Promise<string[]> {
60
+ const include = (options.include ?? DEFAULT_INCLUDE).map(toMatcher);
61
+ const exclude = (options.exclude ?? DEFAULT_EXCLUDE).map(toMatcher);
62
+ const respectInternal = options.respectInternalConvention ?? true;
63
+ // Distinguish `undefined` (no limit, unbounded walk) from `0` (caller explicitly asked
64
+ // for zero results, perhaps via arithmetic that drained their budget). Treating 0 as
65
+ // Infinity would silently turn a probe with depleted budget into a full tree walk.
66
+ const limit = typeof options.limit === 'number' && options.limit >= 0 ? options.limit : Infinity;
67
+
68
+ const matches: string[] = [];
69
+ await walk(rootDir, '', matches, include, exclude, limit);
70
+ matches.sort();
71
+
72
+ if (respectInternal) {
73
+ const filtered: string[] = [];
74
+ for (const match of matches) {
75
+ if (await fileTopJSDocHasInternalTag(path.resolve(rootDir, match))) {
76
+ continue;
77
+ }
78
+ filtered.push(match);
79
+ }
80
+ return filtered;
81
+ }
82
+ return matches;
83
+ }
84
+
85
+ async function walk(
86
+ rootDir: string,
87
+ relativeDir: string,
88
+ out: string[],
89
+ include: RegExp[],
90
+ exclude: RegExp[],
91
+ limit: number
92
+ ): Promise<void> {
93
+ if (out.length >= limit) return;
94
+ const absoluteDir = path.resolve(rootDir, relativeDir);
95
+ let entries: import('node:fs').Dirent[];
96
+ try {
97
+ entries = await fs.readdir(absoluteDir, { withFileTypes: true });
98
+ } catch (error) {
99
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
100
+ return;
101
+ }
102
+ throw error;
103
+ }
104
+
105
+ for (const entry of entries) {
106
+ if (out.length >= limit) return;
107
+ const childRel = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
108
+ if (entry.isDirectory()) {
109
+ // Pre-prune common heavy directories before recursing.
110
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
111
+ continue;
112
+ }
113
+ await walk(rootDir, childRel, out, include, exclude, limit);
114
+ continue;
115
+ }
116
+ if (!entry.isFile()) {
117
+ continue;
118
+ }
119
+ if (exclude.some((re) => re.test(childRel))) {
120
+ continue;
121
+ }
122
+ if (!include.some((re) => re.test(childRel))) {
123
+ continue;
124
+ }
125
+ out.push(childRel);
126
+ }
127
+ }
128
+
129
+ async function fileTopJSDocHasInternalTag(absolutePath: string): Promise<boolean> {
130
+ // Cheap scan of the leading bytes — read just enough to cover any plausible top-of-file
131
+ // doc comment without slurping the whole source.
132
+ let head: string;
133
+ try {
134
+ const handle = await fs.open(absolutePath, 'r');
135
+ try {
136
+ const buffer = Buffer.alloc(2048);
137
+ const result = await handle.read(buffer, 0, buffer.length, 0);
138
+ head = buffer.slice(0, result.bytesRead).toString('utf8');
139
+ } finally {
140
+ await handle.close();
141
+ }
142
+ } catch {
143
+ return false;
144
+ }
145
+
146
+ // Look for a top-of-file JSDoc block, then check whether it carries an @internal tag.
147
+ // The match anchors on tag-position — start of line, optionally after the JSDoc `*`
148
+ // prefix — so prose mentions of "@internal" inside descriptions don't accidentally
149
+ // self-filter the file (the docstring on this very file mentions @internal in prose).
150
+ const match = head.match(/^\s*\/\*\*([\s\S]*?)\*\//);
151
+ if (!match) {
152
+ return false;
153
+ }
154
+ return /^[ \t]*\*?[ \t]*@internal\b/m.test(match[1]);
155
+ }
156
+
157
+ function toMatcher(pattern: string): RegExp {
158
+ // Brace expansion (`{a,b}`) and POSIX character classes (`[abc]`) are not supported.
159
+ // Silently escaping the metacharacters as literals would produce regexes that match
160
+ // patterns like `{ts,tsx}` literally — silent miscompiles. Refuse with a clear error
161
+ // and tell the caller to pass each variant as a separate include glob.
162
+ if (/[{}]/.test(pattern)) {
163
+ throw new Error(
164
+ `walkProjectSources: brace expansion is not supported in glob "${pattern}". ` +
165
+ `Pass each variant as a separate include pattern (e.g., 'src/**/*.ts' AND 'src/**/*.tsx' instead of 'src/**/*.{ts,tsx}').`
166
+ );
167
+ }
168
+ if (/\[(?!\])/.test(pattern)) {
169
+ throw new Error(
170
+ `walkProjectSources: character classes are not supported in glob "${pattern}". ` +
171
+ `Use literal patterns instead.`
172
+ );
173
+ }
174
+ // Escape regex specials, then translate glob tokens. Order matters: handle `**` before `*`.
175
+ let escaped = '';
176
+ let i = 0;
177
+ while (i < pattern.length) {
178
+ const char = pattern[i];
179
+ if (char === '*' && pattern[i + 1] === '*') {
180
+ // `**/` matches zero or more path segments. `**` alone matches anything.
181
+ if (pattern[i + 2] === '/') {
182
+ escaped += '(?:.*/)?';
183
+ i += 3;
184
+ continue;
185
+ }
186
+ escaped += '.*';
187
+ i += 2;
188
+ continue;
189
+ }
190
+ if (char === '*') {
191
+ escaped += '[^/]*';
192
+ i += 1;
193
+ continue;
194
+ }
195
+ if (char === '?') {
196
+ escaped += '[^/]';
197
+ i += 1;
198
+ continue;
199
+ }
200
+ if (/[.+^${}()|[\]\\]/.test(char)) {
201
+ escaped += `\\${char}`;
202
+ i += 1;
203
+ continue;
204
+ }
205
+ escaped += char;
206
+ i += 1;
207
+ }
208
+ return new RegExp(`^${escaped}$`);
209
+ }